Rails 6 page specific JavaScript (and turbolinks) - javascript

What is the correct way to use page-specific JavaScript in Rails 6 (with turbolinks enabled)?
The articles I have found seem to suggest just putting everything in application.js - but this seems messy as it affects every page (and you also need to check the element you want to manipulate exists every time, then add an eventlistener or do nothing at all if it doesn't exist).
Another suggests adding your own folder, putting your custom JS in there and then requiring it from application.js (in effect, the same as above - it's just slightly more clean to look at but everything is still loaded for every page).
What I'd like to achieve is have my JavaScript split and only include it when needed. However, if using 'javascript_pack_tag' in a view to pull it in, this causes turbolinks to get quite upset and keep adding the event listeners, over and over. If you put 'javascript_pack_tag' in the page head, this then invalidates the cache and stops turbolinks from working.
In an ideal world;
The JavaScript would be in it's own custom location
It would be required only for the views that needed it
It would work with turbolinks

This is a burden Turbolinks brings to the stake. When we need to use the full potential of Turbolinks (or Turbo nowadays), we need to control the state of transformations. Meaning we need to bind/unbind (connect/disconnect) javascript events or html elements.
That's probably why the same team from Turbolinks created Stimulus. You can go this way and split all your JS by Stimulus controller and page. Then use javascript_packs_with_chunks_tag at application.html.erb (no need to import or javascript_tag anywhere else). Basically, Rails Webpack can splits your files to be per-page even though everything is imported at application.js file, but the controllers must be imported normally at application.js using import('controllers'). This way Webpacker can splitchunk files and reduce the bundle. Read more about Webpacker chunks and use something like webpack-bundle-analyzer, this helped me a lot to understand Webpacker and javascript imports.
But, even with Stimulus, we need to be idempotent, to avoid duplication of events or elements. And unfortunately you will need to check if element exists, or check if turbolink is caching.
Here goes some things I use to avoid duplication:
// Use at Stimulus `initialize()` or `connect()` or at `turbolinks:load` event, example:
$(document).on("turbolinks:load", () => {
// Avoid loading things again when turbolinks is previewing
if (document.documentElement.hasAttribute("data-turbolinks-preview")) return;
// Sometimes you'll need to check if plugin is already active before trying to initialize
if (table.getData().length) return;
// then starts plugin or your code
table.init();
});
// Use at Stimulus `disconnect()` or at `turbolinks:before-cache` event, example:
$(document).on("turbolinks:before-cache", () => {
// before caching we destroy stuff to avoid duplication
table.destroy();
});
And this post approach can work without Stimulus. Then you can use your custom javascript location, with Turbo and splitting code, as you wanted!

Related

Isolating JS files to be view specific in Rails

In my Rails app, I have a number of JS files I would like to be always available, and have placed them in app/assets/javascripts/globals, compiling them from application.js via //= require_tree ./globals.
However, I have some JS files that are view specific, and would prefer if they were only implemented for certain views or controllers. They're currently compiled in config/initializers/assets.rb via Rails.application.config.assets.precompile += %w( foo.js bar.js ), and accessed via <%= javascript_include_tag "foo/bar", "data-turbolinks-track" => true %> in the respective views.
I've wrapped the essential functions in foo.js & bar.js in conditionals such as if $("#foo").length > 1 ..., which prevents their functionality if the required divs aren't present.
As a result, the JS files aren't "active" until after the view is visited. However, after being visited, the JS code is "active" even after visiting another view. Is there a control mechanism that will ensure that the JS code is being read only for the correlating views?
http://brandonhilkert.com/blog/page-specific-javascript-in-rails/
http://brandonhilkert.com/blog/organizing-javascript-in-rails-application-with-turbolinks/
This is how I've implemented view specific, though it's a real pain.
Essentially, you'll be building a script namespace that contains scripts that are triggered always or only on specific controllers or even actions.
Rails also provides a way of doing controller specific.
http://guides.rubyonrails.org/asset_pipeline.html#controller-specific-assets
I'd honestly try this instead as it's pretty simple. I'm also supposing, looking at the implementation that you could easily do action based as well.
As for you're "active" code issue.
1) Ensure you are using on document ready (and/or page:load for turbolinks depending on version).
2) Ensure that your scripts work to begin with.
3) Ensure that they're working for both page refresh and turbolink visit.
Depending on how and when it breaks, it could mean different problems.

Organizing javascript in Rails app

I have several javascript (coffee script) files in my Rails javascripts directory. The code is logically divided into different files. However, each file starts with $(document).ready -> and some files share common helper functions.
What is the best way to factor out the helper functions? Should I just put them all in some other file that gets included earlier in application.js?
Also, is it normal to have the code divided up the way mine is, where every page does $(document).ready ->? Doesn't this mean that all of my code is called on every page, regardless of whether or not it is relevant? Are there alternatives to this organization?
I'm not sure if this question is specific to Rails or relevant to javascript and jQuery in general.
I do think this is a Rails question, and a good one.
The normal paradigm in Rails, where "global" stuff goes in application.* is a little messed up with the asset pipeline, since the application.js really acts as a manifest, rather than a common file. Of course you could add stuff there, or even create an application.js.coffee for your common code. I decided to create a file called common.js.coffee (and in another case shared.js.coffee), which in my case was automatically handled by the require_tree . directive.
(Update based on comment from #jonathan-tran) In some cases, you may just want methods called on document ready for all pages -- for example, I used this to make a datepicker available to any field in any view. If, instead you want methods (actually global functions) available to be callable, you'll need to export the coffeescript functions to variables, for example by attaching to the window object. You can do both in a shared file.
It is true that if you use generators you'll end up with files for every controller which, if there's no specialized code result in a series of redundant $(document).ready -> statements when assets are compiled. So just get rid of the ones you don't use. But following the pattern of separating functionality specific to a page makes good sense to me and works well -- I know where to look to find stuff and that's worth a lot as a project grows.
And another rule I have learned with Rails: go with the flow. If that's how Rails does it, it's probably a good way. They do indeed think about these things. Don't fix what works :-)

Where should JavaScript with embedded Ruby code go in a Rails 3.1 app?

For a Rails 3.1 app, some of my site wide JavaScript is only included when certain real time, instance specific conditions are met. This means I can't put it in the new asset pipeline's application.js because that isn't parsed by erb for embedded Ruby within the current context. Basically, I'm including keyboard shortcuts, based on the current_user that is logged in.
My question: where should this JavaScript with embedded code go, so that the embedded Ruby is still parsed for each page access with the proper context (i.e. current, logged in user)?
The answer seems to just be to put it in the application.html.erb layout view at the bottom, but this seams like I'm hiding away javascript code in a non intuitive location.
I've tried creating an application2.js.erb file, but then I got errors about undefined variables, which I think might be because the asset engine only parses this file once before the output is cached and the scope isn't correct yet for things like current_user.
So, using application.html.erb works just fine here, and this isn't so much a question of how to get it to work functionally. Instead, I'm wondering if there's a more elegant way to incorporate the asset pipeline model here with my requirements and still keep most of my JavaScript in the assets/javascripts directory.
You should try to create app/assets/javascripts/application2.js.erb (or whatever better name you come up with)
And then put something like this in your app/assets/javascripts/application.js:
//= require application2
And then you can have
<%= javascript_include_tag 'application2' %>
wherever you want - for example in your application.html.erb.
Btw, if you want to customize what's included on a per-view basis you might find content_for useful. Check out this screencast
Ok, about unobtrusive js. It will be just a cocept (HAML):
In your view somewhere
# hotkeys are "Ctrl+C", "Ctrl+A"
-current_user.hotkeys.each do |hotkey|
%hotkey{ "data-key" => hotkey.key, "data-behavior" => hotkey.fn }
Then in your application.js
$(document).ready(function(){
if($("hotkey").length > 0){
$("hotkey").each{function(this){
key = $(this).data("key");
fn = $(this).data("behavior");
$(document).bind('keydown', key, fn);
}}
}
})
So just the same JS will extract from HTML hotkeys data and then bind it.
As some people have pointed out, the two options are:
Put your javascript inside the view (and as you say, this doesn't feel quite right).
Put it in a javascript file. Make a conditional inside your view that includes this javascript file if certain conditions are met.
If you need to pass more instance variables from the controller to your javascript, this gem called gon can make your life easier.
This allows you to use the default asset pipeline using the following javascript:
if(gon.conditional){
//your embedded js code here
}
If you want to know more about this gem, checkout this railcast where everything gets explained.

Multiple References to the JQuery Library in a page

I have an ASP.NET application using a master page. I am adding a reference to the JQuery library in the master page however there are some content pages and user controls that reference the JQuery library directly. Will I need to remove each reference from those pages or can I leave them in place even though I am adding a reference into the master page of the application?
If you're loading jQuery with the Script Manager, it should load only once.
If you still want intellisense, you can use the following trick:
<% if (false) { %>
<script src="....... script tag here
<% } %>
You should remove them, including the library twice has many side-effects, wiping out any plugins defined for example. It'll load fine, but you'll start getting .pluginMethod is not defined, etc.
To avoid the headaches, only include them once, or register the script with the same key, and let ASP.Net do the include, and with the same key, it'll do so only once.
You will need to remove them I'm afraid. Came across this issue myself and the javascript will break.

Putting JavaScript at the end of the page produces an error

I recently read that for a faster web page load it's a good practice to put the JavaScript links at the end. I did, but now the functions of the referenced file doesn't work. If I put the link at the beginning of the page, everything is fine.
Does this thing of putting JavaScript at the end work only under certain circumstances?
I went through some testing with this as well. If you are loading a Javascript file it is faster to put it at the end BUT it does come with some important caveats.
The first is that doing this often made some of my visual effects noticeable. For example, if I was using jQuery to format a table, the table would come up unformatted and then the code would run to reformat it. I didn't find this to be a good user experience and would rather the page came up complete.
Secondly, putting it at the end made it hard to put code in your pages because often functions didn't exist yet. If you have this in your page:
$(function() {
// ...
});
Well that won't work until the jQuery object is defined. If it's defined at the end of your page the above will produce an error.
Now you could argue that all that styling code could be put in the external file but that would be a mistake for performance reasons. I started off doing that on a project and then found my page took a second to run through all the Javascript that had been centralized. So I created functions for the relevant behaviour and then called the appropriate ones in each page, reducing the Javascript load run time to 50-200ms.
Lastly, you can (and should) minimize the load time of Javascript by versioning your Javascript files and then using far-futures Expires headers so they're only loaded once (each time they're changed), at which point where they are in the file is largely irrelevant.
So all in all I found putting putting the Javascript files at the end of the file to be cumbersome and ultimately unnecessary.
You do have to pay attention to the ordering, but libraries like JQuery make it easy to do it right. At the end of the page, include all the .JS files you need, and then, either in a separate file or in the page itself, put the Jquery calls to act on the page contents.
Because JQ deals with css-style selectors, it's very easy to avoid any Javascript in the main body of the page - instead you attach them to IDs and classes.
This is called Unobtrusive Javascript
Every Yahoo YUI example file I remember has almost all the JavaScript at the end. For example,
Simple Event Handling
Basic Drag and Drop
JSON: Adding New Object Members During Parsing
It looks like Yahoo Practice is roughly "library code at the beginning of <body>, active code at the end of <body>."
Beware, though, this may result in the Flash of Unstyled Content syndrome.

Categories