├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular-ui-router-default.js ├── bower.json ├── build.txt ├── files.conf.js ├── gulpfile.js ├── index.d.ts ├── karma.conf.js ├── package.json ├── sample ├── app │ ├── app.js │ └── contacts │ │ ├── contacts-service.js │ │ ├── contacts.detail.html │ │ ├── contacts.detail.item.edit.html │ │ ├── contacts.detail.item.html │ │ ├── contacts.html │ │ ├── contacts.js │ │ └── contacts.list.html ├── assets │ └── contacts.json ├── common │ └── utils │ │ └── utils-service.js ├── css │ └── styles.css └── index.html ├── src └── angular-ui-router-default.ts ├── test └── angular-ui-router-default.spec.ts ├── tsconfig.json └── tslint.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "no-console": "error", 8 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 9 | "semi": ["error", "always"], 10 | "block-scoped-var": "error", 11 | "eqeqeq": "error", 12 | "brace-style": ["error", "1tbs"], 13 | "space-before-function-paren": ["error", "never"], 14 | "space-in-parens": ["error", "never"], 15 | "comma-spacing": ["error", { "before": false, "after": true }] 16 | } 17 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stepan Riha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-ui-router-default 2 | ========================= 3 | [![Build Status](https://travis-ci.org/nonplus/angular-ui-router-default.svg?branch=master)](https://travis-ci.org/nonplus/angular-ui-router-default) 4 | [![CDNJS](https://img.shields.io/cdnjs/v/angular-ui-router-default.svg)](https://cdnjs.com/libraries/angular-ui-router-default) 5 | 6 | Motivation 7 | ---------- 8 | 9 | Abstract state are useful for resolving values used by multiple child states. However, since one cannot navigate to an abstract state (`$state.go('abstract_parent')`) any part of the application that transitions state (`$state.go()`, `ui-sref`, etc.) must explicitly specify a non-abstract child state (`$state.go('abstract_parent.concrete-child')`). 10 | 11 | Abstract are also useful in top-level navigation links, since `ui-sref-active` is set for all their child states. However, since you can't directly navigate to the (`ui-sref="abstract_state"`), implementing these menu items usually requires an `ng-click` handler that navigates to a concrete state. 12 | 13 | The options for [How to: Set up a default/index child state](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-set-up-a-defaultindex-child-state]) are tedious, non-intuitive and depend on URL routing. There is a need for a more convenient way of defining default child states with some great [ideas on how to configure these](https://github.com/angular-ui/ui-router/issues/27). 14 | 15 | This module provides basic support for specifying the default child state as a string. 16 | 17 | Loading the Module 18 | ------------------ 19 | 20 | This module declares itself as ui.router.default, so it can be declared as a dependency of your application as normal: 21 | 22 | ```javascript 23 | var app = angular.module('myApp', ['ng', 'ui.router.default']); 24 | ``` 25 | 26 | Defining Default Child State 27 | ---------------------------- 28 | 29 | In your state definition for an abstract state, add a `default` property with the name of a child state (relative or absolute). 30 | The child state name can be provided statically as a string or dynamically as a function callback. 31 | 32 | When a state transtion targets this abstract state, it will be redirected to the default child state instead. 33 | 34 | ```javascript 35 | $stateProvider 36 | .state('parent', { 37 | abstract: true, 38 | default: '.index', 39 | template: '' 40 | }) 41 | .state('parent.index', { 42 | // ... 43 | }) 44 | .state('parent.page2', { 45 | // ... 46 | }) 47 | .state('another', { 48 | abstract: true, 49 | default: ['$rootScope', function($rootScope) { 50 | return $rootScope.edit ? '.edit' : '.display'; 51 | }] 52 | }) 53 | .state('another.display', { 54 | // ... 55 | }) 56 | .state('another.edit', { 57 | // ... 58 | }) 59 | .state('anotherWithPromise',{ 60 | abstract: true, 61 | default: ['$q',function($q){ 62 | var defer = $q.defer(); 63 | asyncFunctionThatReturnsPromise().then(function(){ 64 | defer.resolve('anotherWithPromise.details'); 65 | }); 66 | return defer.promise; 67 | }] 68 | }) 69 | .state('anotherWithPromise.details',{ 70 | // ... 71 | }) 72 | ``` 73 | 74 | #### Older version (< 0.0.5) 75 | 76 | Older versions of this module specified the default state by assigning it to the `abstract` property: 77 | 78 | ```javascript 79 | $stateProvider 80 | .state('parent', { 81 | abstract: '.index', 82 | template: '' 83 | }) 84 | // ... 85 | ``` 86 | 87 | This behavior is still supported, but is **deprecated**, because it causes TypeScript conflicts. It is recommended 88 | that the `{ abstract: true, default: '.index' }` format is used instead. 89 | 90 | Using Default Child State 91 | ------------------------- 92 | 93 | When a default child state is defined, the application can now navigate to the abstract parent state. 94 | ```javascript 95 | $state.go('parent'); 96 | ``` 97 | 98 | ```html 99 |
  • 100 | Go to Parent 101 |
  • 102 | ``` 103 | 104 | Copyright & License 105 | ------------------- 106 | 107 | Copyright 2015 Stepan Riha. All Rights Reserved. 108 | 109 | This may be redistributed under the MIT licence. For the full license terms, see the LICENSE file which 110 | should be alongside this readme. 111 | -------------------------------------------------------------------------------- /angular-ui-router-default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AngularJS module that adds support for specifying default child views for abstract states when using ui-router. 3 | * 4 | * @link https://github.com/nonplus/angular-ui-router-default 5 | * 6 | * @license angular-ui-router-default v0.0.6 7 | * (c) Copyright Stepan Riha 8 | * License MIT 9 | */ 10 | 11 | (function(angular) { 12 | 13 | "use strict"; 14 | var moduleName = 'ui.router.default'; 15 | if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { 16 | module.exports = moduleName; 17 | } 18 | var max_redirects = 10; 19 | angular.module(moduleName, ['ui.router']) 20 | .config(['$provide', function ($provide) { 21 | $provide.decorator('$state', ['$delegate', '$injector', '$q', function ($delegate, $injector, $q) { 22 | var transitionTo = $delegate.transitionTo; 23 | var pendingPromise; 24 | $delegate.transitionTo = function (to, toParams, options) { 25 | var numRedirects = 0; 26 | var $state = this; 27 | var nextState = to.name || to; 28 | var nextParams = toParams; 29 | var nextOptions = options; 30 | return fetchTarget(); 31 | function fetchTarget() { 32 | var target = $state.get(nextState, $state.$current); 33 | if (!target) { 34 | // default specification is invalid, let ui-router report the problem... 35 | return transitionTo.call($delegate, nextState, nextParams, nextOptions); 36 | } 37 | nextState = target.name; 38 | var absRedirectPromise = getAbstractRedirect(target); 39 | pendingPromise = absRedirectPromise; 40 | return $q.when(absRedirectPromise) 41 | .then(abstractTargetResolved); 42 | function abstractTargetResolved(abstractTarget) { 43 | if (absRedirectPromise !== pendingPromise) { 44 | return $q.reject(new Error('transition superseded')); 45 | } 46 | // we didn't get anything from the abstract target 47 | if (!abstractTarget) { 48 | return transitionTo.call($delegate, nextState, nextParams, nextOptions); 49 | } 50 | checkForMaxRedirect(); 51 | nextState = abstractTarget; 52 | return fetchTarget(); 53 | } 54 | function checkForMaxRedirect() { 55 | if (numRedirects === max_redirects) { 56 | throw new Error('Too many abstract state default redirects'); 57 | } 58 | numRedirects += 1; 59 | } 60 | } 61 | function getAbstractRedirect(state) { 62 | if (!state || !state.abstract || (state.abstract === true && !state.default)) { 63 | return null; 64 | } 65 | return invokeAbstract(state).then(abstractInvoked); 66 | function abstractInvoked(newState) { 67 | if (newState[0] === '.') { 68 | return nextState + newState; 69 | } 70 | else { 71 | return newState; 72 | } 73 | } 74 | } 75 | function invokeAbstract(state) { 76 | var defaultState; 77 | if (state.default) { 78 | defaultState = state.default; 79 | } 80 | else { 81 | defaultState = state.abstract; 82 | } 83 | if (defaultState instanceof Function || defaultState instanceof Array) { 84 | return $q.when($injector.invoke(defaultState)); 85 | } 86 | else { 87 | return $q.when(defaultState); 88 | } 89 | } 90 | }; 91 | return $delegate; 92 | }]); 93 | }]); 94 | 95 | 96 | })(window.angular); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-default", 3 | "main": "angular-ui-router-default.js", 4 | "version": "0.0.6", 5 | "homepage": "https://github.com/nonplus/angular-ui-router-default", 6 | "authors": [ 7 | "Stepan Riha " 8 | ], 9 | "description": "AngularJS module that adds support for specifying default child views for abstract states when using ui-router.", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /build.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * <%= info.description %> 3 | * 4 | * @link <%= info.homepage %> 5 | * 6 | * @license <%= info.name %> v<%= info.version %> 7 | * (c) Copyright <%= info.author %> 8 | * License <%= info.license %> 9 | */ 10 | 11 | (function(angular) { 12 | 13 | <%= contents %> 14 | 15 | })(window.angular); 16 | -------------------------------------------------------------------------------- /files.conf.js: -------------------------------------------------------------------------------- 1 | files = { 2 | libs: [ 3 | 'node_modules/angular/angular.js', 4 | 'node_modules/angular-ui-router/release/angular-ui-router.js' 5 | ], 6 | 7 | src: [ 8 | 'src/angular-ui-router-default.js' 9 | ], 10 | 11 | test: [ 12 | 'node_modules/angular-mocks/angular-mocks.js', 13 | 'test/*.spec.js' 14 | ] 15 | }; 16 | 17 | if (exports) { 18 | var _ = require('lodash'); 19 | _.extend(exports, files); 20 | } -------------------------------------------------------------------------------- /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 = 8083; 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-default.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', ['lint'], 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 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import angular = require("angular"); 2 | 3 | declare module 'angular' { 4 | export namespace ui { 5 | export type StateDefaultSpecifier = string 6 | | ((...args: any[]) => string) 7 | | ((...args: any[]) => ng.IPromise) 8 | | (string | ((...args: any[]) => string))[] 9 | | (string | ((...args: any[]) => ng.IPromise))[]; 10 | interface IState { 11 | default?: StateDefaultSpecifier 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router-default", 3 | "version": "0.0.6", 4 | "description": "AngularJS module that adds support for specifying default child views for abstract states when using ui-router.", 5 | "main": "angular-ui-router-default.js", 6 | "types": "./index.d.ts", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "ts-compile": "./node_modules/gulp/bin/gulp.js 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-default.git" 18 | }, 19 | "author": "Stepan Riha ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/nonplus/angular-ui-router-default/issues" 23 | }, 24 | "homepage": "https://github.com/nonplus/angular-ui-router-default", 25 | "devDependencies": { 26 | "@types/angular-mocks": "^1.5", 27 | "@types/jasmine": "^2.5.35", 28 | "angular": "^1.5", 29 | "angular-mocks": "^1.5", 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.11.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.0.2", 40 | "gulp-util": "^3.0.4", 41 | "gulp-wrap": "^0.11.0", 42 | "jasmine-core": "^2.5.2", 43 | "karma": "^0.12.31", 44 | "karma-chrome-launcher": "^0.1.8", 45 | "karma-firefox-launcher": "^0.1.7", 46 | "karma-jasmine": "^0.3.8", 47 | "lodash": "^3.8.0", 48 | "run-sequence": "^1.2.2", 49 | "tslint": "^3.15.1", 50 | "typescript": "^2.0.3" 51 | }, 52 | "dependencies": { 53 | "@types/angular": "^1.5", 54 | "@types/angular-ui-router": "^1.1" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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.default', 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', 27 | function ($stateProvider, $urlRouterProvider) { 28 | 29 | ///////////////////////////// 30 | // Redirects and Otherwise // 31 | ///////////////////////////// 32 | 33 | // Use $urlRouterProvider to configure any redirects (when) and invalid urls (otherwise). 34 | $urlRouterProvider 35 | 36 | // The `when` method says if the url is ever the 1st param, then redirect to the 2nd param 37 | // Here we are just setting up some convenience urls. 38 | .when('/c?id', '/contacts/:id') 39 | .when('/user/:id', '/contacts/:id') 40 | 41 | // If the url is ever invalid, e.g. '/asdf', then redirect to '/' aka the home state 42 | .otherwise('/'); 43 | 44 | 45 | ////////////////////////// 46 | // State Configurations // 47 | ////////////////////////// 48 | 49 | // Use $stateProvider to configure your states. 50 | $stateProvider 51 | 52 | ////////// 53 | // Home // 54 | ////////// 55 | 56 | .state("home", { 57 | 58 | // Use a url of "/" to set a states as the "index". 59 | url: "/", 60 | 61 | // Example of an inline template string. By default, templates 62 | // will populate the ui-view within the parent state's template. 63 | // For top level states, like this one, the parent template is 64 | // the index.html file. So this template will be inserted into the 65 | // ui-view within index.html. 66 | template: '

    Welcome to the UI-Router Demo

    ' + 67 | '

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

    ' + 69 | '

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

    ' 71 | 72 | }) 73 | 74 | /////////// 75 | // About // 76 | /////////// 77 | 78 | .state('about', { 79 | url: '/about', 80 | 81 | // Showing off how you could return a promise from templateProvider 82 | templateProvider: ['$timeout', 83 | function ( $timeout) { 84 | return $timeout(function () { 85 | return '

    UI-Router Resources

    '; 92 | }, 100); 93 | }] 94 | }) 95 | } 96 | ] 97 | ); 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.detail.item.edit.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{item.type}}

    3 |
    4 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.detail.item.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{item.type}}

    3 |
    {{item.value}}
    4 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 16 |
    17 | 18 | 19 | 20 |
    21 |
    22 |
    23 | 24 | 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /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 | // By specifying the name of a child state, when the abstract state is activated, it 15 | // will activate the specified child state instead. 16 | abstract: true, 17 | default: '.list', 18 | 19 | // This abstract state will prepend '/contacts' onto the urls of all its children. 20 | url: '/contacts', 21 | 22 | // Example of loading a template from a file. This is also a top level state, 23 | // so this template file will be loaded and then inserted into the ui-view 24 | // within index.html. 25 | templateUrl: 'app/contacts/contacts.html', 26 | 27 | // Use `resolve` to resolve any asynchronous controller dependencies 28 | // *before* the controller is instantiated. In this case, since contacts 29 | // returns a promise, the controller will wait until contacts.all() is 30 | // resolved before instantiation. Non-promise return values are considered 31 | // to be resolved immediately. 32 | resolve: { 33 | contacts: ['contacts', 34 | function( contacts){ 35 | return contacts.all(); 36 | }] 37 | }, 38 | 39 | // You can pair a controller to your template. There *must* be a template to pair with. 40 | controller: ['$scope', '$state', 'contacts', 'utils', 41 | function ( $scope, $state, contacts, utils) { 42 | 43 | // Add a 'contacts' field in this abstract parent's scope, so that all 44 | // child state views can access it in their scopes. Please note: scope 45 | // inheritance is not due to nesting of states, but rather choosing to 46 | // nest the templates of those states. It's normal scope inheritance. 47 | $scope.contacts = contacts; 48 | 49 | $scope.goToRandom = function () { 50 | var randId = utils.newRandomKey($scope.contacts, "id", $state.params.contactId); 51 | 52 | // $state.go() can be used as a high level convenience method 53 | // for activating a state programmatically. 54 | $state.go('contacts.detail', { contactId: randId }); 55 | }; 56 | }] 57 | }) 58 | 59 | ///////////////////// 60 | // Contacts > List // 61 | ///////////////////// 62 | 63 | // Using a '.' within a state name declares a child within a parent. 64 | // So you have a new state 'list' within the parent 'contacts' state. 65 | .state('contacts.list', { 66 | 67 | // Using an empty url means that this child state will become active 68 | // when its parent's url is navigated to. Urls of child states are 69 | // automatically appended to the urls of their parent. So this state's 70 | // url is '/contacts' (because '/contacts' + ''). 71 | url: '', 72 | 73 | // IMPORTANT: Now we have a state that is not a top level state. Its 74 | // template will be inserted into the ui-view within this state's 75 | // parent's template; so the ui-view within contacts.html. This is the 76 | // most important thing to remember about templates. 77 | templateUrl: 'app/contacts/contacts.list.html' 78 | }) 79 | 80 | /////////////////////// 81 | // Contacts > Detail // 82 | /////////////////////// 83 | 84 | // You can have unlimited children within a state. Here is a second child 85 | // state within the 'contacts' parent state. 86 | .state('contacts.detail', { 87 | 88 | // Urls can have parameters. They can be specified like :param or {param}. 89 | // If {} is used, then you can also specify a regex pattern that the param 90 | // must match. The regex is written after a colon (:). Note: Don't use capture 91 | // groups in your regex patterns, because the whole regex is wrapped again 92 | // behind the scenes. Our pattern below will only match numbers with a length 93 | // between 1 and 4. 94 | 95 | // Since this state is also a child of 'contacts' its url is appended as well. 96 | // So its url will end up being '/contacts/{contactId:[0-9]{1,4}}'. When the 97 | // url becomes something like '/contacts/42' then this state becomes active 98 | // and the $stateParams object becomes { contactId: 42 }. 99 | url: '/{contactId:[0-9]{1,4}}', 100 | 101 | // If there is more than a single ui-view in the parent template, or you would 102 | // like to target a ui-view from even higher up the state tree, you can use the 103 | // views object to configure multiple views. Each view can get its own template, 104 | // controller, and resolve data. 105 | 106 | // View names can be relative or absolute. Relative view names do not use an '@' 107 | // symbol. They always refer to views within this state's parent template. 108 | // Absolute view names use a '@' symbol to distinguish the view and the state. 109 | // So 'foo@bar' means the ui-view named 'foo' within the 'bar' state's template. 110 | views: { 111 | 112 | // So this one is targeting the unnamed view within the parent state's template. 113 | '': { 114 | templateUrl: 'app/contacts/contacts.detail.html', 115 | controller: ['$scope', '$stateParams', 'utils', 116 | function ( $scope, $stateParams, utils) { 117 | $scope.contact = utils.findById($scope.contacts, $stateParams.contactId); 118 | }] 119 | }, 120 | 121 | // This one is targeting the ui-view="hint" within the unnamed root, aka index.html. 122 | // This shows off how you could populate *any* view within *any* ancestor state. 123 | 'hint@': { 124 | template: 'This is contacts.detail populating the "hint" ui-view' 125 | }, 126 | 127 | // This one is targeting the ui-view="menuTip" within the parent state's template. 128 | 'menuTip': { 129 | // templateProvider is the final method for supplying a template. 130 | // There is: template, templateUrl, and templateProvider. 131 | templateProvider: ['$stateParams', 132 | function ( $stateParams) { 133 | // This is just to demonstrate that $stateParams injection works for templateProvider. 134 | // $stateParams are the parameters for the new state we're transitioning to, even 135 | // though the global '$stateParams' has not been updated yet. 136 | return '
    Contact ID: ' + $stateParams.contactId + ''; 137 | }] 138 | } 139 | } 140 | }) 141 | 142 | ////////////////////////////// 143 | // Contacts > Detail > Item // 144 | ////////////////////////////// 145 | 146 | .state('contacts.detail.item', { 147 | 148 | // So following what we've learned, this state's full url will end up being 149 | // '/contacts/{contactId}/item/:itemId'. We are using both types of parameters 150 | // in the same url, but they behave identically. 151 | url: '/item/:itemId', 152 | views: { 153 | 154 | // This is targeting the unnamed ui-view within the parent state 'contact.detail' 155 | // We wouldn't have to do it this way if we didn't also want to set the 'hint' view below. 156 | // We could instead just set templateUrl and controller outside of the view obj. 157 | '': { 158 | templateUrl: 'app/contacts/contacts.detail.item.html', 159 | controller: ['$scope', '$stateParams', '$state', 'utils', 160 | function ( $scope, $stateParams, $state, utils) { 161 | $scope.item = utils.findById($scope.contact.items, $stateParams.itemId); 162 | 163 | $scope.edit = function () { 164 | // Here we show off go's ability to navigate to a relative state. Using '^' to go upwards 165 | // and '.' to go down, you can navigate to any relative state (ancestor or descendant). 166 | // Here we are going down to the child state 'edit' (full name of 'contacts.detail.item.edit') 167 | $state.go('.edit', $stateParams); 168 | }; 169 | }] 170 | }, 171 | 172 | // Here we see we are overriding the template that was set by 'contacts.detail' 173 | 'hint@': { 174 | template: ' This is contacts.detail.item overriding the "hint" ui-view' 175 | } 176 | } 177 | }) 178 | 179 | ///////////////////////////////////// 180 | // Contacts > Detail > Item > Edit // 181 | ///////////////////////////////////// 182 | 183 | // Notice that this state has no 'url'. States do not require a url. You can use them 184 | // simply to organize your application into "places" where each "place" can configure 185 | // only what it needs. The only way to get to this state is via $state.go (or transitionTo) 186 | .state('contacts.detail.item.edit', { 187 | views: { 188 | 189 | // This is targeting the unnamed view within the 'contacts.detail' state 190 | // essentially swapping out the template that 'contacts.detail.item' had 191 | // inserted with this state's template. 192 | '@contacts.detail': { 193 | templateUrl: 'app/contacts/contacts.detail.item.edit.html', 194 | controller: ['$scope', '$stateParams', '$state', 'utils', 195 | function ( $scope, $stateParams, $state, utils) { 196 | $scope.item = utils.findById($scope.contact.items, $stateParams.itemId); 197 | $scope.done = function () { 198 | // Go back up. '^' means up one. '^.^' would be up twice, to the grandparent. 199 | $state.go('^', $stateParams); 200 | }; 201 | }] 202 | } 203 | } 204 | }); 205 | } 206 | ] 207 | ); 208 | -------------------------------------------------------------------------------- /sample/app/contacts/contacts.list.html: -------------------------------------------------------------------------------- 1 |

    All Contacts

    2 | 7 | -------------------------------------------------------------------------------- /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/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/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | ui-router 37 | 38 | 39 | 61 | 62 | 64 |
    65 | 66 | 67 |
    68 |
    69 |       
    70 |       $state = {{$state.current.name}}
    71 |       $stateParams = {{$stateParams}}
    72 |       $state full url = {{ $state.$current.url.source }}
    73 |       
    75 |     
    76 | 77 | 78 | -------------------------------------------------------------------------------- /src/angular-ui-router-default.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var moduleName = 'ui.router.default'; 4 | 5 | /* commonjs package manager support (eg componentjs) */ 6 | declare var module, exports; 7 | if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports) { 8 | module.exports = moduleName; 9 | } 10 | 11 | var max_redirects = 10; 12 | angular.module(moduleName, ['ui.router']) 13 | .config(['$provide', function($provide: ng.auto.IProvideService) { 14 | 15 | $provide.decorator('$state', ['$delegate', '$injector', '$q', function( 16 | $delegate: ng.ui.IStateService, 17 | $injector: ng.auto.IInjectorService, 18 | $q: ng.IQService 19 | ) { 20 | var transitionTo = $delegate.transitionTo; 21 | var pendingPromise; 22 | $delegate.transitionTo = function(to, toParams, options): ng.IPromise { 23 | var numRedirects = 0; 24 | var $state = this; 25 | var nextState = to.name || to; 26 | var nextParams = toParams; 27 | var nextOptions = options; 28 | 29 | return fetchTarget(); 30 | 31 | function fetchTarget(): ng.IPromise { 32 | var target = $state.get(nextState, $state.$current); 33 | 34 | if (!target) { 35 | // default specification is invalid, let ui-router report the problem... 36 | return transitionTo.call($delegate, nextState, nextParams, nextOptions); 37 | } 38 | 39 | nextState = target.name; 40 | 41 | var absRedirectPromise = getAbstractRedirect(target); 42 | pendingPromise = absRedirectPromise; 43 | return $q.when(absRedirectPromise) 44 | .then(abstractTargetResolved); 45 | 46 | function abstractTargetResolved(abstractTarget) { 47 | if (absRedirectPromise !== pendingPromise) { 48 | return $q.reject(new Error('transition superseded')); 49 | } 50 | // we didn't get anything from the abstract target 51 | if (!abstractTarget) { 52 | return transitionTo.call($delegate, nextState, nextParams, nextOptions); 53 | } 54 | checkForMaxRedirect(); 55 | nextState = abstractTarget; 56 | return fetchTarget(); 57 | } 58 | 59 | function checkForMaxRedirect() { 60 | if (numRedirects === max_redirects) { 61 | throw new Error('Too many abstract state default redirects'); 62 | } 63 | numRedirects += 1; 64 | } 65 | } 66 | 67 | function getAbstractRedirect(state: ng.ui.IState) { 68 | if (!state || !state.abstract || (state.abstract === true && !state.default)) { 69 | return null; 70 | } 71 | return invokeAbstract(state).then(abstractInvoked); 72 | 73 | function abstractInvoked(newState): string { 74 | if (newState[0] === '.') { 75 | return nextState + newState; 76 | } else { 77 | return newState; 78 | } 79 | } 80 | 81 | } 82 | 83 | function invokeAbstract(state: ng.ui.IState) { 84 | var defaultState: ng.ui.StateDefaultSpecifier; 85 | 86 | if (state.default) { 87 | defaultState = state.default; 88 | } else { 89 | defaultState = state.abstract as any; 90 | } 91 | 92 | if (defaultState instanceof Function || defaultState instanceof Array) { 93 | return $q.when($injector.invoke(defaultState as any)); 94 | } else { 95 | return $q.when(defaultState); 96 | } 97 | } 98 | 99 | }; 100 | 101 | return $delegate; 102 | }]); 103 | }]); -------------------------------------------------------------------------------- /test/angular-ui-router-default.spec.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let mock = angular.mock; 4 | type IStateService = ng.ui.IStateService; 5 | 6 | interface StateScope { 7 | state: string; 8 | } 9 | 10 | describe('navigating to state', function() { 11 | 12 | beforeEach(mock.module('ui.router.default')); 13 | 14 | describe("with non-existant absolute state", function() { 15 | it("should throw an informative error", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 16 | expect(function() { 17 | $state.go('somewhere'); $rootScope.$digest(); 18 | }).toThrowError(/^Could not resolve/); 19 | })); 20 | }); // with non-existant absolute state 21 | 22 | describe("with non-existant relative state", function() { 23 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 24 | $stateProvider.state('base', {}); 25 | })); 26 | 27 | it("should throw an informative error", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 28 | $state.go('base'); $rootScope.$digest(); 29 | expect(function() { 30 | $state.go('.somewhere'); $rootScope.$digest(); 31 | }).toThrowError(/^Could not resolve/); 32 | })); 33 | }); // with non-existant relative state 34 | 35 | describe("with abstract = false", function() { 36 | 37 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 38 | $stateProvider 39 | .state('base', { 40 | }) 41 | .state('base.concrete', { 42 | }); 43 | })); 44 | 45 | it("should use specified state", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 46 | $state.go('base'); $rootScope.$digest(); 47 | expect($state.current.name).toEqual('base'); 48 | 49 | $state.go('.concrete'); $rootScope.$digest(); 50 | expect($state.current.name).toEqual('base.concrete'); 51 | 52 | $state.go('^'); $rootScope.$digest(); 53 | expect($state.current.name).toEqual('base'); 54 | })); 55 | 56 | }); // with abstract = false 57 | 58 | 59 | describe("with abstract = true and no default", function() { 60 | 61 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 62 | $stateProvider 63 | .state('base', { 64 | }) 65 | .state('base.abstract', { 66 | abstract: true 67 | }) 68 | .state('base.abstract.child1', { 69 | }) 70 | .state('base.abstract.child2', { 71 | }); 72 | })); 73 | 74 | it("should fail", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 75 | expect(function(){ 76 | $state.go('base.abstract'); 77 | $rootScope.$digest(); 78 | }).toThrowError("Cannot transition to abstract state 'base.abstract'"); 79 | 80 | })); 81 | 82 | it("should fail to ^", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 83 | 84 | $state.go('base.abstract.child2'); $rootScope.$digest(); 85 | 86 | expect(function() { 87 | $state.go('^'); 88 | $rootScope.$digest(); 89 | }).toThrowError("Cannot transition to abstract state 'base.abstract'"); 90 | })); 91 | 92 | }); // with abstract = true and no default 93 | 94 | let members = ["abstract", "default"]; 95 | 96 | for (let member of members) { 97 | 98 | describe("abstract with " + member + " =", () => { 99 | 100 | describe("INVALID_STATE", function() { 101 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 102 | $stateProvider.state('base', { 103 | abstract: true, 104 | [member]: 'INVALID_STATE' 105 | }); 106 | })); 107 | 108 | it("should throw an informative error", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 109 | expect(function() { 110 | $state.go('base'); $rootScope.$digest(); 111 | }).toThrowError(/^Could not resolve.*INVALID_STATE/); 112 | })); 113 | }); // invalid 114 | 115 | describe(".INVALID_STATE", function() { 116 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 117 | $stateProvider.state('base', { 118 | abstract: true, 119 | [member]: '.INVALID_STATE' 120 | }); 121 | })); 122 | 123 | it("should throw an informative error", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 124 | expect(function() { 125 | $state.go('base'); $rootScope.$digest(); 126 | }).toThrowError(/^Could not resolve.*\.INVALID_STATE/); 127 | })); 128 | }); // .invalid 129 | 130 | describe("'.child'", function() { 131 | 132 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 133 | $stateProvider 134 | .state('base', { 135 | }) 136 | .state('base.abstract', { 137 | abstract: true, 138 | [member]: '.child2' 139 | }) 140 | .state('base.abstract.child1', { 141 | }) 142 | .state('base.abstract.child2', { 143 | }); 144 | })); 145 | 146 | it("should transition to child", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 147 | $state.go('base'); $rootScope.$digest(); 148 | expect($state.current.name).toEqual('base'); 149 | 150 | $state.go('.abstract'); $rootScope.$digest(); 151 | expect($state.current.name).toEqual('base.abstract.child2'); 152 | 153 | $state.go('^'); $rootScope.$digest(); 154 | expect($state.current.name).toEqual('base.abstract.child2'); 155 | })); 156 | 157 | }); // with abstract = '.child' 158 | 159 | describe("'.abstractChild'", function() { 160 | 161 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 162 | $stateProvider 163 | .state('base', { 164 | }) 165 | .state('base.abstract', { 166 | abstract: true, 167 | [member]: '.abstractChild' 168 | }) 169 | .state('base.abstract.child1', { 170 | }) 171 | .state('base.abstract.abstractChild', { 172 | abstract: true, 173 | [member]: '.grandChild' 174 | }) 175 | .state('base.abstract.abstractChild.grandChild', { 176 | }) 177 | ; 178 | })); 179 | 180 | it("should transition to concrete grand child", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 181 | $state.go('base'); $rootScope.$digest(); 182 | expect($state.current.name).toEqual('base'); 183 | 184 | $state.go('.abstract'); $rootScope.$digest(); 185 | expect($state.current.name).toEqual('base.abstract.abstractChild.grandChild'); 186 | 187 | $state.go('^'); $rootScope.$digest(); 188 | expect($state.current.name).toEqual('base.abstract.abstractChild.grandChild'); 189 | 190 | $state.go('^.^'); $rootScope.$digest(); 191 | expect($state.current.name).toEqual('base.abstract.abstractChild.grandChild'); 192 | 193 | $state.go('^.^.^'); $rootScope.$digest(); 194 | expect($state.current.name).toEqual('base'); 195 | })); 196 | 197 | }); // with abstract = '.child' 198 | 199 | describe("() => state", function() { 200 | 201 | var state; 202 | 203 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 204 | $stateProvider 205 | .state('base', { 206 | }) 207 | .state('base.abstract', { 208 | abstract: true, 209 | [member]: function() { 210 | return state; 211 | } 212 | }) 213 | .state('base.abstract.child1', { 214 | }) 215 | .state('base.abstract.child2', { 216 | }); 217 | })); 218 | 219 | it("should transition to child", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 220 | $state.go('base'); $rootScope.$digest(); 221 | expect($state.current.name).toEqual('base'); 222 | 223 | state = '.child1'; 224 | $state.go('.abstract'); $rootScope.$digest(); 225 | expect($state.current.name).toEqual('base.abstract.child1'); 226 | 227 | state = '.child2'; 228 | $state.go('^'); $rootScope.$digest(); 229 | expect($state.current.name).toEqual('base.abstract.child2'); 230 | })); 231 | 232 | it("should throw an informative error for INVALID_STATE", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 233 | $state.go('base'); $rootScope.$digest(); 234 | state = 'INVALID_STATE'; 235 | expect(function() { 236 | $state.go('base.abstract'); $rootScope.$digest(); 237 | }).toThrowError(/^Could not resolve.*INVALID_STATE/); 238 | })); 239 | 240 | it("should throw an informative error for .INVALID_STATE", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 241 | $state.go('base'); $rootScope.$digest(); 242 | state = '.INVALID_STATE'; 243 | expect(function() { 244 | $state.go('base.abstract'); $rootScope.$digest(); 245 | }).toThrowError(/^Could not resolve.*\.INVALID_STATE/); 246 | })); 247 | 248 | }); // () => state 249 | 250 | describe("['$rootScope', function($rootScope) => $rootScope.state]", function() { 251 | 252 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 253 | $stateProvider 254 | .state('base', { 255 | }) 256 | .state('base.abstract', { 257 | abstract: true, 258 | [member]: ['$rootScope', function($rootScope: ng.IRootScopeService & StateScope) { 259 | return $rootScope.state; 260 | }] 261 | }) 262 | .state('base.abstract.child1', { 263 | }) 264 | .state('base.abstract.child2', { 265 | }); 266 | })); 267 | 268 | it("should transition to child", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService & StateScope) { 269 | $state.go('base'); $rootScope.$digest(); 270 | expect($state.current.name).toEqual('base'); 271 | 272 | $rootScope.state = '.child1'; 273 | $state.go('.abstract'); $rootScope.$digest(); 274 | expect($state.current.name).toEqual('base.abstract.child1'); 275 | 276 | $rootScope.state = '.child2'; 277 | $state.go('^'); $rootScope.$digest(); 278 | expect($state.current.name).toEqual('base.abstract.child2'); 279 | })); 280 | 281 | it("should throw an informative error for INVALID_STATE", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService & StateScope) { 282 | $state.go('base'); $rootScope.$digest(); 283 | $rootScope.state = 'INVALID_STATE'; 284 | expect(function() { 285 | $state.go('base.abstract'); $rootScope.$digest(); 286 | }).toThrowError(/^Could not resolve.*INVALID_STATE/); 287 | })); 288 | 289 | it("should throw an informative error for .INVALID_STATE", mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService & StateScope) { 290 | $state.go('base'); $rootScope.$digest(); 291 | $rootScope.state = '.INVALID_STATE'; 292 | expect(function() { 293 | $state.go('base.abstract'); $rootScope.$digest(); 294 | }).toThrowError(/^Could not resolve.*\.INVALID_STATE/); 295 | })); 296 | 297 | }); // with abstract = ['$rootScope', function($rootScope: ng.IRootScopeService) { return ...; }] 298 | 299 | describe("() => IPromise that resolves", function() { 300 | 301 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 302 | 303 | $stateProvider 304 | .state('base', { 305 | }) 306 | .state('base.child', { 307 | }) 308 | .state('base.abstract', { 309 | abstract: true, 310 | [member]: ['$q', '$rootScope', function($q: ng.IQService, $rootScope: ng.IRootScopeService) { 311 | var defer = $q.defer(); 312 | setTimeout(function(){ 313 | defer.resolve('base.abstract.child'); 314 | $rootScope.$digest(); 315 | }, 5); 316 | return defer.promise; 317 | }] 318 | }) 319 | .state('base.abstract.child', { 320 | abstract: true, 321 | [member]: ['$q', '$rootScope', function($q: ng.IQService, $rootScope: ng.IRootScopeService) { 322 | var defer = $q.defer(); 323 | setTimeout(function(){ 324 | defer.resolve('.grandchild'); 325 | $rootScope.$digest(); 326 | }, 5); 327 | return defer.promise; 328 | }] 329 | }) 330 | .state('base.abstract.child.grandchild', { 331 | }) 332 | .state('base.abstract2', { 333 | abstract: true, 334 | [member]: ['$q', '$rootScope', function($q: ng.IQService, $rootScope: ng.IRootScopeService) { 335 | var defer = $q.defer(); 336 | setTimeout(function(){ 337 | defer.resolve('base.abstract2.child'); 338 | $rootScope.$digest(); 339 | }, 1); 340 | return defer.promise; 341 | }] 342 | }) 343 | .state('base.abstract2.child', { 344 | }); 345 | })); 346 | 347 | it("should transition from promise", function(done) { 348 | mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 349 | $state.go('base.abstract') 350 | .then(function() { 351 | expect($state.current.name).toBe('base.abstract.child.grandchild'); 352 | }) 353 | .catch(function() { 354 | throw new Error('Should not be here'); 355 | }) 356 | .finally(done); 357 | 358 | $rootScope.$digest(); 359 | }); 360 | }); 361 | 362 | it("should work for relative states", function(done) { 363 | mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 364 | $state.go('base.child'); 365 | $rootScope.$digest(); 366 | $state.go('^.abstract') 367 | .then(function() { 368 | expect($state.current.name).toBe('base.abstract.child.grandchild'); 369 | }) 370 | .catch(function() { 371 | throw new Error('Should not be here'); 372 | }) 373 | .finally(done); 374 | 375 | $rootScope.$digest(); 376 | }); 377 | }); 378 | 379 | it("should override first transition", function(done) { 380 | mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService) { 381 | var firstResolved = false, 382 | secondResolved = false; 383 | $state.go('base.abstract') 384 | .then(function() { 385 | 386 | throw new Error('Should not be here'); 387 | }) 388 | .catch(function(ex) { 389 | expect(ex.message).toBe('transition superseded'); 390 | }) 391 | .finally(function(){ 392 | firstResolved = true; 393 | checkForDone(); 394 | }); 395 | $state.go('base.abstract2').then(function(){ 396 | expect($state.current.name).toBe('base.abstract2.child'); 397 | }) 398 | .finally(function() { 399 | secondResolved = true; 400 | checkForDone(); 401 | }); 402 | $rootScope.$digest(); 403 | 404 | function checkForDone() { 405 | if (firstResolved && secondResolved) { 406 | done(); 407 | } else { 408 | if ($rootScope.$$phase !== 'digest') { 409 | $rootScope.$digest(); 410 | } 411 | } 412 | } 413 | }); 414 | }); 415 | 416 | }); // with promise returned from abstract 417 | 418 | describe('() => IPromise that rejects', function(){ 419 | 420 | beforeEach(mock.module(function($stateProvider: ng.ui.IStateProvider) { 421 | $stateProvider 422 | .state('base', { 423 | }) 424 | .state('base.abstract', { 425 | abstract: true, 426 | [member]: ['$q', '$rootScope', function($q: ng.IQService, $rootScope: ng.IRootScopeService) { 427 | var defer = $q.defer(); 428 | setTimeout(function(){ 429 | defer.reject('This is a rejection'); 430 | $rootScope.$apply(); 431 | }, 5); 432 | return defer.promise; 433 | }] 434 | }) 435 | .state('base.abstract.child', { 436 | }); 437 | })); 438 | 439 | it('should not transition due to rejected promise', function(done){ 440 | mock.inject(function($state: IStateService, $rootScope: ng.IRootScopeService){ 441 | $state.go('base'); 442 | $rootScope.$digest(); 443 | $state.go('base.abstract') 444 | .then(function(){ 445 | done.fail("The transition should've failed"); 446 | }) 447 | .catch(function(err){ 448 | expect($state.current.name).toBe('base'); 449 | expect(err).toBe('This is a rejection'); 450 | }) 451 | .finally(done); 452 | }); 453 | }); 454 | 455 | }); // with reject promise from abstract 456 | 457 | }); 458 | } // for member in members 459 | 460 | }); // navigating to state -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "umd", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "sourceMap": false, 8 | "lib": ["es5", "dom"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------