DEV Community

Jeremy Woertink
Jeremy Woertink

Posted on • Edited on

Getting Lucky with HTMX

A quick intro to HTMX

HTMX 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 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:

<script src="https://unpkg.com/htmx.org@1.3.3"></script>
Enter fullscreen mode Exit fullscreen mode

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/

<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> 
Enter fullscreen mode Exit fullscreen mode

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 is a full-stack framework written in the Crystal 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.

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
Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

Adding Report

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

> lucky gen.model Report
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

class Report < BaseModel
  table do
    column count : Int32
    belongs_to user : User
  end
end
Enter fullscreen mode Exit fullscreen mode

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

table do
  column email : String
  column encrypted_password : String

  # Add this line
  has_many reports : Report
end
Enter fullscreen mode Exit fullscreen mode

Migrate it the DB

> lucky db.migrate
Enter fullscreen mode Exit fullscreen mode

Setting up views

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

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

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
Enter fullscreen mode Exit fullscreen mode

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

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

  def render
    div do
      reports.each do |rep|
        para rep.count
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

    component Reports::List, reports: reports
  end
end
Enter fullscreen mode Exit fullscreen mode

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
  2. Visit the app in browser http://localhost:3000
  3. Click the "go to app" button
  4. Create a new account and sign in
  5. 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);
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

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

  def content
    mount Reports::List, reports: reports    
  end
end
Enter fullscreen mode Exit fullscreen mode

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)

div hx_get: Stats::Index.path, hx_trigger: "every 10s" do
  mount Reports::List, reports: reports
end
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (1)

Collapse
 
megatux profile image
Cristian Molina

Beautiful! I have to start playing with Crystal, Lucky and HTMX <3