Skip to content

rails javascript include external

Daniel Kehoe edited this page Dec 29, 2013 · 76 revisions

Unholy Rails: Adding JavaScript to Rails

by Daniel Kehoe

Last updated 31 December 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. Passing parameters to JavaScript.

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.

If You Are New to Rails

If you’re new to Rails, see What is Ruby on Rails?, the book Learn Ruby on Rails, and recommendations for a Rails tutorial.

Join RailsApps

What is the RailsApps Project?

This is an article from the RailsApps project. The RailsApps project provides example applications that developers use as starter apps. Hundreds of developers use the apps, report problems as they arise, and propose solutions. Rails changes frequently; each application is known to work and serves as your personal “reference implementation.” Each application is accompanied by a tutorial so there is no mystery code. Support for the project comes from subscribers. Please accept our invitation to join the RailsApps project.

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.

Using the Paloma Gem

The Paloma gem offers an easy way to organize JavaScript files using the Rails asset pipeline. It also provides a capability to execute page-specific JavaScript.

Read on for 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. This can be confusing, so think about it for a minute. We call the scripts “page-specific” because we intend to use them on only one (or a few) pages. We want to segregate them in the assets directory and give them a name that corresponds to the controller, view template, or feature where the scripts will be used. Segregating the scripts serves us in development by organizing our files. However, they actually become available “site wide” because they are concatenated into the application.js script. This gives us the performance benefits of the asset pipeline and a single script. It’s up to us as developers to write the JavaScript code to only execute on a specific page where it is needed, at which point it again becomes page-specific.

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.

Using the Paloma Gem

If you use the Paloma gem, you’ll organize JavaScript files like this:

+-javascripts/
| +-application.js (manifest)
| +-paloma/
| | +-users/ (assuming you have a UsersController)
| | | +-new.js (assuming you have JavaScript you want to execute only for the "new" action)
| | +-foobars/ (assuming you have a FoobarsController)
| | | +-show.js (assuming you have JavaScript you want to execute only for the "show" action)
+-stylesheets/

This structure is ideal for executing page-specific (or controller-specific) JavaScript.

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
  • dynamically loaded from within another JavaScript file using a little-known Ajax technique
  • added to an application layout or view template with a script tag

The organizational and performance benefits of the Rails asset pipeline eliminate many reasons to include scripts from external servers. 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 template using the script tag or the javascript_include_tag helper will require an additional server hit.

For small applications with only a few pages, place the JavaScript code in its own file. 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 including an external script separately 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). By far 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. Here we add an vendor/assets/javascripts/jquery.validate.min.js file:

//= 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 it makes sense to include an an external script from an external server.

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 an external script from an external server. 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 (the preferred approach). Finally we’ll look at the how to include an external script directly in a view template as page-specific JavaScript (only for edge cases where performance optimization requires it).

No Dependencies

Some external scripts work independently of your project-specific JavaScript code. For example, you might add the HTML5Shiv script to support HTML5 tags in old versions of Internet Explorer. Your own JavaScript would not be dependent on the file. You don’t have to worry about the order in which it is loaded or executed.

Simple Dependencies

Some external scripts have simple dependency chains.

Suppose you are going to use the Google Maps API throughout your application. You could add this to your application layout file:

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

If you are not going to copy the external file to the Rails asset pipeline, you would load the Google Maps API before your application.js script with a <%= javascript_include_tag %> helper tag. Then you could write JavaScript code that uses methods from the Google Maps API. This is a crude but functional approach; it adds an additional <script> tag to your web pages but the first-page performance drawback of the additional <script> tag is minimized by the possibility that the Google Maps JavaScript library may have been cached in the browser by a visit to another website. On subsequent pages, the script will be available in the cache but the <script> tag still needs to be evaluated. To improve performance, consider dynamic loading (described below).

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.

Dynamic Loading

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. You can include a JavaScript file from within a JavaScript file.

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 want to access the Google Analytics tracking code from Google’s servers. Consider that Google may update and revise the tracking script and you have one more good reason to use Google’s servers.

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 two benefits. You avoid the performance hit of evaluating a second <script> tag. And your application layout is less cluttered when all your JavaScript is consolidated in the application.js script.

Google’s technique uses the JavaScript document.createElement method. With Rails, you can use 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.

Implementing Dynamic Loading

Here’s how we can download (or load from the cache) a JavaScript file from a remote server from within the Rails asset pipeline.

Create a file app/assets/javascripts/jquery.externalscript.js:

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);
};

If you haven’t changed the default manifest file, the jquery.externalscript.js script will be automatically loaded by the //= require_tree . directive. If you’ve removed the //= require_tree . directive, specify the script in the manifest:

//= require jquery
//= require jquery_ujs
//= require jquery.externalscript

Alternatively, you could place the externalScript code in the app/assets/javascripts/application.js file and add //= require_self to the manifest.

Let’s test it by downloading the Google Analytics tracking script.

Create a 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 %>

We use the externalScript function to obtain the Google Analytics script. Then we test for the presence of a variable set by the Google Analytics tracking script and

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.

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 look at ways to use a JavaScript library from an external script as 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 template 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 provides a simple technique to execute JavaScript conditionally on a page.

The Garber-Irish Technique

For a sophisticated approach to namespacing assets and executing JavaScript based on controller name and action, investigate the Garber-Irish technique.

Using the Paloma Gem

The Paloma gem offers an easy way to organize JavaScript files using the Rails asset pipeline. It also provides a capability to execute page-specific JavaScript based on controller name and action.

The Paloma gem simplifies implementation of page-specific JavaScript. I recommend it.

Read on if you’d like to understand how to implement page-specific JavaScript yourself.

Implementing Conditional Execution of Page-Specific JavaScript

We can use a simplified version of the Garber-Irish technique to execute JavaScript conditionally on a page. We use John Firebaugh’s jquery-readyselector script to extend the jQuery .ready() to simplify the conditional test.

Set 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 %>.

Create a file app/assets/javascripts/jquery.readyselector.js:

(function ($) {
  var ready = $.fn.ready;
  $.fn.ready = function (fn) {
    if (this.context === undefined) {
      // The $().ready(fn) case.
      ready(fn);
    } else if (this.selector) {
      ready($.proxy(function(){
        $(this.selector, this.context).each(fn);
      }, this));
    } else {
      ready($.proxy(function(){
        $(this).each(fn);
      }, this));
    }
  }
})(jQuery);

If you haven’t changed the default manifest file, the jquery.readyselector.js script will be automatically loaded by the //= require_tree . directive. If you’ve removed the //= require_tree . directive, specify the script in the manifest:

//= require jquery
//= require jquery_ujs
//= require jquery.readyselector

Alternatively, you could place the code in the app/assets/javascripts/application.js file and add //= require_self to the manifest.

Here’s how you can test the jquery.readyselector.js script for a home/index page:

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

You’ll see execution of JavaScript is restricted to the specified page.

Summary

To recap, I’ve shown an approach where the Rails asset pipeline is used for all JavaScript files. I’ve shown the benefits of delivering all JavaScript in a single application.js script, how to organize JavaScript files for ease of development, and how to dynamically load scripts from external servers where appropriate. Finally, I’ve shown how to restrict execution of JavaScript to a specific page even when a script is available site-wide.

For completeness, I want to present the edge cases where it is not optimal to include all JavaScript in a single application.js script. Keep in mind that you’ll only know if you have an edge case if you conduct performance testing to determine that a single application.js script is not optimal. In most cases, a single application.js script offers the best performance.

Edge Cases

A neat, uncluttered application layout is one of the advantages of using a single application.js script for delivering all JavaScript code.

I’m going to show you how to add clutter to your application layout by adding multiple <script> tags using the javascript_include_tag and <% content_for %> helpers.

We’ll be stepping back in time to use techniques developed before Rails 3.1 was released.

But why? We’ve looked at how we can eliminate the need for multiple <script> tags, even with scripts loaded from external servers. We’ve seen how we can restrict execution of JavaScript to a specific page. These were reasons developers used multiple <script> tags before the benefits of the asset pipeline were well understood.

Here’s an edge case. You might want to use multiple <script> tags on a single page that is not visited by most users, especially if the page requires a script that contains many, many lines of code. (How many? Only performance testing can tell you with certainty.)

Think about it. Suppose one page requires a JavaScript file that is twice as large as your site-wide application.js script. If it is included in the application.js script, every visitor to your site will wait twice as long for the entire application.js script to download on a first visit. On subsequent visits, the file size is of little importance because reading from the local cache is instantaneous relative to the time required for network downloads. So it only matters for a user’s first visit. But what if the “big code” JavaScript file is only used on an administrative page used by one out of ten thousand visitors? Looking at the sum total of all visits, it makes sense to optimize the site’s performance for the first visit of ten thousand users by keeping the administrative JavaScript file out of the application.js script.

Implementation

Here are five steps to add JavaScript code to a page using multiple <script> tags:

  • Add a script to the app/assets/javascripts folder.
  • Remove the //= require_tree . directive from the app/assets/javascripts/application.js manifest file.
  • 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.

Example

Let’s imagine we’re using the MegaAdmin JavaScript library on an administrative page.

There will be a performance cost to download and cache the MegaAdmin JavaScript file. We know that only a small number of visitors to the site will be visiting the admin page so loading it only on one page reduces the performance hit for other users.

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

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

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

Make sure you’ve removed the //= require_tree . directive from the app/assets/javascripts/application.js manifest file so our admin script doesn’t get concatenated into the application.js script.

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

Application Layout

For page-specific JavaScript, you’ll need to add a javascript_include_tag helper 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. Here’s an example of the head section in an 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

We want both the external MegaAdmin script and our local admin.js script available on the admin page. We’ll use the <% content_for %> helper to include the two scripts.

Here’s our imaginary admin view:

<% content_for :head do %>
  <%= javascript_include_tag 'https://example.com/MegaAdmin/v1/' %>
  <%= javascript_include_tag 'admin' %>
<% end %>
<h2>admin Page</h2>
  .
  .
  .

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

The Rails asset pipeline will find our admin.js script in the app/assets/javascripts folder and make it available so it appears with a path of /assets/admin.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://example.com/MegaAdmin/v1/" type="text/javascript"></script>
  <script src="/assets/admin.js" type="text/javascript"></script>
</head>

You see we have three <script> tags in the HTML source. Our site-wide application.js script executes first, followed by the MegaAdmin JavaScript library from an external server, and then our admin.js script. It is not a good idea to use multiple <script> tags on every page of our application, but in this case, just for some admin pages, it makes sense.

Precompiling in Production

In development mode, nothing more is required to use our new admin.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( admin.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.

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 MegaAdmin example, we’ll see an error “admin.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.

Passing Parameters to JavaScript

It is often necessary to pass parameters from a Rails controller to JavaScript. We’ll look at several approaches and consider the issues.

Using the Paloma Gem

The Paloma gem offers an easy way to organize JavaScript files using the Rails asset pipeline. It provides a capability to execute page-specific JavaScript and it provides an easy way to pass parameters to JavaScript.

For example, you might have a UsersController with a destroy action:

def destroy
    user = User.find params[:id]
    user.destroy
    js_callback :params => {:user_id => params[:id]}
end

The js_callback directive makes the parameter available in a corresponding JavaScript file named assets/javascripts/paloma/users/destroy.js:

Paloma.callbacks['users/destroy'] = function(params){
    var id = params['user_id'];
    alert('User ' + id + ' deleted.');
};

If you use the Paloma gem, there’s nothing more you need to do.

Use HTML5 Data Attributes

HTML5 offers a convenient way to store data strings in an HTML page that are not rendered but are available to JavaScript through the DOM (the document object model). The HTML5 specification describes custom data attributes. Custom data attributes are the best way to set parameters that will be used in JavaScript.

Here’s an example. Suppose you have a form that will be manipulated with JavaScript before it is submitted to your application. Using the SimpleForm gem, you might use a form helper and set an HTML5 data attribute. In our example, we obtain the user’s IP address from the request object and set it as a data-ip_address data attribute.

<%= simple_form_for @user, :html => {:class => 'form-vertical', 'data-ip_address' => request.remote_ip}) do |f| %>
.
.
.
<% end %>

The view will render HTML in the browser:

<form accept-charset="UTF-8" action="/users" class="form-vertical" data-ip_address="..." id="new_user" method="post">

Using a jQuery selector, we can obtain the value of the IP address in our JavaScript with:

console.log('IP address: ' + $('#new_user').data('ip_address'));

In general, this is the best way to pass a parameter to JavaScript.

Use Metatags

You may see this technique used in some Rails applications.

Add a <%= yield(:head) %> directive to the head section of the app/views/layouts/application.html.erb application layout 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>

In a view file, add a content_for? view helper:

<% content_for :head do %>
  <%= tag :meta, :name => "ip_address", :content => request.remote_ip %>
<% end %>

The <% content_for :head do %> block injects the IP address in the application layout as a metatag named “ip_address.” Any JavaScript used on the page will have access to the IP address through a jQuery selector:

console.log('IP address: ' + $('meta[name="ip_address"]').attr('content'));

The metatag technique is an alternative to using HTML5 data attributes.

Use a “.js.erb” File

Any Ruby variable can be included in JavaScript code by giving the JavaScript file an .erb extension.

For example, you may have a file app/assets/javascripts/myscript.js.erb that contains:

console.log('IP address: ' + <%= request.remote_ip %>);

This technique of passing a parameter to JavaScript is simple, but it is best to avoid mixing Ruby variables into JavaScript, so try to use HTML5 data attributes instead.

Credits

Daniel Kehoe wrote this article for the RailsApps project.

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.

Learn Ruby on Rails

Clone this wiki locally