HTMX + Rails: Exploring them with a Toy App.
Tags: rails, ruby, htmx, tutorial
Published: 2024-02-27 00:04:04

##📝 Introduction
When it comes to UI, React is king, but in the Rails world We have _Turbo_ to save us from writing as much Javascript code as possible. 😜. Well, HTMX has that same goal and on top of that is THE trend right now. In this article we will take the first steps in HTMX(_from Rails perspective_) with the help of a modest [Google Keep](https://keep.google.com/) clone.

##🚀 Installing HTMX
**1. Install the [rails-htmx](https://github.com/guilleiguaran/rails-htmx) gem and add it to the application's Gemfile:**
```bash
bundle add rails-htmx
```
**2. Installing the dependency:**
Pin the dependency to the importmap:
```bash
bin/importmap pin htmx.org
```
and then import it on your `app/javascript/application.js`:

```javascript
import "htmx.org"
```
**3. The** `response-targets` **Extension:**
Any real-world application will need this extension to handle non `200` http responses. This topic will be addressed later, for now take my word for it and create a `app/javascript/htmx.js` file, something like this will do:

```javascript
import htmx from 'htmx.org';
window.htmx = htmx; // Makes htmx available globally.
```
Then add the extension in your `app/javascript/application.js`
```javascript
import './htmx.js'; // We don't need `import "htmx.org"` anymore.
import "htmx.org/dist/ext/response-targets"
```
##🛠️ Basic configuration
**1. Add the CSRF Token to the htmx requests:**
```html
<!--
Add the X-CSRF-Token to the hx-headers attributes,
this way the X-CSRF-Token is added in all XHR requests done by htmx.
-->
<body hx-headers='{"X-CSRF-Token": "<%= form_authenticity_token %>"}'>
```
##🪁 The Toy App
The code below comes from a [Toy App](https://railshtmxkeep-matedemorphy.replit.app/) i built with the purpose of getting into HTMX. Check the [Repo](https://github.com/matedemorphy/rails_htmx_keep) to get the whole picture. 
##🎢 Road to CRUD
**0. Starting point:** `views/notes/_form.html.erb`
```erb
  <%= form_with model: note, url: false do |form| %>
    <!-- form fields -->
  <% end %>
```
**1. AJAX**
>The core of htmx is a set of attributes that allow you to perform AJAX requests directly from HTML: `hx-get`, `hx-post`, `hx-put`, `hx-patch`, `hx-delete`.

```erb
  <%= form_with model: note, url: false, data: hx(post: url_for(note))  do |form| %>
    <!-- form fields -->
  <% end %>
```
Thanks to gem `rails-htmx` we have at our disposal the `hx` helper wich we can use to easily generate HTMX attributes in our views. This: `hx(post: url_for(note))` will become this: `data-hx-post="/note`.

**2. Targets**

> Allows the response to be loaded into a different element other than the one that made the request.
```erb
 <%= form_with model: note, url: false, data: hx(post: url_for(note), target: "#notes-masonry")  do |form| %>
   <!-- form fields -->
 <% end %>
```
We use the `#` CSS Selector to specify the element on wich the Controller's response should be rendered.

**2. Swapping**

> HTMX offers several ways to render the HTML response from the Controller.

```erb
 <%= form_with model: note, url: false, data: hx(post: url_for(note), target: "#notes-masonry", swap: "beforeend")  do |form| %>
   <!-- form fields -->
 <% end %>
```
We're using `beforeend` wich appends the content after the last child inside the target.

**3. Handling errors**

> By default, non-200 responses are not swapped into the DOM, we need to set this up:

```javascript
const validStatusCodes = [200, 201, 303];

document.body.addEventListener('htmx:beforeSwap', function(evt) {
  if (!validStatusCodes.includes(evt.detail.xhr.status)) {
    evt.detail.shouldSwap = true;
    evt.detail.target = htmx.find("#form-errors"); // Set target for errors.
    evt.detail.isError = false; // Turn off error logging in console.
  }
});
```
And finally:

```erb
 <div id="errors-alert"></div>
 <%= form_with model: note, url: false, data: hx(post: url_for(note), target: "#notes-masonry", swap: "beforeend", target_error: "#errors-alert")  do |form| %>
   <!-- form fields -->
 <% end %>
```
```erb
<!--
Add `hx-ext` attribute, wich enables an htmx extension for an element and all its children; in this case the `response-targets`´extension.
-->
<body hx-ext="response-targets">
</body>
```
HTMX will now also handle `non-200` HTTP response codes, wich may come from:

```ruby
  def create
    @note = Note.new(note_params)

    if @note.save
      render partial: "notes/note", locals: {note: @note}, status: :created
    else
      render partial: "shared/form_errors", locals: {errors: @note.errors.full_messages}, status: :unprocessable_entity
    end
  end
```
**4. Time for** C.R.**<u>U</u>.<u>D</u>** 

> For update we're gonna use `hx-patch` and for delete `hx-delete`.

`controllers/notes_controller.rb`
```ruby
  def color
    if @note.update(color: params[:color])
      render partial: "notes/note", locals: {note: @note}, status: :ok
    end

  def destroy
    @note.destroy!
    head :ok
  end
```
`components/note_component.html.erb`
```erb
<div id="note_<%=@id%>" style="background-color: <%= @color%> !important;">
  <input type="color" hx-target="#note_<%=@id%>" hx-patch=<%= color_note_path(@id) %> hx-swap="outerHTML" hx-vals='js:{"color": event.target.value}' hx-trigger="change">
  <%= button_tag(data: hx(confirm: "Are you sure you wish to delete this note?", delete: note_path(@id), swap: "delete", target: "#note_#{@id}")) %>
</div>
```
👀 _New stuff:_
- `hx-trigger`: Useful if you want a different event than the by-default one to trigger the request (I.e: `click`, for `<button>`). In this case we use `change`.

- `hx-swap="outerHTML"`: We've already talked about `hx-swap`, but this time we swap the HTML response using outerHTML, which replaces the entire target element with the returned content.

- `hx-swap="delete"`: To remove a _note_ from our view once it has been deleted from the database, the `delete` value for the `hx-swap` attribute works perfectly for this purpose because precisely what it does is to delete the target element regardless of the response.

- `hx-confirm`: The widely known _confirm_ wich uses the browser’s `window.confirm`.

🚨 _The essential change:_
- `hx-vals='js:{"color": event.target.value}'`: The `hx-vals` attribute allows you to add to the parameters that will be submitted with an AJAX request. It should be noted that the parameter we want to pass is dynamic since it corresponds to the `value` that the user selected through `<input type="color">`.  The prefix `js:` allows us to _evaluate_ this value but we must consider security issues, especially [Cross-Site Scripting (XSS)](https://owasp.org/www-community/attacks/xss/) vulnerability.

##🪄 A few touch ups.

> _The devil is in the detail_; If we want to achieve a decent UI/UX small features like these cannot be missing:

**1. Swap Transitions**
```erb
<div id="note_<%=@id%>" class="fade-note-out">
  <%= button_tag(data: hx(confirm: "Are you sure you wish to delete this note?", delete: note_path(@id), swap: "delete swap:50ms", target: "#note_#{@id}")) %>
</div>
```

```css
.fade-note-out.htmx-swapping {
  opacity: 0;
  transition: opacity 50ms ease-out;
}
```
Do you remember the _Modifiers_? we added `swap: "delete swap:50ms"` to _fade out_ `"#note_#{@id}"` after the `DELETE` request ends, and then we take advantage of the `.htmx-swapping` built-in class plus some CSS for the transition we need: `opacity: 0;`.

**2. Keyboard Shortcuts**
Here the button that we use to show the modal we use to create/edit notes:
```erb
<%= button_tag("New Note", type: "button", data: {"hx-trigger": "click, keyup[ctrlKey&&key=='M'] from:body", "modal-target": "note-modal", "modal-toggle": "note-modal", "hx-get": new_note_path, "hx-target": "#modal-content", "hx-swap": "innerHTML"}) %>
```
It's necessary overwrite the _click_ event and then we specify the keyup event when Ctrl+M is pressed. The `from:` modifier is used to listen for the keyup event on the _body_ element. It's a pretty basic TailwindCSS modal, but I want to emphasize again how powerful the modifiers are.

**3. Indicators**

```erb
<div class="htmx-indicator">
  <!-- Some pretty .svg or .gif -->
</div>
```
`.htmx-indicator` is a HTMX built-in class that helps us to show spinners or progress indicators while the request is in flight. For our Toy Application, a good place to put it on is `_form.html.erb`, since users may attach images to the note.

##🌠Getting wild with Real-Time

> I hope I haven't excited you too much with such a pompous title. Actually it's just a classic search in Real-Time, however I must say that in my opinion this feature in particular is much easier to implement than with Hotwire.

```erb
<%= search_field_tag("search", "", data: hx(swap: "outerHTML", trigger: "input changed delay:500ms, search", target: "#notes-grid", post: search_notes_path )) %>
```
Easy-peasy, All thanks to _Modifiers_ wich allows _Triggers_ to change its behavior. In this case for the `changed` event we addded the `delay:500ms` modifier in order to delay sending the query until the user stops typing. That's all it took.

Since we use a _search type_ input we will get an `x` in the input field to clear the input. We want to trigger a new POST so we add another trigger comma separated. Once again, that simple.

##🧰 Resources

- [Hotwire / Turbo ➡️ htmx Migration Guide](https://htmx.org/migration-guide-hotwire-turbo/)
- [Official Website](https://htmx.org/)
- [Discord Server](https://htmx.org/discord)
- [VS Code Extension](https://github.com/otovo/htmx-tags)
- [Subreddit](https://www.reddit.com/r/htmx/)
- [HTMX vs. Hotwire discussions](https://discuss.hotwired.dev/search?q=htmx)

## 🔮Final thoughts
Check this out:

{% codepen https://codepen.io/matedemorphy/pen/eYXwZRv %}
As is say before, HTMX is THE trend, and It makes the frontend more accessible for Backend developers, i like it, still I must point out some limitations that I found during the development of the toy application:

**- Capabilities limitations:** Almost all _attributes_ and _extensions_ have them, eventually, you will need some JavaScript.
**- Steep Learning curve:** <u>WHEN COMPARED TO HOTWIRE/TURBO.</u>
**- Doesn't suit into the _Rails Way_:** Of course it is not expected to be, but it's a disadvantage compared to Turbo wich, although it's a language-agnostic framework, its integration with Ruby On Rails is natural.
**- Early days:** It's understood that nothing is born being a superstar just like that, but it has to be said, while everyone is talking about HTMX these days, it still has a small community which makes it difficult to find solutions for problems.

It surpasses me to say whether HTMX is the future or not, what I can and want to do is encourage you to become familiar with it; We must have something clear, HTMX it's an alternative to Hotwire.

_Thanks for reading_ 😊.