geek by association

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

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.

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

Josh Susser 
2006-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!

niket 
2006-04-04 06:33:17

Gr8 idea..!!

Alex Young 
2006-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.

Josh Susser 
2006-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.

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.

Ed Hill 
2006-06-03 09:15:54

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

mdl 
2006-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.

Josh Susser 
2006-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.

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

Steve 
2006-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.

Sponsored by

About

I'm Josh Susser, and this is my geek blog. It's mainly about Ruby on Rails programming,
but other stuff will creep in from time to time. I've been a professional programmer for
over 20 years (with time off for good behavior). I also fancy myself something of a writer,
thus the blog is a good outlet for two of the things that keep me from getting enough sleep.