├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── karma.conf.js ├── package.json ├── test └── ui-router-styles.js └── ui-router-styles.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'npm install' 7 | - 'bower install' 8 | notifications: 9 | email: 10 | - origin.of@gmail.com 11 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | karma: { 4 | travis: { 5 | configFile: 'karma.conf.js', 6 | singleRun: true, 7 | browsers: ['PhantomJS'] 8 | } 9 | } 10 | }); 11 | 12 | grunt.loadNpmTasks('grunt-karma'); 13 | grunt.registerTask('test', ['karma:travis']) 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Mazzuola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-ui-router-styles 2 | ==================== 3 | 4 | [![Build Status](https://travis-ci.org/manuelmazzuola/angular-ui-router-styles.svg)](https://travis-ci.org/manuelmazzuola/angular-ui-router-styles) 5 | 6 | This is a simple module for AngularJS that provides the ability to have route-specific CSS stylesheets, by integrating with Angular [uiRouter](https://github.com/angular-ui/ui-router). 7 | 8 | What does it do? 9 | --------------- 10 | 11 | It allows you to declare partial-specific or route-specific styles for your app using 12 | Angular's ui-router `$stateProvider` service. This solves that problem by allowing you to do something like this: 13 | 14 | ```javascript 15 | app.config(['$stateProvider', function($stateProvider){ 16 | $stateProvider 17 | 18 | .state('state1', { 19 | url: '/state1', 20 | controller: 'State1controller', 21 | template: '
', 22 | data: { 23 | css: [ 24 | 'styles/custom-state1.css', 25 | { 26 | name: 'layout', 27 | href: 'styles/state1-layout.css' 28 | } 29 | ] 30 | } 31 | }) 32 | 33 | .state('state1.state12', { 34 | url: '/:id', 35 | controller: 'State12Controller', 36 | templateUrl: 'views/my-template.html', 37 | data: { 38 | css: [ 39 | 'styles/custom-state1.state12.css', 40 | { 41 | name: 'layout', 42 | href: 'styles/state1.state12-layout.css' 43 | } 44 | ] 45 | } 46 | }) 47 | 48 | .state('state2', { 49 | url: '/state2', 50 | controller: 'State2Controller', 51 | templateUrl: 'views/another-template.html', 52 | data: { 53 | css: ['styles/custom-state2.css', 'styles/another.css'] 54 | } 55 | }) 56 | 57 | .state('state3', { 58 | url: '/state3', 59 | controller: 'State3Controller', 60 | templateUrl: 'views/another-super-template.html', 61 | data: { 62 | css: 'styles/custom-state3.css' 63 | } 64 | }) 65 | // more states can be declared here 66 | }]); 67 | ``` 68 | 69 | * For state1 state we will have CSS: ['styles/custom-state1.css', 'styles/state1-layout.css']. 70 | * For state1.state2 state we will have CSS: ['styles/custom-state1.css', 'styles/custom-state1.state12.css', 'styles/state1.state12-layout.css']. 71 | * For state2 state we will have CSS: ['styles/custom-state2.css', 'styles/another.css']. 72 | * For state3 state we will have CSS: ['styles/custom-state3.css']. 73 | 74 | 75 | How to install: 76 | --------------- 77 | 78 | * Install it with Bower via `bower install angular-ui-router-styles --save` 79 | 80 | * Ensure that your application module specifies `uiRouterStyles` as a dependency: `angular.module('myApplication', ['uiRouterStyles'])` 81 | 82 | * Add the directive `ui-router-styles` to your body tag or wherever you want 83 | ``` 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | * Add css file(s) relative path to the state data object 92 | ```javascript 93 | .state('state1', { 94 | url: '/state', 95 | controller: 'StateCtrl', 96 | templateUrl: 'views/my-template.html', 97 | data: { 98 | css: 'styles/some-overrides.css' 99 | } 100 | }) 101 | ``` 102 | 103 | A simple plunkr to understand the usage: http://plnkr.co/edit/HIcYEj2QRqBCwbZCU0Il?p=preview 104 | 105 | **Things to notice:** 106 | * Specifying a css property on the route is completely optional. If the state doesn't have a css property, the service will simply do nothing for that route. 107 | * You can even have multiple page-specific stylesheets per state, where the css property is an **array** of relative paths or objects contains the name and href attributes. 108 | * If a parent state exists the data object is inherited. 109 | 110 | 111 | This directive does the following things: 112 | 113 | * It compiles (using `$compile`) an html string that creates a set of tags for every item in the `data.css` state property using `ng-repeat` and `ng-href`. 114 | * It appends that compiled set of `` elements to the `` tag. 115 | * It then uses the `$rootScope` to listen for `'$stateChangeSuccess'` events. For every `'$stateChangeSuccess'` event, it cleans all css appended before and adds the new css file(s) to the `` tag if there are any. 116 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-styles", 3 | "version": "1.1.1", 4 | "main": "ui-router-styles.js", 5 | "dependencies": { 6 | "angular": ">=1.4.9", 7 | "angular-ui-router": "^0.2.13" 8 | }, 9 | "devDependencies": { 10 | "angular-mocks": "~1.4.9" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | plugins: [ 4 | 'karma-jasmine', 5 | 'karma-phantomjs-launcher' 6 | ], 7 | 8 | // base path, that will be used to resolve files and exclude 9 | basePath: '', 10 | 11 | // testing framework to use (jasmine/mocha/qunit/...) 12 | frameworks: ['jasmine'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'bower_components/angular/angular.js', 17 | 'bower_components/angular-mocks/angular-mocks.js', 18 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 19 | 'ui-router-styles.js', 20 | 'test/ui-router-styles.js' 21 | ], 22 | browsers: ['PhantomJS'], 23 | singleRun: true 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-styles", 3 | "main": "ui-router-styles.js", 4 | "version": "1.1.1", 5 | "private": false, 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "devDependencies": { 10 | "grunt": "^0.4.5", 11 | "grunt-karma": "^0.12.1", 12 | "jasmine-core": "^2.4.1", 13 | "karma": "^0.13.19", 14 | "karma-jasmine": "^0.3.7", 15 | "karma-phantomjs-launcher": "^0.2.3", 16 | "phantomjs": "^1.9.19" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/ui-router-styles.js: -------------------------------------------------------------------------------- 1 | // test/ui-router-styles.js 2 | 'use strict'; 3 | 4 | describe('Unit: should inject some css', function () { 5 | var $c, $scope, green = 'green', red = 'red', $state; 6 | 7 | // load the directive 8 | beforeEach(module('ui.router', function($locationProvider) { 9 | $locationProvider.html5Mode(false); 10 | })); 11 | 12 | beforeEach(module('uiRouterStyles')); 13 | beforeEach(module(defineStates)); 14 | beforeEach(inject(function(_$compile_, _$rootScope_, _$state_) { 15 | $c = _$compile_; 16 | $state = _$state_; 17 | $scope = _$rootScope_.$new(); 18 | } 19 | )); 20 | 21 | function initStateTo(state) { 22 | $state.go(state); 23 | $scope.$apply(); 24 | expect($state.current.name).toBe(state); 25 | } 26 | 27 | it("should inject the green css", function() { 28 | var element; 29 | element = $c('')($scope); 30 | initStateTo(green); 31 | $scope.$digest(); 32 | expect($scope.routeStyles).toContain('/green') 33 | }); 34 | 35 | it("should inject the red css", function() { 36 | var element; 37 | element = $c('')($scope); 38 | initStateTo(green); 39 | $scope.$digest(); 40 | expect($scope.routeStyles).toContain('/green') 41 | initStateTo(red); 42 | expect($scope.routeStyles).toContain('/red') 43 | expect($scope.routeStyles).not.toContain('/green') 44 | }); 45 | 46 | it("should have the base layout", function() { 47 | var element; 48 | element = $c('')($scope); 49 | initStateTo(red); 50 | $scope.$digest(); 51 | expect($scope.routeStyles).toContain('/red'); 52 | expect($scope.routeStyles).toContain('/layout-base.css'); 53 | expect($scope.routeStyles).not.toContain('/layout-green.css'); 54 | }); 55 | 56 | it("should have the green layout", function() { 57 | var element; 58 | element = $c('')($scope); 59 | initStateTo(green); 60 | $scope.$digest(); 61 | expect($scope.routeStyles).toContain('/green'); 62 | expect($scope.routeStyles).not.toContain('/layout-base.css'); 63 | expect($scope.routeStyles).toContain('/layout-green.css'); 64 | }); 65 | }); 66 | 67 | function defineStates($stateProvider) { 68 | $stateProvider.state('base', { 69 | url: '/base', 70 | template: '
', 71 | abstract: true, 72 | data: { 73 | css: [ 74 | { 75 | name: 'layout', 76 | href: '/layout-base.css' 77 | } 78 | ] 79 | } 80 | }); 81 | 82 | $stateProvider.state('green', { 83 | url: '/green', 84 | parent: 'base', 85 | template: '
', 86 | controller: function() {}, 87 | data: { 88 | css: [ 89 | '/green', 90 | { 91 | name: 'layout', 92 | href: '/layout-green.css' 93 | } 94 | ] 95 | } 96 | }); 97 | 98 | $stateProvider.state('red', { 99 | url: '/red', 100 | parent: 'base', 101 | template: '
', 102 | controller: function() {}, 103 | data: { 104 | css: [ 105 | '/red' 106 | ] 107 | } 108 | }); 109 | } 110 | -------------------------------------------------------------------------------- /ui-router-styles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Manuel Mazzuola 3 | * https://github.com/manuelmazzuola/angular-ui-router-styles 4 | * Inspired by https://github.com/tennisgent/angular-route-styles 5 | */ 6 | 7 | (function() { 8 | 'use strict'; 9 | angular 10 | .module('uiRouterStyles', ['ui.router']) 11 | .directive('uiRouterStyles', uiRouterStylesDirective); 12 | 13 | uiRouterStylesDirective.$inject = ['$rootScope', '$compile', '$state', '$interpolate', '$document']; 14 | function uiRouterStylesDirective($rootScope, $compile, $state, $interpolate, $document) { 15 | var directive = { 16 | restrict: 'EA', 17 | link: uiRouterStylesLink 18 | }; 19 | 20 | return directive; 21 | 22 | function uiRouterStylesLink(scope) { 23 | var start = $interpolate.startSymbol(), end = $interpolate.endSymbol(); 24 | var html = ''; 25 | 26 | scope.routeStyles = []; 27 | 28 | activate(); 29 | 30 | //// 31 | 32 | function activate() { 33 | angular.element($document[0].head).append($compile(html)(scope)); 34 | $rootScope.$on('$stateChangeSuccess', stateChangeSuccessCallback); 35 | } 36 | 37 | // Get the parent state 38 | function $$parentState(state) { 39 | // Check if state has explicit parent OR we try guess parent from its name 40 | var name = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; 41 | // If we were able to figure out parent name then get this state 42 | return name && $state.get(name); 43 | } 44 | 45 | function stateChangeSuccessCallback(evt, toState) { 46 | // From current state to the root 47 | var stylesObject = {}; 48 | scope.routeStyles = []; 49 | 50 | for(var state = toState; state && state.name !== ''; state=$$parentState(state)) { 51 | if(state && state.data && state.data.css) { 52 | if(!Array.isArray(state.data.css)) { 53 | state.data.css = [state.data.css]; 54 | } 55 | 56 | angular.forEach(state.data.css, function(css) { 57 | // css = {name: 'layout', href: '/layout.css'} 58 | if(typeof css === 'object' && css.name && css.href && !stylesObject[css.name]) { 59 | stylesObject[css.name] = css.href; 60 | }else if(typeof css === 'string') { 61 | stylesObject[css] = css; 62 | } 63 | }); 64 | 65 | angular.forEach(stylesObject, function(style) { 66 | if(scope.routeStyles.indexOf(style) === -1) scope.routeStyles.push(style); 67 | }); 68 | } 69 | } 70 | scope.routeStyles.reverse(); 71 | } 72 | } 73 | } 74 | })(); 75 | --------------------------------------------------------------------------------