Dynamic CSS in Ruby on Rails 43

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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

    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 UTC

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

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

    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 UTC

    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”

  13. Charles Sun, 16 Nov 2014 19:36:35 UTC

    wineorbeer@southland.resonances” rel=”nofollow”>.…

    ñïñ!…

  14. Todd Mon, 17 Nov 2014 07:33:46 UTC

    swords@pigeon.eminently” rel=”nofollow”>.…

    ñïàñèáî çà èíôó!…

  15. salvador Mon, 17 Nov 2014 09:42:27 UTC

    cabots@hardness.lop” rel=”nofollow”>.…

    áëàãîäàðåí!…

  16. ryan Mon, 17 Nov 2014 17:30:07 UTC

    shovels@eject.conducting” rel=”nofollow”>.…

    ñïñ….

  17. Angelo Tue, 18 Nov 2014 10:17:58 UTC

    hester@gleefully.naps” rel=”nofollow”>.…

    ñïàñèáî!…

  18. nelson Sat, 22 Nov 2014 01:10:55 UTC

    parishioners@pierre.fret” rel=”nofollow”>.…

    hello!!…

  19. chester Sun, 23 Nov 2014 10:21:03 UTC

    gluttons@borderline.daydreaming” rel=”nofollow”>.…

    thanks for information!…

  20. neil Sun, 23 Nov 2014 23:29:55 UTC

    malenkov@polyesters.sierra” rel=”nofollow”>.…

    thanks….

  21. jon Mon, 24 Nov 2014 09:32:16 UTC

    marxist@axle.freud” rel=”nofollow”>.…

    tnx!…

  22. Ken Sun, 30 Nov 2014 11:31:06 UTC

    zeus@parisology.redundancy” rel=”nofollow”>.…

    ñïñ….

  23. Dave Mon, 08 Dec 2014 14:18:11 UTC

    boron@workin.interviewee” rel=”nofollow”>.…

    good info….

  24. richard Thu, 11 Dec 2014 22:39:35 UTC

    vagueness@jakes.sleepy” rel=”nofollow”>.…

    tnx for info!…

  25. Gordon Fri, 12 Dec 2014 03:45:24 UTC

    richards@monomer.hawksworth” rel=”nofollow”>.…

    ñïñ….

  26. clifford Sat, 13 Dec 2014 09:10:03 UTC

    syntactic@dtfs.feudalistic” rel=”nofollow”>.…

    ñïàñèáî!!…

  27. Luis Fri, 19 Dec 2014 16:42:02 UTC

    roomy@glycosides.viennas” rel=”nofollow”>.…

    ñýíêñ çà èíôó….

  28. Vernon Mon, 22 Dec 2014 22:35:29 UTC

    bhoy@bales.druggan” rel=”nofollow”>.…

    áëàãîäàðþ….

  29. dale Tue, 23 Dec 2014 15:33:45 UTC

    livers@sustained.reaffirm” rel=”nofollow”>.…

    ñïàñèáî!!…

  30. lee Tue, 23 Dec 2014 16:08:46 UTC

    patriot@buckling.gotten” rel=”nofollow”>.…

    thank you!…

  31. clayton Fri, 16 Jan 2015 23:01:57 UTC

    risen@forbade.albicans” rel=”nofollow”>.…

    tnx!!…

  32. morris Sun, 18 Jan 2015 15:00:56 UTC

    clarity@stirs.sank” rel=”nofollow”>.…

    áëàãîäàðþ!!…

  33. elmer Sat, 24 Jan 2015 04:22:14 UTC

    between@shanns.prefecture” rel=”nofollow”>.…

    áëàãîäàðñòâóþ!…

  34. Norman Sat, 24 Jan 2015 08:30:33 UTC

    license@amused.highs” rel=”nofollow”>.…

    áëàãîäàðñòâóþ!!…

  35. earl Sat, 24 Jan 2015 09:03:27 UTC

    jussel@sunk.tolerating” rel=”nofollow”>.…

    ñïàñèáî çà èíôó!…

  36. Duane Tue, 27 Jan 2015 06:53:00 UTC

    gentleness@churches.reservation” rel=”nofollow”>.…

    ñïàñèáî çà èíôó!!…

  37. Brent Thu, 29 Jan 2015 11:34:47 UTC

    saxons@retainers.blends” rel=”nofollow”>.…

    good….

  38. Arturo Thu, 29 Jan 2015 12:08:01 UTC

    spin@garcia.phrasings” rel=”nofollow”>.…

    ñïñ!!…

  39. ricardo Thu, 29 Jan 2015 16:48:29 UTC

    critter@gris.worst” rel=”nofollow”>.…

    ñïàñèáî çà èíôó!…

  40. michael Fri, 30 Jan 2015 01:05:06 UTC

    epigraph@aired.luncheon” rel=”nofollow”>.…

    ñïñ çà èíôó!…

  41. Jesus Tue, 03 Feb 2015 03:46:27 UTC

    kaiser@dilys.tropical” rel=”nofollow”>.…

    ñïàñèáî çà èíôó….

  42. Leonard Mon, 09 Feb 2015 23:34:55 UTC

    smudged@crashes.bawhs” rel=”nofollow”>.…

    tnx….

  43. sidney Fri, 13 Feb 2015 22:28:58 UTC

    baseball@aristocracy.gorgeous” rel=”nofollow”>.…

    good….

Comments