Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Evagoras Charalambous
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Evagoras Charalambous ( @evagorasc )

Persisting An IFrame-Based Video Player Across Page Visits With Hotwire And Lucee CFML

By on

One really nice feature of the Hotwire framework is that you can mark elements as "permanent". This persists the element in its current state across navigation events (both application and restoration). Unfortunately, this feature doesn't play very nicely with IFrames due to the fact that the browser will reload any <iframe> whenever it is moved around in the DOM (Document Object Model). However, if we render the IFrame in between the <head> and <body> elements, we can get around this issue in Hotwire using Lucee CFML.

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

When I was playing around with the Turbo Drive progress bar, I noticed that the <div> for the progress bar was being rendered in between the <head> and <body> tags. At the time, I didn't even know that this was possible. And, in fact, on first page load, any content statically rendered in between these two tags is automatically moved to reside within the <body> tag.

But, after I performed some follow-up exploration for the progress bar, I discovered that the browser has no issue rendering an element in between the <head> and <body> tags if the element is injected via JavaScript. This gave me the idea that injecting a Turbo Frame in between the Head and Body tags might side-step the IFrame reloading browser behavior.

To try this out, I created a simple ColdFusion application that contains two pages and a video player. The user can select to play a video, which will load into the interstitial <turbo-frame> I'm injected into the document. Then, the hope is that said Turbo Frame will be untouched as the user continues to navigate around the application.

Since I can't statically render the <turbo-frame> element in between the <head> and <body> tags, I have to render it using JavaScript. As such, instead of rendering the Turbo Frame directly, I'm rendering a <template> element that contains an inert Turbo Frame. This is being done in my main page layout (truncated below):

<!doctype html>
<html lang="en">
<head>
	<!--- Truncated for snippet. --->
</head>
<body>

	<h1>
		ColdFusion + Hotwire IFrame Video Player Demo
	</h1>

	<!--- Truncated for snippet. --->

	<template data-controller="player-template">
		<turbo-frame id="player" class="player">
			<!---
				This Turbo-Frame is inert and cannot be targeted until the
				template is cloned and the resultant element is injected into the
				document.
			--->
		</turbo-frame>
	</template>

</body>
</html>

As you can see, my <template> element has a Stimulus controller bound to it. The only job of this Stimulus controller is make sure that a single instance of the given Turbo Frame exists in the document:

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

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

class PlayerTemplateController extends Controller {

	/**
	* I get called whenever this controller instance is bound to a host element.
	*/
	connect() {

		// On page load, there is no turbo-frame since it's embedded within a content
		// template. The only goal of this controller is to ensure that a SINGLE instance
		// of said template exists (and is injected in between the HEAD and BODY tags
		// where it won't be affected by Turbo Drive's full-body content replacement).
		if ( ! document.querySelector( "#player" ) ) {

			document.body
				.before( this.element.content.cloneNode( true ) )
			;

		}

	}

}

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

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( "player-template", PlayerTemplateController );

As you can see, when the Stimulus controller is connected to the DOM, it looks to see if it can find the associated Turbo Frame. And, if it can't, it clones the template and injects the resultant element just before the <body> tag.

You'll notice that I'm not including the data-turbo-permanent attribute. In this case, I don't have to because this <turbo-frame> is outside the domain of Hotwire Turbo Drive. Hotwire only manages the content of the <head> and <body> tags - it doesn't touch anything outside of these two tags. As such, this Turbo Frame will be de facto permanent since Hotwire creates a long-running page process context.

And, now that we have our rendered Turbo Frame, we need to render some videos. On the main page of this ColdFusion site, I give the user two video options:

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

		<h2>
			Welcome to This Site
		</h2>

		<ul>
			<li>
				<a href="video.htm?id=meghan-trainor" data-turbo-frame="player">
					Play <strong>Meghan Trainor</strong> - Me Too
				</a>
			</li>
			<li>
				<a href="video.htm?id=dojo-cat" data-turbo-frame="player">
					Play <strong>Dojo Cat</strong> - Go To Down
				</a>
			</li>
		</ul>

	</cfoutput>
</cfmodule>

Notice that both links have data-turbo-frame="player". Even though our injected Turbo Frame is outside the area that Turbo Drive manages in terms of page state, we can still target this frame in our navigation events.

The video player page renders a <turbo-frame> with the same id and embeds an <iframe> that points to YouTube. This page can act as a stand-alone page; but, when progressively enhanced with Turbo Drive, only the contents of the <turbo-frame> will be transcluded into the main page.

<cfscript>

	param name="url.id" type="string" default="";

	switch ( url.id ) {
		case "dojo-cat":
			src = "https://www.youtube.com/embed/TLiGA_wrNp0?autoplay=1";
		break;
		case "meghan-trainor":
			src = "https://www.youtube.com/embed/qDRORgoZxZU?autoplay=1";
		break;
		default:
			src = "";
		break;
	}

</cfscript>
<cfoutput>

	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />

		<base href="#request.appPath#/hotwire.cfm/" />
		<link rel="stylesheet" type="text/css" href="#request.appPath#/dist/main.css"></link>
	</head>
	<body>

		<h1>
			Video Player
		</h1>

		<turbo-frame id="player">

			<!---
				Only show the IFRAME when the SRC is defined. If there is no SRC, just
				return empty content, which will - for all intents and purposes - "close"
				the player. This isn't technically closing the player (ie, clearing the
				Turbo-Frame "src" attribute); but, it's good enough for this demo.
			--->
			<cfif src.len()>
				<iframe
					width="355"
					height="200"
					src="#src#"
					title="YouTube video player"
					frameborder="0"
					allow="autoplay; encrypted-media; picture-in-picture; web-share"
					allowfullscreen>
				</iframe>

				<!--- Hidden by default, only shows when inside injected player. --->
				<p class="player__close">
					<a href="video.htm?id=">
						Close Player
					</a>
				</p>
			</cfif>

		</turbo-frame>

		<p>
			<a href="index.htm">Back to Home</a>
		</p>

	</body>
	</html>

</cfoutput>

And now, the moment that we've been building to! Let's try to open up this ColdFusion application, start playing a video within our injected Turbo Frame, and then attempt to use both application visit and restoration visit events!

Video player is persisting across page navigation in Hotwire Turbo Drive.

As you can see in the Elements tab, our <turbo-frame> element resides in between our <head> and <body> tags. And, once the video player is rendered, it persists without being reloaded even as we navigate through the ColdFusion application including restoration visits using the browser's Back and Forward button.

Accessibility Concerns

As I mentioned in my previous post on rendering elements outside of the Body, there are likely going to be accessibility concerns here. As such, I would be hesitant to render "content" outside of the Body. Does a video player count as "content". Probably. I only suggest that you proceed with caution if you're curious to try this approach.

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