├── .gitignore ├── .travis.yml ├── sample ├── app │ ├── contacts │ │ ├── contacts.detail.item.html │ │ ├── contacts.detail.item.edit.html │ │ ├── contacts.list.html │ │ ├── contacts-service.js │ │ ├── contacts.detail.html │ │ ├── contacts.html │ │ └── contacts.js │ └── app.js ├── common │ └── utils │ │ └── utils-service.js ├── css │ └── styles.css ├── assets │ └── contacts.json └── index.html ├── tsconfig.json ├── files.conf.js ├── bower.json ├── index.d.ts ├── karma.conf.js ├── tslint.json ├── src └── angular-ui-router-title.ts ├── package.json ├── angular-ui-router-title.js ├── gulpfile.js ├── README.md └── test └── angular-ui-router-title.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | src/**/*.js 2 | test/**/*.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | - npm install -------------------------------------------------------------------------------- /sample/app/contacts/contacts.detail.item.html: -------------------------------------------------------------------------------- 1 |
2 |

{{item.type}}

3 |
{{item.value}}
4 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.detail.item.edit.html: -------------------------------------------------------------------------------- 1 |
2 |

{{item.type}}

3 |
4 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.list.html: -------------------------------------------------------------------------------- 1 |

All Contacts

2 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "umd", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "sourceMap": false, 8 | "noEmitOnError": true, 9 | "lib": ["es5", "dom"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /files.conf.js: -------------------------------------------------------------------------------- 1 | files = { 2 | libs: [ 3 | 'node_modules/lodash/index.js', 4 | 'node_modules/angular/angular.js', 5 | 'node_modules/angular-ui-router/release/angular-ui-router.js' 6 | ], 7 | 8 | src: [ 9 | 'src/angular-ui-router-title.js' 10 | ], 11 | 12 | test: [ 13 | 'node_modules/angular-mocks/angular-mocks.js', 14 | 'test/*.spec.js' 15 | ] 16 | }; 17 | 18 | if (exports) { 19 | var _ = require('lodash'); 20 | _.extend(exports, files); 21 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-title", 3 | "version": "0.1.1", 4 | "homepage": "https://github.com/nonplus/angular-ui-router-title", 5 | "authors": [ 6 | "Stepan Riha " 7 | ], 8 | "description": "AngularJS module for updating browser title/history based on the current ui-router state.", 9 | "main": "angular-ui-router-title.js", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts-service.js: -------------------------------------------------------------------------------- 1 | angular.module('uiRouterSample.contacts.service', [ 2 | 3 | ]) 4 | 5 | // A RESTful factory for retrieving contacts from 'contacts.json' 6 | .factory('contacts', ['$http', 'utils', function ($http, utils) { 7 | var path = 'assets/contacts.json'; 8 | var contacts = $http.get(path).then(function (resp) { 9 | return resp.data.contacts; 10 | }); 11 | 12 | var factory = {}; 13 | factory.all = function () { 14 | return contacts; 15 | }; 16 | factory.get = function (id) { 17 | return contacts.then(function(){ 18 | return utils.findById(contacts, id); 19 | }) 20 | }; 21 | return factory; 22 | }]); 23 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import angular = require("angular"); 2 | 3 | declare module "angular" { 4 | interface IRootScopeService { 5 | $title?: string; 6 | $breadcrumbs: { 7 | title: string; 8 | state: string; 9 | stateParams: { ["key"]: any } 10 | }[]; 11 | } 12 | 13 | namespace ui { 14 | interface ITitleService { 15 | title: () => string; 16 | breadCrumbs: () => { 17 | title: string; 18 | state: string; 19 | stateParams: { ["key"]: any } 20 | }[]; 21 | } 22 | 23 | interface ITitleProvider { 24 | documentTitle(inlineAnnotatedFunction: (string | ((...args: any[]) => string))[]): void; 25 | documentTitle(func: ((...args: any[]) => string) & Function): void; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.detail.html: -------------------------------------------------------------------------------- 1 |
2 |

{{contact.name}}

3 | 12 |
13 | 15 | Click on a contact item to view and/or edit it. 16 |
17 |
18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file 2 | module.exports = function (karma) { 3 | 4 | var files = require('./files.conf'); 5 | 6 | karma.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '.', 9 | 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [].concat(files.libs, files.src, files.test), 14 | 15 | // level of logging 16 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 17 | logLevel: karma.LOG_DEBUG, 18 | 19 | // Start these browsers, currently available: 20 | // - Chrome 21 | // - ChromeCanary 22 | // - Firefox 23 | // - Opera 24 | // - Safari 25 | // - PhantomJS 26 | browsers: [ 'Chrome' ] 27 | }) 28 | }; -------------------------------------------------------------------------------- /sample/common/utils/utils-service.js: -------------------------------------------------------------------------------- 1 | angular.module('uiRouterSample.utils.service', [ 2 | 3 | ]) 4 | 5 | .factory('utils', function () { 6 | return { 7 | // Util for finding an object by its 'id' property among an array 8 | findById: function findById(a, id) { 9 | for (var i = 0; i < a.length; i++) { 10 | if (a[i].id == id) return a[i]; 11 | } 12 | return null; 13 | }, 14 | 15 | // Util for returning a random key from a collection that also isn't the current key 16 | newRandomKey: function newRandomKey(coll, key, currentKey){ 17 | var randKey; 18 | do { 19 | randKey = coll[Math.floor(coll.length * Math.random())][key]; 20 | } while (randKey == currentKey); 21 | return randKey; 22 | } 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /sample/css/styles.css: -------------------------------------------------------------------------------- 1 | .slide.ng-leave { 2 | position: relative; 3 | } 4 | .slide.ng-enter { 5 | position: absolute; 6 | } 7 | .slide.ng-enter, .slide.ng-leave { 8 | -webkit-transition: -webkit-transform 0.3s ease-in, opacity 0.3s ease-in; 9 | -moz-transition: -moz-transform 0.3s ease-in, opacity 0.3s ease-in; 10 | -o-transition: -o-transform 0.3s ease-in, opacity 0.3s ease-in; 11 | transition: transform 0.3s ease-in, opacity 0.3s ease-in; 12 | } 13 | .slide.ng-enter, .slide.ng-leave.ng-leave-active { 14 | -webkit-transform: scaleX(0.0001); 15 | -o-transform: scaleX(0.0001); 16 | transform: scaleX(0.0001); 17 | opacity: 0; 18 | } 19 | .slide, .slide.ng-enter.ng-enter-active { 20 | -webkit-transform: scaleX(1); 21 | -o-transform: scaleX(1); 22 | transform: scaleX(1); 23 | opacity: 1; 24 | } 25 | -------------------------------------------------------------------------------- /sample/assets/contacts.json: -------------------------------------------------------------------------------- 1 | { 2 | "contacts":[ 3 | { 4 | "id": 1, 5 | "name": "Alice", 6 | "items": [ 7 | { 8 | "id": "a", 9 | "type": "phone number", 10 | "value": "555-1234-1234" 11 | }, 12 | { 13 | "id": "b", 14 | "type": "email", 15 | "value": "alice@mailinator.com" 16 | } 17 | ] 18 | }, 19 | { 20 | "id": 42, 21 | "name": "Bob", 22 | "items": [ 23 | { 24 | "id": "a", 25 | "type": "blog", 26 | "value": "http://bob.blogger.com" 27 | }, 28 | { 29 | "id": "b", 30 | "type": "fax", 31 | "value": "555-999-9999" 32 | } 33 | ] 34 | }, 35 | { 36 | "id": 123, 37 | "name": "Eve", 38 | "items": [ 39 | { 40 | "id": "a", 41 | "type": "full name", 42 | "value": "Eve Adamsdottir" 43 | } 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /sample/app/contacts/contacts.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "tabs" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "no-var-keyword": false, 18 | "one-line": [ 19 | true, 20 | "check-open-brace", 21 | "check-whitespace" 22 | ], 23 | "quotemark": [ 24 | false, 25 | "double" 26 | ], 27 | "semicolon": [ 28 | true, 29 | "always" 30 | ], 31 | "triple-equals": [ 32 | true, 33 | "allow-null-check" 34 | ], 35 | "typedef-whitespace": [ 36 | true, 37 | { 38 | "call-signature": "nospace", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | } 44 | ], 45 | "variable-name": [ 46 | true, 47 | "ban-keywords" 48 | ], 49 | "whitespace": [ 50 | true, 51 | "check-branch", 52 | "check-decl", 53 | "check-operator", 54 | "check-separator", 55 | "check-type" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /src/angular-ui-router-title.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let documentTitleCallback: (title: string) => string = undefined; 4 | let defaultDocumentTitle = document.title; 5 | 6 | angular.module("ui.router.title", ["ui.router"]) 7 | .provider("$title", function $titleProvider() { 8 | return { 9 | documentTitle: (cb) => { 10 | documentTitleCallback = cb; 11 | }, 12 | $get: ["$state", ($state: ng.ui.IStateService): ng.ui.ITitleService => { 13 | return { 14 | title: () => getTitleValue($state.$current.locals.globals["$title"]), 15 | breadCrumbs: () => { 16 | let $breadcrumbs = []; 17 | var state = $state.$current; 18 | while (state) { 19 | if (state["resolve"] && state["resolve"].$title) { 20 | $breadcrumbs.unshift({ 21 | title: getTitleValue(state.locals.globals["$title"]) as string, 22 | state: state["self"].name, 23 | stateParams: state.locals.globals["$stateParams"] 24 | }); 25 | } 26 | state = state["parent"]; 27 | } 28 | return $breadcrumbs; 29 | } 30 | }; 31 | }] 32 | }; 33 | }) 34 | .run(["$rootScope", "$timeout", "$title", "$injector", function( 35 | $rootScope: ng.IRootScopeService, 36 | $timeout: ng.ITimeoutService, 37 | $title: ng.ui.ITitleService, 38 | $injector 39 | ) { 40 | 41 | $rootScope.$on("$stateChangeSuccess", function() { 42 | var title = $title.title(); 43 | $timeout(function() { 44 | $rootScope.$title = title; 45 | const documentTitle = documentTitleCallback ? $injector.invoke(documentTitleCallback) : title || defaultDocumentTitle; 46 | document.title = documentTitle; 47 | }); 48 | 49 | $rootScope.$breadcrumbs = $title.breadCrumbs(); 50 | }); 51 | 52 | }]); 53 | 54 | function getTitleValue(title) { 55 | return angular.isFunction(title) ? title() : title; 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-title", 3 | "version": "0.1.1", 4 | "description": "AngularJS module for updating browser title/history based on the current ui-router state.", 5 | "main": "angular-ui-router-title.js", 6 | "types": "./index.d.ts", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "ts-compile": "./node_modules/gulp/bin/gulp.js lint ts-compile", 12 | "karma-test": "./node_modules/karma/bin/karma start --browsers Firefox --single-run", 13 | "test": "npm run ts-compile && npm run karma-test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/nonplus/angular-ui-router-title.git" 18 | }, 19 | "author": "Stepan Riha ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/nonplus/angular-ui-router-title/issues" 23 | }, 24 | "homepage": "https://github.com/nonplus/angular-ui-router-title", 25 | "devDependencies": { 26 | "@types/angular-mocks": "^1.5.5", 27 | "@types/jasmine": "^2.5.36", 28 | "angular": "^1.3.15", 29 | "angular-mocks": "^1.3.15", 30 | "angular-ui-router": "^0.2.18", 31 | "gulp": "^3.8.11", 32 | "gulp-bump": "^0.3.0", 33 | "gulp-filter": "^2.0.2", 34 | "gulp-git": "^1.2.3", 35 | "gulp-karma": "0.0.4", 36 | "gulp-serve": "^0.3.1", 37 | "gulp-tag-version": "^1.2.1", 38 | "gulp-tslint": "^6.1.2", 39 | "gulp-typescript": "^3.1.2", 40 | "gulp-util": "^3.0.4", 41 | "gulp-wrap": "^0.11.0", 42 | "jasmine-core": "^2.3.2", 43 | "karma": "^0.12.31", 44 | "karma-chrome-launcher": "^0.1.8", 45 | "karma-firefox-launcher": "^1.0.0", 46 | "karma-jasmine": "^0.3.5", 47 | "lodash": "^3.8.0", 48 | "open": "0.0.5", 49 | "run-sequence": "^1.1.0", 50 | "tslint": "^3.15.1", 51 | "typescript": "^2.0.6" 52 | }, 53 | "dependencies": { 54 | "@types/angular": "^1.5.17", 55 | "@types/angular-ui-router": "^1.1.34" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /angular-ui-router-title.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AngularJS module for updating browser title/history based on the current ui-router state. 3 | * 4 | * @link https://github.com/nonplus/angular-ui-router-title 5 | * 6 | * @license angular-ui-router-title v0.1.1 7 | * (c) Copyright Stepan Riha 8 | * License MIT 9 | */ 10 | 11 | (function(angular) { 12 | 13 | "use strict"; 14 | var documentTitleCallback = undefined; 15 | var defaultDocumentTitle = document.title; 16 | angular.module("ui.router.title", ["ui.router"]) 17 | .provider("$title", function $titleProvider() { 18 | return { 19 | documentTitle: function (cb) { 20 | documentTitleCallback = cb; 21 | }, 22 | $get: ["$state", function ($state) { 23 | return { 24 | title: function () { return getTitleValue($state.$current.locals.globals["$title"]); }, 25 | breadCrumbs: function () { 26 | var $breadcrumbs = []; 27 | var state = $state.$current; 28 | while (state) { 29 | if (state["resolve"] && state["resolve"].$title) { 30 | $breadcrumbs.unshift({ 31 | title: getTitleValue(state.locals.globals["$title"]), 32 | state: state["self"].name, 33 | stateParams: state.locals.globals["$stateParams"] 34 | }); 35 | } 36 | state = state["parent"]; 37 | } 38 | return $breadcrumbs; 39 | } 40 | }; 41 | }] 42 | }; 43 | }) 44 | .run(["$rootScope", "$timeout", "$title", "$injector", function ($rootScope, $timeout, $title, $injector) { 45 | $rootScope.$on("$stateChangeSuccess", function () { 46 | var title = $title.title(); 47 | $timeout(function () { 48 | $rootScope.$title = title; 49 | var documentTitle = documentTitleCallback ? $injector.invoke(documentTitleCallback) : title || defaultDocumentTitle; 50 | document.title = documentTitle; 51 | }); 52 | $rootScope.$breadcrumbs = $title.breadCrumbs(); 53 | }); 54 | }]); 55 | function getTitleValue(title) { 56 | return angular.isFunction(title) ? title() : title; 57 | } 58 | 59 | 60 | })(window.angular); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | var gulp = require('gulp'); 3 | var git = require('gulp-git'); 4 | var bump = require('gulp-bump'); 5 | var filter = require('gulp-filter'); 6 | var tag_version = require('gulp-tag-version'); 7 | var tslint = require("gulp-tslint"); 8 | var runSequence = require('run-sequence'); 9 | var wrap = require("gulp-wrap"); 10 | var gutil = require('gulp-util'); 11 | var serve = require('gulp-serve'); 12 | var karma = require('gulp-karma'); 13 | var files = require('./files.conf'); 14 | var testFiles = [].concat(files.libs, files.src, files.test); 15 | 16 | var port = 8082; 17 | 18 | gulp.task('bump-version', function () { 19 | return gulp.src(['./bower.json', './package.json']) 20 | .pipe(bump({type: "patch"}).on('error', gutil.log)) 21 | .pipe(gulp.dest('./')); 22 | }); 23 | 24 | gulp.task('commit-changes', ['test'], function () { 25 | return gulp.src('.') 26 | .pipe(git.commit('Bumped version number', {args: '-a'})); 27 | }); 28 | 29 | gulp.task('tag-version', function() { 30 | return gulp.src('package.json') 31 | .pipe(tag_version()); 32 | }); 33 | 34 | gulp.task('push-changes', function (cb) { 35 | git.push('origin', 'master', cb); 36 | }); 37 | 38 | gulp.task('release', ['ts-compile', 'test'], function (callback) { 39 | runSequence( 40 | 'bump-version', 41 | 'build', 42 | 'commit-changes', 43 | 'tag-version', 44 | function (error) { 45 | if (error) { 46 | console.log(error.message); 47 | } else { 48 | console.log('RELEASE FINISHED SUCCESSFULLY'); 49 | } 50 | callback(error); 51 | }); 52 | }); 53 | 54 | gulp.task('tag-version', function() { 55 | return gulp.src('./package.json') 56 | .pipe(tag_version()); 57 | }); 58 | 59 | gulp.task('build', ['ts-compile'], function() { 60 | return gulp.src("src/angular-ui-router-title.js") 61 | .pipe(wrap({ src: './build.txt' }, { info: require('./package.json') })) 62 | .pipe(gulp.dest('.')); 63 | }); 64 | 65 | gulp.task('ts-compile', function() { 66 | var ts = require('gulp-typescript'); 67 | var tsProject = ts.createProject('tsconfig.json'); 68 | return tsProject.src(['src/**/*.ts', 'test/**/*.ts']) 69 | .pipe(tsProject()).js 70 | .pipe(gulp.dest('.')); 71 | }); 72 | 73 | gulp.task('serve', serve({ 74 | root: __dirname, 75 | port: port, 76 | middleware: function(req, resp, next) { 77 | console.log(req.originalUrl); 78 | if(req.originalUrl == '/') { 79 | resp.statusCode = 302; 80 | resp.setHeader('Location', '/sample/'); 81 | resp.setHeader('Content-Length', '0'); 82 | resp.end(); 83 | } else { 84 | next(); 85 | } 86 | } 87 | })); 88 | 89 | gulp.task('demo', ['serve'], function() { 90 | require('open')('http://localhost:' + port); 91 | }); 92 | 93 | gulp.task('test', function() { 94 | // Be sure to return the stream 95 | return gulp.src(testFiles) 96 | .pipe(karma({ 97 | configFile: 'karma.conf.js', 98 | action: 'run' 99 | })) 100 | .on('error', function(err) { 101 | // Make sure failed tests cause gulp to exit non-zero 102 | throw err; 103 | }); 104 | }); 105 | 106 | gulp.task('watch', function() { 107 | gulp.src(testFiles) 108 | .pipe(karma({ 109 | configFile: 'karma.conf.js', 110 | action: 'watch' 111 | })); 112 | }); 113 | 114 | gulp.task('lint', function () { 115 | return gulp.src([ 116 | "./src/**/*.ts", 117 | "./test/**/*.ts" 118 | ]) 119 | .pipe(tslint({ 120 | formatter: "verbose" 121 | })) 122 | .pipe(tslint.report()); 123 | }); 124 | -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 61 | 62 | 64 |
65 | 66 | 67 |
68 |
69 |       
70 |       $title = {{$title}}
71 |       
$breadcrumbs = [ 72 | {{crumb}}
]
73 | $state = {{$state.current.name}} 74 | $stateParams = {{$stateParams}} 75 | $state full url = {{ $state.$current.url.source }} 76 | 78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /sample/app/app.js: -------------------------------------------------------------------------------- 1 | // Make sure to include the `ui.router` module as a dependency 2 | angular.module('uiRouterSample', [ 3 | 'uiRouterSample.contacts', 4 | 'uiRouterSample.contacts.service', 5 | 'uiRouterSample.utils.service', 6 | 'ui.router', 7 | 'ui.router.title', 8 | 'ngAnimate' 9 | ]) 10 | 11 | .run( 12 | [ '$rootScope', '$state', '$stateParams', 13 | function ($rootScope, $state, $stateParams) { 14 | 15 | // It's very handy to add references to $state and $stateParams to the $rootScope 16 | // so that you can access them from any scope within your applications.For example, 17 | //
  • will set the
  • 18 | // to active whenever 'contacts.list' or one of its decendents is active. 19 | $rootScope.$state = $state; 20 | $rootScope.$stateParams = $stateParams; 21 | } 22 | ] 23 | ) 24 | 25 | .config( 26 | [ '$stateProvider', '$urlRouterProvider', '$titleProvider', 27 | function ($stateProvider, $urlRouterProvider, $titleProvider) { 28 | 29 | $titleProvider.documentTitle(function($title) { 30 | return $title + ' - ui-router-title'; 31 | }); 32 | 33 | ///////////////////////////// 34 | // Redirects and Otherwise // 35 | ///////////////////////////// 36 | 37 | // Use $urlRouterProvider to configure any redirects (when) and invalid urls (otherwise). 38 | $urlRouterProvider 39 | 40 | // The `when` method says if the url is ever the 1st param, then redirect to the 2nd param 41 | // Here we are just setting up some convenience urls. 42 | .when('/c?id', '/contacts/:id') 43 | .when('/user/:id', '/contacts/:id') 44 | 45 | // If the url is ever invalid, e.g. '/asdf', then redirect to '/' aka the home state 46 | .otherwise('/'); 47 | 48 | 49 | ////////////////////////// 50 | // State Configurations // 51 | ////////////////////////// 52 | 53 | // Use $stateProvider to configure your states. 54 | $stateProvider 55 | 56 | ////////// 57 | // Home // 58 | ////////// 59 | 60 | .state("home", { 61 | 62 | // Use a url of "/" to set a states as the "index". 63 | url: "/", 64 | 65 | resolve: { 66 | // Static $title 67 | $title: function() { return "Home"; } 68 | }, 69 | 70 | // Example of an inline template string. By default, templates 71 | // will populate the ui-view within the parent state's template. 72 | // For top level states, like this one, the parent template is 73 | // the index.html file. So this template will be inserted into the 74 | // ui-view within index.html. 75 | template: '

    Welcome to the UI-Router Demo

    ' + 76 | '

    Use the menu above to navigate. ' + 77 | 'Pay attention to the $state and $stateParams values below.

    ' + 78 | '

    Click these links—Alice or ' + 79 | 'Bob—to see a url redirect in action.

    ' 80 | 81 | }) 82 | 83 | /////////// 84 | // About // 85 | /////////// 86 | 87 | .state('about', { 88 | url: '/about', 89 | 90 | resolve: { 91 | // Static $title 92 | $title: function() { return "About"; } 93 | }, 94 | 95 | // Showing off how you could return a promise from templateProvider 96 | templateProvider: ['$timeout', 97 | function ( $timeout) { 98 | return $timeout(function () { 99 | return '

    UI-Router Resources

    '; 106 | }, 100); 107 | }] 108 | }) 109 | } 110 | ] 111 | ); 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-ui-router-title 2 | ========================= 3 | 4 | [![Build Status](https://travis-ci.org/nonplus/angular-ui-router-title.svg?branch=master)](https://travis-ci.org/nonplus/angular-ui-router-title) 5 | 6 | AngularJS module for updating browser title/history based on the current ui-router state. 7 | 8 | 9 | Motivation 10 | ---------- 11 | 12 | Using ui-router states with `url` configurations enables browser history support and bookmarking of application state. 13 | It is important that the title in the browser history/bookmark represent the application state so that the user can tell 14 | where she's navigating to. 15 | 16 | The module provides a `$title` variable on the `$rootScope` that is populated based on the `$title` value resolved 17 | in `$state.$current` (or one of its parent states). If the current state doesn't resolve a `$title`, 18 | then `$rootScope.$title` will be `undefined`. 19 | 20 | The module also provides a `$breadcrumbs` array that is populated based on the `$title` of `$state.$current` and its parent states. 21 | 22 | The module sets the `document.title` to the value of the `$title` variable or, if configured, to the value returned by a `documentTitle(title)` callback. 23 | The browser sets bookmark and browser history text based on the `document.title`. 24 | 25 | 26 | Installing the Module 27 | --------------------- 28 | Installation can be done through bower: 29 | ``` shell 30 | bower install angular-ui-router-title 31 | ``` 32 | 33 | In your page add: 34 | ```html 35 | 36 | ``` 37 | 38 | 39 | Loading the Module 40 | ------------------ 41 | 42 | This module declares itself as `ui.router.title`, so it can be declared as a dependency of your application as normal: 43 | 44 | ```javascript 45 | var app = angular.module('myApp', ['ng', 'ui.router.title']); 46 | ``` 47 | 48 | 49 | Specifying the $title in the state definition 50 | --------------------------------------------- 51 | 52 | A state defines its title by declaring a `$title` value in its `resolve` block. 53 | It's a good idea for the `$title` to include information from the current state, 54 | so it may need to inject the `$stateParam` or another value that was resolved from them. 55 | 56 | ```javascript 57 | $stateProvider 58 | .state('home', { 59 | ... 60 | resolve: { 61 | // Constant title 62 | $title: function() { return 'Home'; } 63 | } 64 | }) 65 | .state('about', { 66 | url: '/about', 67 | ... 68 | resolve: { 69 | // Constant title 70 | $title: function() { return 'About'; } 71 | } 72 | }) 73 | .state('contacts', { 74 | url: '/contacts', 75 | ... 76 | resolve: { 77 | // List of contacts 78 | contacts: ['Contacts', function(Contacts) { 79 | // Use Contacts service to retrieve list 80 | return Contacts.query(); 81 | }], 82 | // Dynamic title showing number of contacts 83 | $title: ['contacts', function(contacts) { 84 | return 'Contacts (' + contacts.length + ')'; 85 | }] 86 | } 87 | }) 88 | .state('contact', { 89 | url: '/contact/:contactId', 90 | ... 91 | resolve: { 92 | // Single contact 93 | contact: ['Contacts', '$stateParams', function(Contacts, $stateParams) { 94 | // Use Contacts service to retrieve a contact 95 | return Contacts.get({ id: $stateParams.contactId }); 96 | }], 97 | // Dynamic title showing the name of contact 98 | $title: ['contact', function(contact) { 99 | return contact.name; 100 | }] 101 | } 102 | }) 103 | .state('contact.edit', { 104 | url: '/edit', 105 | ... 106 | resolve: { 107 | // Dynamic title appending to parent state's title 108 | $title: ['$title', function($title) { 109 | return $title + " (edit)"; 110 | }] 111 | } 112 | }) 113 | ``` 114 | 115 | 116 | Configuring a custom document.title 117 | ----------------------------------- 118 | 119 | By default, the module will set the `document.title` to the value of `$rootScope.$title`. A common convention is to include 120 | the application name in the document.title. Customization of the `document.title` can be achieved via the `$titleProvier.documentTitle` 121 | callback specification. 122 | 123 | ```javascript 124 | angular.module('myApp', ['ng', 'ui.router.title']) 125 | .config(function($titleProvider) { 126 | $titleProvider.documentTitle(function($rootScope) { 127 | return $rootScope.$title ? $rootScope.$title + " - My Application" : "My Application"; 128 | }); 129 | }); 130 | ``` 131 | 132 | 133 | Using the $title in a header 134 | ---------------------------- 135 | 136 | The `$title` property contains the resolve title and cen be used, for example, to set the contents of an `

    ` tag. 137 | 138 | ```html 139 |

    My Application

    140 | ``` 141 | 142 | 143 | Using the $breadcrumbs 144 | ---------------------- 145 | 146 | The `$breadcrumbs` array contains objects, one for each state that resolves a `$title` value. Each entry contains: 147 | 148 | * `title`: $title value of this state 149 | * `state`: name of the state 150 | * `stateParams`: $stateParams of the state. 151 | 152 | ```html 153 | 159 | ``` 160 | 161 | 162 | Copyright & License 163 | ------------------- 164 | 165 | Copyright 2015 Stepan Riha. All Rights Reserved. 166 | 167 | This may be redistributed under the MIT licence. For the full license terms, see the LICENSE file which 168 | should be alongside this readme. 169 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.js: -------------------------------------------------------------------------------- 1 | angular.module('uiRouterSample.contacts', [ 2 | 'ui.router' 3 | ]) 4 | 5 | .config( 6 | [ '$stateProvider', '$urlRouterProvider', 7 | function ($stateProvider, $urlRouterProvider) { 8 | $stateProvider 9 | ////////////// 10 | // Contacts // 11 | ////////////// 12 | .state('contacts', { 13 | 14 | // With abstract set to true, that means this state can not be explicitly activated. 15 | // It can only be implicitly activated by activating one of its children. 16 | abstract: true, 17 | 18 | // This abstract state will prepend '/contacts' onto the urls of all its children. 19 | url: '/contacts', 20 | 21 | // Example of loading a template from a file. This is also a top level state, 22 | // so this template file will be loaded and then inserted into the ui-view 23 | // within index.html. 24 | templateUrl: 'app/contacts/contacts.html', 25 | 26 | // Use `resolve` to resolve any asynchronous controller dependencies 27 | // *before* the controller is instantiated. In this case, since contacts 28 | // returns a promise, the controller will wait until contacts.all() is 29 | // resolved before instantiation. Non-promise return values are considered 30 | // to be resolved immediately. 31 | resolve: { 32 | // Dynamic $title using resolved 'contacts'. 33 | $title: ["contacts", function(contacts) { 34 | return "Contacts (" + contacts.length + ")"; 35 | }], 36 | contacts: ['contacts', 37 | function( contacts){ 38 | return contacts.all(); 39 | }] 40 | }, 41 | 42 | // You can pair a controller to your template. There *must* be a template to pair with. 43 | controller: ['$scope', '$state', 'contacts', 'utils', 44 | function ( $scope, $state, contacts, utils) { 45 | 46 | // Add a 'contacts' field in this abstract parent's scope, so that all 47 | // child state views can access it in their scopes. Please note: scope 48 | // inheritance is not due to nesting of states, but rather choosing to 49 | // nest the templates of those states. It's normal scope inheritance. 50 | $scope.contacts = contacts; 51 | 52 | $scope.goToRandom = function () { 53 | var randId = utils.newRandomKey($scope.contacts, "id", $state.params.contactId); 54 | 55 | // $state.go() can be used as a high level convenience method 56 | // for activating a state programmatically. 57 | $state.go('contacts.detail', { contactId: randId }); 58 | }; 59 | }] 60 | }) 61 | 62 | ///////////////////// 63 | // Contacts > List // 64 | ///////////////////// 65 | 66 | // Using a '.' within a state name declares a child within a parent. 67 | // So you have a new state 'list' within the parent 'contacts' state. 68 | .state('contacts.list', { 69 | 70 | // Using an empty url means that this child state will become active 71 | // when its parent's url is navigated to. Urls of child states are 72 | // automatically appended to the urls of their parent. So this state's 73 | // url is '/contacts' (because '/contacts' + ''). 74 | url: '', 75 | 76 | // IMPORTANT: Now we have a state that is not a top level state. Its 77 | // template will be inserted into the ui-view within this state's 78 | // parent's template; so the ui-view within contacts.html. This is the 79 | // most important thing to remember about templates. 80 | templateUrl: 'app/contacts/contacts.list.html' 81 | }) 82 | 83 | /////////////////////// 84 | // Contacts > Detail // 85 | /////////////////////// 86 | 87 | // You can have unlimited children within a state. Here is a second child 88 | // state within the 'contacts' parent state. 89 | .state('contacts.detail', { 90 | 91 | // Urls can have parameters. They can be specified like :param or {param}. 92 | // If {} is used, then you can also specify a regex pattern that the param 93 | // must match. The regex is written after a colon (:). Note: Don't use capture 94 | // groups in your regex patterns, because the whole regex is wrapped again 95 | // behind the scenes. Our pattern below will only match numbers with a length 96 | // between 1 and 4. 97 | 98 | // Since this state is also a child of 'contacts' its url is appended as well. 99 | // So its url will end up being '/contacts/{contactId:[0-9]{1,4}}'. When the 100 | // url becomes something like '/contacts/42' then this state becomes active 101 | // and the $stateParams object becomes { contactId: 42 }. 102 | url: '/{contactId:[0-9]{1,4}}', 103 | 104 | resolve: { 105 | // Dynamic $title using resolved 'contact' name. 106 | $title: ['contact', function(contact) { 107 | return contact.name; 108 | }], 109 | contact: ['utils', 'contacts', '$stateParams', function(utils, contacts, $stateParams) { 110 | return utils.findById(contacts, $stateParams.contactId); 111 | }] 112 | }, 113 | 114 | // If there is more than a single ui-view in the parent template, or you would 115 | // like to target a ui-view from even higher up the state tree, you can use the 116 | // views object to configure multiple views. Each view can get its own template, 117 | // controller, and resolve data. 118 | 119 | // View names can be relative or absolute. Relative view names do not use an '@' 120 | // symbol. They always refer to views within this state's parent template. 121 | // Absolute view names use a '@' symbol to distinguish the view and the state. 122 | // So 'foo@bar' means the ui-view named 'foo' within the 'bar' state's template. 123 | views: { 124 | 125 | // So this one is targeting the unnamed view within the parent state's template. 126 | '': { 127 | templateUrl: 'app/contacts/contacts.detail.html', 128 | controller: ['$scope', 'contact', 129 | function ( $scope, contact) { 130 | $scope.contact = contact; 131 | }] 132 | }, 133 | 134 | // This one is targeting the ui-view="hint" within the unnamed root, aka index.html. 135 | // This shows off how you could populate *any* view within *any* ancestor state. 136 | 'hint@': { 137 | template: 'This is contacts.detail populating the "hint" ui-view' 138 | }, 139 | 140 | // This one is targeting the ui-view="menuTip" within the parent state's template. 141 | 'menuTip': { 142 | // templateProvider is the final method for supplying a template. 143 | // There is: template, templateUrl, and templateProvider. 144 | templateProvider: ['$stateParams', 145 | function ( $stateParams) { 146 | // This is just to demonstrate that $stateParams injection works for templateProvider. 147 | // $stateParams are the parameters for the new state we're transitioning to, even 148 | // though the global '$stateParams' has not been updated yet. 149 | return '
    Contact ID: ' + $stateParams.contactId + ''; 150 | }] 151 | } 152 | } 153 | }) 154 | 155 | ////////////////////////////// 156 | // Contacts > Detail > Item // 157 | ////////////////////////////// 158 | 159 | .state('contacts.detail.item', { 160 | 161 | // So following what we've learned, this state's full url will end up being 162 | // '/contacts/{contactId}/item/:itemId'. We are using both types of parameters 163 | // in the same url, but they behave identically. 164 | url: '/item/:itemId', 165 | 166 | resolve: { 167 | // Dynamic $title using parent state's $title and resolved 'item' type. 168 | $title: ["$title", 'item', function($title, item) { 169 | return $title + " (" + item.type + ")"; 170 | }], 171 | item: ['$stateParams', 'contact', 'utils', 172 | function ( $stateParams, contact, utils) { 173 | return utils.findById(contact.items, $stateParams.itemId); 174 | }] 175 | }, 176 | 177 | views: { 178 | 179 | // This is targeting the unnamed ui-view within the parent state 'contact.detail' 180 | // We wouldn't have to do it this way if we didn't also want to set the 'hint' view below. 181 | // We could instead just set templateUrl and controller outside of the view obj. 182 | '': { 183 | templateUrl: 'app/contacts/contacts.detail.item.html', 184 | controller: ['$scope', '$stateParams', '$state', 'item', 185 | function ( $scope, $stateParams, $state, item) { 186 | $scope.item = item; 187 | 188 | $scope.edit = function () { 189 | // Here we show off go's ability to navigate to a relative state. Using '^' to go upwards 190 | // and '.' to go down, you can navigate to any relative state (ancestor or descendant). 191 | // Here we are going down to the child state 'edit' (full name of 'contacts.detail.item.edit') 192 | $state.go('.edit', $stateParams); 193 | }; 194 | }] 195 | }, 196 | 197 | // Here we see we are overriding the template that was set by 'contacts.detail' 198 | 'hint@': { 199 | template: ' This is contacts.detail.item overriding the "hint" ui-view' 200 | } 201 | } 202 | }) 203 | 204 | ///////////////////////////////////// 205 | // Contacts > Detail > Item > Edit // 206 | ///////////////////////////////////// 207 | 208 | // Notice that this state has no 'url'. States do not require a url. You can use them 209 | // simply to organize your application into "places" where each "place" can configure 210 | // only what it needs. The only way to get to this state is via $state.go (or transitionTo) 211 | .state('contacts.detail.item.edit', { 212 | views: { 213 | 214 | // This is targeting the unnamed view within the 'contacts.detail' state 215 | // essentially swapping out the template that 'contacts.detail.item' had 216 | // inserted with this state's template. 217 | '@contacts.detail': { 218 | templateUrl: 'app/contacts/contacts.detail.item.edit.html', 219 | controller: ['$scope', '$stateParams', '$state', 'utils', 220 | function ( $scope, $stateParams, $state, utils) { 221 | $scope.item = utils.findById($scope.contact.items, $stateParams.itemId); 222 | $scope.done = function () { 223 | // Go back up. '^' means up one. '^.^' would be up twice, to the grandparent. 224 | $state.go('^', $stateParams); 225 | }; 226 | }] 227 | } 228 | } 229 | }); 230 | } 231 | ] 232 | ); 233 | -------------------------------------------------------------------------------- /test/angular-ui-router-title.spec.ts: -------------------------------------------------------------------------------- 1 | let mock = angular.mock; 2 | 3 | describe('on $stateChangeSuccess', function() { 4 | 5 | beforeEach(mock.module('ui.router.title')); 6 | 7 | describe("when no $title", function() { 8 | 9 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 10 | 11 | $stateProvider 12 | .state('parent', { 13 | }) 14 | .state('parent.child', { 15 | }) 16 | ; 17 | 18 | })); 19 | 20 | it("should delete $rootScope.$title", mock.inject(function( 21 | $state: ng.ui.IStateService, 22 | $rootScope: ng.IRootScopeService, 23 | $timeout: ng.ITimeoutService 24 | ) { 25 | $rootScope.$title = 'originalTitle'; 26 | 27 | $state.go('parent'); 28 | $timeout.flush(); $rootScope.$digest(); 29 | expect($rootScope.$title).toEqual(undefined); 30 | 31 | $rootScope.$title = 'originalTitle'; 32 | $state.go('parent.child'); 33 | $timeout.flush(); $rootScope.$digest(); 34 | expect($rootScope.$title).toEqual(undefined); 35 | })); 36 | 37 | it("should set $rootScope.$breadCrumbs to []", mock.inject(function( 38 | $state: ng.ui.IStateService, 39 | $rootScope: ng.IRootScopeService, 40 | $timeout: ng.ITimeoutService 41 | ) { 42 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 43 | 44 | $state.go('parent'); 45 | $timeout.flush(); $rootScope.$digest(); 46 | expect($rootScope.$breadcrumbs).toEqual([]); 47 | 48 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 49 | 50 | $state.go('parent.child'); 51 | $timeout.flush(); $rootScope.$digest(); 52 | expect($rootScope.$breadcrumbs).toEqual([]); 53 | })); 54 | 55 | }); // when no $title 56 | 57 | describe("when $title is a function", function() { 58 | 59 | var parentTitle, grandChildTitle; 60 | 61 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 62 | 63 | $stateProvider 64 | .state('parent', { 65 | params: { 66 | param1: null 67 | }, 68 | resolve: { 69 | $title: function() { 70 | return function() { 71 | return parentTitle; 72 | }; 73 | } 74 | } 75 | }) 76 | .state('parent.child', { 77 | }) 78 | .state('parent.child.grandchild', { 79 | params: { 80 | param2: null 81 | }, 82 | resolve: { 83 | $title: function() { 84 | return function() { 85 | return grandChildTitle; 86 | }; 87 | } 88 | } 89 | }) 90 | ; 91 | 92 | })); 93 | 94 | it("should set $rootScope.$title", mock.inject(function( 95 | $state: ng.ui.IStateService, 96 | $rootScope: ng.IRootScopeService, 97 | $timeout: ng.ITimeoutService 98 | ) { 99 | $rootScope.$title = 'originalTitle'; 100 | 101 | parentTitle = 'parent-title 1'; 102 | $state.go('parent'); 103 | $timeout.flush(); $rootScope.$digest(); 104 | expect($rootScope.$title).toEqual('parent-title 1'); 105 | 106 | parentTitle = 'parent-title 2'; 107 | $state.go('parent.child'); 108 | $timeout.flush(); $rootScope.$digest(); 109 | $state.go('parent'); 110 | $timeout.flush(); $rootScope.$digest(); 111 | expect($rootScope.$title).toEqual('parent-title 2'); 112 | 113 | parentTitle = 'parent-title 3'; 114 | $state.go('parent.child'); 115 | $timeout.flush(); $rootScope.$digest(); 116 | expect($rootScope.$title).toEqual('parent-title 3'); 117 | 118 | grandChildTitle = 'grandchild-title 1'; 119 | $state.go('parent.child.grandchild', { param1: "param1" }); 120 | $timeout.flush(); $rootScope.$digest(); 121 | expect($rootScope.$title).toEqual('grandchild-title 1'); 122 | })); 123 | 124 | it("should set $rootScope.$breadcrumbs", mock.inject(function( 125 | $state: ng.ui.IStateService, 126 | $rootScope: ng.IRootScopeService, 127 | $timeout: ng.ITimeoutService 128 | ) { 129 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 130 | 131 | parentTitle = 'parent-title 1'; 132 | $state.go('parent'); 133 | $timeout.flush(); $rootScope.$digest(); 134 | expect($rootScope.$breadcrumbs).toEqual([{ 135 | title: 'parent-title 1', 136 | state: 'parent', 137 | stateParams: { param1: null } 138 | }]); 139 | 140 | parentTitle = 'parent-title 2'; 141 | $state.go('parent.child'); 142 | $timeout.flush(); $rootScope.$digest(); 143 | $state.go('parent'); 144 | $timeout.flush(); $rootScope.$digest(); 145 | expect($rootScope.$breadcrumbs).toEqual([{ 146 | title: 'parent-title 2', 147 | state: 'parent', 148 | stateParams: { param1: null } 149 | }]); 150 | 151 | parentTitle = 'parent-title 3'; 152 | $state.go('parent.child', { param1: "param1" }); 153 | $timeout.flush(); $rootScope.$digest(); 154 | expect($rootScope.$breadcrumbs).toEqual([{ 155 | title: 'parent-title 3', 156 | state: 'parent', 157 | stateParams: { param1: "param1" } 158 | }]); 159 | 160 | grandChildTitle = 'grandchild-title 1'; 161 | $state.go('parent.child.grandchild', { param1: "param1", param2: "param2" }); 162 | $timeout.flush(); $rootScope.$digest(); 163 | expect($rootScope.$breadcrumbs).toEqual([{ 164 | title: 'parent-title 3', 165 | state: 'parent', 166 | stateParams: { param1: "param1" } 167 | }, { 168 | title: 'grandchild-title 1', 169 | state: 'parent.child.grandchild', 170 | stateParams: { param1: "param1", param2: "param2" } 171 | }]); 172 | })); 173 | 174 | }); // when $title is a function 175 | 176 | describe("when $title is a value", function() { 177 | 178 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 179 | 180 | $stateProvider 181 | .state('parent', { 182 | params: { 183 | param1: null 184 | }, 185 | resolve: { 186 | $title: () => 'parent-title' 187 | } 188 | }) 189 | .state('parent.child', { 190 | }) 191 | .state('parent.child.grandchild', { 192 | params: { 193 | param2: null 194 | }, 195 | resolve: { 196 | $title: () => 'grandchild-title' 197 | } 198 | }) 199 | ; 200 | 201 | })); 202 | 203 | it("should set $rootScope.$title", mock.inject(function( 204 | $state: ng.ui.IStateService, 205 | $rootScope: ng.IRootScopeService, 206 | $timeout: ng.ITimeoutService 207 | ) { 208 | $rootScope.$title = 'originalTitle'; 209 | 210 | $state.go('parent'); 211 | $timeout.flush(); $rootScope.$digest(); 212 | expect($rootScope.$title).toEqual('parent-title'); 213 | 214 | $state.go('parent.child'); 215 | $timeout.flush(); $rootScope.$digest(); 216 | expect($rootScope.$title).toEqual('parent-title'); 217 | 218 | $state.go('parent.child.grandchild'); 219 | $timeout.flush(); $rootScope.$digest(); 220 | expect($rootScope.$title).toEqual('grandchild-title'); 221 | })); 222 | 223 | it("should set $rootScope.$breadCrumbs", mock.inject(function( 224 | $state: ng.ui.IStateService, 225 | $rootScope: ng.IRootScopeService, 226 | $timeout: ng.ITimeoutService 227 | ) { 228 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 229 | 230 | $state.go('parent', { param1: "param1" }); 231 | $timeout.flush(); $rootScope.$digest(); 232 | expect($rootScope.$breadcrumbs).toEqual([{ 233 | title: 'parent-title', 234 | state: 'parent', 235 | stateParams: { param1: "param1" } 236 | }]); 237 | 238 | $state.go('parent.child', { param1: "param1" }); 239 | $timeout.flush(); $rootScope.$digest(); 240 | expect($rootScope.$breadcrumbs).toEqual([{ 241 | title: 'parent-title', 242 | state: 'parent', 243 | stateParams: { param1: "param1" } 244 | }]); 245 | 246 | $state.go('parent.child.grandchild', { param1: "param1", param2: "param2" }); 247 | $timeout.flush(); $rootScope.$digest(); 248 | expect($rootScope.$title).toEqual('grandchild-title'); 249 | expect($rootScope.$breadcrumbs).toEqual([{ 250 | title: 'parent-title', 251 | state: 'parent', 252 | stateParams: { param1: "param1" } 253 | }, { 254 | title: 'grandchild-title', 255 | state: 'parent.child.grandchild', 256 | stateParams: { param1: "param1", param2: "param2" } 257 | }]); 258 | })); 259 | 260 | }); // when $title is a value 261 | 262 | describe("when documentTitle configured", function() { 263 | 264 | function documentTitle(title) { 265 | return title + " Document Title"; 266 | } 267 | 268 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider, $titleProvider: ng.ui.ITitleProvider) { 269 | 270 | $stateProvider 271 | .state('parent', { 272 | params: { 273 | param1: null 274 | }, 275 | resolve: { 276 | $title: () => 'parent-title' 277 | } 278 | }) 279 | .state('parent.child', { 280 | }) 281 | .state('parent.child.grandchild', { 282 | params: { 283 | param2: null 284 | }, 285 | resolve: { 286 | $title: () => 'grandchild-title' 287 | } 288 | }) 289 | ; 290 | 291 | $titleProvider.documentTitle(function ($rootScope) { 292 | return documentTitle($rootScope.$title); 293 | }); 294 | 295 | })); 296 | 297 | it("should set document.title and to documentTitle($title)", mock.inject(function( 298 | $state: ng.ui.IStateService, 299 | $rootScope: ng.IRootScopeService, 300 | $timeout: ng.ITimeoutService 301 | ) { 302 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 303 | for (let eltTitle of [].slice.call(document.getElementsByTagName("title"))) { 304 | eltTitle.parentElement.removeChild(eltTitle); 305 | } 306 | document.title = "Initial Title"; 307 | 308 | $state.go('parent', { param1: "param1" }); 309 | $timeout.flush(); $rootScope.$digest(); 310 | assertTitle('parent-title'); 311 | 312 | $state.go('parent.child', { param1: "param1" }); 313 | $timeout.flush(); $rootScope.$digest(); 314 | assertTitle('parent-title'); 315 | 316 | $state.go('parent.child.grandchild', { param1: "param1", param2: "param2" }); 317 | $timeout.flush(); $rootScope.$digest(); 318 | assertTitle('grandchild-title'); 319 | })); 320 | 321 | function assertTitle($title) { 322 | let expecte = documentTitle($title); 323 | expect(document.title).toEqual(expecte); 324 | expect((document.getElementsByTagName("title")[0] as HTMLTitleElement).text).toEqual(expecte); 325 | } 326 | 327 | }); // when documentTitle configured 328 | 329 | describe("when documentTitle not configured", function() { 330 | 331 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider, $titleProvider: ng.ui.ITitleProvider) { 332 | 333 | $stateProvider 334 | .state('parent', { 335 | params: { 336 | param1: null 337 | }, 338 | resolve: { 339 | $title: () => 'parent-title' 340 | } 341 | }) 342 | .state('parent.child', { 343 | }) 344 | .state('parent.child.grandchild', { 345 | params: { 346 | param2: null 347 | }, 348 | resolve: { 349 | $title: () => 'grandchild-title' 350 | } 351 | }) 352 | ; 353 | 354 | $titleProvider.documentTitle(null); 355 | 356 | })); 357 | 358 | it("should set document.title and <title> to $title", mock.inject(function( 359 | $state: ng.ui.IStateService, 360 | $rootScope: ng.IRootScopeService, 361 | $timeout: ng.ITimeoutService 362 | ) { 363 | $rootScope.$breadcrumbs = [{ title: "title", state: "state", stateParams: null }]; 364 | for (let eltTitle of [].slice.call(document.getElementsByTagName("title"))) { 365 | eltTitle.parentElement.removeChild(eltTitle); 366 | } 367 | document.title = "Initial Title"; 368 | 369 | $state.go('parent', { param1: "param1" }); 370 | $timeout.flush(); $rootScope.$digest(); 371 | assertTitle('parent-title'); 372 | 373 | $state.go('parent.child', { param1: "param1" }); 374 | $timeout.flush(); $rootScope.$digest(); 375 | assertTitle('parent-title'); 376 | 377 | $state.go('parent.child.grandchild', { param1: "param1", param2: "param2" }); 378 | $timeout.flush(); $rootScope.$digest(); 379 | assertTitle('grandchild-title'); 380 | })); 381 | 382 | function assertTitle($title) { 383 | let expected = $title; 384 | expect(document.title).toEqual(expected); 385 | expect((document.getElementsByTagName("title")[0] as HTMLTitleElement).text).toEqual(expected); 386 | } 387 | 388 | }); // when documentTitle not configured 389 | 390 | }); // on $stateChangeSuccess --------------------------------------------------------------------------------