Update May 29 2013: The name of the stylesheet files was changed slightly (hyphen replaced by underscore) after feedback in the comments.
In any Rails 3.1 (or newer) app where the user can change the style (e.g. layout, colors, dimensions) of certain items you come to the point where you wish you could use all of the sweetness of the Rails asset pipeline to generate custom stylesheets dynamically during runtime.
After a bit of googling I realized what it comes down to is imitating the process Rails performs when precompiling all assets on deploy, only that we want to want to compile Sass code that was generated dynamically, not read from a static file in app/assets/javascripts. After having a look at the assets:precompile Rake task which does the heavy lifting it's clear that Sprockets::StaticCompiler is the main suspect in this case.
I found a few blog posts dealing with Sprockets::StaticCompiler but most of them were about using it outside of a Rails environment. Nobody seemed to have tried to use it to compile JS code during runtime (correct me if I'm wrong).
I started to fiddle with it and after a couple of days of working on this on and off I came up with a solution that does most of what I wanted. Along the way I opened a Github issue on compass-rails when I had some trouble getting Compass to work and was told that "this is outside of the intended use case". :)
Meat & Potatoes
Imagine an app like Shopify where users can create their own online stores and customize and style it. You'd have a Store model that contains attributes for colors, heights, widths etc. which the user can set for each store individually.
Most likely those values would be stores in a Layout model associated with the store but for simplicity's sake let's assume the user can set just the background color of the store and the color of the "Buy" buttons, and these two values are stored directly in the Store model.
At this point you could skip to the code which I tried to add enough comments to so that it is understandable on its own.
So here's how it works:
- The gruntwork happens in
lib/store_stylesheet.rb. AStoreStylesheetobject is initialized with a store and takes care of compiling the stylesheet for this store and registering it so that Rails can find it. - The dynamic styles for each store are stored in
app/views/stores/styles.scss.erb. In there you can use local variables (storein this case) which are supplied by theStoreStylesheet. - The compiled stylesheet file is stored in
app/assets/stylesheets/stores/id_timestamp.css.scssusing the id andupdated_attimestamp of the store. This is so that every time the store is updated (and potentially a field that is used in the styles), the stylesheet is lazily recompiled when the store is accessed the next time. - Wherever you want to include the compiled stylesheet (in a layout like
app/views/layouts/stores.html.hamlmost likely), you need to check first if it needs to be recompiled. This is more effective than recompiling it in anafter_savecallback onStorefor example, since the user might make many changes before the store is accessed again and this way it only need to be recompiled once.
Caveats & Learnings
- It took a while to figure out that one needs to use the
Sprockets::Environmentinstead of theSprockets::Indexto find the compiled file.Sprockets::EnvironmentandSprockets::Indexare very similar (the former is used in development, the latter in production) except thatSprockets::Indexcaches all accessible files and therefore doesn't find any files compiled during runtime. AccessingSprockets::EnvironmentthroughSprockets::Indexis awkward since it is not exposed through a method. - It also took a while to realize that the compiled asset has to be registered in
Rails.application.config.assets.digeststo be found. Rails complainsid_timestamp.css isn't precompiledif you don't do this. - My belief in the asset pipeline being the best thing since sliced bread was reconfirmed.
The Code
Improve
Are you doing something similar in your app? Is there a better way of doing it?
Let me know in the comments!
Ideas? Critique? Think I'm stupid? Let me know in the comments!
