Convenient CSS and Javascript in Ruby on Rails

Andy Peterson ·

I get tired of hunting through a hierarchy of folders and files, from the views to the public folder, to locate a certain CSS or Javascript file. It’d be convenient to have them right with the markup, but embedding these definitions within your HTML markup is a bad idea for several reasons. For our current project, I proposed we put everything in the view directory so they are easy to find:

app/
  views/
    home/
      index.html.rb
      index.css
      index.js

The convention is clear: to add page-specific CSS code, just create a new file with the same name as the view, in the same folder. Easy to add, edit and remove. The alternative (using the public folder), usually leads to a parallel hierarchy and the inconvenience of that.

I also thought, that this being Rails and all, the files should be included automatically. It turned out to be pretty easy.

The Implementation

Serving the Assets

To serve this file, we need a route and a controller.

First the route:

map.connect '/asset/:path',
    :controller => 'asset', :action => 'serve_asset', :path => /.*.(js|css)/

We need to be careful here, since we’re going to be serving files directly out of our view hierarchy– make sure the route only picks up the javascript and CSS files.

The corresponding controller:

class AssetController < ApplicationController
  def serve_asset
    path = params[:path]
    format = path.sub(/.*.(w+$)/, '1')
    respond_to do |format|
      format.js { render :file=>"#{RAILS_ROOT}/app/views/#{path}"}
      format.css { render(:file=>"#{RAILS_ROOT}/app/views/#{path}")}
    end
  end

Now the assets mentioned above are served as /assets/index.css and /assets/index.js.

Including the Assets

With this convention, we added code to our layout that includes these files automatically, but only if they exist. First, the additional controller code:

class AssetController < ApplicationController
  ...
  def self.include_asset(rel_path)
    return '' unless AssetController.asset_exists?(rel_path)
    case rel_path
      when /.js$/
        "<!--mce:0-->"
      when /.css$/
        "
"
    end
  end

  def self.asset_exists?(rel_path)
    FileTest.exists?(RAILS_ROOT + "/app/views/#{rel_path}")
  end

And the code needed in the (Erector) layout:

head do
  c = self.class
  rawtext AssetController.include_asset("#{c.name.underscore}.js")
  rawtext AssetController.include_asset("#{c.name.underscore}.css")
end

Actually, since it’s Erector, the views are Ruby classes, so there is a hierarchy. These assets may be specific to any one of these:

c = self.class
while c != Object
  rawtext AssetController.include_asset("#{c.name.underscore}.js")
  rawtext AssetController.include_asset("#{c.name.underscore}.css")
  c = c.superclass
end

Discussion

Although some people will bristle at the proliferation of files, I have used this technique repeatedly over the last five years (in Java and PHP projects). I find it leads to sound, modular development. With Ruby, it’ll be pretty straightforward to concat the assets together later on, to reduce requests, or even embed the code directly in the served HTML files, if it’s small enough. It’s also convenient to add page caching in the controller, when the time comes. Adding

  caches_page(:serve_asset)

copies all these files into public/assets (as they are accessed), which conveniently takes your mongrel (or whatever) out of the picture.

You might imagine this keeps you from organizing and structuring your CSS and Javascript; it doesn’t do that. It lowers the barier to using a “local” scope for the CSS and Javascript. It allows you to write page-specific CSS and Javascript, which if you’re developing code quickly, is a lifesaver. We have repeatedly opened a view-specific Javascript file, seen that it no longer applies to the code, and removed it– safely. It’s easy to edit scripts when you know they only effect one page. Without this, I tend to lump things together that don’t really belong, and it becomes hard to tease them apart later.

Thoughts?

What can we help you with?

Tell us a bit about your project, or just shoot us an email.

Interested in a Career at Carbon Five? Check out our job openings.