Hypertext Rails: Working With HTMX
- HTMX is a lightweight extension that makes links and forms feel more smooth, by sending requests asynchronously.
- The library is extremely extensive, but we only use a tiny subset of its functionality.
- You can tell htmx to do things by adding attributes (starting with
hx-
) to your html elements, or by sending headers in html responses.
How We Use htmx
- There are two ways to incorporate htmx into an app. You can have it off-by-default, in which case you apply behaviours selectively only to the elements you want. Or you can have it on-by-default, in which case every
<a>
and<form>
element in the app will be sent asynchronously, unless otherwise specified. - In Hypertext Rails, htmx is on-by-default. To do this, we've added
hx-boost="true"
to the body of our page. - We've also set
hx-target
andhx-select
to#main-content
. This means that by default, when a request is sent from a link or a form, htmx will extract only the contents of the#main-content
div in the response, and put it into the current#main-content
div.
# /app/views/layouts/application.html.erb
<body hx-boost="true" hx-target="#main-content" hx-select="#main-content">
<%= render :partial => "shared/partial_containers" %>
<div id="main-content">
<%= yield %>
</div>
</body>
You'll notice from the above snippet that anything in the shared/_partial_containers.html.erb
is not replaced by default when a new page is loaded (because it's outside the #main-content
div). This partial is where we render modals, toasts and side sliding content, so not having it get updated when a new page loads makes the app feel more native. (See below for more info on overriding this).
General Usage
Outside of toasts and modals, there's very little custom code that's needed. If we just use <a>
tags and <form>
s as usual, htmx should do the rest for us. But there are some stylistic changes that may not be intuitive if you're used to handling state on the frontend.
- Use the url for simple state, and use the
current_url_with
helper to keep code clean and less verbose when passing data around. - When the url is insufficient - for example multi step forms or wizards - use the database for state.
- Try to have one controller action per user action, but feel free to reuse the same view across multiple controller actions.
Override & Customization Snippets
To override an <a>
or <form>
to remove asynchronous loading...
<a hx-boost="false"> This Link won't be touched by htmx</a>
To remove the htmx defaults that will be inherited from the body (in our case hx-target
and hx-select
)
Don't put this directly on an element that needs to have the defaults unset - put it on a parent element.
<div hx-disinherit="*">
<input hx-get="/my-path" hx-target="#alternate_target" >
</div>
To disable scroll to top
Sometimes when an htmx-enabled link is clicked after a page has been scrolled, htmx will automatically scroll to the top of the page. You can disable this by adding hx-swap="innerHTML show:no-scroll"
<a hx-swap="innerHTML show:no-scroll" >My Link</a>
Prevent a response from swapping the main page body
This is useful when you only want to replace the content of modals or toasts but not the main body of the page.
def my_action
response.set_header('HX-Reswap', 'none')
response.set_header('HX-Push-Url', 'false')
end
To Refresh content that's not included in the #main-content
div
You'll notice that anything in shared/_partial_containers.html.erb
is not replaced by default when a new page is loaded (because it's outside the #main-content div). This partial is where we render modals, toasts and side sliding content, so not having it get updated when a new page loads makes the app feel more native. To override this, use the hx-swap-oob
attribute on the div that has the content you want to include. For example, for toasts, we check if the controller has any new toasts, and if it has, we add the attribute, like so...
<div id="toasts" hx-swap-oob="<%= 'true' if flash[:toasts] %>" class="toasts-container fixed top-0 z-20">
</div>
How Modals Work
Triggering a modal is covered in modals. Triggering a modal is as simple as adding ?modal=true
to any request and then adding htmx_support_modal
to the relevant controller action. Behind the scenes, we use htmx's response headers to tell the frontend what to do.
# /app/controllers/application_controller.rb
def htmx_support_modal
if params[:modal].present?
response.set_header('HX-Push-Url','false')
response.set_header('HX-Retarget','#main-modal-container')
response.set_header('HX-Reselect','#modal')
render :layout => "modal"
end
end
# app/views/layouts/modal.html.erb
<div id="modal" :load="isModalOpen=true;$('#modal').showModal();">
<%= yield %>
</div>