Saturday, May 24, 2025
HomeJavaScriptKeyboard Command Extension In HTMX And ColdFusion

Keyboard Command Extension In HTMX And ColdFusion


Earlier this week, I took a have a look at utilizing extensions in an HTMX and ColdFusion software. Extensions enable us to faucet into the occasion life-cycle of nodes withing the doc object mannequin (DOM); which, in flip, permits us to reinforce the doc conduct as HTMX swaps content material into and out of the rendered web page. For this follow-up publish, I take inspiration from the Docket app by Mark Story. Mark’s HTMX and PHP app permits keyboards occasions to set off DOM interplay. I needed to strive constructing one thing related for a ColdFusion demo.

The premise of this ColdFusion demo is tremendous easy. It is a single web page that renders an id worth. There are two hyperlinks: Earlier and Subsequent, which is able to re-render the web page with the given id worth decremented or incremented, respectively. We’ll use an HTMX extension to reinforce these hyperlinks with LeftArrow and RightArrow key occasions, respectively.

I am calling this extension, key-commands. That is the identifier that will likely be handed into the hx-ext attribute, telling HTMX that this extension will likely be energetic in sure components of the DOM tree. The ColdFusion a part of this demo is pretty straight-forward – it is only a CFML template that hyperlinks again to itself:

<cfscript>

	param title="url.id" sort="numeric" default=1;

	minID = 1;
	maxID = 10;

	// Increment / decrement id, however loop to the opposite finish of vary after we're on the
	// edge. We will dwell life on the sting, however we will not keep there for lengthy.
	prevID = ( url.id > minID )
		? ( url.id - 1 )
		: maxID
	;
	nextID = ( url.id < maxID )
		? ( url.id + 1 )
		: minID
	;

</cfscript>
<cfoutput>

	<h1>
		Key Command Extension In HTMX
	</h1>

	<part class="expertise">
		<p>
			#encodeForHtml( url.id )#
		</p>
		<nav hx-boost="true" hx-ext="key-commands">
			<a
				href="index.cfm?id=#encodeForUrl( prevID )#"
				data-keyboard-shortcut="
					ArrowLeft,
					Shift.P
				">
				&larr; Prev
			</a>
			<a
				href="index.cfm?id=#encodeForUrl( nextID )#"
				data-keyboard-shortcut="
					ArrowRight,
					Shift.N
				">
				Subsequent &rarr;
			</a>
		</nav>
	</part>

</cfoutput>

On this ColdFusion code, there are a number of issues to note:

First, we have added hx-boost to the <nav> aspect. Which means that as an alternative of a full-page re-render, HTMX will intercept the navigation hyperlinks and use AJAX to swap the content material of the physique. This is not strictly wanted for this demo; however, I will be logging messages to the console; and, the boosting permits the console to persist throughout navigation.

Second, we have added the hx-ext="key-commands" to the <nav> aspect which tells HTMX that our key-commands extension needs to be notified of occasions that occur in this portion of the DOM. HTMX treats the DOM tree as an occasion bus. As such, when HTMX occasions are triggered on a given aspect, they are going to naturally propagate up the DOM department, and can finally be intercepted by our extension’s onEvent() callback.

Third, we have added the [data-keyboard-shortcut] attribute to the Prev/Subsequent hyperlinks. This attribute will likely be consumed by our key-commands extension; and can bind the given keyboard keys to the .click on() invocation of those hyperlinks. On this demo, I am utilizing the comma to permit a number of keyboard combos to be sure to a single aspect:

  • Prev is sure to each ArrowLeft and Shift.P.
  • Subsequent is sure to each ArrowRight and Shift.N.

All of those keyboard combos will come up throughout pure interactions with the webpage (corresponding to typing into an enter discipline). As such, our HTMX extension will ensure that to solely intercept they keyboard occasions when:

  • They’re triggered on the doc.physique aspect.
  • No different handler has referred to as .preventDefault() on the given occasion.

If we now run this ColdFusion and HTMX demo and use our keyboard occasions, we get the next output – the keyboard utilization will logged to the console:

Console logging showing that the prev and next links have been triggered by the keyboard commands (via our HTMX extension).

As you may see, the prev and subsequent hyperlinks reply to the mouse clicks as you’d anticipate. However, I am additionally capable of set off the prev and subsequent navigation by utilizing the keyboard shortcuts outlined within the [data-keyboard-shortcut] attribute.

There’s fairly a little bit of code in our HTMX extension; so, let’s begin by trying on the public API – the half that HTMX consumes because the interface to the extension. This may embrace three strategies:

  1. init() – HTMX will name this as soon as per web page, no matter what number of hx-ext attributes exist, to permit a centralized initialization of our extension.

  2. getSelectors() – HTMX will at all times let our extension know in regards to the nodes which have hx-ext on them. However, this methodology permits us to outline extra nodes that HTMX ought to course of. In our case, this methodology will inform HTMX in regards to the [data-keyboard-shortcut] attribute.

  3. onEvent() – HTMX will notify our extension of all occasions that occur beneath the related nodes within the DOM tree. Inside this callback, we have now to introspect each the occasion title and the node state to see if the given occasion is related to our extension.

(() => {

	htmx.defineExtension(
		"key-commands",
		{
			init,
			getSelectors,
			onEvent
		}
	);

	// The mappings create the affiliation between the normalized keyboard occasion and the
	// components that they reference [ shortcut => element ]. Because the DOM is processed,
	// mappings will likely be added to and faraway from this assortment.
	var mappings = new Map();

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I initialize the extension. I get referred to as as soon as per web page load, no matter what number of
	* hx-ext attributes there are.
	*/
	operate init () {

		// All shortcut occasions will likely be processed by a centralized handler.
		window.addEventListener( "keydown", processRootKeyboardEvent );

	}

	/**
	* I inform HTMX which components have to be processed (ie, obtain the HTMX therapy) in
	* order for this extension to work correctly. That is along with components that will
	* have already got the [hx-ext] attribute.
	*/
	operate getSelectors () {

		return [ "[data-keyboard-shortcut]" ];

	}

	/**
	* I hook into the HTMX occasion bus, responding to occasions as wanted.
	*/
	operate onEvent ( title, occasion ) {

		// HTMX will inform us about ALL occasions, not simply the occasions which are related to
		// this extension. As such, we have now to have a look at the occasion title and examine the
		// goal node to see if it is related to our key instructions.
		swap ( title ) {
			case "htmx:afterProcessNode":

				if ( occasion.element.elt.dataset.keyboardShortcut ) {

					setupShortcut( occasion.element.elt );

				}

			break;
			case "htmx:beforeCleanupElement":

				if ( occasion.element.elt.dataset.keyboardShortcut ) {

					teardownShortcut( occasion.element.elt );

				}

			break;
		}

	}

	// .... non-public strategies truncated ....

})();

What you may see from the general public API of the HTMX extension is that our keyboard shortcuts work by organising an event-listener on the basis of the doc. As keyboard occasions bubble-up by way of the DOM tree, they are going to finally attain the basis and be intercepted by our occasion listener.

The heavy lifting of occasion interception is finished by the aforementioned occasion listener. The onEvent() callback is just a method by which we are able to add and take away occasions to and from the centralized mappings assortment, respectively. Discover that our onEvent() logic can’t assume something in regards to the given occasion. Or relatively, it has to imagine that it could obtain many irrelevant occasions; and, that it should programmatically narrow-down the occasions that it needs to course of additional.

It is a byproduct of the way in which that occasions work within the DOM tree – it is not an HTMX-specific mechanic. Since HTMX makes use of the DOM tree because the pure occasion bus, the onEvent() callback naturally has to know that it’ll obtain all occasions that bubble up by way of the DOM tree.

The remainder of the HTMX extension logic is predominantly involved with normalizing occasions right into a constant string format in order that they are often in comparison with the mappings assortment. I am utilizing an Angular-inspired approach by which points of the occasion are mapped to English phrases, after which sorted alphabetically, making a canonical illustration.

With that stated, this is the total HTMX extension code with none extra rationalization:

(() => {

	htmx.defineExtension(
		"key-commands",
		{
			init,
			getSelectors,
			onEvent
		}
	);

	// The mappings create the affiliation between the normalized keyboard occasion and the
	// components that they reference [ shortcut => element ]. Because the DOM is processed,
	// mappings will likely be added to and faraway from this assortment.
	var mappings = new Map();

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I initialize the extension. I get referred to as as soon as per web page load, no matter what number of
	* hx-ext attributes there are.
	*/
	operate init () {

		// All shortcut occasions will likely be processed by a centralized handler.
		window.addEventListener( "keydown", processRootKeyboardEvent );

	}

	/**
	* I inform HTMX which components have to be processed (ie, obtain the HTMX therapy) in
	* order for this extension to work correctly. That is along with components that will
	* have already got the [hx-ext] attribute.
	*/
	operate getSelectors () {

		return [ "[data-keyboard-shortcut]" ];

	}

	/**
	* I hook into the HTMX occasion bus, responding to occasions as wanted.
	*/
	operate onEvent ( title, occasion ) {

		// HTMX will inform us about ALL occasions, not simply the occasions which are related to
		// this extension. As such, we have now to have a look at the occasion title and examine the
		// goal node to see if it is related to our key instructions.
		swap ( title ) {
			case "htmx:afterProcessNode":

				if ( occasion.element.elt.dataset.keyboardShortcut ) {

					setupShortcut( occasion.element.elt );

				}

			break;
			case "htmx:beforeCleanupElement":

				if ( occasion.element.elt.dataset.keyboardShortcut ) {

					teardownShortcut( occasion.element.elt );

				}

			break;
		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I return the normalized shortcut from the given keyboard occasion.
	*/
	operate getShortcutFromEvent ( occasion ) {

		var components = [ normalizeEventKey( event.key ) ];

		if ( occasion.altKey ) components.push( "alt" );
		if ( occasion.ctrlKey ) components.push( "ctrl" );
		if ( occasion.metaKey ) components.push( "meta" );
		if ( occasion.shiftKey ) components.push( "shift" );

		return components.type().be a part of( "." );

	}

	/**
	* I return the normalized keyboard occasion key. This helps us cope with some particular
	* characters that may be more durable to learn or have alternate meanings.
	*/
	operate normalizeEventKey ( key ) {

		swap ( key ) {
			case " ":
				return "area";
			break;
			case ".":
				return "dot";
			break;
			default:
				 return key.toLowerCase();
			break;			
		}

	}

	/**
	* I course of the given keyboard occasion that has bubbled as much as the doc root.
	*/
	operate processRootKeyboardEvent ( occasion ) {

		// If we have now no sure mappings, no level in inspecting the occasion.
		if ( ! mappings.dimension ) {

			return;

		}

		// If the person is interacting with a selected aspect (corresponding to an enter aspect),
		// the shortcut may not be related.
		if ( occasion.goal !== doc.physique ) {

			return;

		}

		// If the occasion has been modified by one other handler, assume we should not mess
		// with it any additional.
		if ( occasion.defaultPrevented ) {

			return;

		}

		var shortcut = getShortcutFromEvent( occasion );

		if ( mappings.has( shortcut ) ) {

			occasion.preventDefault();
			mappings.get( shortcut ).click on();

			console.log( `%cProcessed: %c${ shortcut }`, "coloration: darkcyan ; font-weight: daring ;", "coloration: black ;" );

		}

	}

	/**
	* I add the shortcut mapping(s) for the given node.
	*/
	operate setupShortcut ( node ) {

		// A number of shortcuts could be outlined on a single aspect by separating them with
		// a comma.
		var shortcuts = node.dataset.keyboardShortcut.break up( "," ).map(
			( phase ) => {

				return phase
					.trim()
					.toLowerCase()
					.break up( "." )
					.type()
					.be a part of( "." )
				;

			}
		);

		// Retailer the normalized information attribute again into the dataset in order that the teardown
		// course of does not need to cope with normalization. It might probably merely learn the info
		// worth and assume it matches the interior mapping key.
		node.dataset.keyboardShortcut = shortcuts.be a part of( "," );

		for ( var shortcut of shortcuts ) {

			mappings.set( shortcut, node );

		}

	}

	/**
	* I take away the shortcut mapping for the given node.
	*/
	operate teardownShortcut ( node ) {

		var shortcuts = node.dataset.keyboardShortcut.break up( "," );

		for ( var shortcut of shortcuts ) {

			mappings.delete( shortcut );

		}

	}

})();

One factor that I actually like about the way in which Mark Story approached key instructions in his Docket app (which I’ve additionally achieved right here) is to tie every key command to an precise DOM node. Prior to now, once I’ve checked out key instructions in Angular, the instructions have been at all times sure to a view, to not a selected node. However, what I like in regards to the node-specific strategy is that it forces you to at all times have a DOM-representation of a given pathway. Basically, this strategy forces us to create a constant expertise whether or not the person is utilizing the mouse (to click on the node) or the keyboard (to “click on” the node).

Wish to use code from this publish?
Take a look at the license.


https://bennadel.com/4793

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments