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.
Nice writeup. This will come in handy. Good work.
This is sweet! I was wishing for something like this today.
Jeff
Any idea how this affects performance (compared to loading a regular CSS file)?
@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!Gr8 idea..!!
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.
@alex: Seems to be working for me. If I use curl with -D to dump the headers I can see
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.
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.
This was a great little tip - thank you very much for writing it up!
Did this break with the new routing code gutting/re-write in Edge rails? I'm getting the following error:
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.
@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.
The new routing require the :format option to be set, because a dot is now considered a route seperator, so use:
Everything works fine then.
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)
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.
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