Skip to content

Commit 49ec726

Browse files
author
Matt Ginzton
committed
fix($state): reloadOnSearch should not affect non-search param changes.
The handling of `reloadOnSearch: false` caused ui-router to avoid reloading the state in more cases than it should. The designed and documented behavior of this flag is to avoid state reload when the URL search string changes. It was also avoiding state reload when the URL path (or any non-search parameters to the state) changed, and even when state reload was explicitly requested. This change - flips the name of shouldTriggerReload (and the accompanying guard boolean, skipTriggerReloadCheck) to match the direction of the logic: shouldSkipReload and allowSkipReloadCheck - teaches shouldSkipReload to look at the types of the differing parameters, and only skip the reload if the only parameters that differ were search parameters - pulls the test for options.reload to the front of the complex boolean expression. (I think calling $state.reload() explicitly should reload a state even if it has reloadOnSearch:false. But I don't understand exactly why the test for this was where it was, and maybe there's a good reason I'm missing. Also, if the behavior I favor is deemed correct, we could also achieve that by setting allowSkipReloadCheck=false for all truthy values of options.reload, namely, if options.reload===true, and then shouldSkipReload wouldn't even need to look at options. I left a comment about this in the source too and would appreciate feedback.) Fixes angular-ui#1079. Helps with one of the cases broken in angular-ui#582.
1 parent 22a2b10 commit 49ec726

File tree

2 files changed

+57
-7
lines changed

2 files changed

+57
-7
lines changed

src/state.js

+40-6
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
954954
* have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd
955955
* use this when you want to force a reload when *everything* is the same, including search params.
956956
* if String, then will reload the state with the name given in reload, and any children.
957-
* if Object, then a stateObj is expected, will reload the state found in stateObj, and any chhildren.
957+
* if Object, then a stateObj is expected, will reload the state found in stateObj, and any children.
958958
*
959959
* @returns {promise} A promise representing the state of the new transition. See
960960
* {@link ui.router.state.$state#methods_go $state.go}.
@@ -1002,7 +1002,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
10021002

10031003
// Starting from the root of the path, keep all levels that haven't changed
10041004
var keep = 0, state = toPath[keep], locals = root.locals, toLocals = [];
1005-
var skipTriggerReloadCheck = false;
1005+
var allowSkipReloadCheck = true;
10061006

10071007
if (!options.reload) {
10081008
while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) {
@@ -1020,7 +1020,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
10201020
throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'");
10211021
}
10221022

1023-
skipTriggerReloadCheck = true;
1023+
// Question for PR review: Why do we not reset this for options.reload === true?!
1024+
// (shouldSkipReload had an embedded check for options.reload but it wouldn't need it if we did this)
1025+
allowSkipReloadCheck = false;
10241026

10251027
while (state && state === fromPath[keep] && state !== reloadState) {
10261028
locals = toLocals[keep] = state.locals;
@@ -1034,7 +1036,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
10341036
// TODO: We may not want to bump 'transition' if we're called from a location change
10351037
// that we've initiated ourselves, because we might accidentally abort a legitimate
10361038
// transition initiated from code?
1037-
if (!skipTriggerReloadCheck && shouldTriggerReload(to, from, locals, options)) {
1039+
if (allowSkipReloadCheck && shouldSkipReload(to, toParams, from, fromParams, locals, options)) {
10381040
if (to.self.reloadOnSearch !== false) $urlRouter.update();
10391041
$state.transition = null;
10401042
return $q.when($state.current);
@@ -1429,11 +1431,43 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
14291431
return $state;
14301432
}
14311433

1432-
function shouldTriggerReload(to, from, locals, options) {
1433-
if (to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false))) {
1434+
function shouldSkipReload(to, toParams, from, fromParams, locals, options) {
1435+
// If reload was not explicitly requested
1436+
// and we're transitioning to the same state we're already in
1437+
// and the locals didn't change
1438+
// or they changed in a way that doesn't merit reloading
1439+
// (reloadOnParams:false, or reloadOnSearch.false and only search params changed)
1440+
// Then skip the reload.
1441+
// NB: If we skip the reload, $stateParams does not get updated. So people have to manage
1442+
// $location.search() entirely manually, not via $stateParams, if using reloadOnSearch: false.
1443+
if (!options.reload && to === from && (locals === from.locals || (to.self.reloadOnSearch === false && onlySearchParamsChanged(from, fromParams, to, toParams)))) {
14341444
return true;
14351445
}
14361446
}
1447+
1448+
function noteNonSearchParams(params, nonSearchParamsHash) {
1449+
for (var key in params) {
1450+
if (key.indexOf('$$') !== 0 && params[key].location !== 'search') {
1451+
nonSearchParamsHash[key] = true;
1452+
}
1453+
}
1454+
}
1455+
1456+
function onlySearchParamsChanged(from, fromParams, to, toParams) {
1457+
// Identify whether all the parameters that differ between `fromParams` and `toParams` were search params.
1458+
// Return true if the only differences were in search params, false if there were differences in other params.
1459+
// Algorithm: build a set of keys of params that are not search params.
1460+
// Walk that set, comparing the actual values between the fromParams and toParams.
1461+
var nonSearchParams = {};
1462+
noteNonSearchParams(from.params, nonSearchParams);
1463+
noteNonSearchParams(to.params, nonSearchParams);
1464+
for (var key in nonSearchParams) {
1465+
if (fromParams[key] !== toParams[key]) {
1466+
return false;
1467+
}
1468+
}
1469+
return true;
1470+
}
14371471
}
14381472

14391473
angular.module('ui.router.state')

test/stateSpec.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('state', function () {
2929
HH = { parent: H },
3030
HHH = {parent: HH, data: {propA: 'overriddenA', propC: 'propC'} },
3131
RS = { url: '^/search?term', reloadOnSearch: false },
32+
RSP = { url: '^/:doReload/search?term', reloadOnSearch: false },
3233
OPT = { url: '/opt/:param', params: { param: "100" } },
3334
OPT2 = { url: '/opt2/:param2/:param3', params: { param3: "300", param4: "400" } },
3435
AppInjectable = {};
@@ -55,6 +56,7 @@ describe('state', function () {
5556
.state('OPT', OPT)
5657
.state('OPT.OPT2', OPT2)
5758
.state('RS', RS)
59+
.state('RSP', RSP)
5860

5961
.state('home', { url: "/" })
6062
.state('home.item', { url: "front/:id" })
@@ -212,7 +214,20 @@ describe('state', function () {
212214
});
213215
$q.flush();
214216
expect($location.search()).toEqual({term: 'hello'});
215-
expect(called).toBeFalsy();
217+
expect(called).toBeFalsy();
218+
}));
219+
220+
it('does trigger state change for path params even if reloadOnSearch is false', inject(function ($state, $q, $location, $rootScope){
221+
initStateTo(RSP, { doReload: 'foo' });
222+
expect($state.params.doReload).toEqual('foo');
223+
var called;
224+
$rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
225+
called = true
226+
});
227+
$state.transitionTo(RSP, { doReload: 'bar' });
228+
$q.flush();
229+
expect($state.params.doReload).toEqual('bar');
230+
expect(called).toBeTruthy();
216231
}));
217232

218233
it('ignores non-applicable state parameters', inject(function ($state, $q) {
@@ -940,6 +955,7 @@ describe('state', function () {
940955
'OPT',
941956
'OPT.OPT2',
942957
'RS',
958+
'RSP',
943959
'about',
944960
'about.person',
945961
'about.person.item',

0 commit comments

Comments
 (0)