How to Append a Stylesheet to <Head> in Angularjs $Routeprovider

How to include view/partial specific styling in AngularJS

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.

Hope that helps someone else who might be struggling with this issue as much as I was.

Conditionally-rendering css in html head

You should use ng-href instead of href.

<link ng-repeat="stylesheet in stylesheets" ng-href="{{stylesheet.href}}" type="{{stylesheet.type}}" rel="stylesheet" />

Example

AngularJS : dynamic stylesheet link tag fires request too soon

I solved it by adding an ngIf directive to the link tag, so it's not rendered until filename isn't falsy. Kinda dirty I know, but it actually works !

<link rel="stylesheet" data-ng-if="filename" data-ng-href="css/{{ filename }}.css" />

AngularJS setting a CSS file from API using JS

Let me answer your Question 2 first.

When an angular bootstraps, it will first traverse down the DOM nodes, then look for all the appropriate directives, evaluate them, compile and finally link them up. The reason why your second piece of code doesn't work is because your {{css}} evaluates to nothing, since it does not have a proper scope binded to.

What you can do is actually declare an ng-controller at your <head> level, and let this controller do its work. Something like this:

.controller('cssCtrl',['$scope','$http', function($scope,$http){
$http.get('/api/auth/test').
then(function(response) {
$scope.css = response.data.temp.css;
}, function(response) {
alert('Error retrieving css: ' + response);
});
}]

And in your html:

<head ng-controller="cssCtrl">
<link ng-attr-href="{{css}}" rel="stylesheet" type="text/css">
</head>

Here is a plnkr that demonstrates how.

ng-controller will come in handy here as you do not really have to define a route for it, per se. Note that your css will not be loaded until your angular bootstraps finishes and your cssCtrl gets instantiated. There will be some delay in this though, and hence our first question, do we have a better way in doing this?

I would say if you have really conditional presentation logic based on the angular app itself, then use ng-if and ng-class. If you need more low level ones, use ng-style.

If you are talking about really loading the entire new css stylesheet based on certain configuration/settings, then I would say, let the server (backend) handle this. The server will determine what kind of css should be loaded based on the configuration (maybe different regional apps have different css styles) and spawn its site. Your front end app (angular) will just need to listen to the server, and load without really digesting it.

Of course, this is not 100% the best way. If you really need a front-end-determined css, then go for it and design the app in that way! Only you know that use case of the app well. Maybe you can wrap the entire SPA into a MainCtrl and MainView, and let every configuration resolves first before the DOM is manipulated.

AngularJS $routeprovider Routing in Mutitab

Update

Version 1 (Without routing) Plunker Demo

It will redirect to a new view if you use #/jobs syntax. Here is a complete working solution without routes:

Your code on plunker is working because you didn't include ngRoute in your code.

View in plunker demo

app.js

$scope.tabs = [
{ id : 'jobs', label : 'Jobs', templateUrl:'jobs-partial.html' },
{ id : 'invoices', label : 'Invoices',templateUrl: 'invoices-partial.html' },
{ id : 'payments', label : 'Payments',templateUrl: 'payments-partial.html' }
];

$scope.activeTab = $scope.tabs[0];
$scope.changeActiveTab = function (tab) {
$scope.activeTab = tab;
};

$scope.isActiveTab = function (tabId) {
return tabId === $scope.activeTab;
}

Inside index.html

<body ng-controller="TabsCtrl">
<ul class="nav nav-tabs" >
<li ng-class="{active: isActiveTab(tab.id)}" ng-repeat="tab in tabs">
<a href="" ng-click="changeActiveTab(tab.id)" data-toggle="tab">{{tab.label}} </a>
</li>
</ul>

<div class="tab-content">
<div class="tab-pane fade" ng-class="{'in active': isActiveTab(tab.id)}" ng-repeat="tab in tabs"
ng-include="tab.templateUrl">
</div>
</div>
</body>

Version 2 (With routing) Plunker Demo

index.html

<!DOCTYPE html>
<html ng-app="plunkerApp">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<script src="https://code.angularjs.org/1.4.8/angular.js"></script>
<script src="https://code.angularjs.org/1.4.8/angular-route.js"></script>
<script src="app.js"></script>
</head>
<body>
<div ng-view></div>
</body>
</html>

app.js

angular
.module('untitled4App', [
'ngRoute'
])
.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) {
$routeProvider.when('/jobs', {
templateUrl: '/views/jobs-partial.html',
controller: 'JobsCtrl'
}).when('/invoices', {
templateUrl: '/views/invoices-partial.html',
controller: 'InvoicesCtrl'
}).when('/payments', {
templateUrl: '/views/payments-partial.html',
controller: 'PaymentsCtrl'
});

// make this demo work in plunker
$locationProvider.html5Mode(false);
}])
.factory('tabsService', function () {
return {
tabs: function () {
return [
{id: 'jobs', label: 'Jobs'},
{id: 'invoices', label: 'Invoices'},
{id: 'payments', label: 'Payments'}
]
},
activeTab: '',
isActiveTab: function (tabId) {
return tabId === this.activeTab;
}
}
})
.controller('JobsCtrl', ['$scope', 'tabsService', function ($scope, tabsService) {
$scope.tabs = tabsService.tabs();
$scope.tabsService = tabsService;

tabsService.activeTab = $scope.tabs[0].id;

}])
.controller('InvoicesCtrl', ['$scope', 'tabsService', function ($scope, tabsService) {
$scope.tabs = tabsService.tabs();
$scope.tabsService = tabsService;

tabsService.activeTab = $scope.tabs[1].id;

}])
.controller('PaymentsCtrl', ['$scope', 'tabsService', function ($scope, tabsService) {
$scope.tabs = tabsService.tabs();
$scope.tabsService = tabsService;

tabsService.activeTab = $scope.tabs[2].id;
}]);

jobs.partial.html

<ul class="nav nav-tabs">
<li ng-class="{active: tabsService.isActiveTab(tab.id)}" ng-repeat="tab in tabs">
<a href="#/{{tab.id}}">{{tab.label}} </a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade in active">
Jobs
</div>
</div>

invoices-partial.html

<ul class="nav nav-tabs">
<li ng-class="{active: tabsService.isActiveTab(tab.id)}" ng-repeat="tab in tabs">
<a href="#/{{tab.id}}">{{tab.label}} </a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade in active">
Invoices
</div>
</div>

payments-partial.html

<ul class="nav nav-tabs">
<li ng-class="{active: tabsService.isActiveTab(tab.id)}" ng-repeat="tab in tabs">
<a href="#/{{tab.id}}">{{tab.label}} </a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade in active">
Payments
</div>
</div>

Angular JS not loading view

I would like to start with advising you to follow this official tutorial: it shows you the beginnings of how to build an app with AngularJS and views. The reason I would advise this is because there are quite a few basic elements that are missing from the script, mostly the <a> tags. Maybe you can start from the tutorial and build your own code based on that.

Remarks about your code:

  • Add all your script tags with javascript in your <head>. These will then in turn be run by a browser, making sure that all dependencies are met
  • You forgot a comma after "app" in your module initialization
  • Most important, you should change your <p> tags to <a> tags if you want views to work. Angular uses the <a>-tags to make navigation to different views possible. The href attribute of an <a> tag is used for this. Check my code below and you can see that my href attributes are linked to the routeprovider in the when clause.

Below you will find your adjusted code. Hope it helps!

<!doctype html>
<html data-ng-app="app">
<head>
<meta charset="utf-8"/>
<title></title>

<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/bootstrap-responsive.css">
<link rel="stylesheet" type="text/css" href="css/bootstrap-extended.css">
<link rel="stylesheet" type="text/css" href="css/custom.css">
<!-- Java Script -->
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"> </script>
<script type="text/javascript" src="js/bootstrap.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular-route.js"></script>
<script>
var app = angular.module('app', ['ngRoute']);

app.config(function($routeProvider){

$routeProvider
.when('/',
{
controller: 'basicController',
templateUrl: 'partials/viewOne.html'
})
.when('/menu', {
controller: 'basicController',
templateUrl: 'partials/viewTwo.html'
})
.when('/map',
{
controller: 'basicController',
templateUrl: 'partials/viewThree.html'
})
.otherwise({ redirectTo: '/' });
});

app.controller('basicController', ['$scope', function ($scope) {
$scope.message = "Hello!";
}]);
</script>
</head>
<body>
<div id="container">
<nav class="col-xs-12">
<table id="navTable">
<tr>
<td><a href="#">view 1</a></td>
<td><a href="#/menu">view 2</a></td>
<td><a href="#/map">view 3</a></td>
</tr>
</table>
</nav>

<!-- View Placeholder -->
<article data-ng-view="" class="col-xs-6 col-xs-offset-3">
Views goes here...
</article>

<footer class="col-xs-12 navbar-fixed-bottom"></footer>
</div>
</body>
</html>


Related Topics



Leave a reply



Submit