Previous / Next <CSS Selector> Both Use A Depth-First Algorithm In HTMX
In the HTMX JavaScript framework, the hx-target
attribute can reference other elements in the DOM (Document Object Model) by using CSS selectors, convenience keywords (such as this
and next
), and a combination thereof. Most of these are relatively straightforward; but, I ran into surprising results when using previous {CSS selector}
and next {CSS selector}
. I had read the documentation and interpreted it as meaning that the targeted elements were direct descendants of the same parent element. But, this is incorrect. Internally, HTMX is using a depth-first traversal algorithm—from the document root—to find the target nodes; and then, selects the one closest to the trigger element based on a depth-first node traversal order.
This is simple to demonstrate. We can nest a bunch of <section>
elements and then see what happens to a button that uses previous section
as its target:
<cfoutput> | |
<section> | |
<section> | |
<section> | |
<section> | |
<section> | |
Hello | |
</section> | |
</section> | |
</section> | |
</section> | |
</section> | |
<button | |
hx-get="more.cfm" | |
hx-target="previous section"> | |
Replace "previous section" | |
</button> | |
</cfoutput> |
Based on my interpretation of the HTMX documentation, when this button uses hx-target="previous section"
, I had assumed it was going to replace the contents of the top-most <section>
element. However, when we run this ColdFusion code and click on the button, we see the following:
As you can see, it's actually the inner most <section>
element that gets updated. This is actually the element farthest away from the button in terms of the DOM branching; but, it's the closest to the button in terms of DOM traversal order.
The next {selector}
works the same way; which means that it may select a deeply nested target before it selects a sibling element. We can see this in the following ColdFusion page:
<cfoutput> | |
<button | |
hx-get="more.cfm" | |
hx-target="next section"> | |
Replace "next section" | |
</button> | |
<div> | |
<div> | |
<div> | |
<section> | |
Hello | |
</section> | |
</div> | |
</div> | |
</div> | |
<section> | |
There | |
</section> | |
</cfoutput> |
In this code, we have a deeply-nested <section>
(inside some <div>
elements) and a sibling <section>
on the same level as the trigger button. And, when we try to target next section
, we end up targeting the deeply nested one:
As you can see, both the previous {selector}
and the next {selector}
in HTMX use the depth-first node order as their measure of "closest".
The other point to take away from this is that the underlying .querySelectorAll()
is being performed from the document root. Which means that the target of the previous {selector}
and next {selector}
don't even have to be in the same branch of the DOM tree—they only need to be "close" to each other in terms of a depth-first traversal.
Consider this small ColdFusion page:
<cfoutput> | |
<p> | |
<button hx-get="more.cfm" hx-target="next mark"> | |
Replace "next mark" | |
</button> | |
</p> | |
<p> | |
<mark>Hello</mark> | |
</p> | |
</cfoutput> |
In this code, the <button>
is targeting a <mark>
that's actually a cousin of the trigger element. But, again, based on a depth-first traversal, it's the "next" <mark>
relative to the button.
This is actually feature of HTMX because it gives me the ability to target "near elements" without being super tightly coupled to the DOM structure. I'm glad I stumbled over my misunderstanding so that I can start to use these special selectors in the way they were intended to be used.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️