├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── config ├── config.js └── karma.conf.js ├── gulpfile.js ├── index.js ├── package.json ├── statehelper.js ├── statehelper.min.js └── test └── statehelperSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bower_components 3 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mark Lagendijk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui-router.stateHelper 2 | A helper module for AngularUI Router, which allows you to define your states as an object tree. 3 | 4 | ## Installation 5 | 1. `bower install angular-ui-router.stateHelper` or `npm install angular-ui-router.statehelper` 6 | 2. Reference `stateHelper.min.js`. 7 | 3. Add a dependency on `ui.router.stateHelper` in your app module. 8 | 9 | ## Usage 10 | ``` javascript 11 | // NOTE: when using child states with views you should make sure that its parent has a template containing a `ui-view` directive. 12 | angular.module('myApp', [ 'ui.router', 'ui.router.stateHelper' ]) 13 | .config(function(stateHelperProvider){ 14 | stateHelperProvider 15 | .state({ 16 | name: 'root', 17 | templateUrl: 'root.html', 18 | children: [ 19 | { 20 | name: 'contacts', 21 | template: '', 22 | children: [ 23 | { 24 | name: 'list', 25 | templateUrl: 'contacts.list.html' 26 | } 27 | ] 28 | }, 29 | { 30 | name: 'products', 31 | templateUrl: 'products.html', 32 | children: [ 33 | { 34 | name: 'list', 35 | templateUrl: 'products.list.html' 36 | } 37 | ] 38 | } 39 | ] 40 | }) 41 | .state({ 42 | name: 'rootSibling', 43 | templateUrl: 'rootSibling.html' 44 | }); 45 | }); 46 | ``` 47 | 48 | ## Options 49 | 50 | * keepOriginalNames (default _false_) 51 | * siblingTraversal (default _false_) 52 | 53 | ### Dot notation name conversion 54 | By default, all state names are converted to use ui-router's dot notation (e.g. `parentStateName.childStateName`). 55 | This can be disabled by calling `.state()` with options `options.keepOriginalNames = true`. 56 | For example: 57 | 58 | ``` javascript 59 | angular.module('myApp', ['ui.router', 'ui.router.stateHelper']) 60 | .config(function(stateHelperProvider){ 61 | stateHelperProvider.state({ 62 | name: 'root', 63 | templateUrl: 'root.html', 64 | children: [ 65 | { 66 | name: 'contacts', 67 | templateUrl: 'contacts.html' 68 | } 69 | ] 70 | }, { keepOriginalNames: true }); 71 | }); 72 | ``` 73 | 74 | ### Sibling Traversal 75 | Child states may optionally receive a reference to the name of the previous state (if available) and the next state (if available) in order to facilitate sequential state traversal as in the case of building wizards or multi-part forms. Enable this by setting `options.siblingTraversal = true`. 76 | 77 | Example: 78 | ``` javascript 79 | 80 | angular.module('myApp', ['ui.router', 'ui.router.stateHelper']) 81 | .config(function(stateHelperProvider){ 82 | stateHelperProvider.state({ 83 | name: 'resume', 84 | children: [ 85 | { 86 | name: 'contactInfo', 87 | }, 88 | { 89 | name: 'experience', 90 | }, 91 | { 92 | name: 'education', 93 | } 94 | ] 95 | }, { siblingTraversal: true }); 96 | }); 97 | 98 | console.log($state.get('resume.contactInfo').previousSibling) // undefined 99 | console.log($state.get('resume.contactInfo').nextSibling) // 'resume.experience' 100 | 101 | console.log($state.get('resume.experience').previousSibling) // 'resume.contactInfo' 102 | console.log($state.get('resume.experience').nextSibling) // 'resume.education' 103 | 104 | console.log($state.get('resume.education').previousSibling) // 'resume.experience' 105 | console.log($state.get('resume.education').nextSibling) // undefined 106 | ``` 107 | 108 | 109 | ## Name change 110 | Before 1.2.0 `.setNestedState` was used instead of `.state`. In 1.2.0 `setNestedState` was deprecated in favour of `.state`, and chaining was added. This makes it easier to switch between `$stateProvider` and `stateHelperProvider`. 111 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router.stateHelper", 3 | "version": "1.3.1", 4 | "description": "A helper module for AngularUI Router, which allows you to define your states as an object tree.", 5 | "main": "statehelper.js", 6 | "homepage": "https://github.com/marklagendijk/ui-router.stateHelper", 7 | "authors": [ 8 | "Mark Lagendijk " 9 | ], 10 | "keywords": [ 11 | "angular", 12 | "helper", 13 | "ui.router", 14 | "ui-router", 15 | "ui", 16 | "router" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "package.json", 23 | "gulpfile.js", 24 | "bower_components", 25 | "test", 26 | "tests", 27 | "config" 28 | ], 29 | "dependencies": { 30 | "angular": ">=1.2.0", 31 | "angular-ui-router": "~0.2.11" 32 | }, 33 | "devDependencies": { 34 | "angular-mocks": ">=1.2.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testFiles: [ 3 | 'bower_components/angular/angular.js', 4 | 'bower_components/angular-mocks/angular-mocks.js', 5 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 6 | 'statehelper.min.js', 7 | 'test/**/*.js' 8 | ] 9 | }; -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.js'); 2 | module.exports = function(karmaConfig){ 3 | karmaConfig.set({ 4 | 5 | // base path, that will be used to resolve files and exclude 6 | basePath: '../', 7 | 8 | 9 | // frameworks to use 10 | frameworks: ['jasmine'], 11 | 12 | 13 | // list of files / patterns to load in the browser 14 | files: config.testFiles, 15 | 16 | 17 | // test results reporter to use 18 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 19 | reporters: ['progress'], 20 | 21 | 22 | // web server port 23 | port: 9876, 24 | 25 | 26 | // enable / disable colors in the output (reporters and logs) 27 | colors: true, 28 | 29 | 30 | // level of logging 31 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 32 | logLevel: karmaConfig.LOG_WARN, 33 | 34 | 35 | // enable / disable watching file and executing tests whenever any file changes 36 | autoWatch: true, 37 | 38 | 39 | // Start these browsers, currently available: 40 | // - Chrome 41 | // - ChromeCanary 42 | // - Firefox 43 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 44 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 45 | // - PhantomJS 46 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 47 | browsers: ['Chrome'], 48 | 49 | 50 | // If browser does not capture in given timeout [ms], kill it 51 | captureTimeout: 60000, 52 | 53 | 54 | // Continuous Integration mode 55 | // if true, it capture browsers, run tests and exit 56 | singleRun: false 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var ngmin = require('gulp-ng-annotate'); 3 | var uglify = require("gulp-uglify"); 4 | var rename = require("gulp-rename"); 5 | var karma = require("gulp-karma"); 6 | 7 | var config = require('./config/config.js'); 8 | 9 | gulp.task("test", ["minify"], function(){ 10 | return gulp.src(config.testFiles) 11 | .pipe(karma({ 12 | configFile: 'config/karma.conf.js', 13 | action: 'run' 14 | })); 15 | }); 16 | 17 | gulp.task("minify", function(){ 18 | return gulp.src("statehelper.js") 19 | .pipe(ngmin()) 20 | .pipe(uglify()) 21 | .pipe(rename("statehelper.min.js")) 22 | .pipe(gulp.dest("./")); 23 | }); 24 | 25 | gulp.task("default", ["minify", "test"]); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./statehelper'); 2 | module.exports = 'ui.router.stateHelper'; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-router.statehelper", 3 | "version": "1.3.1", 4 | "description": "A helper module for AngularUI Router, which allows you to define your states as an object tree.", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "gulp": "~3.8.0", 9 | "gulp-karma": "^0.0.4", 10 | "gulp-ng-annotate": "^0.3.3", 11 | "gulp-rename": "~0.2.1", 12 | "gulp-uglify": "~0.1.0", 13 | "jasmine-core": "^2.2.0", 14 | "karma": "^0.12.24", 15 | "karma-chrome-launcher": "^0.1.7", 16 | "karma-firefox-launcher": "~0.1.3", 17 | "karma-jasmine": "^0.3.5", 18 | "karma-phantomjs-launcher": "~0.1.1", 19 | "karma-requirejs": "~0.2.1", 20 | "karma-script-launcher": "~0.1.0" 21 | }, 22 | "directories": { 23 | "test": "test" 24 | }, 25 | "scripts": { 26 | "test": "gulp test" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://marklagendijk@github.com/marklagendijk/ui-router.stateHelper.git" 31 | }, 32 | "keywords": [ 33 | "angular", 34 | "helper", 35 | "ui.router", 36 | "ui-router", 37 | "ui", 38 | "router" 39 | ], 40 | "author": "Mark Lagendijk ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/marklagendijk/ui-router.stateHelper/issues" 44 | }, 45 | "homepage": "https://github.com/marklagendijk/ui-router.stateHelper" 46 | } 47 | -------------------------------------------------------------------------------- /statehelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper module for AngularUI Router, which allows you to define your states as an object tree. 3 | * @author Mark Lagendijk 4 | * @license MIT 5 | */ 6 | angular.module('ui.router.stateHelper', [ 'ui.router' ]) 7 | .provider('stateHelper', ['$stateProvider', function($stateProvider){ 8 | var self = this; 9 | 10 | /** 11 | * Recursively sets the states using $stateProvider.state. 12 | * Child states are defined via a `children` property. 13 | * 14 | * 1. Recursively calls itself for all descendant states, by traversing the `children` properties. 15 | * 2. Converts all the state names to dot notation, of the form `grandfather.father.state`. 16 | * 3. Sets `parent` property of the descendant states. 17 | * 18 | * @param {Object} state - A regular ui.router state object. 19 | * @param {Array} [state.children] - An optional array of child states. 20 | * @deprecated {Boolean} keepOriginalNames - An optional flag that prevents conversion 21 | * of names to dot notation if true. (use options.keepOriginalNames instead) 22 | * @param {Object} [options] - An optional options object. 23 | * @param {Boolean} [options.keepOriginalNames=false] An optional flag that 24 | * prevents conversion of names to dot notation if true. 25 | * @param {Boolean} [options.siblingTraversal=false] An optional flag that 26 | * adds `nextSibling` and `previousSibling` properties when enabled 27 | */ 28 | this.state = function(state){ 29 | var args = Array.prototype.slice.apply(arguments); 30 | var options = { 31 | keepOriginalNames: false, 32 | siblingTraversal: false 33 | }; 34 | 35 | if (typeof args[1] === 'boolean') { 36 | options.keepOriginalNames = args[1]; 37 | } 38 | else if (typeof args[1] === 'object') { 39 | angular.extend(options, args[1]); 40 | } 41 | 42 | if (!options.keepOriginalNames) { 43 | fixStateName(state); 44 | } 45 | 46 | $stateProvider.state(state); 47 | 48 | if(state.children && state.children.length){ 49 | state.children.forEach(function(childState){ 50 | childState.parent = state; 51 | self.state(childState, options); 52 | }); 53 | 54 | if (options.siblingTraversal) { 55 | addSiblings(state); 56 | } 57 | } 58 | 59 | return self; 60 | }; 61 | 62 | this.setNestedState = this.state; 63 | 64 | self.$get = angular.noop; 65 | 66 | /** 67 | * Converts the name of a state to dot notation, of the form `grandfather.father.state`. 68 | * @param state 69 | */ 70 | function fixStateName(state){ 71 | if(state.parent){ 72 | state.name = (angular.isObject(state.parent) ? state.parent.name : state.parent) + '.' + state.name; 73 | } 74 | } 75 | 76 | function addSiblings(state) { 77 | state.children.forEach(function (childState, idx, array) { 78 | if (array[idx + 1]) { 79 | childState.nextSibling = array[idx + 1].name; 80 | } 81 | if (array[idx - 1]) { 82 | childState.previousSibling = array[idx - 1].name; 83 | } 84 | }); 85 | } 86 | }]); 87 | -------------------------------------------------------------------------------- /statehelper.min.js: -------------------------------------------------------------------------------- 1 | angular.module("ui.router.stateHelper",["ui.router"]).provider("stateHelper",["$stateProvider",function(e){function t(e){e.parent&&(e.name=(angular.isObject(e.parent)?e.parent.name:e.parent)+"."+e.name)}function a(e){e.children.forEach(function(e,t,a){a[t+1]&&(e.nextSibling=a[t+1].name),a[t-1]&&(e.previousSibling=a[t-1].name)})}var n=this;this.state=function(r){var i=Array.prototype.slice.apply(arguments),l={keepOriginalNames:!1,siblingTraversal:!1};return"boolean"==typeof i[1]?l.keepOriginalNames=i[1]:"object"==typeof i[1]&&angular.extend(l,i[1]),l.keepOriginalNames||t(r),e.state(r),r.children&&r.children.length&&(r.children.forEach(function(e){e.parent=r,n.state(e,l)}),l.siblingTraversal&&a(r)),n},this.setNestedState=this.state,n.$get=angular.noop}]); -------------------------------------------------------------------------------- /test/statehelperSpec.js: -------------------------------------------------------------------------------- 1 | /* globals: beforeEach, describe, it, module, inject, expect */ 2 | describe('ui-router.stateHelper', function(){ 3 | var stateHelperProvider, $stateProvider, rootState, expectedState; 4 | 5 | var $injector; 6 | 7 | var stateHelperProviderState; 8 | 9 | beforeEach(module('ui.router.stateHelper', function(_stateHelperProvider_, _$stateProvider_){ 10 | stateHelperProvider = _stateHelperProvider_; 11 | $stateProvider = _$stateProvider_; 12 | })); 13 | 14 | beforeEach(inject(function(_$injector_){ 15 | $injector = _$injector_; 16 | 17 | rootState = { 18 | name: 'root', 19 | children: [ 20 | { 21 | name: 'login', 22 | templateUrl: '/partials/views/login.html' 23 | }, 24 | { 25 | name: 'backup', 26 | children: [ 27 | { 28 | name: 'dashboard' 29 | } 30 | ] 31 | } 32 | ] 33 | }; 34 | 35 | spyOn($stateProvider, 'state').and.callThrough(); 36 | })); 37 | 38 | describe('.state', function(){ 39 | beforeEach(inject(function(){ 40 | expectedState = { 41 | name: 'root', 42 | children: [ 43 | { 44 | name: 'root.login', 45 | // nextSibling: 'root.backup', 46 | templateUrl: '/partials/views/login.html' 47 | }, 48 | { 49 | name: 'root.backup', 50 | // previousSibling: 'root.login', 51 | children: [ 52 | { 53 | name: 'root.backup.dashboard' 54 | } 55 | ] 56 | } 57 | ] 58 | }; 59 | 60 | stateHelperProviderState = stateHelperProvider.state(rootState, { siblingTraversal: false}); 61 | })); 62 | 63 | it('should set each state', function(){ 64 | expect($stateProvider.state.calls.count()).toBe(4); 65 | }); 66 | 67 | it('should convert names to dot notation, set parent references', function(){ 68 | // Since the states are objects which contain references to each other, we are testing the eventual 69 | // root state object (and not the root state object as it is passed to $stateProvider.$state). 70 | // Because of this we have to test everything at once 71 | 72 | expectedState.children[0].parent = expectedState; 73 | expectedState.children[1].parent = expectedState; 74 | expectedState.children[1].children[0].parent = expectedState.children[1]; 75 | 76 | // expect($stateProvider.state.argsForCall[0][0]).toEqual(expectedState); 77 | expect($stateProvider.state.calls.argsFor(0)[0]).toEqual(expectedState); 78 | }); 79 | 80 | it('should return itself to support chaining', function(){ 81 | expect(stateHelperProviderState).toBe(stateHelperProvider); 82 | }); 83 | }); 84 | 85 | describe('.state with keepOriginalNames set to true', function(){ 86 | beforeEach(inject(function(){ 87 | expectedState = { 88 | name: 'root', 89 | children: [ 90 | { 91 | name: 'login', 92 | // nextSibling: 'backup', 93 | templateUrl: '/partials/views/login.html' 94 | }, 95 | { 96 | name: 'backup', 97 | // previousSibling: 'login', 98 | children: [ 99 | { 100 | name: 'dashboard' 101 | } 102 | ] 103 | } 104 | ] 105 | }; 106 | 107 | stateHelperProvider.state(rootState, { keepOriginalNames: true }); 108 | })); 109 | 110 | it('should not convert names to dot notation, set parent references', function(){ 111 | // Since the states are objects which contain references to each other, we are testing the eventual 112 | // root state object (and not the root state object as it is passed to $stateProvider.$state). 113 | // Because of this we have to test everything at once 114 | 115 | expectedState.children[0].parent = expectedState; 116 | expectedState.children[1].parent = expectedState; 117 | expectedState.children[1].children[0].parent = expectedState.children[1]; 118 | 119 | expect($stateProvider.state.calls.argsFor(0)[0]).toEqual(expectedState); 120 | }); 121 | }); 122 | 123 | describe('.setNestedState', function(){ 124 | it('should support .setNestedState as legacy name', function(){ 125 | stateHelperProvider.setNestedState(rootState); 126 | expect($stateProvider.state.calls.count()).toBe(4); 127 | }); 128 | }); 129 | 130 | describe('children have references to siblings', function (){ 131 | beforeEach(function () { 132 | stateHelperProvider.state(rootState, { siblingTraversal: true }); 133 | }); 134 | 135 | it('should see the next sibling', function (){ 136 | var $state = $injector.get('$state'); 137 | expect($state.get('root.login').nextSibling).toBeDefined(); 138 | expect($state.get('root.login').nextSibling).toBe('root.backup'); 139 | }); 140 | 141 | it('should see the previous sibling', function (){ 142 | var $state = $injector.get('$state'); 143 | expect($state.get('root.backup').previousSibling).toBeDefined(); 144 | expect($state.get('root.backup').previousSibling).toBe('root.login'); 145 | }); 146 | }); 147 | }); 148 | 149 | 150 | 151 | --------------------------------------------------------------------------------