DEV Community

Marcus
Marcus

Posted on

Handle JSON API Results in Htmx

Htmx is a javascript library that "allows you to access AJAX, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext."

In a former post I thought it was fun to compare Alpine.js to Vue.js and showed how similar their approaches are.

The promise of these libraries is: you keep writing HTML and just add javascript behaviors directly in the DOM where you need them. This is great to enhance existing projects or hook into CMS generated output. (And you don't want to build a React app or move an existing website to one.)

With the release of htmx, the slimmer and jquery-free sibling of intercooler.js, I thought it would be interesting to see how htmx compares to Alpine.js or Vue.js.

In short, it hardly compares - the approach is different, even if Alpine.js claims to enhance HTML by sprinkling in javascript.

Htmx simplifies dealing with ajax and updating HTML fragments in the source document. You keep writing HTML and leave the ajax operations to htmx.

<div 
  hx-post="/clicked"
  hx-trigger="click"
  hx-target="#parent-div"
  hx-swap="outerHTML">
  Click Me!
</div>
Enter fullscreen mode Exit fullscreen mode

It comes with a whole set of HTTP headers so you can react on the requests on the server-side and generally, it wants you to serve rendered html back to the client and do the heavy work on the server and not in the client.

I really like this approach, but there are times where you have to deal with data on the client-side, like requesting an API directly and render the results in HTML.

Htmx lets you do that in a basic way, but not as elegant as Alpine.js or Vue.js. It's possible by extending htmx and use a third party template library like mustache, handlebar, or nunjucks to accomplish the goal.

There is a client-side-templates Extension ready, but it's very basic and it didn't work for my special case, where I had to transform the JSON before using it.1

Fortunately, it's easy enough to customize the extension for my needs.

Writing the HTML

The cool thing about htmx is how you can read the attributes and understand what's going to happen:


<div hx-ext="client-side-templates">
  <!-- hx-trigger="load, click" makes sure that api gets called on page load AND on click  !-->
  <button
     type="button"
     hx-trigger="load, click"
     hx-get="https://api.github.com/users/marcus-at-localhost/gists" 
     nunjucks-template="gistlist"
     hx-target="#list"
     hx-swap="innerHTML"
  >Reload</button>

  <script id="gistlist" type="nunjucks">
    {% for gist in gists %}
      <li>
        <a href="{{gist.html_url}}">{{gist.parsed.title}}</a><br>
        <small>{{gist.parsed.description}}</small>
      </li>
    {% endfor %}
  </script>

  <ul id="list"></ul>
</div>
Enter fullscreen mode Exit fullscreen mode

Wrapped in hx-ext="client-side-templates" we know this block is taken care of by an extension.

The button tells us an action is triggered (hx-trigger="load, click") when we click on it, or when it appears in the DOM (on load).

The action is a GET request hx-get="https://api.github.com/users/marcus-at-localhost/gists" to the api.

Then look for a template in nunjucks syntax nunjucks-template="gistlist" and find the target HTML element in the DOM where the rendered template is going to be placed in (hx-target="#list")2

Finally hx-swap="innerHTML" tells us the method htmx inserts the rendered template into the DOM3.

After we added the attributes to the HTML markup we have to define an extension to deal with all the JSON related stuff, like finding the client-side template fragment, manipulating the data object, and render the template.

As I said, the original extension assumed the JSON comes in a format you can work with right away, but this might not be the case.

So this is a minimal working case of my extension:

htmx.defineExtension('client-side-templates', {
  transformResponse : function(text, xhr, elt) {
    var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
    if (nunjucksTemplate) {
      // manipulate the json and create my final data object.
      var data = {
        gists: JSON.parse(text).map((item) => {
          // parser : https://codepen.io/localhorst/pen/ZEbqVZd
          item.parsed = new leptonParser().parse(item.description);
          return item;
        })
      };

      var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
      var template = htmx.find('#' + templateName);
      return nunjucks.renderString(template.innerHTML, data);
    }
    return text;
  }
});
Enter fullscreen mode Exit fullscreen mode

One limitation I've found is the restrictive access to the ajax object and results. I couldn't find a way to cache a request as it was possible in Alpine.js and Vue.js.

In case you need full control, I guess you are better off dealing with it completely in javascript using the fetch API, render the HTML and swap it in.

Another roadblock was the additional HTTP header, htmx adds for its requests. The Github API didn't like them and returned with CORS errors.

In order to remove all htmx headers (since we can't use them anywhere else than the server you have control over) we need to hook into the configRequest.htmx event.

document.body.addEventListener('configRequest.htmx', function(evt) {
    // try to remove x-hx-* headers because gist api complains about CORS
    Object.keys(evt.detail.headers).forEach(function(key) {
      delete evt.detail.headers[key];
    });
  });
Enter fullscreen mode Exit fullscreen mode

And that's basically it.

💡 Please note, the list won't show in the codepens embedded below, because I'm using session storage and that's restricted in an iframe

Alpine.js

Vue.js

Published also here: Handle Json API Results in htmx


  1. the best cut point is probably where the template plugin is doing the manipulation. Maybe just copy the plugin and just the nunjucks part (since that's what you are using) and do the JSON transformation there? -- https://gitter.im/intercooler-js/Lobby?at=5ed2addef0b8a2053ac37859 ↩

  2. How and where you write templates depends on the template engine you are using. Nunjucks allows you to use template fragments from files. Here I just inlined the template. ↩

  3. https://htmx.org/attributes/hx-swap/ ↩

Top comments (6)

Collapse
 
bebop2000 profile image
Maz Iah

OP absolute legend.
This part opened my eyes and solved a potentially major dilemma:

htmx.defineExtension('client-side-templates', {
  transformResponse : function(text, xhr, elt) {
   //...(snipped)
          JSON.parse(text).map((item)
//...(snipped)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
perezperret profile image
Santiago Perez

Hi! I'm curious, have you by any chance tried HTMX out any further. It looks like a great fit for a project, but I haven't found any evidence of use in production.

Collapse
 
marcusatlocalhost profile image
Marcus

I used intercooler.js before (it's the predecessor of HTMX based on jquery) - there is not as much hype around it as it is around alpine.js, but there are attempts to create something similar to livewire/alpine for Twig putyourlightson.com/plugins/sprig#...

I like the approach/philosophy of HTMX or Unpoly.js (which is similar), but it doesn't always fit.
If you have to deal with JSON in the frontend, Alpine or Vue might be better.

Collapse
 
guzmanojero profile image
guzmanojero

I think that the idea is to use both Alpine and htmx

Collapse
 
nirlanka profile image
Nir Lanka ニル

Cool find! I think it makes a lot of sense for quick prototyping. Maybe for production too. Interesting.

Collapse
 
johntom profile image
John Tomaselli

No data shows in the codepen for nunjucks client-side-template.
I know how to load nunjucks template from fastify server but can never get data to loop as the client-side-template
TIA
John