From ce861776a32994e5333b85e3826023f552ade29f Mon Sep 17 00:00:00 2001 From: Stu Salsbury Date: Tue, 7 May 2013 16:39:54 -0700 Subject: [PATCH 1/2] add feature to clearAll and (re)initialize providers to support on-the-fly routing changes --- src/compat.js | 23 ++++ src/state.js | 30 ++++- src/urlRouter.js | 27 +++- test/featureClearAllInitSpec.js | 226 ++++++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 test/featureClearAllInitSpec.js diff --git a/src/compat.js b/src/compat.js index 4adfd5fbb..f70de0136 100644 --- a/src/compat.js +++ b/src/compat.js @@ -47,9 +47,32 @@ function $RouteProvider( $stateProvider, $urlRouterProvider) { return this; } + //initializes the routeProvider + var initialized = false; + function init() { + if (!initialized) { + $stateProvider.init(); + $urlRouterProvider.init(); + initialized = true; + } + } + this.init = function() { init(); return this;}; + + //clears the routeProvider and asks the stateProvider and urlRouterProvider + //to do the same + function clearAll() { + routes = []; + $stateProvider.clearAll(); + $urlRouterProvider.clearAll(); + initialized = false; + } + this.clearAll = function() { clearAll(); return this;}; + this.$get = $get; $get.$inject = ['$state', '$rootScope', '$routeParams']; function $get( $state, $rootScope, $routeParams) { + //initialize the router(s) + init(); var $route = { routes: routes, diff --git a/src/state.js b/src/state.js index 285780081..ed3991a83 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,10 +140,32 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { return this; } + + //initializes the urlRouterProvider + var initialized = false; + function init() { + if (!initialized) { + $urlRouterProvider.init(); + initialized = true; + } + } + this.init = function() { init(); return this; }; + + + //clears all the states and the urlRouterProvider + function clearAll() { + states = {}; + $urlRouterProvider.clearAll(); + initialized = false; + } + 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']; function $get( $rootScope, $q, $templateFactory, $injector, $stateParams, $location, $urlRouter) { + //initialize the router(s) + init(); var TransitionSuperseded = $q.reject(new Error('transition superseded')); var TransitionPrevented = $q.reject(new Error('transition prevented')); @@ -222,7 +244,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 +271,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..2258e7f53 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,33 @@ function $UrlRouterProvider( $urlMatcherFactory) { return this.rule(rule); }; + //initializes the urlRouterProvider + var initialized = false; + function init() { + if (!initialized) { + if (otherwise) { + //there's an otherwise -- make it the last rule + rules.push(otherwise); + } + // + initialized = true; + } + } + this.init = function() {init(); return this;}; + + //clears the rules and clears "otherwise" if it is defined + function clearAll() { + rules = []; + otherwise = null; + initialized = false; + } + this.clearAll = function() {clearAll(); return this;}; + this.$get = [ '$location', '$rootScope', '$injector', function ($location, $rootScope, $injector) { - if (otherwise) rules.push(otherwise); + //initialize the router + init(); // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree function update() { diff --git a/test/featureClearAllInitSpec.js b/test/featureClearAllInitSpec.js new file mode 100644 index 000000000..77e66bfeb --- /dev/null +++ b/test/featureClearAllInitSpec.js @@ -0,0 +1,226 @@ +describe('clearAll/init feature', function () { + + (function() { + //a simple runtime configurer that uses reset 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 and init allow what it does to be possible + angular.module('uiRouterRuntimeConfigurer', ['ui.compat']); + UiRouterRuntimeConfigProvider.$inject = ['$stateProvider', '$routeProvider', '$urlRouterProvider']; + function UiRouterRuntimeConfigProvider($stateProvider, $routeProvider, $urlRouterProvider) { + + //can call init and clearAll on the stateProvider + + function init() { + //the routeProvider inits states and urlRoutes too since it uses them + //hence this shortcut works + $routeProvider.init(); + } + this.init = function() { init(); return this; }; + + function clearAll() { + //the routeProvider clears states and urlRoutes too since it uses them + //hence this shortcut works + $routeProvider.clearAll(); + } + this.clearAll = function() { clearAll(); return this; }; + + function routeWhen(url, route) { + $routeProvider.when(url, route); + } + this.routeWhen = function(url, route) { routeWhen(url, route); return this; }; + + function state(name, definition) { + $stateProvider.state(name, definition); + } + this.state = function(name, definition) { state(name, definition); return this; }; + + + function urlWhen(what, handler) { + $urlRouterProvider.when(what, handler); + } + this.urlWhen = function(what, handler) { urlWhen(what, handler); return this; }; + + function urlOtherwise(rule) { + $urlRouterProvider.otherwise(rule); + } + this.urlOtherwise = function(rule) { urlOtherwise(rule); return this; }; + + this.$get = $get; + function $get() { + var uiRouterRuntimeConfig = {}; + + uiRouterRuntimeConfig.init = function() {init(); return this; }; + uiRouterRuntimeConfig.clearAll = function() { clearAll(); return this; }; + uiRouterRuntimeConfig.routeWhen = function(url, route) { routeWhen(url, route); return this; }; + uiRouterRuntimeConfig.state = function(name, definition) { state(name, definition); return this; }; + uiRouterRuntimeConfig.urlWhen = function(what, handler) { urlWhen(what, handler); return this; }; + uiRouterRuntimeConfig.urlOtherwise = function(rule) { urlOtherwise(rule); return this; }; + + //this service can call clearAll and init 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) + .routeWhen('/northDakota', { redirectTo: '/about' } ) + .urlWhen('/someoneGuessesThisUrl', '/theyWin' ) + .urlOtherwise('/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); + //no need to call init because we're working with the provider and + //the service will do it when it is instantiated + }] + ); + + 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 route/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); + uiRouterRuntimeConfig.init(); + //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); + uiRouterRuntimeConfig.init(); + + //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); + })); + }); + + +}); From 858772c801076e84fbeaeee7e3b9c136c5198acb Mon Sep 17 00:00:00 2001 From: Stu Salsbury Date: Wed, 8 May 2013 09:16:36 -0700 Subject: [PATCH 2/2] removed init and changed urlProvider to not append otherwise --- src/compat.js | 23 -------- src/state.js | 14 ----- src/urlRouter.js | 23 ++------ ...rAllInitSpec.js => featureClearAllSpec.js} | 58 ++++++------------- 4 files changed, 25 insertions(+), 93 deletions(-) rename test/{featureClearAllInitSpec.js => featureClearAllSpec.js} (74%) diff --git a/src/compat.js b/src/compat.js index f70de0136..4adfd5fbb 100644 --- a/src/compat.js +++ b/src/compat.js @@ -47,32 +47,9 @@ function $RouteProvider( $stateProvider, $urlRouterProvider) { return this; } - //initializes the routeProvider - var initialized = false; - function init() { - if (!initialized) { - $stateProvider.init(); - $urlRouterProvider.init(); - initialized = true; - } - } - this.init = function() { init(); return this;}; - - //clears the routeProvider and asks the stateProvider and urlRouterProvider - //to do the same - function clearAll() { - routes = []; - $stateProvider.clearAll(); - $urlRouterProvider.clearAll(); - initialized = false; - } - this.clearAll = function() { clearAll(); return this;}; - this.$get = $get; $get.$inject = ['$state', '$rootScope', '$routeParams']; function $get( $state, $rootScope, $routeParams) { - //initialize the router(s) - init(); var $route = { routes: routes, diff --git a/src/state.js b/src/state.js index ed3991a83..aa23888ee 100644 --- a/src/state.js +++ b/src/state.js @@ -141,22 +141,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { } - //initializes the urlRouterProvider - var initialized = false; - function init() { - if (!initialized) { - $urlRouterProvider.init(); - initialized = true; - } - } - this.init = function() { init(); return this; }; - - //clears all the states and the urlRouterProvider function clearAll() { states = {}; $urlRouterProvider.clearAll(); - initialized = false; } this.clearAll = function() { clearAll(); return this; }; @@ -164,8 +152,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { this.$get = $get; $get.$inject = ['$rootScope', '$q', '$templateFactory', '$injector', '$stateParams', '$location', '$urlRouter']; function $get( $rootScope, $q, $templateFactory, $injector, $stateParams, $location, $urlRouter) { - //initialize the router(s) - init(); var TransitionSuperseded = $q.reject(new Error('transition superseded')); var TransitionPrevented = $q.reject(new Error('transition prevented')); diff --git a/src/urlRouter.js b/src/urlRouter.js index 2258e7f53..616170e50 100644 --- a/src/urlRouter.js +++ b/src/urlRouter.js @@ -83,33 +83,16 @@ function $UrlRouterProvider( $urlMatcherFactory) { return this.rule(rule); }; - //initializes the urlRouterProvider - var initialized = false; - function init() { - if (!initialized) { - if (otherwise) { - //there's an otherwise -- make it the last rule - rules.push(otherwise); - } - // - initialized = true; - } - } - this.init = function() {init(); return this;}; - //clears the rules and clears "otherwise" if it is defined function clearAll() { rules = []; otherwise = null; - initialized = false; } this.clearAll = function() {clearAll(); return this;}; this.$get = [ '$location', '$rootScope', '$injector', function ($location, $rootScope, $injector) { - //initialize the router - init(); // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree function update() { @@ -121,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/featureClearAllInitSpec.js b/test/featureClearAllSpec.js similarity index 74% rename from test/featureClearAllInitSpec.js rename to test/featureClearAllSpec.js index 77e66bfeb..bc70d5cfc 100644 --- a/test/featureClearAllInitSpec.js +++ b/test/featureClearAllSpec.js @@ -1,7 +1,7 @@ -describe('clearAll/init feature', function () { +describe('clearAll feature', function () { (function() { - //a simple runtime configurer that uses reset to redefine ui-router + //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 @@ -13,60 +13,44 @@ describe('clearAll/init feature', function () { //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 and init allow what it does to be possible - angular.module('uiRouterRuntimeConfigurer', ['ui.compat']); - UiRouterRuntimeConfigProvider.$inject = ['$stateProvider', '$routeProvider', '$urlRouterProvider']; - function UiRouterRuntimeConfigProvider($stateProvider, $routeProvider, $urlRouterProvider) { - - //can call init and clearAll on the stateProvider - - function init() { - //the routeProvider inits states and urlRoutes too since it uses them - //hence this shortcut works - $routeProvider.init(); - } - this.init = function() { init(); return this; }; + //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 routeProvider clears states and urlRoutes too since it uses them + //the stateProvider clears states and urlRoutes //hence this shortcut works - $routeProvider.clearAll(); + $stateProvider.clearAll(); } this.clearAll = function() { clearAll(); return this; }; - function routeWhen(url, route) { - $routeProvider.when(url, route); - } - this.routeWhen = function(url, route) { routeWhen(url, route); return this; }; - function state(name, definition) { $stateProvider.state(name, definition); } this.state = function(name, definition) { state(name, definition); return this; }; - function urlWhen(what, handler) { + function when(what, handler) { $urlRouterProvider.when(what, handler); } - this.urlWhen = function(what, handler) { urlWhen(what, handler); return this; }; + this.when = function(what, handler) { when(what, handler); return this; }; - function urlOtherwise(rule) { + function otherwise(rule) { $urlRouterProvider.otherwise(rule); } - this.urlOtherwise = function(rule) { urlOtherwise(rule); return this; }; + this.otherwise = function(rule) { otherwise(rule); return this; }; this.$get = $get; function $get() { var uiRouterRuntimeConfig = {}; - uiRouterRuntimeConfig.init = function() {init(); return this; }; uiRouterRuntimeConfig.clearAll = function() { clearAll(); return this; }; - uiRouterRuntimeConfig.routeWhen = function(url, route) { routeWhen(url, route); return this; }; uiRouterRuntimeConfig.state = function(name, definition) { state(name, definition); return this; }; - uiRouterRuntimeConfig.urlWhen = function(what, handler) { urlWhen(what, handler); return this; }; - uiRouterRuntimeConfig.urlOtherwise = function(rule) { urlOtherwise(rule); return this; }; + uiRouterRuntimeConfig.when = function(what, handler) { when(what, handler); return this; }; + uiRouterRuntimeConfig.otherwise = function(rule) { otherwise(rule); return this; }; - //this service can call clearAll and init from its provider + //this service can call the ui-router providers from its provider return uiRouterRuntimeConfig; } } @@ -100,9 +84,9 @@ describe('clearAll/init feature', function () { .state('HOME', HOME) .state('FOUROHFOUR', FOUROHFOUR) .state('THEYWIN', THEYWIN) - .routeWhen('/northDakota', { redirectTo: '/about' } ) - .urlWhen('/someoneGuessesThisUrl', '/theyWin' ) - .urlOtherwise('/fourOhFour'); + .when('/northDakota', '/about' ) + .when('/someoneGuessesThisUrl', '/theyWin' ) + .otherwise('/fourOhFour'); } function configureAnon(uiRouterRuntimeConfigProviderOrService) @@ -121,8 +105,6 @@ describe('clearAll/init feature', function () { ['uiRouterRuntimeConfigProvider', function(uiRouterRuntimeConfigProvider) { configureAnon(uiRouterRuntimeConfigProvider); - //no need to call init because we're working with the provider and - //the service will do it when it is instantiated }] ); @@ -161,7 +143,7 @@ describe('clearAll/init feature', function () { expect($state.current).toBe(FOUROHFOUR); })); - it('should support route/when', inject(function ($state, $rootScope, $q, $location) { + it('should support urlRouter/when', inject(function ($state, $rootScope, $q, $location) { $location.path("/northDakota"); $rootScope.$apply(); expect($state.current).toBe(ABOUT); @@ -195,7 +177,6 @@ describe('clearAll/init feature', function () { 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); - uiRouterRuntimeConfig.init(); //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"); @@ -212,7 +193,6 @@ describe('clearAll/init feature', function () { configureAdmin(uiRouterRuntimeConfig); var BLOG = {url: '/blog'}; uiRouterRuntimeConfig.state('BLOG', BLOG); - uiRouterRuntimeConfig.init(); //this is kind of dumb example because the admin should be able to change credentials but for demo purposes //this will have to do