Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Stanley Appleman
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Stanley Appleman

Rendering A Local TimeStamp With Stimulus Using Hotwire And Lucee CFML

By
Published in ,

So far, in my exploration of Hotwire, I've looked at several features of Turbo Drive including partial rendering with Turbo Frames and dynamically updating the page with Turbo Streams. According to David Heinemeier Hansson (DHH), the Turbo family of features should get you 90% of the way through your application development. But, that last 10% of features needs to be implemented with custom JavaScript. And for this, Hotwire provides Stimulus controllers; or, what the Rails community refers to as "JavaScript sprinkles". To start looking at Stimulus, I wanted to create a demo that takes a ColdFusion provided UTC millisecond value and renders it in the user's local timezone.

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

In my Angular-fronted ColdFusion applications, I usually return timestamps as UTC milliseconds in my API responses. Then, on the client-side, I can take that millisecond value, new up a Date object, and render a user-specific date/time string. However, since Hotwire applications aren't backed by an API (in the traditional sense), this UTC millisecond timestamp needs to be provided as a data attribute on the DOM (Document Object Model).

Unlike an Angular application, where the JavaScript / TypeScript classes act as the "source of truth" and the DOM is reconciled to reflect the JavaScript values, the Hotwire philosophy seems to treat the DOM as the "source of truth"; and, through the use of the MutationObserver API, will reconcile the JavaScript class properties to reflect the state of the DOM.

TWO-WAY RECONSILIATION: When you update properties in your Controller classes, Stimulus will often update the DOM to reflect those changes. In that sense, state reconciliation does flow in both directions.

This approach drives a lot of the JavaScript configuration into various DOM attributes. We see this in Turbo with attributes like data-turbo-action and data-turbo-preload; and, we see this in Stimulus with attributes like data-controller. In fact, Stimulus uses data attributes to manage Actions, Values, Targets, and Classes.

ASIDE: This emphasis on the "DOM as truth" plays very nicely with the fact that Hotwire is updating the rendering of the page using HTML over the wire. By driving state into the DOM, swapping out portions of the page naturally swaps out portion of the state.

For this local time demo, I'm going to be providing the UTC millisecond time as a "value" attribute. And, I'm going to be providing "daytime" and "nighttime" classes as "class" attributes that will be dynamically applied to the DOM based on the time-of-day.

Here is a truncated snippet of my demo:

<div
	data-controller="local-time"
	data-local-time-tick-count-value="#getTickCount()#"
	data-local-time-day-time-class="local-time--day"
	data-local-time-night-time-class="local-time--night"
	class="local-time">

	Your local time:
	<span data-local-time-target="label"></span>
</div>

The data attributes in this Stimulus controller view use the following conventions:

  • data-controller="{ identifier }"
  • data-{ identifier }-{ value }-value="..."
  • data-{ identifier }-{ class }-class="..."
  • data-{ identifier }-target="..."

Stimulus takes these data attributes and maps them onto a whole host of properties, getters, setters, and callbacks on our controller classes. For example, the following value attribute:

data-local-time-tick-count-value

... results in the following Controller properties and callbacks:

  • Getter: this.tickCountValue
  • Setter: this.tickCountValue
  • Existence: this.hasTickCountValue
  • Change Detection: this.tickCountValueChanged( newValue, oldValue )

My Stimulus controller will then take this value, new up a Date, and render it in the user's local time via the target <span>. Since I am not using the Rails asset pipeline, you'll see at the bottom that I am manually registering my JavaScript class with the local-time identifier:

// Import core modules.
import { Application } from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

class LocalTimeController extends Controller {

	// These static values tell Stimulus which DOM attribute to map to which JavaScript
	// class properties. When the Controller is instantiated, a good number of getters
	// and setters are automatically generated based on the following names.
	static classes = [
		"dayTime",
		"nightTime"
	];
	static targets = [
		"label"
	];
	static values = {
		tickCount: Number
	};

	// ---
	// LIFE-CYCLE METHODS.
	// ---

	/**
	* I run once after the component instance has been bound to the host DOM element. At
	* this point, all of the classes, targets, and values have already been bound.
	*/
	connect() {

		console.info( "Controller connected." );
		this.renderTime();

	}


	/**
	* I get called once after the component instance has been unbound from the host DOM
	* element. At this point, all of the targets have already been disconnected as well.
	*/
	disconnect() {

		console.info( "Controller disconnected." );

	}


	/**
	* I get called once after the "label" target is connected to the controller instance.
	* This gets called BEFORE the main connect() call-back.
	*/
	labelTargetConnected( node ) {

		console.log( "Label target connected." );

	}


	/**
	* I get called anytime the "tick-count" value is changed (including the value
	* initialization). This will get called once BEFORE the main connect() call-back.
	*/
	tickCountValueChanged( newValue, oldValue ) {

		console.group( "Tick Count Changed" );
		console.log( "New value:", newValue );
		console.log( "Old value:", oldValue );
		console.groupEnd();

		if ( ! oldValue ) {

			// When the tickCount value is first set (upon controller initialization), the
			// previous value is "0" (the default). We know that the initial rendering of
			// the local time will be handled by the connect callback; as such, we only
			// want to respond when the value changes occur after the connect event.
			return;

		}

		// Update the rendering to reflect the new time!
		this.renderTime();

	}

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

	/**
	* I update the rendering of the local-time label based on the bound tick-count.
	*/
	renderTime() {

		// Using the server-provided UTC milliseconds value, let's instantiate a Date
		// object in the user's local timezone. This will give us access to locale-
		// specific formatting.
		var localTime = new Date( this.tickCountValue );
		// To make the demo more interesting, I'm arbitrarily classifying some hours as
		// "night" and other hours as "day" so that I can use some CSS class bindings.
		var isNightTime = (
			( localTime.getHours() < 7 ) ||
			( localTime.getHours() > 19 )
		);

		this.labelTarget.textContent = localTime.toLocaleTimeString();

		if ( isNightTime ) {

			this.element.classList.remove( this.dayTimeClass );
			this.element.classList.add( this.nightTimeClass );

		} else {

			this.element.classList.remove( this.nightTimeClass );
			this.element.classList.add( this.dayTimeClass );

		}

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

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( "local-time", LocalTimeController );

As you can see, in my connect() life-cycle method, I call renderTime(). The renderTime() method takes the UTC millisecond value, parses it into a date, and then sets the .textContent of the target span to contain a localized time string. As such, when we run this ColdFusion demo, we get the following output:

View showing the UTC milliseconds value in both local time and server time.

As you can see, the ColdFusion server time is 12:14:44; but, the UTC milliseconds have been rendered as 7:14:44 in the user's local time using our Stimulus controller.

Now, you might notice in that screenshot that there are links to manually set the time to either "Day" or "Night". I added these to see if dynamic changes to the DOM - which, remember, is used as the "source of truth" - will be reflected in the state of my controller class. Here's the full source of my index.cfm page:

<cfmodule template="./tags/page.cfm">
	<cfoutput>

		<h1>
			ColdFusion + Hotwire Local Time Controller Demo
		</h1>

		<div
			id="demo"
			data-controller="local-time"
			data-local-time-tick-count-value="#getTickCount()#"
			data-local-time-day-time-class="local-time--day"
			data-local-time-night-time-class="local-time--night"
			class="local-time">
			Your local time:

			<!---
				Once the Stimulus controller is bound, the text inside this span will be
				replaced with a formatted time string.
			--->
			<span data-local-time-target="label">
				Unknown
			</span>
		</div>

		<p>
			<a href="logout.htm">Logout</a>
		</p>

		<!---
			When Stimulus binds Controllers to the DOM (Document Object Model) tree, it
			also starts to track changes to the DOM using the MutationObserver API. This
			means we can make runtime changes to the DOM from outside the Controller and
			Stimulus will turn around and trigger the Controller's life-cycle hooks. In
			this case, we'll change the "tick-count" value attribute, which will, in turn,
			trigger a re-render of the time.
			--
			NOTE: This is done, in part, because Stimulus "best practices" seem to try and
			treat the DOM as the "source of truth" for the state of the components. This
			approach allows HTML to be dynamically re-rendered with much less friction.
		--->
		<p>
			Dynamically set attribute:
			<a href="javascript:( window.demo.setAttribute( 'data-local-time-tick-count-value', new Date( '2023-02-06T11:30:00-05:00' ).getTime() ) ); void( 0 );">Set to Day</a> ,
			<a href="javascript:( window.demo.setAttribute( 'data-local-time-tick-count-value', new Date( '2023-02-06T03:15:00-05:00' ).getTime() ) ); void( 0 );">Set to Night</a>
		</p>

	</cfoutput>
</cfmodule>

As you can see, those two links at the bottom are using a javascript: protocol to reach into the DOM tree and call setAttribute() on our Stimulus controller view. And, in doing so, we get the following output:

View demonstrating that manually setting the DOM attribute will cause the Stimulus controller class to be updated in response.

As you can see, when we manually update the DOM attribute, Stimulus notices that change (via the MutationObserver API); and, updates the Controller class to reflect the new state of the DOM. Stimulus also invokes our change-callback for the tickCount value, which we then use to update the CSS classList being applied to the host element.

There's a lot more that can go into a Stimulus controllers - I'm just starting to scratch the surface. But, I can see how Stimulus dovetails nicely with Turbo. In fact, Stimulus will seamlessly and automatically connect and disconnect controllers as the state of the DOM is dynamically altered via Turbo Drive, Turbo Frames, and Turbo Streams.

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

Reader Comments

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