How to compile less on the fly inside a Single Page Ember app

My current Ember project is part of a so-called white-labelled product. We had to find a way to dynamically apply a different design to our app for different customers. The differences in design consist of different colors and a different logo for each customer.

Since our frontend was completely decoupled from the backend we had some discussions as to change that or not. We could have the Ember app’s index.html and app.css rendered by the backend, but we needed quick results and decided against that for now. Instead we created an endpoint in the backend where the frontend could request the properties of the custom design.

For our styles we use the preprocessor less. That way we can make use of variables for colors and it is possible for many css classes to incorporate the color values given by only a few top-level color variables. Since we deliver an uncompiled less file to the client now, we need to compile the less code on-the-fly with the custom values we get from the backend before starting the app.

How to programmatically compile less code on the Client

The less documentation on this matter is very sparse and I had to try out a lot and tweak the code until it finally worked. That’s why I want to share my solution here.

After some experimenting I ended up with the following chain of promises:

var customDesignResourceURI = "api/customdesign",
  lessFileURI = "assets/app.less",
  defaultCssFileURI = "assets/app.css";

application.deferReadiness();

// fire requests for the less file and the design data simulaneously
Ember.RSVP.hash({
  lessFile: ajax(lessFileURI),
  designData: ajax(customDesignResourceURI)
}).then(function (results) {
  return window.less.render(results.lessFile, {
    env: "production", // in this mode, less seems to automatically cache the output css in localStorage (or the input? this is unclear)
    errorReporting: "console",
    useFileCache: true,
    sourceMap: false,
    modifyVars: { // pass the custom design vars into the less compiler
      '@color1': results.designData.color1 ? results.designData.color1 : "",
      '@color2': results.designData.color2 ? results.designData.color2 : "",
      '@logo-uri': results.designData.logo_uri ? "'" + results.designData.logo_uri + "'" : ""
    }
  });
}).then(function (output) {
  Ember.Logger.debug("Less source code successfully compiled into css.");
  return output.css;
}).catch(function (error) {
  Ember.Logger.error("There was an error receiving or compiling the source less file or receiving the company's custom design data.");
  Ember.Logger.debug("Loading default css.");
  return ajax(defaultCssFileURI);
}).then(function (css) {
  Ember.Logger.debug("Appending css to DOM.");
  appendCSStoDOM(css);
  application.advanceReadiness();
}).catch(function (error) {
  Ember.Logger.error("Could not load default css. This means loading any styles failed.");
  throw error;
});

What happens here is that we first request the uncustomized less file and the custom design data from the api endpoint. Both requests are done with ic-ajax and return promises, which we combine with the RSVP.hash method. On success we pass the custom design variables into the less compiler and then return the less.render method, which is again a promise. Should the compilation or the requests before the compilation fail, we catch the error and instead request a default version of the css, which is compiled in the build step of the frontend app. The last then takes the result, be it the compiled custom css or the default css, and passes it to a method which appends it to the DOM.

Appending the compiled CSS to the DOM

The last missing piece to make it all work is the code to append the ready css to the DOM. I made this a util on its own:


/**
 * This method takes a string of CSS and appends it to the DOM.
 *
 * It does this by creating a new <style> element and appending it to
 * the <header> of the page.
 *
 * The code was pretty much copied one to one from:
 * http://stackoverflow.com/questions/10274260/programmatically-editing-less-css-code-with-jquery-like-selector-syntax/31836785#31836785
 */

export appendCssToDom function (cssString) {
  var css = document.createElement('style');
  css.type = 'text/css';
  document.getElementsByTagName('head')[0].appendChild(css);

  if (css.styleSheet) { // IE
    try {
      css.styleSheet.cssText = cssString;
    } catch (e) {
      Ember.Logger.error("Couldn't reassign styleSheet.cssText.");
    }
  } else {
    (function (node) {
      if (css.childNodes.length > 0) {
        if (css.firstChild.nodeValue !== node.nodeValue) {
          css.replaceChild(node, css.firstChild);
        }
      } else {
        css.appendChild(node);
      }
    })(document.createTextNode(cssString));
  }
}

Where to execute the code

All the above code is in our app currently executed within an initializer. This makes sure that the css is available before anything in the app is being displayed. I’m planning to move it soon into the model hook of the application route though, so that we can benefit from a loading screen being displayed while the less is still being requested and compiled. In that case we don’t need to defer the readiness of the application. Instead we just return the promise chain from the hook.

This approach requires injecting some additional css though, probably directly into index.html, to keep the loading screen in place while the final css is not available yet. So both approaches have their advantages.

Another thing to pay attention to here is to what happens when we are testing the app. I decided to skip the whole process of less compilation when a TESTING environment variable is set to true and instead in this case I’m appending the default version of the css to index.html.

Conclusion

The reason why examples on compiling less code on the client are so sparse is probably that it is not actually recommended to do it. At least not in production. Mostly for the obvious reason that it is not very efficient and increases startup time a lot (think that all this might have to be executed on a mobile device). I want to make clear that also for us this is only a short term solution and will eventually be replaced by a properly designed solution where the backend will probably deliver the compiled and customized css code to the client. Until then I’m pretty happy with my solution though. It was an interesting thing to work on.