Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Pradnya Kambli
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Pradnya Kambli

Inline Script Tag Execution In Hotwire Turbo And ColdFusion

By
Published in ,

Right now, my Dig Deep Fitness application has no build process. And, every request to the application results in a full page refresh. I'm considering updating it to use Hotwire Turbo for faster navigation. But, I'm not confident in my understanding of how Turbo will interact with all of the inline <script> tags that currently drive the page interactions. As such, I wanted to put together a quick exploration of inline scripts operating within a multi-page ColdFusion application.

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

In my current Dig Deep Fitness architecture, I render the CFML / HTML first; and then, include an inline <script> tag at the bottom of the page which will query for and attach event-bindings to DOM elements that are already rendered on the page. And, since each navigation event completely reloads the page, I never have to worry about cleaning up after myself.

In Hotwire Turbo applications, navigation events replace the <body> content but maintain a persistent process for the overall page. On top of that, navigating to a given page may result in multiple renderings: one rendering to show a "preview" of the page from the Turbo cache; and, one rendering to provide the latest result from the ColdFusion server. Which means, inline script tags may get executed several times in very quick succession.

To explore this, I've created a simple ColdFusion multi-page application (MPA) in which the homepage has two inline <script> tags. The first script tag inserts a random number into the DOM and attaches a click-handler. The second script tag just logs a message to the console; and, includes a data-turbo-temporary attribute to prevent it from being cached.

<cfmodule template="./tags/page.cfm" title="Welcome">
	<p>
		This is the <strong>Home</strong> page.
	</p>
	<p>
		Random number: <mark><!--- NUMBER INSERTED HERE. ---></mark>
	</p>

	<!---
		This INLINE script will execute every time the page renders. But, keep in mind
		that Hotwire Turbo may render the page multiple times in a single visit if it's
		pulling the page from the cache.
	--->
	<script type="text/javascript">

		var target = document.querySelector( "mark" );
		var randomNumber = Math.floor( Math.random() * 100 );

		// Appending the text (not assigning it) to show preview behavior.
		target.textContent += randomNumber;

		// Bind handler to test whether multiple events-handlers are around during various
		// types of navigation.
		target.addEventListener(
			"click",
			() => {

				alert( randomNumber );

			}
		);

		console.log( `Inline script executed: ${ randomNumber }.` );

	</script>
	<!---
		This INLINE script will execute every time the page renders as a fresh response.
		However, since it's marked as TEMPORARY, it will NOT execute when Hotwire Turbo is
		rendering the page from the cache (ie, a preview).
	--->
	<script type="text/javascript" data-turbo-temporary>

		console.log( "TEMPORARY inline script executed." );

	</script>
</cfmodule>

Note that when I'm inserting the random number into the DOM, I'm doing so by appending to the existing textContent property. Normally, I would assign the value; but, appending the value will help demonstrate the interplay with the Turbo cache and page preview rendering.

If we open up with ColdFusion application, the first rendering of the page doesn't do anything special because Hotwire Turbo doesn't actually take over the page rendering until the defer script loads. As such, the very first rendering of the page is "business as usual"; and we see nothing but the two log messages:

Initial rendering of the ColdFusion application.

On the initial load of the ColdFusion application, we can see that each inline script tag executes once. And, that the random number was appended to the empty <mark> tag. But, remember that the Turbo framework hasn't really done anything yet. Turbo doesn't get involved until after the first page load.

Let's now try navigating to the About page and the back to the Home page. Keep in mind that the Home page has been cached by Turbo at this point:

Second rendering of the ColdFusion home page.

We can see from the console logging that Turbo has rendered the page twice. The first rendering is the "preview" render in which Turbo immediately applies the cached version of the page to the current DOM. In the background, Turbo is then making a fetch() request to get the latest Home page content. And, when the fetch() request returns, the second rendering is the application of the latest content to the current DOM (wiping out the preview rendering).

If we look at the console logging more closely, we can see that the rendering operations are slightly different. When the "preview" rendering is applied, there is only one log message. And, when the "latest" rendering is applied, there are two log messages.

This is because the second inline script tags uses the data-turbo-temporary attribute. This attribute tells Turbo to remove the given DOM node from the copy of the page that Turbo puts in the cache. As such, when Turbo overwrites the <body> tag with the cached page, the cached version only contains one script tag.

So far, we've only looked at navigation events that advance the browser history. Let's now look at a "restore" action using the browser's back button. Let's navigate to the About page one more time and then go back to the Home page.

Navigating to the About page and then using the Back button to get back to the Home page.

There are two things to notice in this GIF. First, when we navigate back to the Home page using the back button, we only get the "preview" version of the page. That is, the version of the page that's pulled out of the Turbo cache. When using pop state events, Turbo does not make a request to the server to get the latest version of the page. And, as we saw in the previous demo, the preview version of the page only includes the one script tag without the data-turbo-temporary attribute. As such, we only get one console log message.

The second thing to notice is the random number. From the console logging, we can see that our inline script tag generated the random number 78. However, the page content contains the value 5378. The 53, in this case, is the value from the previous rendering of the page. Or, rather, it's the value embedded in the cached / "preview" version of the page.

This is why I made sure to update the textContent property using string concatenation instead of assignment. This allows us to see that the script tag operates on the cached version of the page, not on the "empty" state of the page.

This behavior is not unique to the Back/Forward button behaviors. But, since the Back button doesn't make a request for live data, we were able to see what was happening. The same thing actually happens in our normal navigation events. Only, the "latest" version of the page is updated so quickly, we don't get a chance to see it.

To demonstrate, I'm going to update the textContent property assignment to take place inside a console.log() statement:

// ... truncated ...

// Appending the text (not assigning it) to show preview behavior.
console.log( target.textContent += randomNumber );

// ... truncated ...

Now, every time the Home page renders, we'll better see the transient state of the DOM in the logging. And, if we now go to the About page and click back to the Home page, we see:

Click through to the Home page with intemediary logging of the textContent value.

Notice that when we navigate to the Home page, the first console.log(textContent) now shows us 7955. This is because 79 was the random number in the cached version of the page. And, the subsequent random number, 55, which is generated during the execution of the cached <script> tag is now being appended to it.

Of course, once the latest version of the page comes back from the server, via the fetch() API, this intermediary value is overwritten and a new random value is generated and applied to the DOM. Which is why we didn't see this behavior taking place in the first demo.

In my local development environment, this all happens very quickly. However, in a production site, where the request latency is much larger, the user can more easily see this. To demonstrate, if I update my Network settings to use Slow 3G and click to the Home page, we can get:

Click through to the Home page with Slow 3G settings enabled.

With the Slow 3G simulation in place, we can see that the progress bar has time to render; and, that the preview rendering of the page contains the 4-digit random number (ie, the new random number appended to the cached random number).

From this entire exploration, we can see that there's nothing inherently wrong with using inline script tags in a Hotwire Turbo and ColdFusion application. They basically just work. But, you have to be aware of how the caching works; and, how multiple script tag executions may present to the user.

Note: When Turbo is rendering a preview version of the page, it places a data-turbo-preview attribute on the <html> tag. In hairy edge-cases, you can use clues like this to subtly influence the way your script executes.

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