Skip to content

Commit

Permalink
feat($state): params are now observable
Browse files Browse the repository at this point in the history
`$stateParams` is now a service with an `$observe()` method to watch parameters on dynamic state parameters. Dynamic state parameters can be declared like so: `params: { foo: { dynamic: true } }`.

Closes #1037, #1038, #64
  • Loading branch information
nateabele committed Apr 22, 2014
1 parent ebd68d7 commit 7a82df7
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function filterByKeys(keys, values) {
var filtered = {};

forEach(keys, function (name) {
filtered[name] = values[name];
if (isDefined(values[name])) filtered[name] = values[name];
});
return filtered;
}
Expand Down
115 changes: 102 additions & 13 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -801,15 +801,30 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
}
}

// If we're going to the same state and all locals are kept, we've got nothing to do.
// But clear 'transition', as we still want to cancel any other pending transitions.
// TODO: We may not want to bump 'transition' if we're called from a location change
// that we've initiated ourselves, because we might accidentally abort a legitimate
// transition initiated from code?
if (shouldTriggerReload(to, from, locals, options)) {
if (to.self.reloadOnSearch !== false) $urlRouter.update();
$state.transition = null;
return $q.when($state.current);
if (to === from && !options.reload) {
var isDynamic = false;

if (toState.url) {
var changes = {};

forEach(toParams, function(val, key) {
if (val != $stateParams[key]) changes[key] = val;
});

isDynamic = objectKeys(changes).length && $urlRouter.isDynamic(toState.url, changes);

if (isDynamic) {
$stateParams.$set(changes);
$urlRouter.push(toState.url, $stateParams, { replace: true });
$urlRouter.update(true);
}
}

if (isDynamic || locals === from.locals) {
if (!isDynamic) $urlRouter.update();
$state.transition = null;
return $q.when($state.current);
}
}

// Filter parameters before we pass them to event handlers etc.
Expand Down Expand Up @@ -900,6 +915,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
$state.params = toParams;
copy($state.params, $stateParams);
$state.transition = null;
$stateParams.$sync();
$stateParams.$off();

if (options.location && to.navigable) {
$urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, {
Expand Down Expand Up @@ -1180,14 +1197,86 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {

return $state;
}
}

function shouldTriggerReload(to, from, locals, options) {
if (to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false))) {
return true;
$StateParamsProvider.$inject = [];
function $StateParamsProvider() {

var observers = {}, current = {};

function unhook(key, func) {
return function() {
forEach(key.split(" "), function(k) {
observers[k].splice(observers[k].indexOf(func), 1);
});
}
}

function observeChange(key, val) {
if (!observers[key] || !observers[key].length) return;

forEach(observers[key], function(func) {
func();
});
}

function StateParams() {
}

StateParams.prototype.$digest = function() {
forEach(this, function(val, key) {
if (val == current[key] || !this.hasOwnProperty(key)) return;
current[key] = val;
observeChange(key, val);
}, this);
};

StateParams.prototype.$set = function(params) {
forEach(params, function(val, key) {
this[key] = val;
observeChange(key);
}, this);
this.$sync();
};

StateParams.prototype.$sync = function() {
copy(this, current);
};

StateParams.prototype.$off = function() {
observers = {};
};

StateParams.prototype.$localize = function(state) {
var localized = new StateParams();

forEach(state.params, function(val, key) {
localized[key] = this[key];
}, this);
return localized;
};

StateParams.prototype.$observe = function(key, func) {
forEach(key.split(" "), function(k) {
(observers[k] || (observers[k] = [])).push(func);
});
return unhook(key, func);
};

this.$get = $get;
$get.$inject = ['$rootScope'];
function $get( $rootScope) {

var global = new StateParams();

$rootScope.$watch(function() {
global.$digest();
});

return global;
}
}

angular.module('ui.router.state')
.value('$stateParams', {})
.provider('$stateParams', $StateParamsProvider)
.provider('$state', $StateProvider);
25 changes: 25 additions & 0 deletions src/urlRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,31 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
port = (port === 80 || port === 443 ? '' : ':' + port);

return [$location.protocol(), '://', $location.host(), port, slash, url].join('');
},

/**
* @ngdoc function
* @name ui.router.router.$urlRouter#shouldReload
* @methodOf ui.router.router.$urlRouter
*
* @description
* Accepts a set of parameters and determines whether a reload should be performed, or
* whether the parameters should be updated inline (i.e. if the parameters have been
* flagged as being dynamic).
*
* @param {UrlMatcher} url The URL against which the parameters are being checked.
* @param {Object} params The object hash of parameters to check.
* @return {Boolean} Returns `true` if the parameters should be updated inline, or `false`
* if they should trigger a reload.
*/
isDynamic: function(urlMatcher, params) {
var cfg, result = objectKeys(params).length > 0;

forEach(params, function(val, key) {
cfg = urlMatcher.parameters(key) || {};
result = result && (cfg.dynamic === true);
});
return result;
}
};
}
Expand Down
24 changes: 15 additions & 9 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('contacts.item.detail');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({ id: 5 });
}));

it('should transition when given a click that contains no data (fake-click)', inject(function($state, $stateParams, $q) {
Expand All @@ -142,51 +142,55 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('contacts.item.detail');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({ id: 5 });
}));

it('should not transition states when ctrl-clicked', inject(function($state, $stateParams, $q) {
expect($state.$current.name).toEqual('');
triggerClick(el, { ctrlKey: true });
expect($stateParams).toEqualData({});

triggerClick(el, { ctrlKey: true });
timeoutFlush();
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should not transition states when meta-clicked', inject(function($state, $stateParams, $q) {
expect($state.$current.name).toEqual('');
expect($stateParams).toEqualData({});

triggerClick(el, { metaKey: true });
timeoutFlush();
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should not transition states when shift-clicked', inject(function($state, $stateParams, $q) {
expect($state.$current.name).toEqual('');
expect($stateParams).toEqualData({});

triggerClick(el, { shiftKey: true });
timeoutFlush();
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should not transition states when middle-clicked', inject(function($state, $stateParams, $q) {
expect($state.$current.name).toEqual('');
expect($stateParams).toEqualData({});

triggerClick(el, { button: 1 });
timeoutFlush();
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should not transition states when element has target specified', inject(function($state, $stateParams, $q) {
Expand All @@ -198,11 +202,13 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should not transition states if preventDefault() is called in click handler', inject(function($state, $stateParams, $q) {
expect($state.$current.name).toEqual('');
expect($stateParams).toEqualData({});

el.bind('click', function(e) {
e.preventDefault();
});
Expand All @@ -212,7 +218,7 @@ describe('uiStateRef', function() {
$q.flush();

expect($state.current.name).toEqual('');
expect($stateParams).toEqual({ id: 5 });
expect($stateParams).toEqualData({});
}));

it('should allow passing params to current state', inject(function($compile, $rootScope, $state) {
Expand Down
35 changes: 23 additions & 12 deletions test/stateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('state', function () {
H = { data: {propA: 'propA', propB: 'propB'} },
HH = { parent: H },
HHH = {parent: HH, data: {propA: 'overriddenA', propC: 'propC'} },
RS = { url: '^/search?term', reloadOnSearch: false },
RS = { url: '^/search?term', params: { term: { dynamic: true } } },
AppInjectable = {};

beforeEach(module(function ($stateProvider, $provide) {
Expand Down Expand Up @@ -152,16 +152,27 @@ describe('state', function () {
expect($state.current).toBe(A);
}));

it('doesn\'t trigger state change if reloadOnSearch is false', inject(function ($state, $q, $location, $rootScope){
initStateTo(RS);
$location.search({term: 'hello'});
var called;
it('does not trigger state change if params are dynamic', inject(function ($state, $q, $location, $rootScope, $stateParams) {
var called = { change: false, observe: false };
initStateTo(RS, { term: 'goodbye' });

$location.search({ term: 'hello' });
expect($stateParams.term).toBe("goodbye");

$rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
called = true
called.change = true;
});

$stateParams.$observe('term', function(val) {
called.observe = true;
});

$q.flush();
expect($location.search()).toEqual({term: 'hello'});
expect(called).toBeFalsy();
expect($location.search()).toEqual({ term: 'hello' });
expect($stateParams.term).toBe('hello');

expect(called.change).toBe(false);
expect(called.observe).toBe(true);
}));

it('ignores non-applicable state parameters', inject(function ($state, $q) {
Expand Down Expand Up @@ -478,7 +489,7 @@ describe('state', function () {
$q.flush();

expect($state.$current.name).toBe('about.person.item');
expect($stateParams).toEqual({ person: 'bob', id: 5 });
expect($stateParams).toEqualData({ person: 'bob', id: 5 });

$state.go('^.^.sidebar');
$q.flush();
Expand Down Expand Up @@ -909,7 +920,7 @@ describe('state', function () {
$state.go('root.sub1', { param2: 2 });
$q.flush();
expect($state.current.name).toEqual('root.sub1');
expect($stateParams).toEqual({ param1: 1, param2: 2 });
expect($stateParams).toEqualData({ param1: 1, param2: 2 });
}));

it('should not inherit siblings\' states', inject(function ($state, $stateParams, $q) {
Expand All @@ -922,7 +933,7 @@ describe('state', function () {
$q.flush();
expect($state.current.name).toEqual('root.sub2');

expect($stateParams).toEqual({ param1: 1, param2: undefined });
expect($stateParams).toEqualData({ param1: 1, param2: undefined });
}));
});

Expand Down Expand Up @@ -1017,7 +1028,7 @@ describe('state', function () {
});
});

describe('state queue', function(){
describe('state queue', function() {
angular.module('ui.router.queue.test', ['ui.router.queue.test.dependency'])
.config(function($stateProvider) {
$stateProvider
Expand Down
14 changes: 14 additions & 0 deletions test/urlRouterSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ describe("UrlRouter", function () {
expect($urlRouter.href(matcher, { param: 5 })).toBeNull();
}));
});

describe("reload checking", function() {
it("should check UrlMatcher parameters", inject(function($urlRouter) {
var m = new UrlMatcher('/:foo/:bar/:baz', {
params: {
foo: { dynamic: true },
bar: { dynamic: true },
baz: {}
}
});
expect($urlRouter.isDynamic(m, { foo: "1", bar: "2" })).toBe(true);
expect($urlRouter.isDynamic(m, { bar: "1", baz: "2" })).toBe(false);
}));
});
});

});

0 comments on commit 7a82df7

Please sign in to comment.