Getting Lucky with HTMX
Tags: lucky, crystal, webdev, htmx
Published: 2023-05-06 16:11:06

---
title: Getting Lucky with HTMX
published: true
description: Use HTMX with Lucky to get an interactive feeling site with little to no javascript and 100% typesafety
tags: lucky, crystal, webdev, htmx
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-05-05 22:32 +0000
---

## A quick intro to HTMX

[HTMX](https://htmx.org/) is a small, dependency-less javascript library that sprinkles some fancy bindings on to your normal markup without you needing to write javascript. It gives you the ability to have things like asynchronous data fetching and dynamic content updates.

I came across this post by [Chris James](https://quii.dev/HTMX_is_the_Future) that sounded right up my alley.

I was following right along, and things were sounding great, but then I read this line

> I did not write any JavaScript.

and I was sold!

For me, many of my applications just need minimal javascript for things like search autocomplete, toggling the view of something, or just delay loading hefty content.

I decided to try this out in a Lucky app to see how quick can I get _something_ to show... (hint: it was really quick)

### Example code

To get started, you just need to include their package:

```html
<script src="https://unpkg.com/htmx.org@1.3.3"></script>
```

Then somewhere on your page, you just use HTML attribute designated by their API to augment your markup.

This example taken from the docs site https://htmx.org/examples/click-to-load/

```html
<tr id="replaceMe">
  <td colspan="3">
    <button class='btn' hx-get="/contacts/?page=2" 
                        hx-target="#replaceMe" 
                        hx-swap="outerHTML">
         Load More Agents... <img class="htmx-indicator" src="/img/bars.svg">
    </button>
  </td>
</tr> 
```

This will call back to your server hitting the `/contacts/?page=2` path which will return some HTML. Then it takes that markup and replaces the `<tr id="replaceMe">` with the content that was fetched.

## Using Lucky for the server

[Lucky](https://luckyframework.org/) is a full-stack framework written in the [Crystal](https://crystal-lang.org/) programming language. One of the neat benefits of using Lucky with Crystal is the typesafety you get.

Lucky doesn't use standard HTML, but instead uses Crystal methods that generate HTML. This ensures that your markup is always rendered properly. No misspelling a tag name, or forgetting to close a tag. No passing in the wrong type and accidentally rendering something that looks like `[object Object]`, or worse, rendering an empty string to the page.

### Example code

This is an example of a simple HTML page in Lucky.

```crystal
class Products::ShowPage < MainLayout
  needs product : Product
 
  def content
    # recognizable HTML tags
    div class: "flex flex-col p-4" do
      h1 product.title, class: "text-xl"
      # use of sharable components
      mount Products::Images, product: product
      # built-in methods for displaying text
      simple_format(product.description)
    end
  end
end
```

## Lucky meets HTMX

Getting these two things together doesn't take too long. I will do a quick demonstration of an app where a user logs in, and on their "dashboard", some numbers are displayed from a timer. You can imagine that these numbers would be fed in to a chart or report that could update without the need of refreshing the browser.

> At this point, if you're not familiar with Crystal or Lucky, just do your best to follow along. I'm skipping the setup/install process for brevity.

Check out the [Lucky tutorial](https://luckyframework.org/guides/tutorial/overview) for later.

### Generating the app

From the CLI, we will bootstrap the app.

```
> lucky init
Project name?: htmx_demo
API only or full support for HTML and Webpack? (api/full): full
Generate authentication? (y/n): y
> cd htmx_demo
> ./script/setup
```

### Adding Report

This is a `Report` model that just has a `count` property. It'll be associated to a user.

```
> lucky gen.model Report
```

Next we will edit that new migration in `db/migrations/` to add our new fields.

```crystal
def migrate
  create table_for(Report) do
    primary_key id : Int64
    add_timestamps
    add count : Int32
    add_belongs_to user : User, on_delete: :cascade
  end
end
```

Now update the model in `src/models/report.cr`

```crystal
class Report < BaseModel
  table do
    column count : Int32
    belongs_to user : User
  end
end
```

And lastly, update the `User` model in `src/models/user.cr`

```crystal
table do
  column email : String
  column encrypted_password : String

  # Add this line
  has_many reports : Report
end
```

Migrate it the DB

```
> lucky db.migrate
```

### Setting up views

First we will add HTMX to our layout in `src/components/shared/layout_head.cr`

```crystal
# add this line to the `head` block
script src: "https://unpkg.com/htmx.org@1.3.3"
```

For the next part, we will be setting up the user's "dashboard" to render this "report". We will first generate a new component to use for this report.

```
> lucky gen.component Reports::List
```

Then update that component in `src/components/reports/list.cr`

```crystal
class Reports::List < BaseComponent
  needs reports : Array(Report)

  def render
    div do
      reports.each do |rep|
        para rep.count
      end
    end
  end
end
```

Now we can update the user's dashboard on `src/pages/me/show_page.cr`

```crystal
def content
  h1 "This is your profile"
  h3 "Email:  #{@current_user.email}"

  # the actual HTMX magic
  div hx_get: Stats::Index.path, hx_trigger: "every 10s"
end
```

Here we create a normal div that tells HTMX to do `HTTP GET` to the `Stats::Index.path` (which doesn't exist yet), and we tell it to do that "every 10s".


### Setting up the actions

Now we need to define that `Stats::Index` so it knows where to fetch the data.

```
> lucky gen.action.browser Stats::Index
```

We can update that action in `src/actions/stats/index.cr`

```crystal
class Stats::Index < BrowserAction
  get "/stats" do
    reports = ReportQuery.new.user_id(current_user.id)

    component Reports::List, reports: reports
  end
end
```

### Wrap it up

That's actually all it takes for the MVP just to see something in action. We can now boot up the app, create an account, and check it out.

1. Boot the app `lucky dev`
1. Visit the app in browser `http://localhost:3000`
1. Click the "go to app" button
1. Create a new account and sign in
1. See that there's nothing to display

While you're on that page, go back to your terminal, and open a new terminal tab/window. We can jump in to the sql console and add some records manually to watch  them show up on the page!

```
> lucky db.console

# insert into reports (user_id, count) values (1, 1);
# insert into reports (user_id, count) values (1, 9);
# insert into reports (user_id, count) values (1, 4);
```

I think you get the idea :D 

## Recap

Every 10 seconds, a call will be made back to the `Stats::Index` action which will return the `Reports::List` component markup. We do have an issue here where if someone tries to visit `/stats`, they'll get some broken markup. We can fix that by detecting HTMX calls.

```crystal
get "/stats" do
  reports = ReportQuery.new.user_id(current_user.id)

  if request.headers["HX-Request"]? == "true"
    component Reports::List, reports: reports
   else
    html Stats::IndexPage, reports: reports
  end
end
```

Then we'd create that `Stats::IndexPage` to render the component

```crystal
# src/pages/stats/index_page.cr
class Stats::IndexPage < MainLayout
  needs reports : Array(Report)

  def content
    mount Reports::List, reports: reports    
  end
end
```

Next, you'll notice that when the `/me` page first loads, there's no data... It takes 10 seconds before we see something. We can make some default data by telling the `Me::ShowPage` that it `needs reports : Array(Report)`

```crystal
div hx_get: Stats::Index.path, hx_trigger: "every 10s" do
  mount Reports::List, reports: reports
end
```

There's still a lot left to explore with this, and obviously HTMX isn't going to cover every case when it comes to building an interactive site, but if you don't need a ton of interactivity, this is a beautiful solution!