Learning AJAX made easy - Build an app with htmx
Tags: beginners, ajax, html, ruby
Published: 2020-06-08 10:34:02

I recently stumbled upon htmx. It's a javascript library allows you to run ajax requests, server side events and websockets directly from your html. No extra JavaScript needed, clean and to the point. Why do I think it's one of the coolest things I've seen lately?

For one, at the moment most of my dev work involves building small apps, prototypes and automating processes. I love anything that reduces the amount of work on the front end.

Beyond that, I take care of running a coding bootcamp in Singapore. My students, who arrive with 0 web development knowledge, often struggle with understanding AJAX. Any tool that eases the learning process is more than welcome.

Thus, I decided to write a get-started tutorial about htmx.     

## About htmx

htmx is the heir to intercooler.js. It uses html attributes to trigger AJAX calls. You can use the attributes to target an element in the dom, and replace it with the html that comes back in your response. The library supports websockets and server-side events too. 

It's beautiful simplicity. Basically you can write html like this

```html
<form class="nes-select is-dark">
	<!-- have the selection form send a value via AJAX -->
      <select hx-get="/fighters"
				      name="fighter"
				      hx-target="#fighter-avatar"
				      hx-swap="outerHTML">
	      <option selected> -- select a fighter -- </option>
	      <option value="ryu">Ryu</option>
	      <option value="sagat">Sagat</option>
	      <option value="chunli"> Chun-Li </option>
    </select>
  </form>
```

And start building something like this. No page reloads,  no additional JS. 

![streetfigtherdemo](https://res.cloudinary.com/dhodayze1/image/upload/v1591611478/htmx%20article/demo.gif)

---

htmx does not bring the sophistication of React or Vue, and it doesn't have to. The library excels at reducing the amount of moving pieces when implementing common UI patterns. There's excellent [examples](https://htmx.org/examples) in their documentation. 

## Wait, what's AJAX?

AJAX stands for Asynchronous JavaScript and XML. It's a technique for creating dynamic webpages.  The client and the server can exchange data in the background, and the browser's javascript can parse  what the server sends to update the website without page reloads.

![ajax-explained](https://res.cloudinary.com/dhodayze1/image/upload/v1591611677/htmx%20article/ajax.gif)

Features like infinite page scrolls or immediate search suggestions are powered by AJAX. The cool thing about htmx is that we can implement these features writing any javascript ourselves. 

## A to-do list for Ryu

Here's a tutorial on how to build a yet-another-crud-to-do app with htmx. Sinatra will work as our backend and nes.css will help us with styling. By the end of the walkthrough, we'll end up with something like this. 

![complete](https://res.cloudinary.com/dhodayze1/image/upload/v1591611489/htmx%20article/complete.gif)

If you are familiar with the stack and want to jump directly to the htmx code examples, skip to the second feature right away or [get the code](https://github.com/libsyz/htmx-to-do-app). Take it from here if you want to build it step by step. 

# Setup

### 1. Install Sinatra

Sinatra is a lightweight web application framework written in Ruby that wraps all the goodness of  Rack middleware into a convenient DSL. It's super handy for building small apps and APIs. Assuming you have Ruby and Bundler installed, start a new project folder and type the following in your terminal.

```bash
bundle init
```

open your Gemfile and add:

```bash
gem "sinatra"
```

and install the gem with:

```bash
bundle install
```

Now, let's create an `app.rb` file where we will keep the app and routing logic, and a folder named public where we will keep our css ( Sinatra uses this folder to serve assets by default ) 

```bash
# in your project folder 

touch app.rb && mkdir public
```

In the app file, we'll require sinatra and write our first route

```ruby
# app.rb

require 'sinatra'

get '/' do
	"Welcome to your app!"
end
```

Now run:

```ruby
ruby app.rb
```

That will run the app on port 4567 in your localhost. Go to your favorite browser and type `http://localhost:4567`. You should see something like this.

![welcome](https://res.cloudinary.com/dhodayze1/image/upload/v1591611870/htmx%20article/welcome.png)

### 2. Install  htmx

For now, we are going to import htmx directly in our main page. First, let's create  a folder named views (sinatra uses `./views` as a default for rendering templates) and touch a file called `index.erb` inside

```bash
mkdir views
touch views/index.erb
```

erb (embedded Ruby) allows us to write Ruby into our html. Our `index.erb` file work as a template where we will inject other html dynamically.

Within you index file, start an html skeleton and add the htmx llibrary in the document head. 

```html
<!-- views/index.erb --> 

<html>
	<head>
	  <meta charset="UTF-8">
	  <title>htmx + sinatra ftw</title>
		<script src="https://unpkg.com/htmx.org@0.0.4"></script>
	</head>
	<body>
		
	</body>
</html>
```

### 3. Set up the Front End

We'll use nes.css, an 8-bit themed library to style our app. Add it on the head of your index.erb file. Also, create a style.css file in your public folder. 

```html
touch public/style.css
```

```html
<!-- views/index.erb --> 

<html>
	<head>
	  <meta charset="UTF-8">
	  <title>htmx + sinatra ftw</title>
		<script src="https://unpkg.com/htmx.org@0.0.4"></script>
		<link href="https://unpkg.com/nes.css@latest/css/nes.min.css" rel="stylesheet">		
		<link rel="stylesheet" href="./style.css">
	</head>
	
	</body>
	</body>
</div>
~~<~~/html>
```

The tutorial is focused on the interaction of between htmx and sinatra, so  I won't be covering the css in depth. Copy the code on this [gist](https://gist.github.com/libsyz/5587f710e567aff54196bd7cf4ab8701) and paste it on your `style.css` file. On your html, make sure you are importing the `style.css` after the nes.css library. You can go [here](https://nostalgic-css.github.io/NES.css/) to have an overview of how the css components work. 

We're all set up, let's go into building the app!

# Spec 1: Displaying Tasks

## 1. Create a Task Model

We need to represent the notion of a task. Let's keep it really simple:

- A task has a description
- A task can be done or not.
- A new task should not be done by default.
- We can mark a task as done
- We can check if it's done or not.

In a file called `task.rb` we'll create a `Task` class that benefits from ruby Structs to get the job done

```bash
touch task.rb
```

```ruby
# task.rb

class Task < Struct.new(:description, :done)
    def initialize(description, done = false)
      super
    end

    def mark_as_done!
      self.done = true
    end

    def done?
      done
    end
end
```

## 2. Create a Task Repository

There needs to be a place where we keep our tasks and manage them. The task repository will do exactly that. First, create a `task_repo.rb` file. 

For now, we'll start off the class with a couple of tasks already so we can show them on the index page. We will also set an `#all` method that allows us to access the tasks. A `#seed` method called on initialization will set a couple of tasks in the list for us already.  

```bash
touch task_repo.rb
```

```ruby
# task_repo.rb

require_relative 'task'

class TaskRepo
	def initialize
    @tasks = []
    seed
  end

  def all
    @tasks
  end

  private

  def seed
    @tasks << Task.new("Meet Ken") << Task.new("Practice my hadouken")
  end
end
```

Now, we have two jobs to do. First, getting access to the repository that we just created, and then passing the tasks over to a template. Go to the `app.rb` file and add the following before your first get route:

```ruby
#app.rb

require_relative 'task_repo'

class Sinatra::Application
	def initialize
	  @tasks = TaskRepo.new
	  super
	end
end

get '/' do
```

This little trick will make a new instance of our repository available across the Sinatra App, keeping our tasks alive as long as the server runs. 

## 4. Create the tasks list

Add the following lines to your app.rb file. They will make the get route to 'localhost:4567' render the tasks file into index.erb file we wrote before.

```ruby
#app.rb

#...

get '/' do
	erb :tasks, layout: :index
end

```

Now, let's flesh out the view. We need to iterate through the tasks on the repo, and display their descriptions.  Create a tasks.erb file in your views folder and add the following:

```bash
touch views/tasks.erb
```

```html
<!-- views/tasks.erb --> 
	
<ul class="nes-list is-disc">
  <% @tasks.all.each do |task| %>
    <li> </p> <%= task.description %> </p>
  <% end %>
</ul>

```

The `<% %>` tags that you see are the syntax that allow you to use ruby code within html. The subtle differences are: 

- Whenever we wrap code in `<% %>`, we execute it as ruby code, but we don't render it on the page
- When we use `<%= %>`, we render the ruby code that sits inside the tags.

So with the snippet above we are iterating through the tasks, and rendering each of the tasks  within an unordered list. 

Now we, can adjust the `index.erb` layout so the tasks show up.

```html
<!-- views/index.erb -->

<body> 
	<div class="nes-container main">
    <div class="list">
      <div class="list-title">
        <h2> Ryu's to-do list </h2>
        <img class="ryu" src="./ryu_8_bit.png" alt="ryu">
				<!-- you might have to source your own ryu image --> 
      </div>
      <%= yield %>
    </div>
</body> 
```

The keyword `yield` is what Ruby uses to call blocks that have been passed to a method. In this context, yield will call the template that we are choosing on the get '/' route and render it. 

When you run your server with `ruby app.rb` on your terminal, you should be be able to see the tasks.  

![tasks](https://res.cloudinary.com/dhodayze1/image/upload/v1591611458/htmx%20article/validation.gif)

# Spec 2: Adding Tasks

For now it's all been static. Let's spice things up with htmx.

We can start by creating a form where we can write tasks and a button that allows us to submit them. Add the following after to your `.list` element in `index.erb`, but still within `.nes-container`

  

```html
<!-- views/index.erb --> 

<div class="nes-container main">
	<div class="list">
	....
 </div>

	<form class="nes-field">
	      <input class="nes-input" id="task-form" name="task" type="text" placeholder="new task"/>
	
	**      <button hx-post="/tasks"
	              hx-include="input[name='task']"
	              hx-target=".nes-list"
	              hx-swap="outerHTML"
	              class="nes-btn is-primary">
		      Add Task
	      </button>
	 </form>

</div>
```

Now, check out all the `hx-` attributes on the button. Whenever click the button is clicked, the following will happen: 

- Send a post request to `/tasks`
- In the parameters of the post request, include the value in the input with the name 'task'. ( You can pass any css selector into `hx-include` )
- Inject the response into the element with the class `.nes-list`, which is in our tasks template.
- `hx-swap="outerHTML"` indicates that the whole element should be replaced

And all that interactivity beautifully expressed in 4 lines of markup. There's no need to declare the click event, since requests are triggered by the "natural" event of an element.

Now, if you try that out, you will get a 404 error. We need Sinatra to handle the request and send html back. Go to `app.rb` and add the following.

```ruby
#app.rb

post '/tasks' do
	@tasks.all << Task.new(params['task'])
	erb :tasks
end
```

This will refresh our tasks with the new addition. The good news is that now we can add new tasks via AJAX. The bad news is that we can also add empty tasks, and our form stays filled after submitting the task.

![add-to-do](https://res.cloudinary.com/dhodayze1/image/upload/v1591611473/htmx%20article/add-to-do.gif)

# Spec 3: Form Cleaning and Simple Validation

Time to get something more proper. First, we need to make sure the user actually sent a task. Change your post route for the following. 

```ruby
#app.rb

post '/tasks' do
  if params['task'].length.positive?
    @clear_form = true
    @tasks.all << Task.new(params['task'])
  else
    @error = "please enter a task"
  end
    erb :tasks
end
```

So, if we have a task in the params, we will set a flag to clear the form in the view, and we'll create a new task. Otherwise, we'll pass an error message to the view. Let's replace the markup in our `view/tasks.erb` file for the following.

```html
<!-- views/tasks.erb --> 

<% if @clear_form  %>
  <input class="nes-input form-error" id="task-form" hx-swap-oob="true" name="task" type="text" placeholder="new task">
<% end %>

<ul class="nes-list is-disc">
  <% @tasks.all.each do |task| %>
	  <li>  <p class="nes-text"> <%= task.description %> </p> </li>
  <% end %>
 </ul>
```

The `hx-swap-oob` attribute means out-of-box swap. It allows you to do html swaps outside of the target we declared on our form, which was `.nes-list`. It's important that you don't nest elements with `hx-swap-oob` attributes if you want them to work. 

It will target the element with the id `#task-form` and replace it, while the rest of the html will be replacing the old `.nes-list` element. We now get an updated list of tasks plus a fresh form.

Now, the unhappy path. Inside your `.nes-list` element, add the following: 

```html
<!-- views/tasks.erb --> 

<ul class="nes-list is-disc">
	<% if @error %>
	  <p class="nes-text is-error"> <%= @error %> </p>
	<% end %>

	....
</ul>
```

This should render the same to do list with a validation error message.  The app now should be in this state.

![validation](https://res.cloudinary.com/dhodayze1/image/upload/v1591611458/htmx%20article/validation.gif)

# Spec 4: Marking a Task as Done

To check off tasks, we need some way of interacting with our task list. What about inserting a span that, when clicked, sends a request for marking a given task as done? With htmx we can do something like this. 

```html
<!-- heads up, this won't work in our app --> 

<ul class="nes-list">
<% @tasks.all.each_with_index do |task, index| %>
  <li> <%= task.description %>  
		<span hx-patch=<%= "task/#{index}" %> mark as done </span>
  </li>
	<% end %>
</ul>
```

Unfortunately, our Sinatra backend will struggle with parsing this request. It will not recognize the `patch` value natively, since many browsers only support `get` and `post`. 

We need a workaround to override the `post` method -  We can include  `_method`as a key in the params and set `"patch"` as the value, which Sinatra will recognise. Frameworks like Rails also use this technique.

Thus, the view could go as follows: 

- When the task is not done, give the user the abilty to mark it as done.
- When the task is done, let the user know with a visual cue

Let's add the following into our iterator:  

```html
<!-- views/tasks.erb --> 

<ul class="nes-list">
	 ....

	<% @tasks.all.each_with_index do |task, index| %>
		<% if task.done? %>
		  <li>
		    <p class="nes-text is-disabled"> <%= task.description %> </p>
		  </li>
	  <% else %>
		  <li>
		    <p class="nes-text"> <%= task.description %>
		      <span class="nes-text is-primary"
				        hx-post=<%= "tasks/#{index}"%>
				        hx-target=".nes-list"
				        hx-swap="outerHTML"
				        hx-include=<%="[name=_method][data-mark='#{index}']"%>> done
		      </span>
		      <input type="hidden" name="_method" value="patch" data-mark=<%="#{index}" %>>
		    </p>
		  </li>
	  <% end %>
	<% end %>
</ul>
```

By doing so, now Sinatra can recognise the patch action, and assign it to the right to-do via the index on the array. We'll use the task id as the reference to know which task is the one to check.

```ruby
patch '/tasks/:id' do
  @tasks.all[params[:id].to_i].mark_as_done!
  erb :tasks
end
```

By now, we can mark tasks as done.

![mark-as-done](https://res.cloudinary.com/dhodayze1/image/upload/v1591611493/htmx%20article/mark_as_done.gif)

# Spec 5: Deleting a Task

Finally, let's close the tour on htmx with deleting. We'll use the same approach we had on marking tasks as done. In the same vein, we need to include an element that affords the delete action. We'll add a small 'X' icon next to the tasks that have been done to allow deletion. 

```html
<!-- views/tasks.erb --> 

<ul class="nes-list is-disc">

....

	<% @tasks.all.each_with_index do |task, index| %>
		<% if task.done? %>
	  <li>
	    <p class="nes-text is-disabled"> <%= task.description %>
	      <i hx-post=<%= "tasks/#{index}"%>
	            hx-target=".nes-list"
	            hx-swap="outerHTML"
	            class="nes-icon close is-small"
	            hx-include=<%= "[name=_method][data-delete='#{index}']" %>>
	      </i>
	    </p>
	      <input type="hidden" name="_method" value="delete" data-delete=<%= "#{index}" %>>
	  </li>
	  <% else %>
			<!-- view logic for marking task as done -->
			....
	  <% end %>
	<% end %>
</ul>
```

And on Sinatra, you need a route for deleting

```ruby
delete '/tasks/:id' do
  @tasks.all.delete_at(params[:id].to_i)
  erb :tasks
end
```

Now, if the task has not been done, we can mark it as done. If the task is done, we can delete it from the list. Ryu's to-do app should now look like this

![complete](https://res.cloudinary.com/dhodayze1/image/upload/v1591611489/htmx%20article/complete.gif)

# Thanks for reading!

htmx allows you to implement modern UI patterns with simple, straightforward html.  I truly hope that the library continues to evolve and grow. Take a look at their documentation over [here](https://htmx.org/). If you feel like playing around with the app, you can get the code [here](https://github.com/libsyz/htmx-to-do-app). A few things you could try out with htmx to take the app further:

- Add a button to delete all tasks
- Make a better validation, now you could add a task made of spaces!
- Disable the `.task-form` button by default, and only enable it when there's text on the input
- Fade in the tasks when they show on screen

---

***I’m Miguel and I take care of Product and Growth for [Le Wagon](https://www.lewagon.com/singapore) in Singapore 🇸🇬. Our immersive coding bootcamps build  the tech skills and product mindset you need to kick-start your tech career, upskill in your current job, or launch your own startup.***

***If you want to exchange thoughts on how to create outstanding learning experiences in tech feel free to contact me at miguel.jimenez@lewagon.org.***