0

I'm working on an application that renders many, separate "sites" as subdirectories- e.g. /client/1, /client/2, etc. For each of these, two color values can be specified in the admin portion of the application.

I'd like to know if there's a method to inject the values, which were initially posted to and then retrieved from the back-end API by Ember, into a SCSS file for preprocessing?

I've found no solution thus far.

bynines
  • 11
  • 2
  • No, it is not possible. When your ember app starts to work and can operate user data, sass files are already compiled to css. So at this moment there is no way to change variables. – Gennady Dogaev Apr 05 '16 at 22:54
  • @GennadyDogaev https://github.com/medialize/sass.js/ – typeoneerror Apr 06 '16 at 00:04
  • Possible duplicate of http://stackoverflow.com/questions/17787845/how-to-control-sass-variable-with-javascript – cimmanon Apr 06 '16 at 00:27
  • @typeoneerror I don't see how this is relevant to question and my comment – Gennady Dogaev Apr 06 '16 at 04:37
  • @GennadyDogaev it displays how you can render Sass in the browser with javascript, including changing variables at run-time. – typeoneerror Apr 06 '16 at 04:44
  • @typeoneerror yes, It can compile some code, passed to it. But bynines needs to re-compile __all__ scss files. So application should somehow read these files (which by the way are not included in built application), then find and replace variables and then compile into css and replace old css. Not that is absolutely impossible, but it is a significant amount of work. Also, this can be a long-running task (few seconds) and perfomance depends on a client's hardware. And there will be no possibility to cache results. It's better to use a backend for recompilation, if that functionality is needed – Gennady Dogaev Apr 06 '16 at 09:20

1 Answers1

3

In our Ember/Rails application, we are generating CSS files for each client based on some settings in the database. For example, our Tenant model has two fields:

{
    primary_color: 'ff3300',
    secondary_color: '00ff00'
}

We expose routes

scope '/stylesheets', format: 'css' do
  get 'tenant/:tenant_id', to: 'stylesheets#tenant_css'
end

And our controller looks something like this:

class StylesheetsController < ApplicationController

  layout nil

  rescue_from 'ActiveRecord::RecordNotFound' do
    render nothing: true, status: 404
  end

  def tenant_css
    # fetch model
    tenant = Tenant.find(params[:tenant_id])

    # cache the css under a unique key for tenant
    cache_key = "tenant_css_#{tenant.id}"

    # fetch the cache
    css = Rails.cache.fetch(cache_key) do
      # pass sass "params"
      render_css_for 'tenant', {
        primary_color: tenant.primary_color,
        secondary_color: tenant.secondary_color
      }
    end

    render_as_css css
  end

  protected

  # our renderer, could also add a custom one, but simple enough here
  def render_as_css(css)
    render text: css, content_type: 'text/css'
  end

  # looks for a template in views/stylesheets/_#{template}.css.erb
  def render_css_for(template, params = {})
    # load the template, parse ERB w params
    scss = render_to_string partial: template, locals: { params: params }
    load_paths = [Rails.root.join('app/assets/stylesheets')]
    # parse the rendered template via Saas
    Sass::Engine.new(scss, syntax: :scss, load_paths: load_paths).render
  end

end

This way, you can link to /stylesheets/tenant/1.css which will render the CSS for the tenant using the Sass engine.

In this case, in views/stylesheets/_tenant.css.erb, you'd have something like this (it's an ERB file but you can use Sass in there now):

@import "bootstrap-buttons";

<% if params[:primary_color].present? %>
  $primary-color: <%= params[:primary_color] %>;
  h1, h2, h3, h4, h5, h6 {
    color: $primary-color;
  }
<% end %>

<% if params[:secondary_color].present? %>
  $secondary-color: <%= params[:secondary_color] %>;
  a {
    color: $secondary-color;
    &:hover {
      color: darken($secondary-color, 10%);
    }
  }
<% end %>

You'll note that I can now use @import to import anything in your stylesheet path for the Sass engine (in this case, I can utilize some helpers from Bootstrap Sass lib).

You'll want to have some sort of cache cleaner to wipe the cache when your model backing the CSS is updated:

class Tenant < ActiveRecord::Base
  after_update do
    Rails.cache.delete("tenant_css_#{id}")
  end
end

So that's the Rails side in a nutshell.

In Ember, my guess is you'll want to load the stylesheet based on an ID, so that stylesheet cannot be hard-coded into "index.html". Ember CSS Routes addon might serve you well, but I found that it just appends <link> to the header, so if you need to swap CSS stylesheets at any time, this won't work. I got around this in a route like so:

afterModel(model, transition) {
  // dynamically form the URL here
  const url = "/stylesheets/tenant/1";

  // build link object
  const $link = $('<link>', { rel: 'stylesheet', href: url, id: 'tenant-styles' });

  // resolve the promise once the stylesheet loads
  const promise = new RSVP.Promise((resolve, reject) => {
    $link.on('load', () => {
      $link.appendTo('head');
      resolve();
    }).on('error', () => {
      // resolve anyway, no stylesheet in this case
      resolve();
    });
  });

  return promise;
},

// remove the link when exiting
resetController(controller, isExiting, transition) {
  this._super(...arguments);
  if (isExiting) {
    $('#tenant-styles').remove();
  }
}

You could also add a blank element in the <head> and then use Ember Wormhole to format a <link> tag and render into the "wormhole".

Edit

You could also look into rendering Sass directly in the client application. For something as simple as two colors, this wouldn't have much of performance impact, especially if you used a service worker or similar to cache the results.

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
typeoneerror
  • 55,990
  • 32
  • 132
  • 223