Using Rails 3.1, where do you put your "page specific" JavaScript code?

80,866

Solution 1

I appreciate all the answers... and I don't think they are really getting at the problem. Some of them are about styling and don't seem to relate... and others just mention javascript_include_tag... which I know exists (obviously...) but it would appear that the Rails 3.1 way going forward is to wrap up all of your Javascript into 1 file rather than loading individual Javascript at the bottom of each page.

The best solution I can come up with is to wrap certain features in div tags with ids or classes. In the javascript code. Then you just check if the id or class is on the page, and if it is, you run the javascript code that is associated with it. This way if the dynamic element is not on the page, the javascript code doesn't run - even though it's been included in the massive application.js file packaged by Sprockets.

My above solution has the benefit that if a search box is included on 8 of the 100 pages, it will run on only those 8 pages. You also won't have to include the same code on 8 of the pages on the site. In fact, you'll never have to include manual script tags on your site anywhere ever again - except to maybe preload data.

I think this is the actual answer to my question.

Solution 2

The Asset Pipeline docs suggest how to do controller-specific JS:

For example, if a ProjectsController is generated, there will be a new file at app/assets/javascripts/projects.js.coffee and another at app/assets/stylesheets/projects.css.scss. You should put any JavaScript or CSS unique to a controller inside their respective asset files, as these files can then be loaded just for these controllers with lines such as <%= javascript_include_tag params[:controller] %> or <%= stylesheet_link_tag params[:controller] %>.

Link to: asset_pipeline

Solution 3

For the page-specific js you can use Garber-Irish solution.

So your Rails javascripts folder might look like this for two controllers - cars and users:

javascripts/
├── application.js
├── init.js
├── markup_based_js_execution
├── cars
│   ├── init .js
│   ├── index.js
│   └── ...
└── users
    └── ...

And javascripts will look like this:

// application.js

//= 
//= require init.js
//= require_tree cars
//= require_tree users

// init.js

SITENAME = new Object();
SITENAME.cars = new Object;
SITENAME.users = new Object;

SITENAME.common.init = function (){
  // Your js code for all pages here
}

// cars/init.js

SITENAME.cars.init = function (){
  // Your js code for the cars controller here
}

// cars/index.js

SITENAME.cars.index = function (){
  // Your js code for the index method of the cars controller
}

and markup_based_js_execution will contain code for UTIL object, and on DOM-ready UTIL.init execution.

And don't forget to put this to your layout file:

<body data-controller="<%= controller_name %>" data-action="<%= action_name %>">

I also think that it is better to use classes instead of data-* attributes, for the better page-specific css. As Jason Garber have mentioned: page-specific CSS selectors can get really awkward (when you use data-*attributes)

I hope this will help you.

Solution 4

I see that you've answered your own question, but here's another option:

Basically, you're making the assumption that

//= require_tree .

is required. It's not. Feel free to remove it. In my current application, the first I'm doing with 3.1.x honestly, I've made three different top level JS files. My application.js file only has

//= require jquery
//= require jquery_ujs
//= require_directory .
//= require_directory ./api
//= require_directory ./admin

This way, I can create subdirectories, with their own top level JS files, that only include what I need.

The keys are:

  1. You can remove require_tree - Rails lets you change the assumptions it makes
  2. There's nothing special about the name application.js - any file in the assets/javascript subdirectory can include pre-processor directives with //=

Hope that helps and adds some details to ClosureCowboy's answer.

Sujal

Solution 5

Another option: to create page- or model-specific files, you could create directories inside your assets/javascripts/ folder.

assets/javascripts/global/
assets/javascripts/cupcakes
assets/javascripts/something_else_specific

Your main application.js manifest file could be configured to load its files from global/. Specific pages or groups of pages could have their own manifests which load files from their own specific directories. Sprockets will automatically combine the files loaded by application.js with your page-specific files, which allows this solution to work.

This technique can be used for style_sheets/ as well.

Share:
80,866
Fire Emblem
Author by

Fire Emblem

Updated on June 03, 2020

Comments

  • Fire Emblem
    Fire Emblem almost 4 years

    To my understanding, all of your JavaScript gets merged into 1 file. Rails does this by default when it adds //= require_tree . to the bottom of your application.js manifest file.

    This sounds like a real life-saver, but I am a little concerned about page-specific JavaScript code. Does this code get executed on every page? The last thing I want is for all of my objects to be instantiated for every page when they are only needed on 1 page.

    Also, isn't there potential for code that clashes too?

    Or do you put a small script tag at the bottom of the page that just calls into a method that executes the javascript code for the page?

    Do you no longer need require.js then?

    Thanks

    EDIT: I appreciate all the answers... and I don't think they are really getting at the problem. Some of them are about styling and don't seem to relate... and others just mention javascript_include_tag... which I know exists (obviously...) but it would appear that the Rails 3.1 way going forward is to wrap up all of your JavaScript into 1 file rather than loading individual JavaScript at the bottom of each page.

    The best solution I can come up with is to wrap certain features in div tags with ids or classes. In the JavaScript code, you just check if the id or class is on the page, and if it is, you run the JavaScript code that is associated with it. This way if the dynamic element is not on the page, the JavaScript code doesn't run - even though it's been included in the massive application.js file packaged by Sprockets.

    My above solution has the benefit that if a search box is included on 8 of the 100 pages, it will run on only those 8 pages. You also won't have to include the same code on 8 of the pages on the site. In fact, you'll never have to include manual script tags on your site anywhere ever again.

    I think this is the actual answer to my question.

  • jrhorn424
    jrhorn424 about 12 years
    +1 This is great to know for a newbie like me. I'd give it +2 if I could.
  • TheAries
    TheAries about 12 years
    One little detail: there's this notion in your answer that once the js is browser cached, it has no impact. This isn't quite true. The browser does indeed avoid the download, if the js file is properly cached, but it still compiles the code on every page render. So, you have to balance tradeoffs. If you have a lot of JS in aggregate, but only some is used per page, you might be able to improve page times by breaking the JS apart.
  • TheAries
    TheAries about 12 years
    For more on the practical effects of that compilation step I'm talking about, see 37 Signals' explanation of how pjax impacted Basecamp Next: 37signals.com/svn/posts/…
  • Ryan
    Ryan about 12 years
    That's a fair point. After reading the article and looking back on projects where I've used the above solution, I realize I wrote essentially the same "send the changed HTML" solution they mention in the article. The frequent re-compiling of JS hadn't been an issue in my projects because of that. The compilation step is something I'll keep in mind when I'm working on less "desktop application" oriented sites.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser almost 12 years
    @sujal Exactly. The Rails core team is notorious for abysmal JavaScript management. Feel free to ignore their suggestions and just use the good parts of the asset pipeline. :)
  • Marnen Laibow-Koser
    Marnen Laibow-Koser almost 12 years
    Downvoting for "Call me crazy, but I want ALL of my JS compiled and minified into application.js when I deploy." You really don't want this, as it makes the user load JavaScript he doesn't need and it makes your handlers look for attributes that aren't even going to be there. Having everything in app.js is tempting, and Rails certainly makes it easy, but the proper thing to do is to modularizing JavaScript better.
  • Ryan
    Ryan almost 12 years
    You're entitled to a different opinion... and technically entitled to downvote over a difference of opinion. However, it would be nice to see some justification as to why one large and cached file is inferior to forcing multiple HTTP requests to grab modularized JS. Furthermore, you are mistaken regarding the handler search procedure. The tag values are NOT searched. Only one search is ever performed and it pulls all elements that have a data-jstag attribute. It doesn't search by tag name, it just finds all elements that have tags and then instantiates only the needed objects.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser almost 12 years
    @Ryan Caching is a thorny issue either way. I'd rather have the client fetch a bunch of independent, separately cacheable JS files: if page 1 needs modules A and B, and page 2 needs modules B and C, then a request to page 2 after page 1 only needs to get module C. But I will admit that this is made more difficult by JS not having any good native way to include one client-side script from within another. Also, thanks for the correction on the data-jstag handling. That probably helps performance, but doesn't address my basic issue with this method. As @sujal said, it's lots of unused code.
  • zsljulius
    zsljulius over 11 years
    This is the most elegant way to do it. But also, you will need to remove the line //= require_tree . from the application.js.coffee
  • jackyalcine
    jackyalcine over 11 years
    No way, that's a crapload of JavaScript to load for one page. Doesn't even matter if it's cached.
  • Nishant
    Nishant over 11 years
    Wont this cause problems after assets are precompiled and at run time the <%= raw ... %> will return a 404 ?
  • Admin
    Admin about 11 years
    This looks like it might be able to answer the question. Could you please add more to the answer to flesh it out?
  • Bill Garrison
    Bill Garrison about 11 years
    I totally agree with this method. The other methods seem very clunky and still end up loading a giant js file. The project im working on has almost 2mb worth of JS files / plugins etc AFTER being combined / minified.
  • Ross Hambrick
    Ross Hambrick about 11 years
    I'm fairly new to Rails, but it seems to me that this should be the default behavior.
  • Benjamin Crouzier
    Benjamin Crouzier almost 11 years
    In my experience, using this technique with css breaks turbolinks. So it might be the case with js too. (turbolinks is included by default in rails 4)
  • elsurudo
    elsurudo over 10 years
    If you depend on other libraries, such as Bootstrap, this could present problems as well, as they wouldn't be included. This is definitely one of those moments when I am disappointed in Rails.
  • elsurudo
    elsurudo over 10 years
    Thanks a lot for this advice. I not have several "top-level" JS files, depending on the module of my app. Works well.
  • Sujimichi
    Sujimichi over 10 years
    For action specific control I have this in my layout, as not every action for every controller has specific JS. page_specific_js = "#{params[:controller]}_#{params[:action]}" and then; javascript_include_tag page_specific_js if Rails.application.assets.find_asset page_specific_js
  • Deborah
    Deborah over 10 years
    I think the asset pipeline is un-sweet, since it creates a bunch of files that often shouldn't be used. So for me relying on the asset pipeline is creating a dependency on an inefficient system.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 10 years
    @DeborahSpeece When does the asset pipeline create files that shouldn't be used? Are you confusing the asset pipeline (good) with require_tree / (bad)?
  • DusanV
    DusanV over 10 years
    Do the controller specific actions still get minified? Are they added to the single js file that is created by sprockets, or elk this lead to multiple requests for asset files?
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 10 years
    @Jason They still get processed by the pipeline, and included as separate, modular, independently cacheable files.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 10 years
    Downvoting. This is ridiculously convoluted -- not to mention insecure (due to the eval) if your HTML gets compromised by a cracked server or a malicious userscript.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 10 years
    Downvoting. Inline JavaScript is never a good idea. Even if it's glue code, it should be in an external file.
  • Marnen Laibow-Koser
    Marnen Laibow-Koser over 10 years
    Downvoting. Inline JavaScript is never advisable. HTML should only contain markup. JS and CSS should be in separate, reusable files.
  • zelanix
    zelanix almost 10 years
    +1 The important point here for me is that you can replace //= require_tree . with //= require_directory . so you can keep all existing files where they are and create new directories for page specific files.
  • martincarlin87
    martincarlin87 about 9 years
    this looks like the way to go, I'm going to see if I can implement it in my own app, thanks for the detailed answer.
  • Bhargav Rao
    Bhargav Rao about 8 years
    Whilst this may theoretically answer the question, it would be preferable to include the essential parts of the answer here, and provide the link for reference.
  • yekta
    yekta almost 8 years
    The very fact that there are multiple suggested ways to DoItRight (i.e. make anything work) is evidence to me that this is not documented well andor the entire approach is flawed. I also take issue with the statement that "this is the most elegant way to do it". Magic is generally not good and I honestly feel the entire asset pipeline is full of magic. Conventions however can be good, but I feel that line is crossed too much with the asset pipeline and this is a great example of it. </end-of-rant>.