htmx

What is HTMX? Speed Up Web Development with HTML

Introduce

HTMX is a JavaScript library that empowers HTML to create dynamic web applications. By adding simple HTML attributes, you can unlock powerful features like AJAX requests, smooth CSS transitions, WebSocket connections, and Server-Sent Events handling, all directly within your HTML markup.

Carson Gross conceived HTMX in 2013, driven by the need to surpass the constraints of traditional HTML for dynamic web development. Standard HTML restricts HTTP requests to <a> and <form> tags, triggered solely by clicks and submissions, limiting developers to GET and POST methods. Moreover, these interactions result in full-page refreshes, disrupting user flow.

This blog explores how HTMX extends HTML’s capabilities, liberating it from these limitations to empower developers in building highly interactive and efficient web applications.

Using HTMX

You can easily integrate htmx into your project by adding a script tag within the <head> of your HTML document:

<head>
  <script src="//unpkg.com/htmx.org@1.9.5"></script>
</head>

HTMX boasts several advantages:

  • Small Size: It’s lightweight (~14kb min.gz’d).
  • No Dependencies: You don’t need other libraries.
  • Extensible: Easily add custom features.
  • IE11 Compatible: Works even with older browsers.
  • Reduced Code: htmx often requires significantly less code compared to frameworks like React.

Let’s explore how to make a simple AJAX request using htmx.

Making AJAX Requests with HTMX (GET)

So, let’s begin by creating a few endpoints for this example.

func getRoot(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "./static/index.HTML")
}

func hi(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "<div>hi</div>")
}
func main() {
    http.HandleFunc("GET /", getRoot)
    http.HandleFunc("GET /hello", hi)
    http.ListenAndServe(":3333", nil)
}

In the above code example, we made two endpoints: / to serve the HTML file and /hello will return an HTML element. htmx gets the body of the response and renders it on the page, therefore the response of our function should be HTML code.

Now we will see how to use the hello endpoint in HTML:

<div hx-post="/hello">hello</div>

As seen above, the endpoint is working. The content of the div is replaced by the response.
Because we did not define any specific event to trigger the AJAX request, it will be triggered by clicking the hello text.

We can explicitly define it like this:

<div hx-get="/hello" hx-trigger="click">hello</div>

htmx has other predefined events like this:

  • hx-trigger="mouseenter" is triggered every time that the mouse enters the element
  • hx-trigger="mouseenter once" is triggered when the mouse enters the element for the first time and won’t trigger after that
  • hx-trigger="click[ctrlKey]" is triggered when the element is clicked while holding the Ctrl key
  • hx-trigger="every 2s" is triggered every two seconds

You can find more on this subject in the htmx docs.

Currently, our example above replaces the element that makes the AJAX request, but we can specify which element is going to get replaced using the hx-target attribute:

<div id="replace" class="replace">replace me</div>
<div hx-get="/hello" hx-trigger="click" hx-target="#replace">click me to replace the element above</div>

We can also use a class name like .replace or give elements like body as the target.

In the following example we have two foo elements:

<foo id="1" class="replace">replace me 1</foo>
<foo id="2" class="replace">replace me 2</foo>
<bar hx-post="/hello" hx-trigger="click" hx-target="foo">replace foo element</bar>

The first of the foo elements will be taken as the target. So in this example the first foo element with id=1 will be replaced by the response.

Making a todo app

Now we have learned the basics, let’s write a todo app.

First start with making a submit form for adding a todo task.

For that, we will only need an input box and a button in our HTML code:

<div id="app" class="container">
  <div id="todo-list"></div>
  <form
    id="myForm"
    hx-post="/save"
    hx-trigger="submit"
    hx-target="#todo-list"
  >
    <input type="text" name="task" required />
    <input type="submit" value="save" />
  </form>
</div>

Now let’s have a look at what is needed on the server side:

func save(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()
    insert(r.FormValue("task")) // save to database
    taskList := "<div>" + r.FormValue("task") + " </div>"
    io.WriteString(w, taskList)
}

func main() {
    ...
    http.HandleFunc("/save", save)
    ...
}

In the HTML above, the form will send a post request to the /save endpoint and htmx will replace the content in the div with the id todo-list. The response will replace everything in the target element i.e., the element with id todo-list will be replaced and looks like this:

<div id="todo-list">
  <div>item 1</div>
</div>

After adding another todo item to the list, the HTML document looks like this:

<div id="todo-list">
  <div>item 2</div>
</div>

That is not quite what we want. We want to append the new todo item after the last todo item. So we use the attribute hx-swap to let htmx know where we want the response to render. The beforeend value will append the HTML response before the end of the targeted element.

After the swap the code will look like this:

<div id="app" class="container">
  <div id="todo-list"></div>
  <form
    id="myForm"
    hx-post="/save"
    hx-trigger="submit"
    hx-target="#todo-list"
    hx-swap="beforeend"
  >
    broadcast<input type="text" name="task" required />
    <input type="submit" value="save" />
  </form>
</div>

Adding the first item (item 1):

<div id="todo-list">
  <div>item 1</div>
</div>

After adding the second item (item 2):

<div id="todo-list">
  <div>item 1</div>
  <div>item 2</div>
</div>

Adding multiple todo items is working now, but on page load (e.g. if we refresh the page) the list of tasks is not being loaded.
To make that work, we need to make a GET request.

<div id="todo-list" hx-get="/taskList" hx-trigger="load"></div>

In the above HTML, I have added hx-get with value ‘/tasklist’ and hx-trigger with value ‘load’. The ‘load’ value specifies when the request is sent. In this case it’s when the HTML file is loaded.

func listTask(w http.ResponseWriter, r *http.Request) {
    tastList := getTODO() // gets data from the database and returns HTML
    io.WriteString(w, tastList)
}
func main() {
    ...
    http.HandleFunc("/save", listTask)
    ...
}

Now the list is shown even when the page is refreshed.

For edit let’s try something different: instead of doing all our operations in a single page, we will be navigating to another page to do the edit.

First, let’s make clicking on the todo item go to a different page that will contain a form for updating the todo item list.
For that, we need to get an anchor tag instead of a div with the todo text inside a div.

On the server side, I will get the task and return the proper form for updating that task.

func main() {
    ...
    http.HandleFunc("/task/", individualTask)
    ...
}

Example response from server:

<div hx-boost="true">
      <a href="/task/1">clean room</a>
</div>

Here the hx-boost attribute will replace the body element and add the previous page to the history entry.
That means the back button on browsers will work perfectly.

For the actual editing, it will be similar to the other examples.

<div>
  <div>clean room</div>
  <form id="myForm2" hx-put="/edit" hx-target="#app">
    <input type="text" name="task" required="" />
    <input type="hidden" name="old" value="1" />
    <input type="submit" value="edit" />
  </form>
</div>

Now let’s perform deletion on the list.

First, we have to make a delete button.

For that, I will update the list template and add a delete form:

<div hx-boost="true" class="new-element">
  <a href="/task/1">clean room</a>
  <form
    hx-trigger="click"
    hx-delete="/delete"
    hx-target="closest .new-element"
    hx-swap="outerHTML swap:1s"
  >
    <input type="hidden" name="task" value="1" />
    delete
  </form>
</div>

Most of the snippets in the delete form are similar to the other examples. But in the hx-swap="outerHTML swap:1s" attribute, I added an extra value. This value will delay the swapping of the element by 1 second because I want to do a CSS animation during that time.

While htmx is doing the swapping of the element, it also adds the htmx-swapping class to the element.

Now I can add any CSS effect I want to the htmx-swapping class.

.htmx-swapping {
  opacity: 0;
  transition: opacity 1s ease-out;
}

WebSockets

Currently, if we add a todo task to our list, it won’t be shown in another tab or browser opening the same page. This can be achieved using WebSockets. Fortunately htmx supports WebSockets.

<div id="ws" hx-ws="connect:/ws" hx-trigger="task"></div>

The hx-ws attribute takes the WebSockets endpoint and makes a connection to it.

After adding this line to the code, I just have to create a WebSocket that listens for input by the user and broadcasts it to all clients.

import(
    ...
    "golang.org/x/net/websocket"
    "sync"
    ...
)

func main(){
    ...
    server := NewServer()
    http.Handle("/ws", WebSockets.Handler(server.WebSocketsHandler)) // handles the saving and broadcasting of data
    ...
}

Update the form:

<form
  id="ws-form"
  hx-ws="send:submit"
  hx-target="#todo-list"
  hx-swap="beforeend"
>
  <input type="text" name="task" required />
  <input type="submit" value="save ws" />
</form>

In the hx-ws attribute of the form we are telling htmx to send the values to the WebSockets endpoint on the submit event.

func (s *Server) WebSocketsHandler(ws *WebSockets.Conn) {
    broadcast() // broadcast the response
}
func main(){
    ...
    http.Handle("/ws", WebSockets.Handler(server.WebSocketsHandler))
    ...
}

Now we can see our list getting updated everywhere.

These were some basics of htmx to get started with. Now you can build on it and make something more interesting by yourself.

And if you want to learn more about htmx, have a look at the docs here.

Source: Dev.to

Shortlink: /FnuAUvG9

Leave a Reply