clppxq
Last Updated: February 25, 2016
·
7.33K
· gsans

Angular — Dynamically injecting CSS files using $route.resolve and promises

AngularJs Meetup South London Collection | this article

Meet the monolithic CSS Stylesheet

It is a common practice, to use a monolithic CSS stylesheet for the whole application.

This file, lets say my-app.css, will be the result of various steps: concatenating, less/sass compiling and minifying using a building tool like Grunt or Gulp.

This is all good and fine for small sized websites but what happens when the website keeps growing and more CSS is added to it? As you keep adding new sections and components, it gets bigger and bigger. An one day it hits your desk… The first visit to your app is taking few seconds to load… You dig a little into it and find that there is a big monolithic CSS sitting in your browser.

What options are there for you now? Well, you clearly have to split your CSS into separate files. A sensible option would be using different sections of your application to do it. In Angular this translates mainly to your routes.

Angular routes already handle loading the corresponding templates and attaching the controller but still it doesn’t support CSS files.

You could inject your CSS to your template but then you will be missing on browsers cache and making your templates bigger.

Splitting CSS files using $route.resolve

In order to inject our CSS file while navigating to a new route we will use $route.resolve. We are going to use _resolve _in a way that navigation will only happen after we have injected our CSS. See $routeProvider.when() for more details.

// Routing setup
.config(function ($routeProvider) {
  $routeProvider
  .when('/home', {
      controller: 'homeCtrl', 
      templateUrl: 'home.tpl.html'
  }).when('/users', {
      controller: 'usersCtrl', 
      controllerAs: 'vm',
      templateUrl: 'users.tpl.html',
      resolve: {
        load: ['injectCSS', function (injectCSS) {
          return injectCSS.set("users", "users.css");
        }]
      }
  }).otherwise({
      // default page
      redirectTo: '/home'
  });
})

We moved the actual code to a factory Service called _injectCSS _that contains a function _set(id, url) _returning a promise. This is the actual implementation of the service.

.factory("injectCSS", ['$q', '$http', 'MeasurementsService', function($q, $http, MeasurementsService){
  var injectCSS = {};

  var createLink = function(id, url) {
    var link = document.createElement('link');
    link.id = id;
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    return link;
  }

  var checkLoaded = function (url, deferred, tries) {
    for (var i in document.styleSheets) {
      var href = document.styleSheets[i].href || "";
      if (href.split("/").slice(-1).join() === url) {
        deferred.resolve();
        return;
      }
    }
    tries++;
    setTimeout(function(){checkLoaded(url, deferred, tries);}, 50); 
  };

  injectCSS.set = function(id, url){
    var tries = 0,
      deferred = $q.defer(),
      link;

    if(!angular.element('link#' + id).length) {
      link = createLink(id, url);
      link.onload = deferred.resolve;
      angular.element('head').append(link);
    }
    checkLoaded(url, deferred, tries);

    return deferred.promise;
  };

  return injectCSS;
}])

This code takes ideas from different sources and adds some personal choices in. See VIISON/RequireCSS and stackoverflow.

In order to inject our CSS we:

  • inject a link tag into the head element
  • hook to link.onload event (fires only in some browsers)
  • check document.styleSheets for changes

In case the CSS fails to load, Eg. network failure, it will keep on going after the browser adds an entry on styleSheets.

Findings

While working on the final solution I found out about a couple of things I didn’t know before:

  • dynamically injecting a CSS files is not as easy as it seems. There are lots of quirks along the way. This solution only covers the surface and I am just getting to see why the Angular team left it out.
  • $http apparently doesn’t use browsers cache opposed to the href on the link element. $http will always make an initial request even after being cached on the browser using the link tag.
  • It makes no difference having called $http with the same CSS file. The link tag will ignore it and take the same time to parse it and paint it on screen.> Use this code with precaution as it has not been tested on all browsers.

Resources

Find an online workbench I used to try different configurations here.

AngularJs Meetup South London Collection | this article