Tintin81
Tintin81

Reputation: 10207

How to load page specific custom Javascript functions in a Rails 7 app with Turbo, ImportMaps and Stimulus?

I just updated my Rails 6 app to Rails 7 and I am finding it difficult to hook my old "Vanilla Javascript" code into Rails' new setup with Turbo, Stimulus, and ImportMaps.

To make things easier, I simple ran rails new NewAppName on the command line and then migrated my old legacy files bit by bit into the new application.


The ImportMap was generated for me by Rails. I only added the last line to include my old custom Javascript files.

config/importmap.rb:

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"

pin_all_from "app/javascript/components", under: "components" # these are the custom JS files from my old Rails 6 app

app/views/layouts/application.html.erb:

<head>
  ...
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
  <%= yield :head %><!-- load page specific custom JS here -->
  ...
</head>

This is a typical JS file from my old app.

app/javascript/components/table_row.js:

function TableRow() {

  const rows = document.querySelectorAll('table tr[data-url]');

  rows.forEach(row => {
    row.addEventListener('click', function(e) {
      handleClick(row, e);
    });
  });

  function handleClick(row, e) {
    const url = row.dataset.url;
    window.document.location = url;
  }

}

document.addEventListener('turbo:load', TableRow);

I replaced DOMContentLoaded with turbo:load in all my JS files to make it work with Turbo (hope that's correct?).


This is how I inject page-specific JS on certain pages:

app/views/quotes/index.html.erb

<% content_for(:head) do %>
  <%= javascript_import_module_tag "components/table_row" %>
<% end %>

The problem now is that all this works somehow, however sometimes it doesn't and it's not very reliable. For example, the table_row.js file should be loaded on index pages but sometimes it's not loaded and I don't really understand why.

I suspect that I mis-configured Turbo or Stimulus or my ImportMap somehow but don't really know where.

Can anybody tell me what I'm missing here?

Upvotes: 3

Views: 2197

Answers (1)

Maxence
Maxence

Reputation: 2339

I am not using Importmaps but I rather bundle in the back end with jsbundling and esbuild and serve my static assets the good old way with Sprockets. So I will try to not say something that would not apply to your case .

But the first thing I see is that you were initializing your sprinkles in Rails 6 with DOMContentLoaded which basically means you were not using Turbolinks. Then using the newer Turbo now kinda breaks your JS.

What is Turbo ? Turbo is an attempt to make the loading of your assets more efficient as the head of your DOM is basically untouched between reloads => Your JS stays the same from page to page. (You can force reload the head though but I won't get into that). On top of being more efficient it makes your page look more fluid.

If you add stimulus and leverage the Turbo frames and Turbo streams you can have a very efficient Single Page App. Yeah basically Rails 7 is really into SPA territory. (yet with a front-end much simpler than react or advanced frameworks like angular etc)

Ok now what I see :

Your sprinkle function TableRow() is added into the <head></head> with a content_for helper.

There are two things here :

  • if the first page you visit has TableRow() in :head then fine, it will be included in the Javascript of your app. Though it will not be replayed between pages. Just because document.addEventListener('turbo:load', TableRow); is also included in it, and then will be played only at first load.
  • if the first page you visit doesn't have TableRow() in :head, then I am not even sure it will be included in the Javascript if you happen to later visit a page that has the right content_for helper. Just because the Javascript is not changing between pages.

This is probably why you see inconsistent results.

The solution would be to force Turbo to reload the Javascript every time you visit a new page (just like the old Rails way). But to be honest you are kinda killing all the goodness in turbo in doing that.

Now a solution

If you are using Stimulus then you can fix all that. Stimulus is a fantastic small library that can attach any sprinkle to your DOM easily.

So you can create a Stimulus controller, and add your TableRow() inside the Stimulus controller connect() method. The connect method is some Javascript that is played each time the controller is attached to a DOM element (and this DOM elements is appended to the page). So basically all your Javascript is static and not changing from page to page. But when Stimulus see the hook to your specific controller it will trigger the connect() method and play your TableRow() that lives inside it.

Basically in the old time the Javascript was played once and for good each time it was loaded on a page. Now it is basically permanent across pages, and some Stimulus hooks play that Javascript when needed.

I won't go into Stimulus code here as this answer may look pretty bloated, but now you get a picture of Stimulus / turbo.

Upvotes: 6

Related Questions