Dynamic CSS in Ruby on Rails 12

Posted by scientific on September 26, 2006


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.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. linoj Sat, 28 Apr 2007 14:21:59 EDT

    the route needs a file format, e.g.
    map.connect ‘rcss/:rcssfile.css’, :controller => ‘rcss’, :action => ‘rcss’

  2. linoj Sat, 28 Apr 2007 14:34:11 EDT

    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

  3. science Sat, 28 Apr 2007 17:03:38 EDT

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

  4. linoj Sun, 29 Apr 2007 01:51:01 EDT

    lastly, (this wasn’t obvious to me at first) in your layout the link tag might look like this:

    < %= stylesheet_link_tag '/rcss/style' %>
  5. nealf Thu, 05 Jul 2007 02:20:11 EDT

    Using rails 1.2.3, had to adjust the route to…
    map.connect ‘rcss/:rcssfile.:format’, :controller => ‘rcss’, :action => ‘rcss’

  6. science Mon, 19 Nov 2007 13:52:23 EST

    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.

  7. monica Wed, 05 Dec 2007 03:57:36 EST

    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!

  8. science Wed, 05 Dec 2007 09:40:21 EST

    Monica, what’s your routes.rb file look like? Do you have an entry for:

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

    Send us what you’ve got and hopefully we can work this out for you!

  9. science Mon, 10 Dec 2007 09:32:03 EST

    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!

  10. monica Tue, 11 Dec 2007 08:12:05 EST

    Yes, thank you very much. You were a great help!!!!

  11. science Thu, 20 Dec 2007 09:58:09 EST

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

  12. BTreeHugger Sun, 22 Jun 2008 07:43:09 EDT

    Thanks a bunch! You might want to tinker with your code-rendering on this site so that it doesn’t change your quotes to Unicode ones; or provide a download link as plain-text.

    Incidentally, I had to change your test-case so that it tests a match on type, not Content-Type:

    #ensure content-type is text/css
    assert_match @response.headers['type'], “text/css”

Comments