diff --git a/src/state.js b/src/state.js index 285780081..aa23888ee 100644 --- a/src/state.js +++ b/src/state.js @@ -10,7 +10,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { if (!state) throw new Error("No such state '" + stateOrName + "'"); } else { state = states[stateOrName.name]; - if (!state || state !== stateOrName && state.self !== stateOrName) + if (!state || state !== stateOrName && state.self !== stateOrName) throw new Error("Invalid or unregistered state"); } return state; @@ -69,7 +69,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } else { params = state.params = url ? url.parameters() : state.parent.params; } - + var paramNames = {}; forEach(params, function (p) { paramNames[p] = true; }); if (parent) { forEach(parent.params, function (p) { @@ -140,6 +140,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { return this; } + + //clears all the states and the urlRouterProvider + function clearAll() { + states = {}; + $urlRouterProvider.clearAll(); + } + this.clearAll = function() { clearAll(); return this; }; + // $urlRouter is injected just to ensure it gets instantiated this.$get = $get; $get.$inject = ['$rootScope', '$q', '$templateFactory', '$injector', '$stateParams', '$location', '$urlRouter']; @@ -222,7 +230,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { exiting = fromPath[l]; if (exiting.self.onExit) { $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); - } + } exiting.locals = null; } @@ -249,7 +257,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); - + return $state.current; }, function (error) { if ($state.transition !== transition) return TransitionSuperseded; diff --git a/src/urlRouter.js b/src/urlRouter.js index 08bf7c84e..616170e50 100644 --- a/src/urlRouter.js +++ b/src/urlRouter.js @@ -1,7 +1,7 @@ $UrlRouterProvider.$inject = ['$urlMatcherFactoryProvider']; function $UrlRouterProvider( $urlMatcherFactory) { - var rules = [], + var rules = [], otherwise = null; // Returns a string that is a prefix of all strings matching the RegExp @@ -83,10 +83,16 @@ function $UrlRouterProvider( $urlMatcherFactory) { return this.rule(rule); }; + //clears the rules and clears "otherwise" if it is defined + function clearAll() { + rules = []; + otherwise = null; + } + this.clearAll = function() {clearAll(); return this;}; + this.$get = [ '$location', '$rootScope', '$injector', function ($location, $rootScope, $injector) { - if (otherwise) rules.push(otherwise); // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree function update() { @@ -98,6 +104,12 @@ function $UrlRouterProvider( $urlMatcherFactory) { break; } } + if (!handled && otherwise) { + handled = otherwise($injector, $location); + if (handled) { + if (isString(handled)) $location.replace().url(handled); + } + } } $rootScope.$on('$locationChangeSuccess', update); diff --git a/test/featureClearAllSpec.js b/test/featureClearAllSpec.js new file mode 100644 index 000000000..bc70d5cfc --- /dev/null +++ b/test/featureClearAllSpec.js @@ -0,0 +1,206 @@ +describe('clearAll feature', function () { + + (function() { + //a simple runtime configurer that uses clearAll to redefine ui-router + //behavior at *runtime* (as opposed to config time). + // + //A note about this provider/service + //if the ui-router services exposed the configruation + //functions that live in their providers + //then this provider/service wrapper would not be necessary. + //However, by latching onto the providers with + //this service, runtime (re)configuration is possible. + //In other words -- the ui-router services *could* + //do this themselves; the features of this "test" provider/service + //may show up as a pull for ui-router seperately :) + //for now, clearAll allows what it does to be possible + angular.module('uiRouterRuntimeConfigurer', ['ui.state']); + UiRouterRuntimeConfigProvider.$inject = ['$stateProvider', '$urlRouterProvider']; + function UiRouterRuntimeConfigProvider($stateProvider, $urlRouterProvider) { + + function clearAll() { + //the stateProvider clears states and urlRoutes + //hence this shortcut works + $stateProvider.clearAll(); + } + this.clearAll = function() { clearAll(); return this; }; + + function state(name, definition) { + $stateProvider.state(name, definition); + } + this.state = function(name, definition) { state(name, definition); return this; }; + + + function when(what, handler) { + $urlRouterProvider.when(what, handler); + } + this.when = function(what, handler) { when(what, handler); return this; }; + + function otherwise(rule) { + $urlRouterProvider.otherwise(rule); + } + this.otherwise = function(rule) { otherwise(rule); return this; }; + + this.$get = $get; + function $get() { + var uiRouterRuntimeConfig = {}; + + uiRouterRuntimeConfig.clearAll = function() { clearAll(); return this; }; + uiRouterRuntimeConfig.state = function(name, definition) { state(name, definition); return this; }; + uiRouterRuntimeConfig.when = function(what, handler) { when(what, handler); return this; }; + uiRouterRuntimeConfig.otherwise = function(rule) { otherwise(rule); return this; }; + + //this service can call the ui-router providers from its provider + return uiRouterRuntimeConfig; + } + } + angular.module('uiRouterRuntimeConfigurer').provider('uiRouterRuntimeConfig', UiRouterRuntimeConfigProvider); + + }()); //self-invoking function + + var log, logEvents, logEnterExit; + function eventLogger(event, to, toParams, from, fromParams) { + if (logEvents) log += event.name + '(' + to.name + ',' + from.name + ');'; + } + function callbackLogger(what) { + return function () { + if (logEnterExit) log += this.name + '.' + what + ';'; + }; + } + + var HOME = {url: '/'}, + ABOUT = {url: '/about'}, + ADMIN = {url: '/admin'}, + LOGIN = {url: '/login'}, + THEYWIN = {url: '/theyWin'}, + FOUROHFOUR = {url: '/fourOhFour'}; + + + function configureBase(uiRouterRuntimeConfigProviderOrService) + { + return uiRouterRuntimeConfigProviderOrService + .clearAll() + .state('ABOUT', ABOUT) + .state('HOME', HOME) + .state('FOUROHFOUR', FOUROHFOUR) + .state('THEYWIN', THEYWIN) + .when('/northDakota', '/about' ) + .when('/someoneGuessesThisUrl', '/theyWin' ) + .otherwise('/fourOhFour'); + } + + function configureAnon(uiRouterRuntimeConfigProviderOrService) + { + return configureBase(uiRouterRuntimeConfigProviderOrService) + .state('LOGIN', LOGIN); + } + + function configureAdmin(uiRouterRuntimeConfigProviderOrService) + { + return configureBase(uiRouterRuntimeConfigProviderOrService) + .state('ADMIN', ADMIN); + } + + angular.module('test', ['uiRouterRuntimeConfigurer']).config( + ['uiRouterRuntimeConfigProvider', + function(uiRouterRuntimeConfigProvider) { + configureAnon(uiRouterRuntimeConfigProvider); + }] + ); + + beforeEach(module('test')); + + function $get(what) { + return jasmine.getEnv().currentSpec.$injector.get(what); + } + + function testSet() { + it('should work as always', inject(function ($state, $q) { + var trans = $state.transitionTo(HOME, {}); + $q.flush(); + expect(resolvedValue(trans)).toBe(HOME); + })); + + it('should allow transitions by name', inject(function ($state, $q) { + $state.transitionTo('ABOUT', {}); + $q.flush(); + expect($state.current).toBe(ABOUT); + })); + + it('should always have $current defined', inject(function ($state) { + expect($state.$current).toBeDefined(); + })); + + it('should have the correct location', inject(function ($state, $q, $location) { + $state.transitionTo('FOUROHFOUR', {}); + $q.flush(); + expect($location.path()).toBe(FOUROHFOUR.url); + })); + + it('should support otherwise', inject(function ($state, $rootScope, $q, $location) { + $location.path("/nonExistent"); + $rootScope.$apply(); + expect($state.current).toBe(FOUROHFOUR); + })); + + it('should support urlRouter/when', inject(function ($state, $rootScope, $q, $location) { + $location.path("/northDakota"); + $rootScope.$apply(); + expect($state.current).toBe(ABOUT); + })); + + it('should support urlRouter/when', inject(function ($state, $rootScope, $q, $location) { + $location.path("/someoneGuessesThisUrl"); + $rootScope.$apply(); + expect($state.current).toBe(THEYWIN); + })); + + } + + describe('initially configured states', function() { + testSet(); + + it('the anonymous user should not have the admin route', inject(function ($state, $rootScope, $q, $location) { + $location.path("/admin"); + $rootScope.$apply(); + expect($state.current).toBe(FOUROHFOUR); + })); + + it('the anonymous user should the login route', inject(function ($state, $rootScope, $q, $location) { + $location.path("/login"); + $rootScope.$apply(); + expect($state.current).toBe(LOGIN); + })); + + }); + + describe('when the user logs in as admin', function() { + it('the admin user will not have the login route but will have the admin route', inject(function (uiRouterRuntimeConfig, $state, $rootScope, $q, $location) { + configureAdmin(uiRouterRuntimeConfig); + //this is kind of dumb example because the admin should be able to change credentials but for demo purposes + //this will have to do + $location.path("/login"); + $rootScope.$apply(); + expect($state.current).toBe(FOUROHFOUR); + $location.path("/admin"); + $rootScope.$apply(); + expect($state.current).toBe(ADMIN); + })); + }); + + describe('when the admin user creates a blog page in her angular-based CMS system', function() { + it('she will be able to preview it because the route will be added on the fly', inject(function (uiRouterRuntimeConfig, $state, $rootScope, $q, $location) { + configureAdmin(uiRouterRuntimeConfig); + var BLOG = {url: '/blog'}; + uiRouterRuntimeConfig.state('BLOG', BLOG); + + //this is kind of dumb example because the admin should be able to change credentials but for demo purposes + //this will have to do + $state.transitionTo('BLOG'); + $q.flush(); + expect($state.current).toBe(BLOG); + })); + }); + + +});