Advanced Turbo Drive Rendering Techniques

When working with Turbo in Rails, understanding advanced rendering techniques can significantly improve the frontend user experience. Here's a closer look at some of these methods.

Turbo Drive - Render Interception

Turbo Rails introduces the concept of an application visit. Essentially, Turbo Drive can intercept regular link clicks, replacing the HTML without a full reload of the associated JavaScript and CSS. Before the new content is displayed, the turbo:before-render event is triggered. This event allows developers to pause the rendering, potentially adding custom animations or page transitions, as in the Turn library by Dom Christie. The following StackBlitz example demonstrates this with a custom fly-out animation.

This example combines a fly-out CSS animation with a loop that adds this CSS class to SVG elements with a short delay. Rendering is then resumed after one second:

.fly-out {
  animation: fly-out 1s normal ease-in-out;
  animation-fill-mode: forwards;
}

@keyframes fly-out {
  0% {
    transform: translate3d(0, 0px, 0);
  }
  100% {
    color: transparent;
    transform: translate3d(0, -2000px, 0);
  }
}
document.querySelectorAll('svg').forEach((element, index) => {
  element.classList.add('fly-out');
  element.style.animationDelay = `${index * 100}ms`;
});

setTimeout(() => {
  event.detail.resume();
}, 1000);

It's important to note that handling restoration visits, which pull HTML from the cache, requires careful consideration. To avoid unwanted animations during such visits, it's necessary to check if the current document is a preview and then continue rendering:

if (!document.documentElement.hasAttribute('data-turbo-preview')) {
  // animation logic
} else {
  // just proceed if rendering from the cache
  event.detail.resume();
}

Turbo Drive - Custom Rendering

Beyond just pausing and resuming rendering, Turbo Drive also provides a render method in the turbo:before-render event. This method allows for a custom transition between the current and new body element. The following StackBlitz example uses GSAP to implement JavaScript animations:

In the event, handler, we overwrite event.detail.render with a custom method that implements a fancy GSAP transition between two images.

The navigation element is simply replaced, while the image swapping is more intricate. The old image is given a higher z-index, allowing the new one to appear beneath it:

oldImage.setAttribute('style', 'opacity: 1; z-index: 10;');

oldImage.insertAdjacentElement('afterend', newImage);

Two GSAP animations are then used: one for the new image's fade-in and another for the old image's fade-out.

newImage.addEventListener('load', () => {
  newImage.setAttribute(
    'style',
    'opacity: 0; z-index: 0; filter: invert(100%) blur(16px);'
  );
  gsap.to(oldImage, {
    opacity: 0,
    filter: 'invert(100%) blur(16px)',
    duration: 2,
    ease: 'power2.inOut',
  });
  gsap.to(newImage, {
    opacity: 1,
    filter: 'invert(0%) blur(0px)',
    duration: 2,
    ease: 'power2.inOut',
  });

  setTimeout(() => {
    oldImage.remove();
  }, 2000);
});

The final step is to remove the old image from the DOM altogether.

Again, it's crucial to ensure that the page isn't sourced from Turbo Drive's snapshot cache:

if (!document.documentElement.hasAttribute('data-turbo-preview')) {
  // rendering logic
}

Caveat

Note that this method completely swaps out the default Turbo Drive rendering mechanism. This means that we have to diligently clean up the DOM ourselves.

Turbo Drive - Cache Lifecycle

Cache management in Turbo Drive can be challenging, especially when navigating by browser history (i.e., the back and forward buttons of your browser). In the past, many jQuery plugins would attach to the DOM upon page load, but Turbo's caching would remove these event listeners and their associated data.

The following StackBlitz example illustrates this scenario:

The example showcases a third-party script, Greeter, without a teardown method. It does nothing more than render a friendly, time-sensitive greeting and a (randomized) weather report upon turbo:load.

The problem with caching: Upon navigation to a page that has an equivalent cache entry, a stale portion of the DOM (an old greeting) is loaded.

The solution is to replace the widget-container's innerHTML with a loading spinner before caching, ensuring fresh content for users. This method also helps when a cached snapshot contains heavy content, like images, which might be loaded more than once.

document.addEventListener('turbo:before-cache', function (event) {
  document.querySelector('#widget-container').innerHTML =
    '<svg class="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
      <!-- spinner svg -->
    </svg>';
});

In summary, Turbo offers a range of techniques to enhance the frontend user experience in Rails applications. By understanding and implementing these methods, developers can create more efficient and user-friendly web applications.

P.S. Join The Hotwire Club on Patreon to get priority access to these nibbles.

Supercharge Your Rails App

Upgrade Your Tech Stack for a Smoother Dev Experience and Increased Profits.

Order a Review