Make JavaScript Work Seamlessly with or without Turbo in Ruby on Rails

If you're working on a Ruby on Rails project, you've likely encountered Turbo — a powerful tool from Hotwire that optimizes page loads and updates without requiring a full page refresh. It's fantastic for building fast and interactive web applications, but it can sometimes throw a wrench into your existing JavaScript functions.

Let's dive into a practical example. Imagine you have a simple JavaScript function that removes a CSS class .js-only from elements once the page is loaded. Here's how you can make sure this function works whether Turbo is present or not.

The Scenario

You have elements on your page with the class .js-only, and you want these elements to have this class removed as soon as JavaScript is available. Here's our initial function:

const removeJsOnlyClass = () => {
    document.querySelectorAll('.js-only').forEach(element => {
        element.classList.remove('js-only');
    });
};

The Problem

This function works great when the page loads, but with Turbo, the page load event isn't always fired. Instead, Turbo uses its own custom events. This can cause your function not to run when navigating through your application.

The Solution

To make sure your function runs both on a full page load and when Turbo renders a new page, you need to listen to both the standard DOMContentLoaded event and the Turbo-specific turbo:render event. Here's how you can do that:

const removeJsOnlyClass = () => {
    document.querySelectorAll('.js-only').forEach(element => {
        element.classList.remove('js-only');
    });
};

document.addEventListener('DOMContentLoaded', removeJsOnlyClass, { once: true });
document.addEventListener('turbo:render', removeJsOnlyClass);

Why This Works

By combining these event listeners, your function is guaranteed to run:

  • Once when the page initially loads (thanks to DOMContentLoaded).

  • Each time Turbo loads new content without a full page refresh (thanks to turbo:render).

💡
This approach ensures your JavaScript works seamlessly in a Turbo-powered Ruby on Rails application, providing a smooth and consistent user experience.

Creating Your Own Custom Event Listener

To avoid writing both listeners each time you need this functionality, you can create a custom function that handles this for you. This way, you can simply call your custom function and let it take care of the rest.

Here’s how you can create a helper function for this:

const addTurboAndDomListener = (eventName, callback) => {
    document.addEventListener('DOMContentLoaded', callback, { once: true });
    document.addEventListener('turbo:render', callback);
};

const removeJsOnlyClass = () => {
    document.querySelectorAll('.js-only').forEach(element => {
        element.classList.remove('js-only');
    });
};

addTurboAndDomListener('removeJsOnlyClass', removeJsOnlyClass);

This makes your code cleaner and ensures that any function you want to run on both full page load and Turbo renders can be easily managed.

Conclusion

Integrating Turbo into your Ruby on Rails project doesn't have to complicate your JavaScript. By understanding the events Turbo uses and how to hook into them, you can ensure your JavaScript functions operate smoothly across your application. With just a few lines of code, you can make sure your scripts are always up and running, keeping your app interactive and responsive. And with a handy custom event listener function, you can keep your code DRY (Don't Repeat Yourself) and maintainable.