Compile Your SCSS According to Your Model on Rails

I already create some white label products and most of the time there is still the same problem… customization. It is really hard to match design needs of customers, you can let them upload there logo, change some texts, even provide different templates for a widget for example but one thing hard is to change colors or fonts or stuffs like that. Of course you can provide a service to include their own stylesheet but if you change the css of your widget, all css of your customers have to be tested again and some can not work and moreover you cannot control what they do and if there is a problem even because of them they will call you to say “it doesn’t work! please, fix it!”.

Against this, I wanted to find a clear solution where the customer just set the variables he wants like the main color, the border radius, shadows… everything you want and you can control. With all of this and thanks to SCSS variable it is possible to customize a CSS.

Step 1: Get all variables

Ok here whatever you want, a model with a has_many variables with key: value, a hash stored, a key database like Redis… this is up to you, you just need to be able to get all your variables. For this I will have a Mongoid model with just a text field with all variables separated by a \n like that I can edit quickly in a textarea and I just have a function variables to return a hash with all variables extracted.

Step 2: Inject variables in SCSS

If you already use some CSS frameworks like Twitter Bootstrap or Foundation for example you can find SASS version where you can override all variables of the framework. We will do the same. Imagine you have a file with all your variables like that.

_variables.css.scss
1
2
3
$primary-color: red;
$secondary-color: blue;
$grid-width: 100%;

To be able to override some variables you need to append the !default otherwise this variable couldn’t be modified (like that you can decide what access you want to give to your customer).

_variables.css.scss
1
2
3
$primary-color: red default!; // customer can change the primary color, if he does nothing the color will be red
$secondary-color: blue default!; // customer can change the secondary color, if he does nothing the color will be blue
$grid-width: 100%; // this cannot be modified by the customer

Now we need to inject those variables in our CSS. Let’s create the final CSS which will use the default application template but with variable overwritten. For that we will use a .erb file like that we will be able to write some rails variable in our file. This is pretty simple, just a loop on our variables and then just put the property with the value.

_template.css.scss.erb
1
2
3
4
5
<% variables.each do |property, value| %>
  $<%= property %>: <%= value %>;
<% end %>

@import "the_basic_design_of_my_application";

Here we are, our file can use the application design using some variables defined from the rails application (in a model or whatever you decided).

Step 3: Compilation

Ok now the tricky part, we will have to compile the erb to put all variables in our SCSS then compile the SCSS to have our CSS with the variables chosen by the customer.

To compile the erb we will just use the render method from the ActionView::Base class, we will give the list of variables as locals and in our case the template will be “template.css”.

1
2
3
4
5
6
7
def compile_erb template
  ActionView::Base.new(MyApp::Application.assets.paths).render({
    partial: template,
    locals: { variables: variables },
    formats: :scss
  })
end

Ok then, compile SCSS, this will be almost the same but with a different engine to compile, here we will use Sass::Engine.

1
2
3
4
5
6
7
8
def compile_scss template
  sass_engine = Sass::Engine.new(compile_erb(template), {
    syntax: :scss,
    style: Rails.env.development? ? :nested : :compressed,
    load_paths: MyApp::Application.assets.paths
  })
  sass_engine.render
end

Ok we are done now you can just call the method compile_scss and you will have your CSS so you can print it in your page ;)

No this is a joke don’t do that! we will drop this in a file and even more put this file on s3 using carrierwave.

Step 4: Save the CSS

Let’s add in our model a column to store the css path and mount this column as uploader.

partner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Partner
  include Mongoid::Document
  field :name,   type: String
  field :css,    type: String
  field :config, type: String, default: "primary-color: black"
  mount_uploader :css, CssUploader

  TEMPLATE = "template.css"

private
  def variables
    res = {}
    list = config.split("\n")
    list.collect do |line|
      split = line.split ":"
      res[split[0].strip] = split[1].strip if split.length == 2
    end
    res
  end

  def compile_erb
    ActionView::Base.new(MyApp::Application.assets.paths).render({
      partial: Partner::TEMPLATE,
      locals: { variables: variables },
      formats: :scss
    })
  end

  def compile_scss
    sass_engine = Sass::Engine.new(compile_erb, {
      syntax: :scss,
      style: Rails.env.development? ? :nested : :compressed,
      load_paths: MyApp::Application.assets.paths
    })
    sass_engine.render
  end
end

For the uploader I let you have a look at carrierwave gem.

So now let’s trigger the compilation of our CSS and save it in our uploader everytime we change some variables

partner.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Partner
  # ...
  before_save :compile_css

private
  #...
  def compile_css
    return if config_changed? && css.present?
    require 'tempfile'

    file = Tempfile.open [name, ".css"]
    begin
      file.write compile_scss
      file.flush
      self.css = file
    ensure
      file.close
      file.unlink
    end
  end
end

That’s it, now everytime you will change your variables, the new CSS will be compiled and then save using carrierwave

Step 5: Add it to your DOM

This is the final step, really easy, just in your HTML now you can simply add your partner CSS like that

application.html.erb
1
2
3
4
5
<html>
  <head>
    <%= stylesheet_link_tag @partner.css if @partner %>
  </head>
</html>

That’s all !! This post is a bit long and some part a bit complex but this can be really useful in some case, especially when you work on some products where you want to let people customize their design. For now it’s the best solution I found if anyone have another I would be happy to ear it :)

PS: This technique was inspired by the excellent Twitter bootstrap customization and now it’s possible to do the same ;)

Update

If you are using Compass, you will have some problems in production. The solution is pretty simple you just have to include by yourself the paths of compass. compass_paths = Compass.sass_engine_options[:load_paths].collect { |path| path.try(:root) }.compact with this on the compile_scss function you just have to add those paths load_paths: MyApp::Application.assets.paths + compass_paths. That’s it now you can use compass even in production without problems ;)

Comments

Copyright © 2014 - Anthony Estebe -