389

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.

sg7
  • 6,108
  • 2
  • 32
  • 40
Fire Emblem
  • 5,961
  • 3
  • 24
  • 37
  • 11
    "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."—Only becase the Rails core team is, and has always been, really bad at knowing how to manage JavaScript. Small files are generally better (see my comments elsewhere). When it comes to JavaScript, the Rails way is rarely the right way (except for the asset pipeline, which kicks ass, and the encouragement of CoffeeScript). – Marnen Laibow-Koser Aug 18 '12 at 06:53
  • So you'll include your page-specific js files on every page? I think that's a waste, I agree more with ClosureCowboy's answer. – gerky Sep 04 '12 at 16:24
  • 1
    Did you have a look at the accepted answer for this question? http://stackoverflow.com/questions/6571753/rails-3-1-asset-pipeline-how-to-load-controller-specific-scripts – rassom Sep 13 '12 at 16:14
  • Psh think I'm going to stick to Require.js :) There is a compiler for RequireJS that bundles up the files that are referenced by your module. – Sebastian Patten Nov 06 '12 at 04:16
  • @beefjerky "So you'll include your page-specific js files on every page?" Certainly not. On each page, I only include the JS files I actually need for that page. If they're properly modularized, they'll be quickly cached by the browser. – Marnen Laibow-Koser Jan 28 '13 at 03:50
  • @Ziggy It may be consistent, but it's not good. :) – Marnen Laibow-Koser Apr 05 '13 at 01:22
  • For those that want a full understanding of how this works and what is best please read railsapps.github.io/rails-javascript-include-external.html . This is by far the best documentation I have seen on this topic. It is a great read not only for Rails but for anyone dealing in web dev. This is why it is best to do things the rails way. I will surely be using Unholy Rails for my future questions. --Note: This is my comment to a similar question for those that have seen it before.-- @MarnenLaibow-Koser I think you will really like this read. – DutGRIFF Jan 22 '14 at 22:24
  • @DutGRIFF Although this is one of the better discussions I've seen of an oft-misunderstood topic, it encourages exactly the sort of big kitchen-sink JS files that I would advocate avoiding. Read it for its discussion of the pipeline, but ignore its recommendations. – Marnen Laibow-Koser Jan 23 '14 at 19:22
  • 1
    @DutGRIFF In other words: no, it's not best to do things the Rails way in this case (or at least, don't put everything in `application.js`), and in fact the reference you supplied points out why this is so: downloading is the slowest part of the JS execution process. Many little files are more cacheable than one big one. The Unholy Rails folks don't seem to realize, then, that their recommendations are inconsistent with the principles they're trying to adhere to, and therefore their recommendations should not be taken seriously. – Marnen Laibow-Koser Jan 23 '14 at 22:25
  • I do agree with a lot of what you are saying. I can definitely see where a large site with many page specific scripts would have problems with the kitchen-sink JS file but once it caches it could be a good thing for the rest of that site. I would hate to load an extremely large JS file on my mobile device (slow internet) just to follow a link to another site. For instance paying my Harley bill. Good info. Thanks. – DutGRIFF Jan 24 '14 at 00:39
  • 1
    @DutGRIFF No, a large JS file would not normally be a good thing even once cached. See my comments elsewhere on this page: small files can target specific pages better, and can be cached at a finer granularity. I don't see any good use case for a single large file unless there is no page-specific code *at all*. – Marnen Laibow-Koser Jan 24 '14 at 21:54
  • Same for Rails 3: http://stackoverflow.com/questions/3437585/best-way-to-add-page-specific-javascript-in-a-rails-3-app?lq=1 – Ciro Santilli OurBigBook.com Jul 14 '14 at 09:41
  • if you use the asset pipeline and coffee script implemention, you can covert js to coffeescript here. http://js2coffee.org/ – Nick N Oct 24 '14 at 05:26

29 Answers29

156

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

always-a-learner
  • 3,671
  • 10
  • 41
  • 81
meleyal
  • 32,252
  • 24
  • 73
  • 79
  • 50
    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 – zsljulius Sep 03 '12 at 21:57
  • 2
    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. – Bill Garrison Mar 29 '13 at 14:16
  • 2
    I'm fairly new to Rails, but it seems to me that this should be the default behavior. – Ross Hambrick Apr 08 '13 at 21:26
  • 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) – Benjamin Crouzier Jul 23 '13 at 09:15
  • 1
    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 Aug 07 '13 at 15:14
  • 12
    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` – Sujimichi Oct 07 '13 at 17:29
  • 2
    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? – Jason Dec 23 '13 at 15:19
  • 1
    @Jason They still get processed by the pipeline, and included as separate, modular, independently cacheable files. – Marnen Laibow-Koser Jan 23 '14 at 19:31
  • 2
    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. ``. – yekta May 18 '16 at 21:47
77

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.

Chandru
  • 1,306
  • 13
  • 21
welldan97
  • 3,071
  • 2
  • 24
  • 28
  • 4
    What if you need a variable available to all actions in the users controller, but not available in other controllers? Doesn't this method have some scoping issues? – tybro0103 Mar 06 '12 at 14:55
  • @tybro0103, I think to implement this behavior you would like to write something like `window.varForOneController='val'` in this controller init function. Also gon gem can help here(https://github.com/gazay/gon). There can be other workarounds. – welldan97 Mar 06 '12 at 18:49
  • Curroently I try not to use page-specific js. I try to stay with `$(.classname)` selectors, but I don't put much logic in js. – welldan97 Mar 06 '12 at 18:51
  • 1
    @welldan97 Downvoting not for your explanation -- which is excellent -- but because the Garber-Irish structure is evil. It loads all your JS on every page, and depends on classes and IDs on the element to sort things out. That's a sure sign of fighting the DOM: under normal circumstances the element should not need a class or ID, since there's only ever one in a document. The proper way to do this is simply to remove the `//= require_tree .` and use page-specific JavaScript. If you're actively trying not to do that, then you're striving for bad practice. – Marnen Laibow-Koser Jun 27 '12 at 22:43
  • 2
    @MarnenLaibow-Koser Personally I believe that loading all js on every page is good for the most projects when you combine all js into one file and minimize it. I believe it overall works faster for user. At least its more like conflict one js file vs many(i.e. look at http://stackoverflow.com/questions/555696/put-javascript-in-one-js-file-or-break-it-out-into-multiple-js-files). Also there is nothing bad in using classes and ids on body if it makes code simpler and works for you. Modernizr(http://modernizr.com/) does this, and some other libs too. – welldan97 Jun 28 '12 at 18:27
  • @MarnenLaibow-Koser Overall as I said I try not to use page specific js anymore. Mostly because it makes code more complex. I found that's it's better to use js dependent on class. I.e. instead of `$('#page1 .datepicker')` use something like `$('.datepicker.special')`. It makes code more reusable. And I still stay with one file for all js approach. – welldan97 Jun 28 '12 at 18:34
  • @welldan97 It sounds like where you're referring to "page specific js", you're really talking about page-specific IDs, right? If so, I misunderstood. I agree that `$('.datepicker.special')` is better than `$('#page1 .datepicker')`. Better still, though, is to put `//= require special_datepicker` in page1's HTML or JS -- that way you don't load anything you don't need, and you don't clutter your HTML with extra classes. You can't do this with one big JS file, which is why one big file is a bad idea. – Marnen Laibow-Koser Jun 28 '12 at 19:03
  • @welldan97 "There is nothing bad in using classes and ids on body if it makes code simpler": wrong. First, it never makes code simpler, because its only purpose is to facilitate the inclusion of JS code that will not get run and therefore should not have been included -- that is, it represents a failure to modularize JS code properly. Second, it represents a failure to understand the proper use of the DOM. Classes and IDs are only for selection within one HTML document. Since there is only one `` element, it doesn't need an ID. I'm aware that the practice is common, but it's still bad. – Marnen Laibow-Koser Jun 29 '12 at 00:14
  • @Marnen While I agree with the comment about the body element, the fact that people are using the Garber-Irish may indicate that a change is coming. On the other hand, I think that your idea of using conditional inclusion of JS code is a misunderstanding of the term "modular". In compiled code, all modules are compiled and included from the get go: JS should be no different. The benefits of modularity are re-usability and organization. It is my understanding that separate script tags leads to longer page load time. – Ziggy Mar 29 '13 at 21:42
  • @Ziggy I don't think a change is coming; rather, I think people are forgetting the fundamentals of the craft of Web development. As far as modularity, I don't see how your comment about compilation is relevant. JS isn't compiled. Also, modularity is about code organization, not what gets compiled with what. – Marnen Laibow-Koser Apr 05 '13 at 01:17
  • @Ziggy As for separate ` – Marnen Laibow-Koser Apr 05 '13 at 01:19
  • @Ziggy "In compiled code, all modules are compiled and included from the get go: JS should be no different." On what basis do you say that last sentence? (I think you're dead wrong, in case it wasn't apparent from my earlier posts, but I'd like to know what you're saying here.) – Marnen Laibow-Koser Apr 05 '13 at 01:20
  • 2
    @MarnenLaibow-Koser the rails asset pipeline, to me, seems like a good candidate for comparison with compilation. A programmer writes their javascript in nice decoupled modules, and then it is lumped together, minified, and served. Just as in the case of compiled languages, there will always be programmers who think they are a step ahead of the compiler... but I think this is rarely true. – Ziggy Apr 05 '13 at 03:18
  • @MarnenLaibow-Koser the quotation you cite above, it represents me agreeing with the rails asset pipeline way of doing things. It also comes from an intuition regarding bottle-necks. I compare the choice between including many conditional script-tags vs many conditional statements to the 'von-neumann bottleneck'. You may have to do many many conditional checks in your code, but it is worth while if you avoid one more script inclusion because of the difference in the time it takes to execute a condition vs load a script. – Ziggy Apr 05 '13 at 03:21
  • @MarnenLaibow-Koser having said that: I'm not carrying a torch. I could be convinced that a browser's caching strategy might be clever enough that we could benefit more by splitting scripts up and including them conditionally. Basically, what it comes down to, is whether the framework I am using has adopted that strategy or not. On the surface, I came to this question looking for the "way to conditionally include script tags", and only after some reading did I decide to throw in with the pipeline. I leave these sorts of decisions to the teams of geniuses responsible for the frameworks. – Ziggy Apr 05 '13 at 03:25
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/27600/discussion-between-ziggy-and-marnen-laibow-koser) – Ziggy Apr 05 '13 at 03:26
  • @Ziggy The Rails asset pipeline is not as smart as the typical compiler, so your analogy with trying to outsmart the compiler is inapt. Rather, I'm talking about packaging scripts so that, considering pipeline and everything else, the best results are achieved. In most cases, that means lots of small script files. That's not outsmarting the pipeline; it's working with it. – Marnen Laibow-Koser Apr 07 '13 at 18:18
  • @Ziggy I think you're under a misapprehension as to what the asset pipeline is. The asset pipeline is not the same thing as `require_tree .`. It is perfectly reasonable to use the asset pipeline only for translation from CoffeeScript/Sass/whatever, and then use HTML tags to include the resulting JS or CSS files individually. While it's true that a large JS file only requires one HTTP connection, it can't be partially cached. – Marnen Laibow-Koser Apr 07 '13 at 18:36
  • @Ziggy Example: if you have one page that requires a.js and b.js, and another that requires b.js and c.js, your approach would require ab.js for the first page and bc.js for the second -- forcing the client to download the contents of b.js twice -- or would use abc.js on both pages, forcing the browser to interpret a lot of unnecessary code. But if you keep the JS files separate, then if the client visits both pages, b.js can be retrieved from the cache and only the actual new code has to come from the server. Less network activity, less unnecessary JS code: this is the proper practice. – Marnen Laibow-Koser Apr 07 '13 at 18:40
  • @Ziggy As for the "teams of geniuses", the Rails core team has IMHO never, since the first version of Rails, really known how to handle JS properly. I'm perfectly happy to ignore some of their suggestions, since I know well that some of these ideas are quite bad. Remember, don't assume that the core team is perfect. – Marnen Laibow-Koser Apr 07 '13 at 18:41
64

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 is 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.

starball
  • 20,030
  • 7
  • 43
  • 238
sujal
  • 1,595
  • 1
  • 13
  • 14
  • 8
    +1 This is great to know for a newbie like me. I'd give it +2 if I could. – jrhorn424 Mar 17 '12 at 22:16
  • 6
    @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 Jun 27 '12 at 22:39
  • 1
    Thanks a lot for this advice. I not have several "top-level" JS files, depending on the module of my app. Works well. – elsurudo Aug 08 '13 at 07:42
  • 1
    +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. – zelanix May 08 '14 at 09:30
41

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.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
ClosureCowboy
  • 20,825
  • 13
  • 57
  • 71
  • 13
    You've made me crave cupcakes now.. Dangit! – Chuck Bergeron Oct 12 '11 at 21:43
  • I really like this solution. The only problem I have with it is that those extra manifests are not compressed/uglified. They are properly compiled though. Is there a solution or am I missing something? – clst Dec 08 '11 at 16:31
  • 1
    does this mean that the browser loads one js file, that is a combination of global + page specific file? – lulalala Mar 06 '12 at 06:44
  • Could you take a look at my question if your available? http://stackoverflow.com/questions/17055213/javascripts-not-precompiled – Maximus S Jun 12 '13 at 02:22
  • 1
    @clst I believe this is the answer you're looking for: http://guides.rubyonrails.org/asset_pipeline.html#precompiling-assets – FrontierPsycho Jun 21 '13 at 10:49
  • Thank you. That look like what I needed back then. I have now given up on the idea and compile everything in one big file. My JS code is executed based on the class attributes of . It seems not worth the hassle to split it. – clst Jun 24 '13 at 09:12
23

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.

Fire Emblem
  • 5,961
  • 3
  • 24
  • 37
  • But you actually want those manual ` – Marnen Laibow-Koser Jun 30 '12 at 16:38
  • 4
    @MarnenLaibow-Koser the reason for not adding manual script tags to each unique page is that you have to download that script content on every page view. If you are able to package all javascript into application.js using the asset pipeline, then the user downloads those scripts one time only, and pulls application.js from the cache on all subsequent page loads – jakeonrails Jan 09 '13 at 22:11
  • @jakeonrails "the reason for not adding manual script tags to each unique page is that you have to download that script content on every page view"—quite wrong. The script will be downloaded once, then will be fetched from the browser's cache on further requests. "If you are able to package all javascript into application.js using the asset pipeline, then the user downloads those scripts one time only"—true, but at the cost of lots of unnecessary code. If you can structure your JS into many little files instead of one big one, you get caching benefits without unnecessary code. – Marnen Laibow-Koser Jan 28 '13 at 03:45
  • See also my comment at http://stackoverflow.com/questions/6167805/using-rails-3-1-where-do-you-put-your-page-specific-javascript-code/6171275#comment15056011_8991853. – Marnen Laibow-Koser Jan 28 '13 at 03:48
  • 1
    @MarnenLaibow-Koser I think it would have been better to say that if you package everything into one script, your user only has to download 1 script for any page of your site. If you have multiple scripts for different parts of your app, then obviously the user must download more than one script. Both methods will be cached, of course, but in most cases (small-medium apps), serving one application.js one time will be more efficient for downloading. Parsing of the JS may be another story, depending on what you serve up. – jakeonrails Jan 28 '13 at 19:18
  • @jakeonrails "serving one application.js one time will be more efficient for downloading"—I don't believe you're right, at least not unless you can expect a visitor to actually visit pages where all that functionality is necessary. And even if he *does*, the extra code causes problems. Nice modular JS is more maintainable, and cacheable at a more granular level, than one big application.js file. I'd argue that due to the other properties I just mentioned, modular JS is also, on balance, more efficient than one big file. IMHO, one big file is almost never the right solution. – Marnen Laibow-Koser Jan 30 '13 at 21:51
  • @MarnenLaibow-Koser what about Single Page Applications using backbone/angular/ember, etc? Or apps that use the upcoming Turbolinks from Rails 4? Those are situations where you incur the cost of downloading once, parsing, once, and then your entire app is ready for use and there's no need to download new parts as the user navigates to different sections. With mobile latency being what it is, I think this download-once philosophy makes sense as well. I'm definitely not saying you are wrong, just there are some situations where it makes sense. – jakeonrails Jan 31 '13 at 02:10
  • @jakeonrails When there's one page, of course you download all the JavaScript as part of it -- this is beyond obvious. For a multiple-page application, it's usually worth modularizing the JS as I've been describing. (I usually prefer writing multiple-page apps, because I believe very strongly in the principle of progressive enhancement.) – Marnen Laibow-Koser Jan 31 '13 at 03:22
  • @jakeonrails Also, you're conflating two different considerations: whether to have one HTML page or many, and whether to have one JS file or many. – Marnen Laibow-Koser Jan 31 '13 at 03:23
  • @MarnenLaibow-Koser Because browser caches use a 'least-recently-used' policy, many small javascript files that are used in 8 out of 100 pages will actually drop from the cache and need to be redownloaded again and again. One application.js file will stay in the cache longer since it is referenced on every page. See also http://stackoverflow.com/a/4976931/29182. Google recommends deferred loading of user initiated functions. However, they say that this is only worthwhile if there are at least 25 such function. https://developers.google.com/speed/docs/best-practices/payload#DeferLoadingJS – Ziggy Jun 19 '13 at 20:23
  • @Ziggy But a large file will use up more cache space, so it will need to be dropped sooner than would a small file at the same place in the "least-recently-used" queue. Small files are more cacheable for this reason. – Marnen Laibow-Koser Jun 25 '13 at 19:26
  • @MarnenLaibow-Koser right, but I argue that the small file used on 8/100 pages will never be in the same place in the least-recently-used queue. It will always be much farther down. I see what you mean, but take jQuery for example, it is like 11800 lines long and is 200K... but this is many times smaller than even a small image. One large minified js file in the cache per rich-client site will still only represent a small minority of cached data, and if the user visits those sites frequently (as in gmail, or stackoverflow) that one file will remain very cacheable. PS: I enjoy our talks :) – Ziggy Jun 25 '13 at 20:29
  • @Ziggy FWIW, jQuery is only 81.5K minified or 32K gzipped. Small images (such as are used for UI elements) are often only a couple of K. If your small images are "many times" larger than 200K, you're doing it wrong. :) As for caching one large file, it's all or nothing. If you have a 200K file in the cache and you need 20K of cache space, you can't drop only 20K. If instead of that 200K file, you have ten 20K files, you can selectively drop. Better performance for the user, better modularization of your code. – Marnen Laibow-Koser Jun 26 '13 at 14:52
  • 1
    @Ziggy Also, if the small file is only used on 8 out of 100 pages, why should the code sit in the user's cache all the time? Better to actually drop the stuff that's not needed. – Marnen Laibow-Koser Jun 26 '13 at 14:54
16

I realize I'm coming to this party a bit late, but I wanted to throw in a solution that I've been using lately. However, let me first mention...

The Rails 3.1/3.2 Way (No, sir. I don't like it.)

See: http://guides.rubyonrails.org/asset_pipeline.html#how-to-use-the-asset-pipeline

I'm including the following for the sake of completeness in this answer, and because it's not an unviable solution... though I don't care much for it.

The "Rails Way" is a controller-oriented solution, rather than being view-oriented as the original author of this question requested. There are controller-specific JS files named after their respective controllers. All of these files are placed in a folder tree that is NOT included by default in any of the application.js require directives.

To include controller-specific code, the following is added to a view.

<%= javascript_include_tag params[:controller] %>

I loathe this solution, but it's there and it's quick. Presumably, you could instead call these files something like "people-index.js" and "people-show.js" and then use something like "#{params[:controller]}-index" to get a view-oriented solution. Again, quick fix, but it doesn't sit well with me.

My Data Attribute Way

Call me crazy, but I want ALL of my JS compiled and minified into application.js when I deploy. I don't want to have to remember to include these little straggler files all over the place.

I load all of my JS in one compact, soon-to-be browser cached, file. If a certain piece of my application.js needs to be fired on a page, I let the HTML tell me, not Rails.

Rather than locking my JS to specific element IDs or littering my HTML with marker classes, I use a custom data attribute called data-jstags.

<input name="search" data-jstag="auto-suggest hint" />

On each page, I use - insert preferred JS library method here - to run code when the DOM has finished loading. This bootstrapping code performs the following actions:

  1. Iterate over all elements in the DOM marked with data-jstag
  2. For each element, split the attribute value on space, creating an array of tag strings.
  3. For each tag string, perform a lookup in a Hash for that tag.
  4. If a matching key is found, run the function that is associated with it, passing the element as a parameter.

So say I have the following defined somewhere in my application.js:

function my_autosuggest_init(element) {
  /* Add events to watch input and make suggestions... */
}

function my_hint_init(element) {
  /* Add events to show a hint on change/blur when blank... */
  /* Yes, I know HTML 5 can do this natively with attributes. */
}

var JSTags = {
  'auto-suggest': my_autosuggest_init,
  'hint': my_hint_init
};

The bootstrapping event is going to apply the my_autosuggest_init and my_hint_init functions against the search input, turning it into an input that displays a list of suggestions while the user types, as well as providing some kind of input hint when the input is left blank and unfocused.

Unless some element is tagged with data-jstag="auto-suggest", the auto-suggest code never fires. However, it's always there, minified and eventually cached in my application.js for those times that I need it on a page.

If you need to pass additional parameters to your tagged JS functions, you'll have to apply some creativity. Either add data-paramter attributes, come up with some kind of parameter syntax, or even use a hybrid approach.

Even if I have some complicated workflow that seems controller-specific, I will just create a file for it in my lib folder, pack it into application.js, and tag it with something like 'new-thing-wizard'. When my bootstrap hits that tag, my nice, fancy wizard will be instantiated and run. It runs for that controller's view(s) when needed, but is not otherwise coupled to the controller. In fact, if I code my wizard right, I might be able to provide all configuration data in the views and therefore be able to re-use my wizard later for any other controller that needs it.

Anyway, this is how I've been implementing page specific JS for a while now, and it has served me well both for simple site designs and for more complex/rich applications. Hopefully one of the two solutions I've presented here, my way or the Rails way, is helpful to anyone who comes across this question in the future.

Ryan
  • 1,751
  • 15
  • 18
  • 6
    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. – sujal Apr 10 '12 at 15:47
  • For more on the practical effects of that compilation step I'm talking about, see 37 Signals' explanation of how pjax impacted Basecamp Next: http://37signals.com/svn/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui – sujal Apr 10 '12 at 15:50
  • 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. – Ryan Apr 18 '12 at 20:22
  • 2
    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. – Marnen Laibow-Koser Jun 30 '12 at 16:41
  • 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. – Ryan Jul 07 '12 at 18:08
  • @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. – Marnen Laibow-Koser Jul 10 '12 at 14:50
7

This has been answered and accepted long ago, but I came up with my own solution based on some of these answers and my experience with Rails 3+.

The asset pipeline is sweet. Use it.

First, in your application.js file, remove //= require_tree.

Then in your application_controller.rb create a helper method:

helper_method :javascript_include_view_js //Or something similar

def javascript_include_view_js
    if FileTest.exists? "app/assets/javascripts/"+params[:controller]+"/"+params[:action]+".js.erb"
        return '<script src="/assets/'+params[:controller]+'/'+params[:action]+'.js.erb" type="text/javascript"></script>'
    end
end

Then in your application.html.erb layout file, add your new helper among the existing javascript includes, prefixed with the raw helper:

<head>
    <title>Your Application</title>
    <%= stylesheet_link_tag "application", :media => "all" %>
    <%= javascript_include_tag "application" %>
    <%= raw javascript_include_view_js %>
</head>

Voila, now you can easily create view-specific javascript using the same file structure you use everywhere else in rails. Simply stick your files in app/assets/:namespace/:controller/action.js.erb!

Hope that helps someone else!

Mike A
  • 2,501
  • 2
  • 23
  • 30
  • 1
    Wont this cause problems after assets are precompiled and at run time the `<%= raw ... %>` will return a 404 ? – Nishant Jan 25 '13 at 13:12
  • 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. – Deborah Nov 21 '13 at 20:29
  • 1
    @DeborahSpeece When does the asset pipeline create files that shouldn't be used? Are you confusing the asset pipeline (good) with `require_tree /` (bad)? – Marnen Laibow-Koser Dec 11 '13 at 04:15
6

You can add this line in your layout file (e.g. application.html.erb) to automatically load the controller specific javascript file (the one that was created when you generated the controller):

<%= javascript_include_tag params[:controller] %>

You also could add a line to automatically load a script file in a per-action basis.

<%= javascript_include_tag params[:controller] + "/" + params[:action] %>

Just put your page scripts into a subdirectoriy named after the controller name. In these files you could include other scripts using =require. It would be nice to create a helper to include the file only if it exists, to avoid a 404 fail in the browser.

mcubik
  • 135
  • 1
  • 6
6
<%= javascript_include_tag params[:controller] %>
Mr Bohr
  • 125
  • 3
  • 8
  • 2
    This looks like it might be able to answer the question. Could you please add more to the answer to flesh it out? –  Mar 21 '13 at 12:14
6

Maybe you will find pluggable_js gem as suitable solution.

peresleguine
  • 2,343
  • 2
  • 31
  • 34
5

The LoadJS gem is another option:

LoadJS provides a way to load page-specific Javascript code in a Rails app without loosing the magic provided by Sprockets. All your Javascript code will continue by minified in one Javascript file but some portions of it will only be executed for certain pages.

https://github.com/guidomb/loadjs

Community
  • 1
  • 1
meleyal
  • 32,252
  • 24
  • 73
  • 79
3

Philip's answer is quite good. Here is the code to make it work:

In application.html.erb:

<body class="<%=params[:controller].parameterize%>">

Assuming your controller is called Projects, that will generate:

<body class="projects">

Then in projects.js.coffee:

jQuery ->
  if $('body.projects').length > 0  
     $('h1').click ->
       alert 'you clicked on an h1 in Projects'
Rahil Sondhi
  • 656
  • 6
  • 7
  • Downvoting: any solution that puts a class on the `` is *ipso facto* incorrect. See my comments elsewhere on this page. – Marnen Laibow-Koser Jun 30 '12 at 16:43
  • Don't do this. That problem here is that every time you add one of these, you're adding another piece of js that needs to be executed on page load. Could definitely cause some performance degradation as your project grows. – ifightcrime Jun 10 '15 at 16:03
2

I agree with your answer, to check if that selector is there, use:

if ($(selector).length) {
    // Put the function that does not need to be executed every page
}

(didn't see anyone add the actual solution)

Adi Inbar
  • 12,097
  • 13
  • 56
  • 69
Kyle C
  • 4,077
  • 2
  • 31
  • 34
2

You can also group the js in folders and continue to use the asset pipeline to load your javascript selectively depending on the page.

Community
  • 1
  • 1
Kenny Grant
  • 9,360
  • 2
  • 33
  • 47
2

I don't see an answer that really puts it all together and lays it out for you. Thus, I'll try to put meleyal, sujal (a la ClosureCowboy), the first part of Ryan's answer, and even Gal's bold statement about Backbone.js... all together in a way that is short and clear. And, who knows, I might even meet Marnen Laibow-Koser's requirements.

Example edits

assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require lodash.underscore.min
...


views/layouts/application.html.erb

  ...
  </footer>

  <!-- Javascripts ================================================== -->
  <!-- Placed at the end of the document so the pages load faster -->
  <%= javascript_include_tag "application" %>
  <%= yield :javascript %>

</body>
</html>


views/foo/index.html.erb

...
<% content_for :javascript do %>
  <%= javascript_include_tag params[:controller] %>
<% end %>


assets/javascripts/foo.js

//= require moment
//= require_tree ./foostuff


assets/javascripts/foostuff/foothis.js.coffee

alert "Hello world!"


Brief description

  • Remove //= require_tree . from application.js and list only the JS that each page shares.

  • The two lines shown above in application.html.erb tell the page where to include application.js and your page-specific JS.

  • The three lines shown above in index.html.erb tells your view to look for some page-specific JS and include it at a named yield region called ":javascript" (or whatever you want to name it). In this example, the controller is "foo" so Rails will attempt to include "foo.js" at the :javascript yield region in the application layout.

  • List your page-specific JS in foo.js (or whatever the controller is named). List common libraries, a tree, directories, whatever.

  • Keep your custom page-specific JS someplace where you can easily reference it apart from your other custom JS. In this example, foo.js requires the foostuff tree so put your custom JS there, such as foothis.js.coffee.

  • There are no hard rules here. Feel free to move things around and perhaps even create multiple yield regions of various names in various layouts if needed. This just shows one possible first step forward. (I don't do it exactly like this given our use of Backbone.js. I might also choose to drop foo.js down into a folder called foo instead of foostuff but haven't decided that yet.)

Notes

You can do similar things with CSS and <%= stylesheet_link_tag params[:controller] %> but this is beyond scope of the question.

If I missed a glaring best practice here, send me a note and I'll conisder adapting. Rails is fairly new to me and, honestly, I'm not terribly impressed so far with the chaos it brings by default to enterprise development and all the traffic the average Rails program generates.

juanitogan
  • 1,698
  • 1
  • 22
  • 37
2

JavaScripts are only merged when you tell Rails (Sprockets, rather) to merge them.

yfeldblum
  • 65,165
  • 12
  • 129
  • 169
  • Of course. I guess I ask because Rails' defaults includes everything in the folder... which means David intends for you to do that. But like I said in the other comment to @rubyprince, I am unsure about execution when it is done this way. I am thinking I have to disable `//= require_tree .` ? – Fire Emblem May 29 '11 at 13:21
  • @FireEmblem Yes. `require_tree .` is usually a bad idea. – Marnen Laibow-Koser Jun 30 '12 at 16:44
2

This is how i solved the styling issue: (excuse the Haml)

%div{:id => "#{params[:controller].parameterize} #{params[:view]}"}
    = yield

This way i start all the page specific .css.sass files with:

#post
  /* Controller specific code here */
  &#index
    /* View specific code here */
  &#new
  &#edit
  &#show

This way you can easily avoid any clashes. When it comes to .js.coffee files you could just initialize elements like;

$('#post > #edit') ->
  $('form > h1').css('float', 'right')

Hope this helped some.

zeeraw
  • 5,197
  • 1
  • 21
  • 27
  • 1
    Read the last bit again please, for javascript you can take advantage of the same structure used for stylesheets for initializing view specific functions. – zeeraw May 30 '11 at 06:21
  • Philip, `$('#post > #edit') ->` seems to be invalid. How do you scope jQuery to work within a scope? – Ramon Tayag Sep 24 '11 at 09:41
  • 2
    Recently I've started loading all controller specific java scripts and style sheets by calling this in the application.html.haml; `= javascript_include_tag "application"` and `= javascript_include_tag params[:controller]` this way I can keep javascript code confined without having to specify a scope inside of the file. – zeeraw Sep 24 '11 at 16:25
1

I have another solution, which although primitive works fine for me and doesn't need any fancy selective loading strategies. Put in your nornal document ready function, but then test the current windows location to see if it is the page your javascript is intended for:

$(document).ready(function() {
   if(window.location.pathname.indexOf('/yourpage') != -1) {
          // the javascript you want to execute
   }
}

This still allows all the js to be loaded by rails 3.x in one small package, but does not generate much overhead or any conflicts with pages for which the js isn't intended.

1

ryguy's answer is a good answer, even though its been downvoted into negative points land.

Especially if you're using something like Backbone JS - each page has its own Backbone view. Then the erb file just has a single line of inline javascript that fires up the right backbone view class. I consider it a single line of 'glue code' and therefore the fact that its inline is OK. The advantage is that you can keep your "require_tree" which lets the browser cache all the javascript.

in show.html.erb, you'll have something like:

<% provide :javascript do %>
  <%= javascript_include_tag do %>
    (new app.views.ProjectsView({el: 'body'})).render();
  <% end %>
<% end do %>

and in your layout file, you'll need:

<%= yield :javascript %>
Gal
  • 5,537
  • 1
  • 22
  • 20
1

Move all your commom JS files to a sub-folder like 'app/assets/javascript/global' then in the application.js, modify the //= require_tree . line to //= require_tree ./global.

Now you are free to put your controller-specific JS on the 'app/assets/javascript/' root and they will not be included in compiled JS, being used just when you call them via = javascript_include_tag on your controller/view.

Adi Inbar
  • 12,097
  • 13
  • 56
  • 69
Gedean Dias
  • 1,063
  • 1
  • 10
  • 24
1

Though you have several answers here, I think your edit is probably the best bet. A design pattern that we use in our team that we got from Gitlab is the Dispatcher pattern. It does something similar to what you're talking about, however the page name is set in the body tag by rails. For example, in your layout file, just include something like (in HAML):

%body{'data-page' => "#{controller}:#{action}" }

Then only have one closure and a switch statement in your dispatcher.js.coffee file in your javascripts folder like so:

$ ->
  new Dispatcher()

class Dispatcher
  constructor: ->
    page = $('body').attr('data-page')
    switch page
      when 'products:index'
        new Products() 
      when 'users:login'
        new Login()

All you need to do in the individual files (say products.js.coffee or login.js.coffee for example) is enclose them in a class and then globalize that class symbol so you can access it in the dispatcher:

class Products
  constructor: ->
    #do stuff
@Products = Products

Gitlab has several examples of this that you might want to poke around with in case you're curious :)

onetwopunch
  • 3,279
  • 2
  • 29
  • 44
1

Paloma project offers interesting approach to manage page specific javascript code.

Usage example from their docs:

var UsersController = Paloma.controller('Users');

// Executes when Rails User#new is executed.
UsersController.prototype.new = function(){
   alert('Hello Sexy User!' );
};
zavg
  • 10,351
  • 4
  • 44
  • 67
1

Step1. remove require_tree . in your application.js and application.css.

Step2. Edit your application.html.erb(by rails default) in layout folder. Add "params[:controller]" in the following tags.

<%= stylesheet_link_tag    'application', params[:controller], media: 'all', 'data-turbolinks-track' => true %>

<%= javascript_include_tag 'application', params[:controller], 'data-turbolinks-track' => true %>

Step3. Add a file in config/initializers/assets.rb

%w( controller_one controller_two controller_three ).each do |controller|
  Rails.application.config.assets.precompile += ["#{controller}.js", "#{controller}.js.coffee", "#{controller}.css", "#{controller}.scss"]
end

references: http://theflyingdeveloper.com/controller-specific-assets-with-rails-4/

etlds
  • 5,810
  • 2
  • 23
  • 30
  • Whilst this may theoretically answer the question, [it would be preferable](//meta.stackoverflow.com/q/8259) to include the essential parts of the answer here, and provide the link for reference. – Bhargav Rao Mar 14 '16 at 15:26
0

I did it previously using this method: http://theflyingdeveloper.com/controller-specific-assets-with-rails-4/ . Super-easy, relies on controllers to select the proper js to load.

Eddie Prislac
  • 145
  • 1
  • 10
0

I combined some answers into:

Application helper:

module ApplicationHelper
  def js_page_specific_include
    page_specific_js = params[:controller] + '_' + params[:action]
    if Rails.application.assets.find_asset(page_specific_js).nil?
      javascript_include_tag 'application', 'data-turbolinks-track' => true
    else
      javascript_include_tag 'application', page_specific_js, 'data-turbolinks-track' => true
    end
  end
end

layouts/application.html.haml:

 <!DOCTYPE html>
%html{lang: 'uk'}
  %head   
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
   bla-bla-bla
    = js_page_specific_include   
   bla-bla-bla  
kapellan
  • 301
  • 4
  • 6
0

First: remove \\=require_treefrom application.js Second: all your JS code must be alocated at /app/assets/javascritpt and all your CSS code must be alocated at /app/assets/stylesheets

Nicollas
  • 250
  • 2
  • 9
0

I haven't tried this out, but it looks like the following is true:

  • if you have a content_for that is javascript (e.g. with real javascript within it), sprockets would not know about it and thus this would work the same way as it does now.

  • if you want to exclude a file from the big bundle of javascript, you would go into config/sprockets.yml file and modify the source_files accordingly. Then, you would just include any of the files that you excluded where needed.

Bill Eisenhauer
  • 6,183
  • 2
  • 30
  • 28
  • Is excluding files or using custom javascript on the page itself the "right way" then? Is that how David intended people to use it? – Fire Emblem May 29 '11 at 23:57
  • @FireEmblem I don't much care what David intended, because I don't think David understands how to organize JavaScript properly. – Marnen Laibow-Koser Jun 30 '12 at 16:46
-2

Following the lead from Ryan, here's what I have done-

application.js.coffee

$ ->
    view_method_name = $("body").data("view") + "_onload"
    eval("#{view_method_name}()") if eval("typeof #{view_method_name} == 'function'")
    view_action_method_name = $("body").data("view") + "_"+$("body").data("action")+"_onload"
    eval("#{view_action_method_name}()") if eval("typeof #{view_action_method_name} == 'function'")

users.js.coffee (controller specific coffeescript,e.g controller:users, action:dashboard)

window.users_dashboard_onload = () ->
    alert("controller action called")
window.users_onload = () ->
    alert("controller called")

application.html.haml

%body{:data=>{:view=>controller.controller_name, :action=>controller.action_name}}
Kruttik
  • 404
  • 3
  • 7
  • 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 Jan 25 '14 at 01:37
-3

Here's how to do it especially if you don't have to execute tons of libraries for your specific page, but only to run a few hundreds lines of JS more or less.

Since it's perfectly fine to embed Javascript code into HTML, just create under app/views shared.js directory and place there your page/pages specific code inside my_cool_partial.html.erb

<script type="text/javascript"> 
<!--
  var your_code_goes_here = 0;
  function etc() {
     ...
  }
-->
</script>

So now from wherever you want you simply do:

  = render :partial => 'shared.js/my_cool_partial'

And that's it, k?

valk
  • 9,363
  • 12
  • 59
  • 79