Exploring Extensions In HTMX
The HTMX JavaScript framework operates by traversing your Document Object Model (DOM) looking for elements that match a set of criteria, such as the existence of an hx-get
or an hx-post
attribute; and then, processes the matching elements by applying new behaviors. As it orchestrates these modifications, it emits a series of events on the DOM. Informally, these events can be a way for any JavaScript library to hook into the HTMX life-cycle by consuming the DOM as an event-bus; but, HTMX also provides an official way of doing this by using extensions.
An HTMX extension is nothing more than a data-structure that defines methods that HTMX invokes at various points in the node processing life-cycle. To be honest, I don't fully understand most of these hooks; so, today I just want to look at the getSelectors()
and onEvent()
methods.
Aside: internally, HTMX uses the
.hasOwnProperty()
method to see if a given hook is defined on an extension. As such, you can't use a JavaScript class instance to define this data structure if your class architecture uses the inherited prototype chain - HTMX simply won't see it.
HTMX doesn't process every node in your DOM. Instead, it queries the DOM using CSS and XPath selectors in order to find nodes that need the HTMX treatment. Once it finds these nodes, it then initializes them and starts emitting events.
An HTMX extension, for the purposes of this conversation, can participate in both parts of that process. Meaning, it can tell HTMX which nodes to process via getSelectors()
; and then, it can listen to events and override behaviors on the processed nodes via onEvent()
.
The relationship between these aspects is a little confusing. It's best to think of them as being completely separate and mostly unrelated mechanisms. Meaning, the getSelectors()
value provided by your extension doesn't directly relate to which events are passed to your onEvent()
callback.
Consider this ColdFusion snippet for an HTMX extension called ben-ben
:
<body>
<p hx-ext="ben-ben">
<a href="a.cfm">A</a>
<a href="b.cfm">B</a>
<a href="c.cfm">C</a>
</p>
</body>
When this ColdFusion page runs, the ben-ben
extension only hooks into the life-cycle of a single DOM node: the <p>
on which it is defined. That's because this is the only element that's processed by HTMX. However, what if we add hx-boost
to the body:
<body hx-boost="true">
<p hx-ext="ben-ben">
<a href="a.cfm">A</a>
<a href="b.cfm">B</a>
<a href="c.cfm">C</a>
</p>
</body>
From the ben-ben
extension's perspective, nothing has changed. However, from the HTMX perspective, the entire DOM is now under the control of the hx-boost
attribute. Which means, HTMX will now process every <a>
tag in the body, including those that reside under the hx-ext
portion of the DOM. Which means, the ben-ben
extension can now hook into the life-cycle events of the three descendant anchor tags.
This is an important nuance to understand because it means that your extension will have to sift-through the event stream, looking for relevant events. Your extension cannot assume that it will only be notified about extension-relevant nodes.
Consider it this way:
The
hx-ext
allows your extension to hook into specific branches of the DOM event-bus (using youronEvent()
callback as the hook).The
getSelectors()
method tells HTMX which additional nodes in that DOM branch need to be processed by HTMX.
Let's look at a concrete HTMX extension example. This extension, ben-ben
, provides additional selectors for cool-beans
as either an attribute or a CSS class. Then it logs the textContent
of the nodes when the htmx:afterProcessNode"
event is emitted:
<script type="text/javascript">
// Note: you can't use a Class instance to define an extension because HTMX uses the
// .hasOwnProperty() call internally when integrating the extension logic. As such, it
// won't see methods that are defined on an object's prototype chain.
htmx.defineExtension(
"ben-ben",
{
/**
* I define the collection of selectors that HTMX will use to identify which
* notes will be affected by the extension. This is in addition to any node
* that has hx-ext="ben-ben" on it.
*/
getSelectors () {
return [ "[cool-beans]", ".cool-beans" ];
},
/**
* For nodes that are targeted by this extension, all extension hooks within
* the HTMX framework will call this method and provide the extension with an
* opportunity to augment and override behaviors.
*/
onEvent ( name, event ) {
if (
// The body is processed due to the root hx-ext attribute. But, I'm
// omitting it here since it has so much textContent, it messes up my
// pretty console-logging.
( event.detail.elt === document.body ) ||
( name !== "htmx:afterProcessNode" )
) {
return;
}
console.group( `Event: ${ name }` );
console.log( event.detail.elt );
console.log( event.detail.elt.textContent.replace( /\s+/g, " " ) );
console.groupEnd();
}
}
);
</script>
As you can see, an HTMX extension is just a vanilla object with methods. Let's now apply it to a ColdFusion page:
<!---
Note: the getSelectors() is only meaningful for DESCENDANTS of a node that has the
extension applied. As such, it's the root [hx-ext] on the BODY that allows other nodes
within the document to be targeted by the extension via the getSelectors() value.
--->
<body hx-ext="ben-ben">
<h1>
Exploring Extensions In HTMX
</h1>
<p hx-ext="ben-ben">
This <strong>will be</strong> processed by the extension
due to the <code>[hx-ext]</code> attribute.
</p>
<p cool-beans>
This <strong>will be</strong> processed by the extension
due to the <code>[cool-beans]</code> attribute.
</p>
<p class="cool-beans">
This <strong>will be</strong> processed by the extension
due to the <code>.cool-beans</code> CSS class.
</p>
<p>
This <em>won't be</em> seen by extension. It's not special.
</p>
<p>
<!---
TO BE CLEAR: our demo extension WILL SEE this node, even though it has nothing
to do with our logic, since it's being processed by HTMX. Extensions don't
only see their own nodes - they see EVERY node that dove-tails with a given
hook. It's up to the extension to determine which nodes should be ignored
within the hook logic.
--->
<a href="about.cfm" hx-boost="true">About (hx-boost)</a>
</p>
</body>
When we run this ColdFusion page, we can see which nodes were passed into the onEvent()
hook in our extension:
As you can see from the console logging, our ben-ben
extension hooked into the life-cycle events of:
Any node that had the
hx-ext="ben-ben"
attribute.Any node with either
.cool-beans
or[cool-beans]
that resided under one of the relevanthx-ext
attributes.The
hx-boost
'ed link under the roothx-ext
attribute.
This last point (3) is, again, a reminder that the "nodes to process" and the "events to intercept" are two completely unrelated parts of the HTMX processing life-cycle. Even though the ben-ben
extension didn't specifically target the hx-boost
link, the link was processed by hx-boost
under a portion of the DOM controlled by the ben-ben
extension. As such, the onEvent()
callback in our extension is given the opportunity to intercept it.
HTMX extensions can hook into additional parts of the HTMX life-cycle, including content swapping and request transformation. But, I'm still letting the getSelectors()
and onEvent()
mechanics marinate in my brain.
Want to use code from this post? Check out the license.
Reader Comments
One point of clarity on which nodes get processed. While the extension can help HTMX identify which nodes are processed during the initialization of a DOM branch, all elements that get removed from the DOM (by HTMX) are "de-initialized." Which means that every element that HTMX removes receives, at the very least, a
htmx:beforeCleanupElement
event. Which means, your extension will receive many of these events.As a quick follow-up, here's another post exploring an HTMX extension that binds keyboard shortcuts to links:
www.bennadel.com/blog/4793-keyboard-command-extension-in-htmx-and-coldfusion.htm
So, it turns out that when the
init()
method is called, it's passed a reference to an API. At first, I thought this was just a convenience reference to the public api; but, this is not the case. Theinit()
argument exposes a more extensive, qausi-private API for extension authors:www.bennadel.com/blog/4800-htmx-extensions-have-access-to-an-extended-api.htm
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →