
Josh Susser and others have produced excellent articles on creating a Ruby on Rails controller which serves up ERB view templates as CSS files.
Josh’s article is here:
dirt-simple-rcss-templates I do think there are some missing pieces to these writeups, and I’ve tried to simplify the code a little and make it more aligned to what I understand as the “Rails way.”(Updated with minor bug fix - now correctly using “gsub” instead of “delete”)
(Updated to include a functional test suite)
To create a dynamic erb-based CSS file template generator try following these steps. First add to your “routes.rb” file the following code:
map.connect ‘rcss/:rcssfile.css’, :controller => ‘rcss’, :action => ‘rcss’
Then generate a controller called “rcss” by using
script/generate controller rcss
If you don’t like the name “rcss” you can replace it with any word you like (but be aware that you have to keep the route contoller/action names in sync with your actual controller). I chose not to use the controller name “stylesheets” (as other authors did) because I didn’t want my application (or myself!) to get confused as to whether I’m calling a dynamic or static CSS file. In the rcss_controller.rb file, the simplest code I was satisfied with is:
class RcssController < ApplicationController
layout nil
session :off
# serve dynamic stylesheet with name defined
# by filename on incoming URL parameter :rcss
def rcss
# :rcssfile is defined in routes.rb
if @stylefile = params[:rcssfile]
#prep stylefile with relative path and correct extension
@stylefile.gsub!(/.css$/, ”)
@stylefile = “/rcss/” + @stylefile + “.rcss”
render(:file => @stylefile, :use_full_path => true, :content_type => “text/css”)
else #no method/action specified
render(:nothing => true, :status => 404)
end #if @stylefile..
end #rcss
end
Then create a dynamic rcss template. Try starting with the filename “default.rcss” - Put this file in the /apps/views/rcss folder. For content you can put any CSS content you like. Here’s a basic example of dynamic code:
.Stylefile <%=@stylefile%>
This will return the filename that you are using to generate this dynamic CSS file. Run your web app server on localhost port 3000 and try going to
http://localhost:3000/rcss/default.css
This should return a text/css file with the content:
.Stylefile /rcss/default.rcss
That’s it! You should now have a dynamic CSS generator all your own! Whit this generator you can create almost any dynamic content for CSS files. Most fundamentally you can create constants that you refer to across multiple CSS files. You could also allow users to specify in their preferences how they prefer to see their stylesheet content laid out, and remember that in a database. Then you can use that info to generate custom stylesheets for each user or group of users! From here, I want to provide some more detailed and production-ready code, extending what we’ve done above. The version above is unable to detect when a CSS file is requested but the corresponding rcss file does not exist in the view folder. Below, I added some file detection code that raises a custom error if the CSS file doesn’t have a corresponding .rcss file in the /app/views/rcss folder. This code could break if the /apps/views/rcss folder location changes relative to RAILS_ROOT. I added a new exception class that will make failures easier to detect in the log files. I added some caching code too, that causes each CSS file to be served up for four hours (feel free to adjust this to whatever is appropriate).
class CoreERR_CSSFileNotFound < StandardError
end
class RcssController < ApplicationController
layout nil
session :off
# serve dynamic stylesheet with name defined
# by filename on incoming URL parameter :rcss
def rcss
# :rcssfile is defined in routes.rb
if @stylefile = params[:rcssfile]
#prep stylefile with relative path and correct extension
@stylefile.gsub!(/.css$/, ”)
@stylefile = “/rcss/” + @stylefile + “.rcss”
#check for existence of @stylefile on filesystem - raise system error if not found
if not(File.exists?(”#{RAILS_ROOT}/app/views#{@stylefile}”))
raise CoreERR_CSSFileNotFound
end
# set caching because we have a good css file to ship
expires_in 4.hours
render(:file => @stylefile, :use_full_path => true, :content_type => “text/css”)
else #no method/action specified
render(:nothing => true, :status => 404)
end #if @stylefile..
end #rcss
end
Where would production code be without a testing harness? I’ve written up a testing suite that tests for many components. It assumes that you have a file “test.rcss” in the app/views/rcss folder. It assumes that file has a line in it that reads exactly as follows (you can have other lines in this file too, but you MUST have this line or you have to change the testing suite accordingly):
.Stylefile <%=@stylefile%>
Then add the following code into your “rcss_controller_test.rb” file in the test folder. Put this method code within the RcssControllerTest class:
# call rcss method in rcss controler with "test.css" testing file
# test.rcss file will be called and should return in the body
# a line of dynamically generated text: ".Stylefile test.rcss"
def test_testRCSS
get :rcss, {:rcssfile => 'test.css'}
#ensure local variable names stylesheet correctly
assert_equal assigns(:stylefile), 'test.rcss'
#ensure content-type is text/css
assert_match @response.headers['Content-Type'], “text/css”
#ensure we get a successful response
assert_response :success
#ensure we process using correct rcss template
assert_template ‘/rcss/test.rcss’
#test for specific dynamic content in test.rcss page
assert_match “.Stylefile test.rcss”, @response.body
#ensure that the map routing is working correctly
assert_routing ‘/rcss/test.css’, {:controller => “rcss”, :action => “rcss”, :rcssfile => ‘test.css’}
end
All code in this article is placed in the public domain.
the route needs a file format, e.g.
map.connect ‘rcss/:rcssfile.css’, :controller => ‘rcss’, :action => ‘rcss’
also needed to change the test to:
def test_testRCSS
get :rcss, {:rcssfile => ‘test.css’}
#ensure local variable names stylesheet correctly
assert_equal assigns(:stylefile), ‘/rcss/test.rcss’
#ensure content-type is text/css
assert_match @response.headers['Content-Type'], “text/css; charset=utf-8″
#ensure we get a successful response
assert_response :success
#ensure we process using correct rcss template
assert_template ‘/rcss/test.rcss’
#test for specific dynamic content in test.rcss page
assert_match “.Stylefile /rcss/test.rcss”, @response.body
#ensure that the map routing is working correctly
assert_routing ‘/rcss/test.css’, {:controller => “rcss”, :action => “rcss”, :rcssfile => ‘test’}
end
Thanks for the updated code linoj - greatly appreciated!
(Older versions of Rails didn’t consider a “.” as a separator, so I think my routes.rb code used to work but your code fixes this for new Rails versions.)
lastly, (this wasn’t obvious to me at first) in your layout the link tag might look like this:
Using rails 1.2.3, had to adjust the route to…
map.connect ‘rcss/:rcssfile.:format’, :controller => ‘rcss’, :action => ‘rcss’
From Alexander May via email:
“I’ve created a extension to do syntax highlighting for rcss files in
ActiveState’s Komodo IDE (which admittedly has a smaller following
then Textmate) . http://codingfrenzy.alexpmay.com/2007/11/syntax-coloring-for-dynamic-css.html”
Thanks Alex! Science appreciates progress.
hi guys,
I get a problem all the time:
no route found to match “/rcss/default.css” with {:method=>:get}
It seems to be ’stupid’ but I’m not able to get the solution.
I’ve been looking at the routes.rb, trying to modify them,… but no way.
May you please give me some advice?
I’m looking forward to making it works…. thanks a lot in advance!
Monica, what’s your routes.rb file look like? Do you have an entry for:
Send us what you’ve got and hopefully we can work this out for you!
Just to close out the comments with Monica - Science got a copy of Monica’s original application and it yielded to the Method. The code above still works and Monica is off and running!
Yes, thank you very much. You were a great help!!!!
Note: it’s possible to include a dynamic file as a stylesheet just like normal! Something like so:
< %= stylesheet_link_tag('/rcss/default', {:media => “all”})%>You could of course map a route for this in routes.rb. I’ll leave that as an exercise for the reader. : )