How we structure JavaScript and CSS in Ruby on Rails

At Planet, we do a lot of work for startups at all stages of product lifecycle. When we jump into a project after it has already been created, we are often surprised at the lack of organization, flexibility, and modularity of Ruby on Rails projects.

You have the asset pipeline, treat it as your friend. It is okay to separate files.

Remember the old days of a single stylesheet for a project? That was ridiculously stupid. One file, a number of frontend devs, equals disaster. But in Rails projects we now have the asset pipeline to create different manifests or sets of stylesheets. I’ll go through stylesheets first and then tie everything into javascript architecture.

There are multiple types of stylesheets you need to consider.
1. Chrome and layout stylesheets. These include header, footer, logo, and other universal layout components.
2. Reusable patterns. Similar to bootstrap, your forms, lists, content boxes and more all go here.
3. Page-specific styles. Nearly every page has some special style and all those should be scoped to your page here.
4. Third party. Bootstrap, clearfix, modal, and other styles from projects that aren’t your own go here.
5. Browsers. I keep browser specific styles here. In other words, Internet Explorer.

Screenshot of stylesheets directory

Set page-specific IDs to the body tag to scope your pages

1. Create helpers in application_helper.rb

  def page_id
    if id = content_for(:body_id) and id.present?
      return id
    else
      base = controller.class.to_s.gsub("Controller", '').underscore.gsub("/", '_')
      return "#{base}-#{controller.action_name}"
    end
  end
  
  def page_class
    controller.class.to_s.gsub("Controller", '').underscore.gsub("/", '_')+" "+content_for(:page_class)
  end

2. Set page ID and class in application.html.erb

<body id="<%= page_id %>" class="<%= page_class %>">

3. Now the ID for a body will be controller-view. For example, on Recognize the homepage ID is “home-index”.

4. Structure your stylesheet page styles by controller and have each file be a view.
Screenshot of folder structure

If you have controller-wide styles then create a file called the controller. For instance home can be: home/home.sass, home/index.sass, home/tour.sass, etc.

Home.sass will look like…

.home
  p
    font-weight: 600

The home index stylesheet will look like…

#home-index
  p
   font-weight: 100

Now everything is scoped and isolated. If you change something in the home-index you are 100% sure it will only affect that page. The result is far lower bugs and more confidence for developers to create new features.

Some follow the paradigm if giving the view and controller class names on the body.

.home.index
 p
  font-weight: 100

The problem with this is will never do styles specific to index across all controllers so why would I want a class of index?

Regardless, the reason why I do #controller-view is for JavaScript, and it a good time to get into that.

Make your JavaScript also scoped by controller-view (the page).

Now that we have our page ID of controller-view, we use that for the key of a page-specific javascript object. The object ends up looking like this:

JavaScript page object

You can create another layer of architecture to not have all the pages loaded when not in use. But removing the pages object all together only lowered memory by 2mb, so insignificant.

Create page-specific JavaScripts and run it on page load or view change

To start the page-specific JavaScript, use the following snippet in a init.js file.

 var dataScript = document.body.getAttribute("id");
 window.R = window.R || {};

 if (R.pages && R.pages[dataScript]) {
  R.currentPage = new R.pages[dataScript]();
 }

If there is a page-specific JavaScript, it will run it, if not, then it won’t. Simple!

Here’s what a page-specific JavaScript file looks like.

window.R = window.R || {};
window.R.pages = window.R.pages || {};

window.R.pages["home-index"] = function() {
 alert("Dude you are so on the home index!");
};

By declaring a function as pages[“home-index”], it is only executed if you are actually on the home-index. Perfect!

But what about JavaScript that is used across all views in a controller? The solution is to create an abstract class, or a base class, for the controller and have all the views extend that class. (I know JavaScript doesn’t have classes, but this is the best way to describe it.)

window.R = window.R || {};
window.R.pages = window.R.pages || {};

window.R.pages["home-index"] = (function($, window, undefined) {

  var Index = function() {
    Index.superclass.constructor.apply(this, arguments);
  };
  
  R.utils.inherits(Index, R.pages.home);

  return Index;
})(jQuery, window);

Here I extend the R.pages.home for the index. I never directly instantiate window.R.pages.home, it is executed via its views – index.js.

A screenshot of the JavaScript folder.
Screenshot of page JavaScript

What about AMD?

The one thing that isn’t awesome about this is it isn’t using an AMD pattern. To use AMD (such as require.js) with the asset pipeline in Rails would need a system that loops over all the JavaScript view files and create manifests that can be compiled by Rails. Another approach is using an additional build with a frontend JavaScript minification and combining library.

Isolating assets lowers your chances of bugs

By keeping everything scoped and organized not only do future developers have an easier time getting started, but it also reduces bugs, which also helps future developers. One of the biggest fears of new devs is breaking existing functionality. By knowing what you are doing only affects one page or widget, then the dev can be confident they can innovate rather than regress.

What about JavaScript that is across controllers?

Just like for the stylesheets, create a patterns/ directory that contains all the reusable JavaScript functionality.

This architecture described above has been used by our team for a couple of years now and we are super happy with it and I hope you are too!