14

I have a WYSIWYG editor that I have built into the site and customised. There are a lot of Javascript files that only need to be loaded on the pages with the WYSIWYG editor; currently they are loaded on every page (and even break other Javascript on certain pages).

Currently the Javascript files are in assets/javascript/wysiwyg/ and aren't included as the require files in application.js but are still included on every page because of the assets pipeline (I think).

I want to know if I can exclude these files from the other pages. Is it possible to move them from the assets pipeline to the public/ directory and import them (in the coffee script files, maybe?) into the appropriate views?

Michael Gaskill
  • 7,913
  • 10
  • 38
  • 43
Rob
  • 1,835
  • 2
  • 25
  • 53
  • 1
    Possible duplicate of [Using Rails 3.1, where do you put your "page specific" javascript code?](http://stackoverflow.com/questions/6167805/using-rails-3-1-where-do-you-put-your-page-specific-javascript-code) – Graham Slick May 21 '16 at 09:04
  • 1
    @GrahamSlick I don't believe that this is the same question. The referenced page was a question about the page-specific javascript files that are created within your app (e.g. `person.js` for the `Person` model). If I'm reading it correctly, this question seems to be about how to load Javascript files manually on a per-page basis. To my mind, that's actually a different question that the referenced question. It may still be a duplicate of another question, though. – Michael Gaskill May 21 '16 at 20:33

2 Answers2

23

You can put any Javascript files that you want to load manually in the public/javascripts/lib directory of your application, and they will not be included in the assets pipeline. You can then load them manually on the pages that need them.

For instance, in one project, I use the Chosen jQuery plugin, and I load it like so:

<script type="text/javascript" src="/javascripts/lib/chosen.jquery.min.js"></script>

Rails will source the public files from public/, so you only need to reference your files from there (remove the public/ bit).

This project is fairly large, with 88 controllers, 662 actions, and a total of 38 custom javascript libraries that get used sporadically around the app, including markdown editors, charting libraries, and even jQuery UI.

To manage the sprawl and to keep each page as tight as possible, I have done 2 things: 1) in my controller, I set an instance variable,@page_libs, to list the libs to load, and 2) the layout uses the values in @page_libs to include the specialty Javascript when required.

A controller action might look like this:

def edit
  @products = products.find(params[:id])
  @page_libs = [:ui, :textile]
end

And the app/views/layouts/application.html.erb includes this in the correct place:

<%- if @page_libs&.include?(:ui) || @page_libs&.include?(:table) %>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script>
    <script type="text/javascript" src="/javascripts/lib/chosen.jquery.min.js"></script>
<% end -%>
<%- if @page_libs&.include?(:swiper) %>
    <script type="text/javascript" src="/javascripts/lib/idangerous.swiper.min.js"></script>
<% end -%>
<%- if @page_libs&.include?(:table) %>
    <script type="text/javascript" src="/javascripts/lib/jquery.handsontable.full.js"></script>
<% end -%>
<%- if @page_libs&.include?(:textile) %>
    <script type="text/javascript" src="/javascripts/lib/textile.js" charset="utf-8"></script>
<% end -%>

Note that the first include is for jQuery UI, which I load from a CDN, rather than from my app's public. This technique works just as well with external libraries, as well as those that you host. In fact, most pages in my app only depend on 2 external libraries (jQuery and Underscore.js), but have the option of loading up to 16 other Javascript libraries from external sources. Limiting external libraries on the page can significantly reduce your page load times, which is a direct performance boost for your application.

Sometimes, a Javascript library will include CSS components, as well. Or, you may even have page-specific CSS to include. The same approach can be taken with external stylesheet. These are the corresponding page-specific stylesheet "includes" for the above Javascript libraries:

<%- if @page_libs&.include?(:ui) %>
    <link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css">
    <link rel="stylesheet" type="text/css" href="/stylesheets/lib/chosen.min.css">
<% end -%>
<%- if @page_libs&.include?(:swiper) %>
    <link rel="stylesheet" type="text/css" href="/stylesheets/lib/idangerous.swiper.css">
<% end -%>

This way, I have a single point in the project to manage the libraries, regardless of how many (or few) are required for a page. I'll probably eventually create a series of custom before_action handlers in the ApplicationController to define which the libraries a page needs included. Something like this:

before_action: :include_library_ui,     only: [:new, :edit]
before_action: :include_library_swiper, only: [:show]

This would clean up the controller actions a bit more and make it easier to identify dependencies. However, given the size of my codebase and the pressing tasks remaining, I haven't taken this leap yet. Maybe it will inspire you to do so to start out.

Michael Gaskill
  • 7,913
  • 10
  • 38
  • 43
  • Thanks I'll use this method. One problem is that there are like 50 js files, I dont want to link them all individually. is there a way to link the folder they are in and all js files in it included. – Rob May 22 '16 at 00:41
  • When you say 'link', do you mean in the header? I believe that you have to do them one at a time. if you do a directory listing from your console, you should be able to create an editor script / macro that will make that a moment's work. Then, your challenge is to decide the files that you want included in which conditions. – Michael Gaskill May 22 '16 at 00:49
  • Yea thats what I thought, Ill link them one at a time. Cheers – Rob May 22 '16 at 01:17
  • 1
    A good way to clean up the script tags in your `layouts/application.html.erb` file would be to define a view helper that maps values from `@page_libs` to the full string value of the corresponding `link` / `script` tag. Then, your application layout could simply call `<% @page_libs.each do |l| %> <%= import_tag(l) %> <% end %>` – Ryan Lue Jan 05 '18 at 07:39
  • 1
    Hey @MichaelGaskill - I like your simple solution, thanks! – Ox Smith Jan 13 '18 at 21:00
  • Also see this https://stackoverflow.com/questions/7061575/rails-3-1-pipeline-exclude-javascript-file?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa to exclude some of the js files to be loaded in every page – dani24 Apr 13 '18 at 18:00
  • @ Michael Gaskill: How can use public folder in Rails Engine ?. – D T May 24 '18 at 09:19
  • @DT I don't understand what you're asking. Do you mean how do you access resources from the public folder of your app in this manner? There's really no difference; you simply use the appropriate URL for your application resource, instead of an off-site resource URL. – Michael Gaskill May 24 '18 at 15:49
10

This is getting old now and we're in a Webpacker world with Rails 6, but if you want to keep things more simple in the old school Sprockets way, you might like the approach described below. Note that it really is per-view - other answers have good approaches for wider scope per-controller stuff.

Have your main layout declare a 'content for' section. For example, in application.htm.erb:

<!DOCTYPE html>
<html>
  <head>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    ...etc...

    <%= content_for :head %>

  </head>
  ...
</html>

Here, :head is just a label and you can give it any name you want. You can have as many such declarations in your layout as you want, too. They're just Rails's way of letting views insert extra stuff into those bits of your outer layout. So - you use this from within individual views to add your JS file(s) <script> tags inside the <head> section of the layout.

For example, suppose I have manually "installed" the zxcvbn.js library by copying its one JavaScript file into vendor/assets/javascripts/zxcvbn.js. I have an edit.html.erb page where this is required, so at the top of that file, I add:

<% content_for :head do %>
  <%= javascript_include_tag('zxcvbn', 'data-turbolinks-track': 'reload') %>
<% end %>

...removing the Turbolinks attribute if you're not using it. This means that when ERB compiles the page, it'll substitute that tag inside the 'content for head' part of the layout, so the script tag ends up in its place. This will of course mean an extra HTTP fetch when the page loads, but only in the views where it is used. In the above example the JS library is pretty big and would usually only used for one or two places related to users changing passwords; so this is a big win over having it lumped into a compiled application.js and served everywhere, even though it's almost never used.

The content_for stuff is quite clever by virtue of being quite simple under the hood. If your view was built from several partials, with more than one of them making the declarations, they don't overwrite each other. Each just gets concatenated into the right place, so the end result is pretty much what you'd expect without any nasty surprises.

There's one more step you need to avoid an exception from Sprockets because the asset you're trying to include isn't precompiled. You need to tell Sprockets it exists; for some reason, that's not automatically determined. In config/initializers/assets.rb, declare "out-of-band" / unknown files that aren't otherwise included in the e.g. application.js manifest file:

Rails.application.config.assets.precompile += %w( zxcvbn.js )

The out-of-box Rails-generated assets.rb has comments explaining this and a commented-out example in place.

This is all cooperating normally with the asset pipeline, so it works as well (or badly, depending on your experience!) as anything else in the pipeline, with debug versions in development mode and minified content in production (subject to your pipeline configuration).

Andrew Hodgkinson
  • 4,379
  • 3
  • 33
  • 43