Select Page

Listen For Stimulus Custom Events Outside Of Hotwire

Ben Nadel
Published: April 10, 2023

One of the elegant features of Hotwire is that the base Controller includes a .dispatch() event that makes it very easy to emit custom events from any Stimulus controller. These custom events can then be handled by other Stimulus controllers higher-up in the DOM (Document Object Model). One thing that might not be obvious at first is that these custom events are native DOM events. Meaning, they’re fundamentally the same as the more familiar events such as click, submit, and mouseenter. which means we can listen for / bind to Stimulus custom events from outside our Hotwire application.

View this code in my ColdFusion + Hotwire Demos project on GitHub.

Over the last few weeks, I’ve been incrementally migrating my ColdFusion blog to use Hotwire. Until the migration is complete, some of the functionality is provided by new Hotwire code; and, some of the functionality is provided by legacy JavaScript code. These two realms must interact in order to keep the site running smoothly.

Part of the way in which I’m gluing the new code and the legacy code together is by updating the legacy code to bind to Stimulus events. This way, I can migrate low-level user interface widgets to Stimulus while still allowing legacy orchestration code to tie an entire user interface together.

Take, for example, the “Emoji Button”. My blog has a set of emoji buttons that allow the user to quickly inject an emoji character into a comment. Each emoji is encoded as a set of hexadecimal characters. And, since some emoji require multiple codepoints, I’m using the data-hex attribute to store a space-delimited list of hex values:

	data-hex="2764 FE0F">
	Red Heart

The role of the emoji Stimulus controller is to take that data-hex value, translate it into an emoji character, and emit an emoji event with the generated character in the event.detail:

// Import core modules.
import { Application } from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export class EmojiController extends Controller {
	* I emit the "emoji" event for the associated hex-encoded code-points.
	emitEmoji( event ) {
		var codepoints =
			.split( " " )
				( hex ) => {
					return( parseInt( hex, 16 ) );
				detail: {
					emoji: String.fromCodePoint( ...codepoints ),
					codepoints: codepoints
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
window.Stimulus = Application.start();
// When not using the Ruby On Rails asset pipeline / build system, Stimulus doesn't know
// how to map controller classes to data-controller attributes. As such, we have to
// explicitly register the Controllers on Stimulus startup.
Stimulus.register( "emoji", EmojiController );

As you can see, this Stimulus controller takes the hex, translates it into Unicode codepoints, and then combines the codepoints in order to generate an emoji. It then emits this emoji in a custom event via the .dispatch() method.

On the legacy side of the application, I can then bind to this emoji event, extract the event data, and use it to update the user interface. For the purposes of this demo, I’m going to implement the “legacy” logic in an inline Script tag. This script tag will take the emoji and insert it into an Input element:

	emojis = [
			name: "Grinning Face",
			hex: "1F600"
			name: "Heart",
			hex: "2764 FE0F"
			name: "Fire",
			hex: "1F525"
<cfmodule template="./tags/page.cfm">
		<div id="myDiv">
				<input id="myInput" type="text" autofocus />
				<cfloop item="emoji" array="#emojis#">
						data-hex="#encodeForHtmlAttribute( emoji.hex )#">
						#encodeForHtml( )#
		<!--- LEGACY LOGIC. --->
		<script type="text/javascript">
			// Even though our Emoji controller is a Stimulus controller, the events that
			// it emits are still native DOM (Document Object Model) events. Which means,
			// we can create "bridge" code that glues our legacy logic and our Hotwire
			// logic together as we migrate a legacy application. In this case, I'm going
			// to listen for the custom event, "emoji" (from the "emoji" controller), and
			// then use it to insert the desired emoji into the input using the following
			// legacy logic.
				( event ) => { "Emoji Event" );
					console.log( "Glyph:", event.detail.emoji );
					console.log( "Hex:", event.detail.hex );
					console.log( "Codepoints:", event.detail.codepoints );
					// Legacy logic to insert emoji in input.
					var insertAt = myInput.selectionEnd;
					var nextSelectionAt = ( insertAt + event.detail.emoji.length );
					myInput.value = (
						myInput.value.slice( 0, insertAt ) +
						event.detail.emoji +
						myInput.value.slice( insertAt )
					myInput.selectionStart = myInput.selectionEnd = nextSelectionAt;

As you can see, I’m using the native .addEventListener() to bind to the emoji:emoji event in the same way that I would bind to any other event. And, when we run this Hotwire and ColdFusion application and click on the buttons, we get the following output:

Clicking on the Fire emoji button allows the legacy code to insert the Fire emoji into the legacy input.

As you can see, my “legacy controller” (ie, the Script tag) was able to bind to the custom emoji event emitted by my Stimulus controller. This native event interoperability allows me to incrementally upgrade my ColdFusion application to use Hotwire.

Want to use code from this post?
Check out the license.