2005-09-15

Server-side CSS Constants For Rails

CSS is missing one quite important feature. It does not include any way to define variables or constants. This can be easily fixed using server-side CSS scripting.

Introduction

When you declare color of the element you have to provide literal value for it. The same goes for font sizes and other attributes. If your site is rich and you have plenty of selectors this means lots of repetitions of given value across CSS file(s).

Every time you want to adjust given color in your theme you have to do search and replace on your CSS file. This certainly is not the DRY way.

There are several solutions to this problem, for example:
  • Keep color declarations in separate CSS file. This separates color information from layout but you still have to follow search and replace pattern. And sooner or later (I bet on sooner) selectors in layout and color files will go out of sync.
  • Use multiple classes for HTML elements. It is possible to provide two (or more) CSS classes in class attribute – one responsible for layout and another for colors. Unfortunatelly this brings unwanted elements of layout to HTML structure. I personally consider it almost as bad as using inline style attribute.

Further Reading

You can find discussion on this topic on Ruby on Rails weblog: Augmenting CSS with variables and more

Luke Redpath provides a simple implementation in his blog: Dynamic CSS in Rails

Discussion about “Shaun Inman’s similar PHP project on Eric Meyer’s blog”: http://meyerweb.com/eric/thoughts/2005/08/31/the-constants-gardener/

Adding Functionality

Luke’s solution will work well in most deployments. Still it would be nice to have implementation that:
  • Integrates into Rails
  • Allows to use Rails caching capabilities
  • Uses standard controller to drive the CSS generation (for example to deliver different stylesheets for different browsers depending on User-Agent header).
  • Provides access to Rails model (for example to use per-user color theme settings stored in the data base)

Implementation

ActionView::Base allows to create and register new template handlers so let us use this:

Step One

First for the toughest part: creating a template handler. Create a file named rcss_handler.rb with following contnent in lib directory of you application.


require 'erb'

class RcssHandler
  include ERB::Util
  def initialize(action_view)
    @action_view = action_view
  end

  def render(template, local_assigns)
    @action_view.controller.headers["Content-Type"] = 'text/css'
    b = binding

    local_assigns.stringify_keys!
    local_assigns.each { |key, value| eval "#{key} = local_assigns[\"#{key}\"]", b }

    ERB.new(template, nil, '-').result(b)
  end
end

ActionView::Base::register_template_handler 'rcss', RcssHandler

It ensures proper content type of sent data and applies all variables defined in controller to the template. The last line introduces type handler to ActionView.

Code for adding variables to template scope is taken directly from ActionView::Base implementation.

Step Two

Making the handler visible to your application. Edit your config/environment.rb file and add following line somewhere (preferably at the end):


require 'rcss_handler'

Step Three

Now the handler is ready to use and will be applied to all templates with type ‘rcss’. To feed it with one we need to create proper controller.


class RcssController < ApplicationController
  def render_rcss
    if params[:rcss] =~ /\.css$/
      template = $`
    else
      template = params[:rcss]
    end
    render :action => template, :type => 'rcss', :layout => false
  end
end

RegEx is not necessary, it simply makes sure so that we support CSS URLs with and without .css extention.

Note two less commonly used options sent to render method:
  • :type => 'rcss' makes sure that Rcss handler will be used
  • :layout => false skips layout rendering, otherwise parsed CSS would end up wrapped in HTML from default layout

Step Four

Now it is time to add nice routing: open config/routes.rb and add following lines before the most generic mapping:


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

Step Five

Create your first RCSS file. Create file in app/views/rcss/default.rcss with following contents:


<% 
    highlight_backgroud = "gray" 
    highlight_color = "red" 
%>
* {
    background-color: <%= highlight_backgroud %>;
    color: <%= highlight_color %>;
}

Step Six

Use rcss file in your layout using following line in <head/>:


<link href="/rcss/default.css" media="screen" rel="Stylesheet" type="text/css" />

Remember to restart the server before testing – only then your new configuration will be taken into account.

Enjoy your new, completly unreadible application layout.

Room For Improvement

  • Provide template handler for CSS Server-Side Constants (needs customized parser)
  • Ensure proper caching (add caching headers)
  • Provide helper similar to stylesheet_link_tag
  • ...

License

All code in this post is licensed under MIT license:

Copyright (c) 2005 Lukasz "Bragi Ragnarson" Piestrzeniewicz

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.