How to include view/partial specific styling in AngularJS?
Asked Answered
I

7

135

What is the proper/accepted way to use separate stylesheets for the various views my application uses?

Currently I'm placing a link element in the view/partial's html at the top but I've been told this is bad practice even though all modern browsers support it but I can see why it's frowned upon.

The other possibility is placing the separate stylesheets in my index.html's head but I would like it to only load the stylesheet if its view is being loaded in the name of performance.

Is this bad practice since styling won't take effect until after the css is loaded form the server, leading to a quick flash of unformatted content in a slow browser? I have yet to witness this although I'm testing it locally.

Is there a way to load the CSS through the object passed to Angular's $routeProvider.when?

Iodate answered 4/3, 2013 at 2:9 Comment(1)
I validated your "quick flash of unformatted content" assertion. I used css <link> tags in this format, with the latest Chrome, the server on my local machine (and "Disable cache" on to simulate "first load" conditions). I imagine that pre-inserting a <style> tag in the html partial on the server would avoid this problem.Sternberg
G
150

I know this question is old now, but after doing a ton of research on various solutions to this problem, I think I may have come up with a better solution.

UPDATE 1: Since posting this answer, I have added all of this code to a simple service that I have posted to GitHub. The repo is located here. Feel free to check it out for more info.

UPDATE 2: This answer is great if all you need is a lightweight solution for pulling in stylesheets for your routes. If you want a more complete solution for managing on-demand stylesheets throughout your application, you may want to checkout Door3's AngularCSS project. It provides much more fine-grained functionality.

In case anyone in the future is interested, here's what I came up with:

1. Create a custom directive for the <head> element:

app.directive('head', ['$rootScope','$compile',
    function($rootScope, $compile){
        return {
            restrict: 'E',
            link: function(scope, elem){
                var html = '<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="{{cssUrl}}" />';
                elem.append($compile(html)(scope));
                scope.routeStyles = {};
                $rootScope.$on('$routeChangeStart', function (e, next, current) {
                    if(current && current.$$route && current.$$route.css){
                        if(!angular.isArray(current.$$route.css)){
                            current.$$route.css = [current.$$route.css];
                        }
                        angular.forEach(current.$$route.css, function(sheet){
                            delete scope.routeStyles[sheet];
                        });
                    }
                    if(next && next.$$route && next.$$route.css){
                        if(!angular.isArray(next.$$route.css)){
                            next.$$route.css = [next.$$route.css];
                        }
                        angular.forEach(next.$$route.css, function(sheet){
                            scope.routeStyles[sheet] = sheet;
                        });
                    }
                });
            }
        };
    }
]);

This directive does the following things:

  1. It compiles (using $compile) an html string that creates a set of <link /> tags for every item in the scope.routeStyles object using ng-repeat and ng-href.
  2. It appends that compiled set of <link /> elements to the <head> tag.
  3. It then uses the $rootScope to listen for '$routeChangeStart' events. For every '$routeChangeStart' event, it grabs the "current" $$route object (the route that the user is about to leave) and removes its partial-specific css file(s) from the <head> tag. It also grabs the "next" $$route object (the route that the user is about to go to) and adds any of its partial-specific css file(s) to the <head> tag.
  4. And the ng-repeat part of the compiled <link /> tag handles all of the adding and removing of the page-specific stylesheets based on what gets added to or removed from the scope.routeStyles object.

Note: this requires that your ng-app attribute is on the <html> element, not on <body> or anything inside of <html>.

2. Specify which stylesheets belong to which routes using the $routeProvider:

app.config(['$routeProvider', function($routeProvider){
    $routeProvider
        .when('/some/route/1', {
            templateUrl: 'partials/partial1.html', 
            controller: 'Partial1Ctrl',
            css: 'css/partial1.css'
        })
        .when('/some/route/2', {
            templateUrl: 'partials/partial2.html',
            controller: 'Partial2Ctrl'
        })
        .when('/some/route/3', {
            templateUrl: 'partials/partial3.html',
            controller: 'Partial3Ctrl',
            css: ['css/partial3_1.css','css/partial3_2.css']
        })
}]);

This config adds a custom css property to the object that is used to setup each page's route. That object gets passed to each '$routeChangeStart' event as .$$route. So when listening to the '$routeChangeStart' event, we can grab the css property that we specified and append/remove those <link /> tags as needed. Note that specifying a css property on the route is completely optional, as it was omitted from the '/some/route/2' example. If the route doesn't have a css property, the <head> directive will simply do nothing for that route. Note also that you can even have multiple page-specific stylesheets per route, as in the '/some/route/3' example above, where the css property is an array of relative paths to the stylesheets needed for that route.

3. You're done Those two things setup everything that was needed and it does it, in my opinion, with the cleanest code possible.

Genovera answered 5/12, 2013 at 15:54 Comment(15)
Holy moly, thanks for this! Exactly what I was looking for :). Just tested it now and it works perfectly (plus easy to implement). Maybe you should do create a pull request for this and get it into the core. I know the guys of AngularJS were looking into scoped css, this could be a step in the right direction?Dunbarton
Those guys are way smarter than I am. I'm sure they would have thought up this (or a similar) solution before and have chosen not to implement it into the core for whatever reason.Genovera
What is the correct place for the css file? Does css: 'css/partial1.css' imply the css folder in the root of the angular app folder?Ensign
Its relative to your index.html file. So in the example above, index.html would be at the root and the css folder would in the root, containing all of the css files. but you can structure your app however you want, so long as you use the correct relative paths.Genovera
hey, great approach. Had a small question. How would it work with stateProvider (ui-router). Same approach doesn't seem to work.Harbison
@Harbison I'm not very familiar with ui-router so I'm not entirely sure how you'd do something like this. I know that's not much help, but you may be able to ask a new SO question and reference this answer in your quesiton and see if anyone out there can point you in the right direction.Genovera
Is there a way to modify this to provide javascript specific to each view? Currently in our footer we have all of our javascript includes even if some of the scripts are only needed for one or two views.Janenejanenna
It would be easy enough to modify this code to allow for page-specific javascript. The problem is that you need all of your page-specific controllers to be immediately ready for the app when the $routeProvider sets up the next route. If you don't have an angular module already loaded into memory for your next route's controller (ie. 'Partial1Ctrl'), angular will throw an exception saying that it doesn't know what controller you're talking about. Javascript typically has to be loaded upfront so that angular knows about everything before the route changes.Genovera
Awesome! I really like this solution since it effectively allows me to specify route-specific styling in the same place I specify all of my other route-specific options. I've changed my answer to this one.Iodate
For anyone coming to this after the fact the Array.isArray() polyfill doesn't work, it is missing .call before the = test. So it should look like: Object.prototype.toString.call(obj) == "[object Array]" (sourced from: here)Vitia
How would you deal with generated css files? E.g. with dynamical naming for caching purposes, the actual css file would be sth like: css: 'css/1734561.partial1.css' Maestas
You'd have to update the css: property each time you generate a new file. I can't think of any other way to do it. Its just as if you put the <link> tag in your HTML <head>. You'd have to update the references either way.Genovera
sorry, I use this script but when I change view, the style disappears... do you know why this happens??Bracey
@Kappys, the script removes the style for the previous view when you move to a new view. If you don't want that to happen, simply remove the following code from the directive: angular.forEach(current.$$route.css, function(sheet){ delete scope.routeStyles[sheet]; });.Genovera
Great solution! Worth mentioning that the ng-app attribute needs to be set outside the head tag, like on the html tag.Fever
C
34

@tennisgent's solution is great. However, I think is a little limited.

Modularity and Encapsulation in Angular goes beyond routes. Based on the way the web is moving towards component-based development, it is important to apply this in directives as well.

As you already know, in Angular we can include templates (structure) and controllers (behavior) in pages and components. AngularCSS enables the last missing piece: attaching stylesheets (presentation).

For a full solution I suggest using AngularCSS.

  1. Supports Angular's ngRoute, UI Router, directives, controllers and services.
  2. Doesn't required to have ng-app in the <html> tag. This is important when you have multiple apps running on the same page
  3. You can customize where the stylesheets are injected: head, body, custom selector, etc...
  4. Supports preloading, persisting and cache busting
  5. Supports media queries and optimizes page load via matchMedia API

https://github.com/door3/angular-css

Here are some examples:

Routes

  $routeProvider
    .when('/page1', {
      templateUrl: 'page1/page1.html',
      controller: 'page1Ctrl',
      /* Now you can bind css to routes */
      css: 'page1/page1.css'
    })
    .when('/page2', {
      templateUrl: 'page2/page2.html',
      controller: 'page2Ctrl',
      /* You can also enable features like bust cache, persist and preload */
      css: {
        href: 'page2/page2.css',
        bustCache: true
      }
    })
    .when('/page3', {
      templateUrl: 'page3/page3.html',
      controller: 'page3Ctrl',
      /* This is how you can include multiple stylesheets */
      css: ['page3/page3.css','page3/page3-2.css']
    })
    .when('/page4', {
      templateUrl: 'page4/page4.html',
      controller: 'page4Ctrl',
      css: [
        {
          href: 'page4/page4.css',
          persist: true
        }, {
          href: 'page4/page4.mobile.css',
          /* Media Query support via window.matchMedia API
           * This will only add the stylesheet if the breakpoint matches */
          media: 'screen and (max-width : 768px)'
        }, {
          href: 'page4/page4.print.css',
          media: 'print'
        }
      ]
    });

Directives

myApp.directive('myDirective', function () {
  return {
    restrict: 'E',
    templateUrl: 'my-directive/my-directive.html',
    css: 'my-directive/my-directive.css'
  }
});

Additionally, you can use the $css service for edge cases:

myApp.controller('pageCtrl', function ($scope, $css) {

  // Binds stylesheet(s) to scope create/destroy events (recommended over add/remove)
  $css.bind({ 
    href: 'my-page/my-page.css'
  }, $scope);

  // Simply add stylesheet(s)
  $css.add('my-page/my-page.css');

  // Simply remove stylesheet(s)
  $css.remove(['my-page/my-page.css','my-page/my-page2.css']);

  // Remove all stylesheets
  $css.removeAll();

});

You can read more about AngularCSS here:

http://door3.com/insights/introducing-angularcss-css-demand-angularjs

Contracture answered 13/1, 2015 at 18:17 Comment(2)
I really like your approach here, but was wondering how it could be used in a production app where all the css styles need to be concatenated together? For html templates I use $templateCache.put() for the production code and it would be nice to do something similar for css.Ewens
If you need to get concatenated CSS from the server, you can always do something like /getCss?files=file1(.css),file2,file3 and server would respond with all 3 files in given order and concatenated.Highstepper
P
13

Could append a new stylesheet to head within $routeProvider. For simplicity am using a string but could create new link element also, or create a service for stylesheets

/* check if already exists first - note ID used on link element*/
/* could also track within scope object*/
if( !angular.element('link#myViewName').length){
    angular.element('head').append('<link id="myViewName" href="myViewName.css" rel="stylesheet">');
}

Biggest benefit of prelaoding in page is any background images will already exist, and less lieklyhood of FOUC

Pretoria answered 4/3, 2013 at 2:38 Comment(9)
Wouldn't this accomplish the same thing as just including the <link> in the <head> of the index.html statically, though?Iodate
not if the when for the route hasn't been called. Can put this code in controller callback of when within the routeProvider, or perhaps within resolve callback which likely triggers soonerPretoria
Oh okay, my bad, that clicks no. Looks pretty solid except could you explain how its preloading if I'm injecting it in when anyways?Iodate
it's not preloading if you append it in routeprovider...that comment was about including it in head of main page when page is servedPretoria
-_- sorry, I'm lacking on sleep if you can't tell. Anyway, that's kind of where I'm at now. Trying to figure out if the overhead of loading all my stylesheets at once is better than having some FOUC when the user switches views. I guess that's really not an Angular-related question as much as it is about web app UX. Thanks though, I'll probably go with your suggestion if I decide not to do preloading.Iodate
This post, answer and comments got me started in the right direction but it also created new questions for me which I address in my new post #18491382 Basically I am asking where exactly should I put the code. Any ideas?Addams
I'm also having issues with FOUC and the fact that one of the modules I use (ng-grid) doesn't work with this. The CSS loads too late for ng-grid to get the correct size of the elements on the page.Elnoraelnore
Just a thought, but the ng-cloak directive may help with some FOUC issues. It explicitly hides the element it's attached to while templates are being compiled.Quits
oo i learned a new acronym - for those wondering : FOUC = flash of unstyled contentHonoria
E
5

@sz3, funny enough today I had to do exactly what you were trying to achieve: 'load a specific CSS file only when a user access' a specific page. So I used the solution above.

But I am here to answer your last question: 'where exactly should I put the code. Any ideas?'

You were right including the code into the resolve, but you need to change a bit the format.

Take a look at the code below:

.when('/home', {
  title:'Home - ' + siteName,
  bodyClass: 'home',
  templateUrl: function(params) {
    return 'views/home.html';
  },
  controler: 'homeCtrl',
  resolve: {
    style : function(){
      /* check if already exists first - note ID used on link element*/
      /* could also track within scope object*/
      if( !angular.element('link#mobile').length){
        angular.element('head').append('<link id="home" href="home.css" rel="stylesheet">');
      }
    }
  }
})

I've just tested and it's working fine, it injects the html and it loads my 'home.css' only when I hit the '/home' route.

Full explanation can be found here, but basically resolve: should get an object in the format

{
  'key' : string or function()
} 

You can name the 'key' anything you like - in my case I called 'style'.

Then for the value you have two options:

  • If it's a string, then it is an alias for a service.

  • If it's function, then it is injected and the return value is treated as the dependency.

The main point here is that the code inside the function is going to be executed before before the controller is instantiated and the $routeChangeSuccess event is fired.

Hope that helps.

Enidenigma answered 29/8, 2013 at 12:34 Comment(0)
H
2

Awesome, thank you!! Just had to make a few adjustments to get it working with ui-router:

    var app = app || angular.module('app', []);

    app.directive('head', ['$rootScope', '$compile', '$state', function ($rootScope, $compile, $state) {

    return {
        restrict: 'E',
        link: function ($scope, elem, attrs, ctrls) {

            var html = '<link rel="stylesheet" ng-repeat="(routeCtrl, cssUrl) in routeStyles" ng-href="{{cssUrl}}" />';
            var el = $compile(html)($scope)
            elem.append(el);
            $scope.routeStyles = {};

            function applyStyles(state, action) {
                var sheets = state ? state.css : null;
                if (state.parent) {
                    var parentState = $state.get(state.parent)
                    applyStyles(parentState, action);
                }
                if (sheets) {
                    if (!Array.isArray(sheets)) {
                        sheets = [sheets];
                    }
                    angular.forEach(sheets, function (sheet) {
                        action(sheet);
                    });
                }
            }

            $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {

                applyStyles(fromState, function(sheet) {
                    delete $scope.routeStyles[sheet];
                    console.log('>> remove >> ', sheet);
                });

                applyStyles(toState, function(sheet) {
                    $scope.routeStyles[sheet] = sheet;
                    console.log('>> add >> ', sheet);
                });
            });
        }
    }
}]);
Handle answered 2/11, 2014 at 23:24 Comment(1)
I didn't exactly need the remove and add everywhere as my css was messed up, but this was of great help with ui-router ! Thanks :)Sulfur
C
1

If you only need your CSS to be applied to one specific view, I'm using this handy snippet inside my controller:

$("body").addClass("mystate");

$scope.$on("$destroy", function() {
  $("body").removeClass("mystate"); 
});

This will add a class to my body tag when the state loads, and remove it when the state is destroyed (i.e. someone changes pages). This solves my related problem of only needing CSS to be applied to one state in my application.

Cavell answered 15/4, 2017 at 12:20 Comment(0)
D
0

'use strict'; angular.module('app') .run( [ '$rootScope', '$state', '$stateParams', function($rootScope, $state, $stateParams) { $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; } ] ) .config( [ '$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {

            $urlRouterProvider
                .otherwise('/app/dashboard');
            $stateProvider
                .state('app', {
                    abstract: true,
                    url: '/app',
                    templateUrl: 'views/layout.html'
                })
                .state('app.dashboard', {
                    url: '/dashboard',
                    templateUrl: 'views/dashboard.html',
                    ncyBreadcrumb: {
                        label: 'Dashboard',
                        description: ''
                    },
                    resolve: {
                        deps: [
                            '$ocLazyLoad',
                            function($ocLazyLoad) {
                                return $ocLazyLoad.load({
                                    serie: true,
                                    files: [
                                        'lib/jquery/charts/sparkline/jquery.sparkline.js',
                                        'lib/jquery/charts/easypiechart/jquery.easypiechart.js',
                                        'lib/jquery/charts/flot/jquery.flot.js',
                                        'lib/jquery/charts/flot/jquery.flot.resize.js',
                                        'lib/jquery/charts/flot/jquery.flot.pie.js',
                                        'lib/jquery/charts/flot/jquery.flot.tooltip.js',
                                        'lib/jquery/charts/flot/jquery.flot.orderBars.js',
                                        'app/controllers/dashboard.js',
                                        'app/directives/realtimechart.js'
                                    ]
                                });
                            }
                        ]
                    }
                })
                .state('ram', {
                    abstract: true,
                    url: '/ram',
                    templateUrl: 'views/layout-ram.html'
                })
                .state('ram.dashboard', {
                    url: '/dashboard',
                    templateUrl: 'views/dashboard-ram.html',
                    ncyBreadcrumb: {
                        label: 'test'
                    },
                    resolve: {
                        deps: [
                            '$ocLazyLoad',
                            function($ocLazyLoad) {
                                return $ocLazyLoad.load({
                                    serie: true,
                                    files: [
                                        'lib/jquery/charts/sparkline/jquery.sparkline.js',
                                        'lib/jquery/charts/easypiechart/jquery.easypiechart.js',
                                        'lib/jquery/charts/flot/jquery.flot.js',
                                        'lib/jquery/charts/flot/jquery.flot.resize.js',
                                        'lib/jquery/charts/flot/jquery.flot.pie.js',
                                        'lib/jquery/charts/flot/jquery.flot.tooltip.js',
                                        'lib/jquery/charts/flot/jquery.flot.orderBars.js',
                                        'app/controllers/dashboard.js',
                                        'app/directives/realtimechart.js'
                                    ]
                                });
                            }
                        ]
                    }
                })
                 );
Decalogue answered 10/8, 2017 at 12:4 Comment(1)
A simple code example with no context is rarely a sufficient answer to a question. Furthermore, this question already has a highly accepted answer.Cati

© 2022 - 2024 — McMap. All rights reserved.