Skip to content

rails javascript include external

Daniel Kehoe edited this page Oct 15, 2012 · 76 revisions

Unholy Rails: Adding JavaScript to Rails

by Daniel Kehoe

Last updated 15 October 2012

Rails and JavaScript topics: How to include external JavaScript files and jQuery plugins in Rails 3.1 or newer. Using page-specific JavaScript in Rails. Organizing JavaScript files.

What is the best way to add a JavaScript library to a Rails application? Use the Rails asset pipeline? Or include directly in a view template? This article explains what to do when your application is not wholly Rails.

This is a guide for developers using the example apps from the RailsApps repository. The RailsApps project provides open source applications and detailed tutorials for Rails. Next time you build an app, try using our Rails Composer tool; it’s a better way to build a starter app.

Got feedback or a suggestion? Comments welcome below.

Contents

Rules of Thumb

In summary, here are rules of thumb to guide your use of JavaScript in Rails:

  • Logically organize your site-wide scripts in the app/assets/javascripts/ folder.
  • Copy external JavaScript libraries (such as jQuery plugins) to the vendor/assets/javascripts folder.
  • Let the Rails asset pipeline combine them all in one minimized application.js file.
  • List scripts in the app/assets/javascripts/application.js manifest.

In almost all applications, there is no need to add external JavaScript libraries directly in a view template. Use the Rails asset pipeline, even for JavaScript used on just one page (page-specific JavaScript). Copy external scripts to your application and you’ll gain the performance benefits of the Rails asset pipeline and avoid complexity.

The Rails asset pipeline will become even more important in Rails 4.0 with the new Turbolinks feature. Turbolinks improves performance by keeping the current page instance alive and replacing only the page BODY (plus the title in the HEAD). As long as the HEAD element is identical between pages, the Turbolinks mechanism can deliver its “turbo” speed boost. This adds to the importance of avoiding any extra script tags on specific pages.

Principles for Performance

It’s difficult to sort out all the advice and folklore around Rails and JavaScript. Here are basic principles to improve website performance.

JavaScript is single-threaded, meaning that only one operation can be performed at a time. The browser can only be executing JavaScript or rendering the UI at any moment.

Downloading files takes much longer than parsing and executing browser code. Modern web browsers can download files (scripts, CSS files, or images) in parallel. Modern web browsers cache files to minimize download requests, both within a site and across sites (in the case of popular JavaScript libraries such as Google Analytics or jQuery). But even with parallel downloads and caching, multiple files can be slower than single files.

Content delivery networks (CDNs) are faster at delivering popular JavaScript libraries than your own web server. However, once a file is cached (after the first download), CDNs offer no advantages. CDNs make sense for landing pages (the first page that a visitor encounters) but not so much for every page in a large site (where caching is at work). CDNs for popular JavaScript libraries offer no advantages if a visitor has a library cached from a visit to another site.

You can easily and cheaply set up a CDN for your own application with a service such as CloudFront. This gives you the advantage of a CDN for files you’ve added to the Rails asset pipeline, allowing you to combine popular JavaScript libraries with your own code for first-page delivery faster than your own server. But again, the only advantage is for first-page delivery.

Inline JavaScript (mixed in your HTML code) blocks loading and rendering the page. Plus it is messy to mix JavaScript, Ruby, and HTML in a view template. Keep JavaScript (or CoffeeScript) in its own files in the Rails assets directories.

The fewer <script> tags you use, the faster your pages will load. Modern web browsers download scripts in parallel but each script tag has to be parsed and evaluated to determine if a file is cached and current. Dynamic loading (from within another script) is faster than using an additional script tag.

Scripts that are concatenated into a single file (such as application.js with the Rails asset pipeline) minimize download time and can be cached for site-wide use.

External JavaScript libraries can be copied and concatenated into a single file to minimize download time. Make your own copy of an external library when your application requires a specific version. When you want to rely on a third party to update and maintain the library, don’t make a copy; use dynamic loading.

External JavaScript libraries that are likely to be cached from visits to other websites can be dynamically loaded from within your local JavaScript code. Dynamically loading scripts allows use of cached files, allows loading scripts asnychronously, and eliminates the overhead of parsing and evaluating multiple script tags.

Certain external JavaScript libraries that introduce security vulnerabilities, such as code that handles credit cards, should not be copied into your application asset directories. Instead, include the external script in an application.js script through dynamic loading.

When a single application.js script combines JavaScript used site-wide with JavaScript intended for use on individual pages, conditional execution of page-specific JavaScript tied to elements on an individual page reduces execution overhead.

In most cases, downloading a single script that combines site-wide and page-specific JavaScript (for a first page) and reading from a cache (for subsequent pages) will take less time than downloading multiple scripts on individual pages. The exception to this rule could be a very lengthy script that is used on only a single page that is not visited by most of the site’s visitors (for example, an administrative page). This exceptional case merits adding an additional script to an individual page using a second script tag, rather than including page-specific “big code” in the application.js script. Only performance testing can tell you whether this optimization is warranted.

Finally, a word about persistent folklore. You may have encountered the frequently repeated advice to “always place JavaScript at the bottom of the page just before the closing </body> tag”. This was once true because web browsers loaded scripts sequentially and blocked loading and rendering until each script was complete. This is no longer true; modern browsers do “preload scanning” and begin loading all scripts in parallel, whether listed in the head element or at the bottom of the page. External JavaScript often is loaded asynchronously and is written so it won’t execute until the page is loaded and the DOM is ready. Loading a script in the head element is no longer a bad practice.

For a deeper and more detailed look at recommended practices for using JavaScript in a web application, look to advice from web performance optimization experts such as Steve Souders and Nicholas C. Zakas.

Now that we’ve considered principles to guide our evaluation, let’s look at the specifics of using JavaScript in Rails. But first, step back and consider why this is so complicated.

JavaScript’s Missing Directive

The C language has #include, Java has import, Perl has use or require, PHP has include or require, and Ruby has require. These directives add the contents of one file into another. Often these directives are used to incorporate code libraries provided by other developers. Some languages also have a package manager that provides a standard format for distributing programs and libraries (Ruby has RubyGems). What’s the equivalent in JavaScript? Nothing. JavaScript doesn’t have a native package manager or import directive.

Instead, you’re expected to include all the JavaScript files you require for a web page in a series of HTML <script> tags typically placed between the <head> tags at the top of an HTML file. The order of placement is important. The web browser compiles the JavaScript code sequentially. If your code requires an external library, the external script must be listed first. Each <script> tag requires a separate download and introduces a delay. JavaScript’s “missing include” leaves framework developers looking for ways to improve performance.

That’s where the Rails asset pipeline comes in.

Rails and the Asset Pipeline

Rails 3.1 introduced the asset pipeline in August 2011.

Before 3.1, Rails did little to manage JavaScript. Developers used the javascript_include_tag helper to construct a <script> tag and add scripts directly to a view template or application layout. Before 3.1, developers used the helper to add every script required for an application.

The Rails asset pipeline improves website performance by concatenating multiple JavaScript files into a single script, allowing the developer to segregate code in separate files for development efficiency, but eliminating the performance penalty of multiple <script> tags.

The Rails asset pipeline adds some of the functionality of a package manager for project-specific JavaScript code. You can organize multiple JavaScript files in the app/assets/javascripts folder. The default app/assets/javascripts/application.js file serves as a manifest file, specifying which files you require. By default, the file’s //= require_tree . recursively includes all JavaScript files in the app/assets/javascripts directory. Sprockets, the mechanism that powers the Rails asset pipeline, will concatenate and minify all the specified JavaScript files into a single application.js script which you can include in your application layout with the <%= javascript_include_tag "application" %> statement. Sprockets also performs preprocessing so you can write JavaScript as CoffeeScript or include Ruby code as an ERB file. Order of execution is still important; a manifest file must list each JavaScript file in dependent order.

For more about the Rails asset pipeline, see:

The Rails asset pipeline is innovative and useful. For the simplest use case, where a developer intends to use multiple scripts on every page of an application, the Rails asset pipeline is a no-brainer. But documentation for the Rails asset pipeline offers no guidance for two common implementation requirements: JavaScript libraries obtained from third parties (such as jQuery plugins) and scripts that are only used on a single page (page-specific JavaScript).

This article addresses these concerns.

Where to Stick Your JavaScript

Whether you use the Rails asset pipeline or add a <script> tag directly to a view, you have to make a choice about where to put any local JavaScript file.

We have a choice of three locations for a local JavaScript file:

  • the app/assets/javascripts folder
  • the lib/assets/javascripts folder
  • the vendor/assets/javascripts folder

Here are guidelines for selecting a location for your scripts:

  • Use app/assets/javascripts for JavaScript you create for your application.
  • Use lib/assets/javascripts for scripts that are shared by many applications (but use a gem if you can).
  • Use vendor/assets/javascripts for copies of jQuery plugins, etc., from other developers.

In the simplest case, when all your JavaScript files are in the app/assets/javascripts folder, there’s nothing more you need to do.

Add JavaScript files anywhere else and you will need to understand how to modify a manifest file.

Mysterious Manifests

There are two kinds of files in a JavaScript assets folder:

  • ordinary JavaScript files
  • manifest files

You can also have CoffeeScript files and ERB files which are variations on ordinary JavaScript files.

Manifest files have the same .js file extension as ordinary JavaScript files. Manifest files and ordinary JavaScript files can be combined in a single file. This makes manifest files mysterious, or at least non-obvious.

The default app/assets/javascripts/application.js file is a manifest file. It’s a manifest file because it contains directives:

//= require jquery
//= require jquery_ujs
//= require_tree .

Directives tell Sprockets which files should be combined to build a single JavaScript script. Each file that contains manifest directives becomes a single JavaScript script with the same name as the original manifest file. Thus the app/assets/javascripts/application.js manifest file becomes the application.js script.

All scripts in the app/assets/javascripts folder are automatically added to the default application.js script when the manifest file includes the default //= require_tree . directive. See below for suggestions why you might want to change this default.

If you add a script to the vendor/… folder and you wish to have it combined with your project code in the application.js script for use throughout your application, you must specify it with a directive in the manifest (details below). The same is true for the lib/… folder.

Organizing Your Scripts

Rails is all about following conventions to save effort and simplify teamwork. But there is no well-known and accepted practice for organizing your JavaScript files. Here’s advice I’ve found about organizing your scripts:

Leave a comment below if you’d like to suggest ways to organize your JavaScript files. Here’s my advice about organizing your JavaScript files.

Default Locations

In a simple application, you can collect all the JavaScript files in the app/assets/javascripts folder and rely on the default //= require_tree . directive to combine the scripts into a single application.js script. Here we add google-analytics.js and admin.js files to the default directory.

+-javascripts/
| +-application.js (manifest)
| +-google-analytics.js
| +-admin.js
+-stylesheets/

There’s nothing to configure and it works as long as you don’t have any requirements to load the scripts in a particular order. Here’s the default app/assets/javascripts/application.js manifest file:

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree .

The jQuery and Twitter Bootstrap scripts are included from gems and all scripts in the default directory are included.

In a complex application, use subdirectories to organize your scripts. Here are suggestions.

Site-wide Scripts

You can create a folder app/assets/javascripts/sitewide for scripts that are used on all (or many) pages of the application. Here we place the google-analytics.js file in a directory we use for site-wide scripts:

+-javascripts/
| +-application.js (manifest)
| +-sitewide/
| | +-google_analytics.js
+-stylesheets/

In the manifest file app/assets/javascripts/application.js, remove the //= require_tree . directive and replace it with //= require_tree ./sitewide to automatically include all scripts in the sitewide directory.

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree ./sitewide

The jQuery and Twitter Bootstrap scripts are included from gems and any scripts in a sitewide directory are included.

There’s nothing more you need to do for site-wide scripts.

Page-Specific Scripts

Following the principles described above, you will frequently include page-specific JavaScript in the application.js script to be combined with site-wide scripts. If you have only a few page-specific scripts, place them in the top-level app/assets/javascripts folder. For example, you might have an admin.js script you use on only a few administrative pages. Add it as an app/assets/javascripts/admin.js file:

+-javascripts/
| +-application.js (manifest)
| +-admin.js
| +-sitewide/
| | +-google_analytics.js
+-stylesheets/

You’ll need to explicitly specify this script in the app/assets/javascripts/application.js manifest file if you’ve removed the //= require_tree . directive as described above. Note that we drop the file extension when we specify the filename in the manifest file. Set up the app/assets/javascripts/application.js manifest file like this:

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree ./sitewide
//= require admin

The jQuery and Twitter Bootstrap scripts are included from gems; any scripts in a sitewide directory are included; and the admin.js script is included.

Namespaces for Complex Applications

In a complex application, you can use the assets directory structure to organize scripts for a “namespacing” effect. Choose an organizational structure that suits your application.

In this example, we have a single admin.js script that is used with all the views rendered by an AdminController. We have articles.js and comments.js scripts that correspond to view templates or partials that are used with a ContentController. You might consider another organizational scheme; the folder and file names can be anything that makes sense to you.

+-javascripts/
| +-application.js (manifest)
| +-admin/
| | +-admin.js
| +-content/
| | +-articles.js
| | +-comments.js
| +-sitewide/
| | +-google_analytics.js
+-stylesheets/

You’ll need to explicitly specify each script in the app/assets/javascripts/application.js manifest file. Set up the app/assets/javascripts/application.js manifest file like this:

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require_tree ./sitewide
//= require ./admin/admin
//= require ./content/articles
//= require ./content/comments

The jQuery and Twitter Bootstrap scripts are included from gems; any scripts in a sitewide directory are included; and the scripts in the admin and content directories are explicitly included.

Create Your Own Gems When You Can

By convention, lib/assets/javascripts is intended for scripts used on more than one project. Consider putting these in a gem to gain the benefits of RubyGems as a package manager if you use these scripts across different applications. You get easy version management and you can bundle update <gem> when you need the latest version of a JavaScript library.

The Rails asset pipeline will recognize scripts from any gem that contains files in these locations. If you wish to have a script from a gem combined with your project code in the application.js script for use throughout your application, you must specify it with a directive in the manifest. The examples above show how jQuery and Twitter Bootstrap scripts are included from gems.

Here’s where to place scripts in gems:

  • app/assets/javascripts will probably not be used for scripts in a gem
  • lib/assets/javascripts for scripts you share across your own projects
  • vendor/assets/javascripts for gemified jQuery plugins, etc., from open source projects

Here’s an article that shows how to create a gem containing JavaScript code:

Use Open Source Gems

It is ideal to use gemified versions of JavaScript code from open source projects such as jQuery because this gives you the advantage of RubyGems as a package manager. The jquery-ui-rails gem from Jo Liss is an excellent example.

Unfortunately, few JavaScript projects are intended solely for Rails so there seldom is a gemified version of the JavaScript. Instead, the files are offered for downloading or from a content delivery network as external scripts.

External scripts are where JavaScript in Rails gets complicated.

External Scripts

The Rails asset pipeline is a powerful tool for managing project-specific JavaScript code; however, it doesn’t offer a facility for managing JavaScript files that are obtained outside of your own application. External scripts, those that are downloaded by a web browser from a remote web server, can be handled in three ways:

  • copied locally and managed with the asset pipeline
  • included from within another JavaScript file using a little-known Ajax technique
  • added to an application layout or view with the javascript_include_tag helper

You Probably Should Use Not Use External Scripts

The organizational and performance benefits of the Rails asset pipeline eliminate almost all reasons to use external scripts. In most cases, you’ll want to copy the external script locally and manage it with the asset pipeline. I’ll show you how to do that below.

Consider the benefits of the Rails asset pipeline. When there is a only a single application.js script to download, the browser will cache it on first encounter and after that will load it from the browser cache. Each script you add directly to a view using the javascript_include_tag will require an additional server hit.

For small applications with only a few pages, place the JavaScript code in its own file (for organizational efficiency) and give it a name that corresponds to the controller or view. Then let Sprockets concatenate and minify all your JavaScript files into a single application.js script. The default directive //= require_tree . in the app/assets/javascripts/application.js manifest file will recursively include all JavaScript files in the app/assets/javascripts directory. Or remove the //= require_tree . directive and list each file individually. The script will be available throughout the application but you’ll only use it on a few pages. I’ll show you how to limit execution to a single page below.

For large applications, it may seem the browser will be more efficient if each page only gets the script it needs. In fact, the Rails asset pipeline will be faster delivering a single application.js file in almost all cases.

You’ll only know if there’s a performance benefit to downloading an external script if you actually profile performance. For rudimentary analysis, use the Network tab in the WebKit developer tool (in Chrome or Safari) or Firebug (in Firefox). You can use the Yslow tool for a detailed analysis of web page performance factors (see an article from New Relic on Improving Site Performance with YSlow). The best tool for analysis of web page performance is the free WebPagetest.org.

Copy External Scripts Locally

It’s easiest to simply copy an external script to your own application. By convention, the preferred location is in the vendor/assets/javascripts folder. It will work in the app/assets/javascripts folder but that’s not where it belongs.

Potential headaches with shifting versions can be minimized by using Michael Grosser’s vendorer gem to install and update external scripts.

To make a script available as part of the site-wide application.js script, you must specify it in your app/assets/javascripts/application.js manifest, like this:

//= require jquery
//= require jquery_ujs
//= require jquery.validate.min

In most cases, it is best to copy an external script locally and let the Rails asset pipeline combine it with your own project code. You’ll avoid complexity, gain the benefit of managing all your JavaScript in one place, and (in most cases) gain performance benefits.

Now let’s consider the edge cases where the use of an external script makes sense.

Using External Scripts

If a script is delivered by a content delivery network, likely to be cached in a web browser by visits to other sites, and used throughout your application, you might include the external script directly. The Google Analytics tracking script is an example.

You’d also want to use an external script when copying the script locally would introduce a security vulnerability. Scripts that process credit card data are an example.

Here we’ll look closely at how to use external scripts. First, we’ll consider how an external script interacts wih local scripts (dependencies). Then we’ll look at options for including external scripts in the asset pipeline. Finally we’ll look at the how to include an external script directly in a view as page-specific JavaScript.

No Dependencies

Some external scripts work independently of your project-specific JavaScript code. For example, you might add the HTML5Shiv <%= javascript_include_tag 'http://html5shiv.googlecode.com/svn/trunk/html5.js' %> to your application layout to support HTML5 tags in old versions of Internet Explorer. Your own JavaScript would not be dependent on the file. You could load it either before or after your application.js script (though in the case of HTML5Shiv you’d want to load it before any CSS files).

Simple Dependencies

Some external scripts have simple dependency chains.

If you were going to use the Google Maps API throughout your application, here’s what you would add to your application layout file:

<%= javascript_include_tag 'http://maps.googleapis.com/maps/api/js?sensor=false' %>
<%= javascript_include_tag 'application' %>

You would load the Google Maps API before your application.js script with the <%= javascript_include_tag %>. Then you could write JavaScript code that uses methods from the Google Maps API.

Complex Dependencies

Now consider the problem of external scripts that are dependent on jQuery. For example, you might wish to use a jQuery plugin. It has to be loaded after the application.js script which loads jQuery.

You could set up your application layout like this:

<%= javascript_include_tag 'application' %>
<%= javascript_include_tag 'http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/jquery.validate.min.js' %>
<%= javascript_include_tag 'code_that_uses_the_validation_plugin' %>

This is the kind of complexity that the asset pipeline is intended to eliminate. Instead of segregating your code and loading it as a separate file, use the asset pipeline. There are two ways to load the jQuery plugin in the middle of the application.js script. You can copy the external library to your own application as described above. Or you can include an external JavaScript file from within a local JavaScript file, which apparently cannot be done.

Including a JavaScript File From Within a JavaScript File

I said that apparently one cannot insert an external script in the middle of the asset pipeline. In fact it can be done, despite JavaScript’s lack of an import directive.

The technique is used on millions of web pages and you may have used it without realizing it:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXXX-XX']);
_gaq.push(['_trackPageview']);
(function() {
  var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();

Does that look familar? It’s the Google Analytics tracking code. It creates a tiny script using document.createElement and inserts it into the page where it dynamically and asynchronously downloads the full Google Analytics script.

The Google Analytics tracking code is delivered by a content delivery network, likely to be cached in a web browser by visits to other sites, and used throughout an application. In this case, the Rails asset pipeline doesn’t offer any performance advantages so you might include the Google Analytics tracking code as an external script.

You could add it directly to the application layout. Instead, you can use the asset pipeline and include it from within another JavaScript file. Using the asset pipeline gives you a benefit: Your application layout is less cluttered when all your JavaScript is consolidated in the application.js script.

You can use the document.createElement method or let jQuery help you.

JQuery offers the jQuery getScript method. It will load any JavaScript file from within a JavaScript file. The getScript method has one big limitation: It doesn’t retrieve scripts from the browser cache. To overcome this limitation, we can define a similar method that looks for a cached script before attempting a download of an external script.

Here’s how we use jQuery to download (or load from the cache) a JavaScript file from within the Rails asset pipeline. Create a file app/assets/javascripts/google_analytics.js.erb:

jQuery.externalScript = function(url, options) {
  // allow user to set any option except for dataType, cache, and url
  options = $.extend(options || {}, {
    dataType: "script",
    cache: true,
    url: url
  });
  // Use $.ajax() since it is more flexible than $.getScript
  // Return the jqXHR object so we can chain callbacks
  return jQuery.ajax(options);
};

With the externalScript function in place, we can add the Google Analytics script to our asset pipeline. Add to the file app/assets/javascripts/google_analytics.js.erb:

<% if Rails.env == 'production' %>
  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']);
  _gaq.push(['_trackPageview']);
  ga_src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  $.externalScript(ga_src).done(function(script, textStatus) {
    console.log('Script loading: ' + textStatus );
    if (typeof _gat != 'undefined') {
      console.log('Okay. GA file loaded.');
    }
    else
    {
      console.log('Problem. GA file not loaded.');
    }
  });
<% end %>

Notice that we only load the Google Analytics script for tracking traffic in production mode. To do so, we use the .erb file extension so it will be preprocessed by Tilt.

If you want to use the externalScript method in other JavaScript files, move it to app/assets/javascripts/application.js and add //= require_self to the manifest.

With this technique, you’ve added an external script to the site-wide application.js script without copying it locally.

Now let’s consider cases where you want to use JavaScript on a specific page, not throughout the entire application. If external scripts are complicated, page-specific JavaScript in Rails gets even more complex.

Page-Specific JavaScript

First we’ll consider how to execute JavaScript on a specific page. Then we’ll consider where to put a local JavaScript file that contains page-specific JavaScript. Finally we’ll look at ways to combine a JavaScript library from an external script with local page-specific JavaScript.

Conditional Execution of Page-Specific JavaScript

Often JavaScript is written to interact with specific elements on a page; if so, the browser will evaluate the JavaScript on every page but only activate it if the appropriate page element is present. The JavaScript you need for that page can be part of the site-wide application.js script or it can be included on the page with a javascript_include_tag statement in the view or application layout.

JavaScript execution can be determined by

  • the presence of unique elements on the page, or
  • attributes in the HTML body tag.

You can simply test for an element’s unique HTML element ID:

$('document').ready(function() {
  if($('#main').length){
    console.log("element exists");
  }
});

A more organized approach is to test for attributes in the HTML body tag:

  • Set class or id attributes on your page’s body element.
  • Use functions in your JavaScript that parse those classes or ids and call the appropriate functions.

First you must modify the application layout. Replace the <body> statement in the app/views/layouts/application.html.erb file:

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

Assuming a page is generated by a Projects controller, the rendered page will include:

<body class="projects">

Use this condition in your JavaScript code:

$('document').ready(function() {
  if ($('body.projects').length) {
    console.log("Page generated by the projects controller.");
  }
});

This approach can be refined by setting both the controller name and action in the HTML body tag:

<body class="<%= controller_name %> <%= action_name %>">

You have a choice of syntax: use either <%= params[:controller] %> or <%= controller_name %>.

Use John Firebaugh’s jquery-readyselector gem to extend the jQuery .ready() to simplify the conditional test:

$('.project.index').ready(function() {
  console.log("Page-specific JavaScript on the projects/index page.");
});

For a sophisticated variation on this approach, investigate the Garber-Irish technique.

Now that we’ve seen how to execute JavaScript conditionally, let’s consider where to add the JavaScript file that contains the code.

Using the lib/assets Folder for Page-Specific JavaScript

We have a choice of three locations for a local JavaScript file:

  • the app/assets/javascripts folder
  • the lib/assets/javascripts folder
  • the vendor/assets/javascripts folder

In development mode, Sprockets will precompile every file in each of these folders. See Precompiling in Production (below) for configuration issues for deployment.

The best location for a page-specific JavaScript file is the lib/assets/javascripts folder if you want to leave the app/assets/javascripts/application.js file intact.

Consider the alternatives. If you place a page-specific JavaScript file in the app/assets/javascripts folder, Sprockets will combine it with all the other scripts in the folder and make it available throughout the application as part of a single large application.js script. You can change the Rails default configuration to prevent this by removing the //= require_tree . directive, but the change is not necessary if you place the script in the lib/assets/javascripts folder.

You can also place a page-specific JavaScript file in the vendor/assets/javascripts folder. That works but, by convention, that folder is for third-party assets such as jQuery plugins.

The Alternative to Using lib/assets/javascripts

Here’s another way to handle page-specific JavaScript. It allows you to place your page-specific scripts in the app/assets/javascripts folder. It requires you to remove the //= require_tree . directive from the manifest and specify each script individually (or specify other manifest files).

You may see other developers using this approach. They may be using it to enforce logical organization on their application-wide JavaScript. That’s a good reason to adopt this approach. However, this approach is not necessary if you simply want to accommodate page-specific scripts. Use the approach described above, instead.

First you must modify the app/assets/javascripts/application.js file to remove the //= require_tree . directive or else your page-specific JavaScript code will be included on every page. After removing the //= require_tree . directive, add a directive for each specific file you want compiled and concatenated into the application.js script. For example, if you want Twitter Bootstrap from the bootstrap-sass gem:

//= require jquery
//= require jquery_ujs
//= require bootstrap

Specifying each script in the manifest makes it possible to remove the //= require_tree . directive and still create a concatenated site-wide application.js script.

Where External Scripts and Page-Specific JavaScript are Appropriate

Let’s consider cases where external scripts and page-specific JavaScript can be useful.

An external script and page-specific JavaScript has value when copying an external file to your server increases vulnerability.

Also, page-specific JavaScript may have value when a script is used on a single page that is not visited by most users.

Consider this example. I want to use the Stripe JavaScript library to initiate transactions for credit card payment. I don’t want to store the Stripe code on my server because of the risk that someone could gain access to my server and change the code to intercept credit card data.

We use the Stripe script only on a single page where the visitor makes a credit card payment. There will be a performance cost to download and cache the Stripe JavaScript file. We know that only a small number of visitors to the site will be visiting the payment page so using page-specific JavaScript reduces the performance hit for other users.

First, let’s write a local script that uses the the Stripe JavaScript library. Here is our example lib/assets/javascripts/payment.js file:

$(function() {
  if (typeof Stripe != 'undefined') {
    console.log('Stripe JavaScript file loaded.');
  }
  else
  {
    console.log('Problem: Stripe JavaScript file not loaded.');
  }
});

We can use it to test if the external JavaScript file is loaded.

Now we’ll consider how to add page-specific JavaScript directly to the view that renders the page.

Application Layout for Page-Specific JavaScript

For page-specific JavaScript, you’ll need to add a javascript_include_tag to the head section of your page view. One approach is to modify your controller to use a custom layout for the view (see a range of approaches in the RailsGuide Layouts and Rendering in Rails). I suggest you leave your controller alone. Such a minor customization defeats the purpose of the site-wide application layout; fortunately, Rails offers a better option.

Set up your application layout with a <%= yield(:head) %> statement so you can inject additional tags in the head section of the view. See an example of a Rails Default Application Layout from the RailsApps project. Here’s an example of the head section in the app/views/layouts/application.html.erb file:

<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= content_for?(:title) ? yield(:title) : "App_Name" %></title>
  <meta name="description" content="<%= content_for?(:description) ? yield(:description) : "App_Name" %>">
  <%= stylesheet_link_tag "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
  <%= yield(:head) %>
</head>

The <%= yield(:head) %> statement follows the <%= javascript_include_tag "application" %> so you can add additional scripts and still use jQuery in any page-specific JavaScript code you add.

Using content_for for Page-Specific JavaScript

Imagine a payment form used in our application. We want both the external Stripe script and our local payment.js script available on the payment page. We’ll use the <% content_for %> helper to include the two scripts.

Here’s our imaginary payment view:

<% content_for :head do %>
  <%= javascript_include_tag 'https://js.stripe.com/v1/' %>
  <%= javascript_include_tag 'payment' %>
<% end %>
<h2>Payment Page</h2>
<%= form_for(resource, :as => resource_name, :url => payment_path(resource_name)) do |f| %>
  .
  .
  .
  (credit card data entered here)
  .
  .
  .
  <div><%= f.submit "Purchase" %></div>
<% end %>

The <% content_for :head ... %> block allows us to add page-specific JavaScript files to the view.

The Rails asset pipeline will find our payment.js script in the lib/assets/javascripts folder and make it available so it appears with a path of /assets/payment.js.

If we view HTML source, we will see generated HTML that looks like this:

<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>App_Name</title>
  <meta name="description" content="App_Name"/>
  <link href="/assets/application.css" media="all" rel="stylesheet" type="text/css" />
  <script src="/assets/application.js" type="text/javascript"></script>
  <meta content="authenticity_token" name="csrf-param" />
  <meta content="..." name="csrf-token" />
  <script src="https://js.stripe.com/v1/" type="text/javascript"></script>
  <script src="/assets/payment.js" type="text/javascript"></script>
</head>

In development mode, this works without any changes to the Rails default configuration.

Script Name as Parameter

You don’t need to specify the actual script name in the javascript_include_tag. You can give the script the same name as the controller and use:

<%= javascript_include_tag "#{params[:controller]}" %>

Or combine the controller name and view as the name of the script and use:

<%= javascript_include_tag "#{params[:controller]}.#{params[:action}" %>

This is useful for a complex application where you’ve organized the asset folder into subfolders and given each script a name that corresponds with a controller and view.

Precompiling in Production

In development mode, nothing more is required to use our new payment.js script on any page where it is needed. The asset pipeline “live compiles” all the JavaScript files it finds and makes them available for use.

For production, we must make an important configuration change so our new script is precompiled and available on deployment.

Add this to the file config/environments/production.rb:

config.assets.precompile += %w( payment.js )

If we’ve created a CoffeeScript or ERB file, we don’t need to include the .coffee or .erb file extension.

When you precompile assets in production mode, the Rails asset pipeline will automatically process the app/assets/javascripts/application.js file and any additional files listed in its manifest to produce a concatenated and minified site-wide application.js script.

Any other scripts that you wish to use on a page in addition to the site-wide application.js script must be specified by the config.assets.precompile statement or else they will not be precompiled and made available in production mode.

Sprockets will look for files designated in the config.assets.precompile statement and create JavaScript files with the same names. If the file contains manifest directives, it will combine other files to make a single script.

If you don’t make this configuration change, you won’t see the error until your application is deployed in production.

Page-Specific JavaScript Summary

With these four steps, you’ll be able to easily use page-specific JavaScript code:

  • Add any local script to the lib/assets/javascripts folder.
  • Use <%= yield(:head) %> in the application layout.
  • Use <% content_for :head ... %> in the view.
  • Modify config/environments/production.rb to add config.assets.precompile for your script.

Testing in Production Mode

How can you tell if you’ve configured your application to serve the scripts needed in production?

Test it.

To test, you must enable your Rails web server to deliver static assets. Modify the config/environments/production.rb file:

# Disable Rails's static asset server (Apache or nginx will already do this)
config.serve_static_assets = true

Be sure to switch this back after trying out your application locally in production mode.

Then try running your server locally in production mode:

$ rake db:migrate RAILS_ENV=production
$ rake assets:precompile
$ rails server -e production

Visit the web pages that use your scripts and check functionality. In our Stripe example, we’ll see an error “payment.js isn’t precompiled” unless we set config.assets.precompile to include it.

Use rake assets:clean to remove the precompiled assets when you return to development mode.

Credits

Daniel Kehoe wrote this article for the RailsApps project.

Was this useful to you? Follow me on Twitter: rails_apps
and tweet some praise. I’d love to know you were helped out by the article.

Thank you to Peter Cooper (@peterc), Pat Shaughnessy (@pat_shaughnessy), Eric Berry (@cavneb), Ken Collins (@metaskills), Jo Liss (@jo_liss), Stephen Ball (@StephenBallNC), and Andrey Koleshko (@ka8725) for technical review and advice.