Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add feature to clearAll and (re)initialize providers to support on-the-fly routing changes #124

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions src/urlRouter.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down
206 changes: 206 additions & 0 deletions test/featureClearAllSpec.js
Original file line number Diff line number Diff line change
@@ -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);
}));
});


});