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="<small class="hashtag text-monospace text-muted">#parent-div</small>"
hx-swap="outerHTML">
Click Me!
</div>
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="<small class="hashtag text-monospace text-muted">#list</small>"
hx-swap="innerHTML"
>Reload</button>
<script id="gistlist" type="nunjucks"></script>
<ul id="list"></ul>
</div>
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;
}
});
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];
});
});
And that's basically it.
💡 Please note, the list won't show in the codepens embedded below, because I'm using session storage.
Alpine.js
See the Pen Alpine.js fetch data on `x-init` by Marcus Obst (@localhorst) on CodePen.
Vue.js
See the Pen Vue.js fetch by Marcus Obst (@localhorst) on CodePen.
-
"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 ↩
-
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. ↩