Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Kev McCabe
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Kev McCabe

Exploring Extensions In HTMX

By
Published in , Comments (3)

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 your onEvent() 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:

  1. Any node that had the hx-ext="ben-ben" attribute.

  2. Any node with either .cool-beans or [cool-beans] that resided under one of the relevant hx-ext attributes.

  3. The hx-boost'ed link under the root hx-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

16,020 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.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel