Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created February 13, 2023 13:44
Using Stimulus To Preload Links On Hover In Hotwire And Lucee CFML
<cfmodule template="./tags/page.cfm" section="home">
<cfoutput>
<h2>
Welcome to Our Site!
</h2>
<p>
Copy, copy, copy....
</p>
<h3>
Check Out Our Products
</h3>
<ul class="products">
<cfloop index="productID" from="101" to="110">
<li
data-controller="hover-preload"
data-hover-preload-delay-value="500"
class="products__item">
<a
href="product.htm?id=#productID#"
data-hover-preload-target="link"
class="products__link">
Product #ucase( productID )#
</a>
</li>
</cfloop>
</ul>
</cfoutput>
</cfmodule>
// Import core modules.
import { Application } from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";
import * as Turbo from "@hotwired/turbo";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
class HoverPreloadController extends Controller {
// Every link target contained within the controller's scope will be preloaded when
// the hover timer has completed.
static targets = [ "link" ];
// The hover threshold (ie, duration the user has to hover before the link preloading
// kicks-in) can be configured.
static values = {
delay: {
type: Number,
default: 300
}
};
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I run once when the controller has been instantiated, but before it has been
* connected to the host DOM element. A single instantiated controller may be connected
* to the host element multiples times during the life-time of a page.
*/
initialize() {
this.hoverTimer = 0;
}
/**
* 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() {
if ( this.hasLinkTarget ) {
this.setupEvents();
}
}
/**
* 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() {
this.teardownEvents();
}
// ---
// PRIVATE METHODS.
// ---
/**
* I handle the mouseenter event on the host element.
*/
handleMouseenter = ( event ) => {
this.hoverTimer = window.setTimeout( this.handleTimeout, this.delayValue );
};
/**
* I handle the mouseleave event on the host element.
*/
handleMouseleave = ( event ) => {
window.clearTimeout( this.hoverTimer );
this.hoverTimer = 0;
};
/**
* I handle the timeout event triggered from the hover interactions.
*/
handleTimeout = () => {
this.teardownEvents();
this.preloadLinks();
};
/**
* I ask the Turbo session to preload all of the link targets embedded within this
* controller.
*/
preloadLinks() {
for ( var target of this.linkTargets ) {
console.log( "Preloading links for:", target.href.split( "/" ).at( -1 ) );
// CAUTION: The "preloader" is an UNDOCUMENTED PROPERTY of the session (at
// least as far as I can tell) - I only found it by looking at the source code
// for the Turbo project. As such, use this technique at your own risk!
Turbo.session.preloader.preloadURL( target );
}
}
/**
* I setup the hover events on the host element.
*/
setupEvents() {
this.element.addEventListener( "mouseenter", this.handleMouseenter );
this.element.addEventListener( "mouseleave", this.handleMouseleave );
}
/**
* I teardown the hover events on the host element. Any pending timer will be cleared.
*/
teardownEvents() {
window.clearTimeout( this.hoverTimer );
this.hoverTimer = 0;
this.element.removeEventListener( "mouseenter", this.handleMouseenter );
this.element.removeEventListener( "mouseleave", this.handleMouseleave );
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
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( "hover-preload", HoverPreloadController );
<cfscript>
param name="url.id" type="string";
// Adding a sleep to exaggerate the benefits of preloading the content.
sleep( 500 );
</cfscript>
<cfmodule template="./tags/page.cfm">
<cfoutput>
<h2>
Product #encodeForHtml( url.id )#
</h2>
<p>
Copy, copy, copy....
</p>
<script type="text/javascript">
// Logging the render time so that we can see CACHED vs LIVE rendering.
console.log(
"Product #encodeForJavaScript( url.id )# rendered at",
new Date().toLocaleTimeString()
);
</script>
</cfoutput>
</cfmodule>
<div
data-controller="hover-preload"
data-hover-preload-delay-value="500">
<a href="my-link.htm" data-hover-preload-target="link">
My link....
</a>
</li>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment