Dirt simple .rcss templates

— March 23, 2006 at 13:47 PST


In my Rails app I have a need to dynamically generate stylesheets based on user settings. Using ERb to process a template into CSS is the obvious way to go, so I wanted to have .rcss templates in my app. I even found a RCSS project on RubyForge that claims to do just that, but it actually doesn't do ERb at all anymore.

I thought I'd have to create a new template system to handle .rcss templates using the ActionView.register_template_handler call and making a handler class, etc., but that way lies madness. The docs are rather vague and the source code is twisted and (like most of Rails) mostly lacking anything like useful comments.

Anyway, I managed to figure out an incredibly simple way to process .rcss templates with ERb that doesn't involve making a new template system.

The key is using render(:file => ...) to grab the .rcss file. When rendering a file, Rails will do ERb processing on the file just as with normal .rhtml templates. So I created a Stylesheets controller to handle the CSS file requests. My solution looks like this:

The stylesheet URL

http://domain.com/stylesheets/style.css

The route:

map.connect 'stylesheets/:rcss', :controller => 'stylesheets', :action => 'rcss'

The controller:

class StylesheetsController < ApplicationController
  layout  nil
  session :off
  def rcss
    if rcss = params[:rcss]
      file_base = rcss.gsub(/\.css$/i, '')
      file_path = "#{RAILS_ROOT}/app/views/stylesheets/#{file_base}.rcss"
      @color = '#f77' # example setting
      render(:file => file_path, :content_type => "text/css")
    else
      render(:nothing => true, :status => 404)
    end
  end
end

The .rcss templates go into app/views/stylesheets/, just as if they were view templates for the Stylesheets controller. I guess you could make this part of an existing controller and put the .rcss files into its view directory too, but I wanted to keep things separate for my app.

Obviously, my example action above is simplistic as it doesn't use model data to set instance variables to communicate settings to the .rcss template. I have a variant that uses another URL param to grab a model from the database and get style settings to use in the template, but that's standard Rails and not very interesting so I'm not going to show it here. For now, pretend that the line that sets @color actually does something useful.

So given all that, lets see how it works.

The template, style.rcss:

p { color: <%= @color %>; }

The result, style.css:

p { color: #f77; }

Just for grins, here's what my log shows for that request:

Processing StylesheetsController#rcss (for 127.0.0.1 at 2006-03-23 12:47:07) [GET]
  Parameters: {"action"=>"rcss", "controller"=>"stylesheets", "rcss"=>"style.css"}
Rendering script/../config/../app/views/stylesheets/style.rcss
Completed in 0.00977 (102 reqs/sec) | Rendering: 0.00654 (66%) | DB: 0.00000 (0%) | 200 OK [http://localhost/stylesheets/style.css]

A slightly ranty footnote: This experience typifies working with Ruby on Rails for me. The solution I eventually figured out took less time to implement than it took me to write this article about doing so. However, it took me several days of investigation to understand how Rails was doing things well enough to figure out the solution. While the Rails source code is the ultimate authority on how things work, digging through those sources for answers can be extremely frustrating. There are hardly any comments in the code itself, and when there are comments they often aren't maintained and say something that is obviously inconsistent with the code itself. If the maintainers of Ruby on Rails want more people using Rails and contributing to it, they need to start commenting the code so that working with it isn't so hard.

UPDATE: This article is out of date and needs some tweaking to work with Rails 1.2 and up (see comments below). An updated Rails 2.0 compatible approach is described in Simpler than dirt: RESTful Dynamic CSS.

Also, Chris Abad has created a simple_rcss plugin based on the recipe in this article.

15 commentscss, rails

Comments
  1. Michael Trier2006-03-23 15:13:08

    Nice writeup. This will come in handy. Good work.

  2. Jeff Coleman2006-03-28 21:13:54

    This is sweet! I was wishing for something like this today.

    Jeff

  3. Tobias Varland2006-03-29 14:07:11

    Any idea how this affects performance (compared to loading a regular CSS file)?

  4. Josh Susser2006-03-29 14:44:14

    @Tobias: I'm sure it's going to take longer to generate a CSS from a template than to serve up a static file, but I don't know how much longer. If it's a performance problem, I can always cache the generated files. And I am planning on timestamping the URL according to the updated_at field on the relevant model, so the browser should cache the file and that may obviate any need for server side caching. Profiling will tell!

  5. niket2006-04-04 06:33:17

    Gr8 idea..!!

  6. Alex Young2006-04-13 13:47:13

    Not sure why, but the :content_type=>'text/css' didn't work for me at all. I had to set a separate @headers['Content-type'] = 'text/css' for the result to get recognised. This is on Rails 1.1.2.

  7. Josh Susser2006-04-13 15:15:25

    @alex: Seems to be working for me. If I use curl with -D to dump the headers I can see

    Content-Type: text/css; charset=UTF-8
    

    And if I link to it as a stylesheet from a html page it works fine for styling content in my browser. I just updated to Rails 1.1.2, so it's the latest edge version.

  8. Dave Teare2006-04-22 11:19:00

    This is sweet! Thanks!

    Any idea on how to enable TextMate to support rcss files? i.e. it would be nice to have TextMate colour the css AND support ERB autocompletion.

  9. Ed Hill2006-06-03 09:15:54

    This was a great little tip - thank you very much for writing it up!

  10. mdl2006-06-09 18:34:07

    Did this break with the new routing code gutting/re-write in Edge rails? I'm getting the following error:

    no route found to match "/stylesheets/application.css" with {:method=>:get}

    I've got the route up high in routes.rb, and I've got a logger statement in the Stylesheets rcss action that never fires, including when the app serves up a non-rcss stylesheet (which are always successful).

    I know the routing rewrite went into Edge a few days ago, and I'd love to be able to say "this used to work!" but unfortunately I'm just trying to implement this today.

  11. Josh Susser2006-06-10 12:18:02

    @mdl: I haven't tried this with the new edge routing code yet. Perhaps you could roll back to a revision of the Rails trunk before the routing changes and see if it works there. The new routing stuff looks nice, especially the :format feature, and I'll probably update the rcss recipe to take that into account as soon as it's stable.

  12. Jamie van Dyke2006-07-24 10:49:01

    The new routing require the :format option to be set, because a dot is now considered a route seperator, so use:

    map.connect 'stylesheets/:rcss.:format', :controller => '/stylesheets', :action => 'rcss'
    

    Everything works fine then.

  13. Stephane JAIS2006-08-17 15:57:32

    Nice, just what I was looking for.

    I have a customized version that manages cache control: http://www.cantinasw.net/rcss.html (coudn't paste the source, it got truncated)

  14. Steve2006-08-17 20:03:51

    This is nearly perfect for me, except I can't pass a variable to my rcss.. I have a helper in the application.rb that returns a model row, but when i call @var = gettherow before the render, i get an error that it's null. If someone can rework this to work with the :locals => {} option as in the render :partial, that would be sweet. that seems to work perfect for me.. Right now I just have to add a <% var = gettherow %> to the top of my rcss.. works.

  15. Steve Midgley2006-09-26 09:28:42

    Hi,

    I thought this article was great. I've taken the core ideas and reworked them in a way I'm more comfortable with:

    http://www.misuse.org/cms/article.php?story=20060926084103529

    I hope these additions are useful to someone!

    Steve

Sorry, comments for this article are closed.