Skip to content
This repository was archived by the owner on Sep 20, 2020. It is now read-only.

Commit 27d6c8c

Browse files
feat(sticky): Allow reloading of partial state tree
- $state.go(state, params, { reload: 'state.subtree' }) reloads state.subtree and all children (exit/enter) chore(extras): fix some bugs introduced by the modular build system Closes #139
1 parent d84311e commit 27d6c8c

File tree

5 files changed

+143
-16
lines changed

5 files changed

+143
-16
lines changed

src/core.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ mod_core.provider("uirextras_core", function() {
161161
inherit: inherit
162162
};
163163

164-
_.extend(this, core);
164+
angular.extend(this, core);
165165

166166
this.$get = function() {
167167
return core;

src/sticky.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ angular.module("ct.ui.router.extras.sticky").config(
122122

123123
// ------------------------ Decorated transitionTo implementation begins here ---------------------------
124124
$state.transitionTo = function (to, toParams, options) {
125+
var DEBUG = $stickyStateProvider.debugMode();
125126
// TODO: Move this to module.run?
126127
// TODO: I'd rather have root.locals prototypally inherit from inactivePseudoState.locals
127128
// Link root.locals and inactives.locals. Do this at runtime, after root.locals has been set.
@@ -140,6 +141,8 @@ angular.module("ct.ui.router.extras.sticky").config(
140141
var toStateSelf = $state.get(to, rel); // exposes findState relative path functionality, returns state.self
141142
var savedToStatePath, savedFromStatePath, stickyTransitions;
142143
var reactivated = [], exited = [], terminalReactivatedState;
144+
toParams = toParams || {};
145+
arguments[1] = toParams;
143146

144147
var noop = function () {
145148
};
@@ -234,6 +237,19 @@ angular.module("ct.ui.router.extras.sticky").config(
234237
return state;
235238
}
236239

240+
// TODO: This may be completely unnecessary now that we're using $$uirouterextrasreload temp param
241+
function stateUpdateParamsSurrogate(state, toParams) {
242+
var oldOnEnter = state.self.onEnter;
243+
state.self.onEnter = function () {
244+
_StickyState.stateEntering(state, toParams, oldOnEnter, true);
245+
};
246+
restore.addRestoreFunction(function () {
247+
state.self.onEnter = oldOnEnter;
248+
});
249+
250+
return state;
251+
}
252+
237253
function stateExitedSurrogate(state) {
238254
var oldOnExit = state.self.onExit;
239255
state.self.onExit = function () {
@@ -255,11 +271,43 @@ angular.module("ct.ui.router.extras.sticky").config(
255271
savedToStatePath = toState.path;
256272
savedFromStatePath = fromState.path;
257273

258-
var currentTransition = {toState: toState, toParams: toParams || {}, fromState: fromState, fromParams: fromParams || {}, options: options};
274+
// Try to resolve options.reload to a state. If so, we'll reload only up to the given state.
275+
var reload = options && options.reload;
276+
var reloadStateTree = (reload === true ? savedToStatePath[0].self : $state.get(reload, rel));
277+
// If options.reload is a string or a state, we want to handle reload ourselves and not
278+
// let ui-router reload the entire toPath.
279+
if (options && reload && reload !== true)
280+
delete options.reload;
281+
282+
var currentTransition = {
283+
toState: toState,
284+
toParams: toParams || {},
285+
fromState: fromState,
286+
fromParams: fromParams || {},
287+
options: options,
288+
reloadStateTree: reloadStateTree
289+
};
259290

260291
pendingTransitions.push(currentTransition); // TODO: See if a list of pending transitions is necessary.
261292
pendingRestore = restore;
262293

294+
// If we're reloading from a state and below, temporarily add a param to the top of the state tree
295+
// being reloaded, and add a param value to the transition. This will cause the "has params changed
296+
// for state" check to return false, and the states will be reloaded.
297+
if (reloadStateTree) {
298+
var params = reloadStateTree.$$state().params;
299+
var ownParams = reloadStateTree.$$state().ownParams;
300+
301+
var tempParam = new $urlMatcherFactoryProvider.Param('$$uirouterextrasreload');
302+
params.$$uirouterextrasreload = ownParams.$$uirouterextrasreload = tempParam;
303+
currentTransition.toParams.$$uirouterextrasreload = Math.random();
304+
305+
restore.restoreFunctions.push(function() {
306+
delete params.$$uirouterextrasreload;
307+
delete ownParams.$$uirouterextrasreload;
308+
});
309+
}
310+
263311
// $StickyStateProvider.processTransition analyzes the states involved in the pending transition. It
264312
// returns an object that tells us:
265313
// 1) if we're involved in a sticky-type transition
@@ -309,7 +357,7 @@ angular.module("ct.ui.router.extras.sticky").config(
309357
terminalReactivatedState = surrogate;
310358
} else if (value === "updateStateParams") {
311359
// If the state params have been changed, we need to exit any inactive states and re-enter them.
312-
surrogate = stateEnteredSurrogate(toState.path[idx]);
360+
surrogate = stateUpdateParamsSurrogate(toState.path[idx]);
313361
surrogateToPath.push(surrogate);
314362
terminalReactivatedState = surrogate;
315363
} else if (value === "enter") {

src/stickyProvider.js

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function $StickyStateProvider($stateProvider) {
88
var inactiveStates = {}; // state.name -> (state)
99
var stickyStates = {}; // state.name -> true
1010
var $state;
11+
var DEBUG = false;
1112

1213
// Called by $stateProvider.registerState();
1314
// registers a sticky state with $stickyStateProvider
@@ -16,8 +17,10 @@ function $StickyStateProvider($stateProvider) {
1617
// console.log("Registered sticky state: ", state);
1718
};
1819

19-
this.enableDebug = function (enabled) {
20-
DEBUG = enabled;
20+
this.enableDebug = this.debugMode = function (enabled) {
21+
if (angular.isDefined(enabled))
22+
DEBUG = enabled;
23+
return DEBUG;
2124
};
2225

2326
this.$get = [ '$rootScope', '$state', '$stateParams', '$injector', '$log',
@@ -72,10 +75,11 @@ function $StickyStateProvider($stateProvider) {
7275
// it as a Exit/Enter, thus the special "updateStateParams" transition.
7376
// If a parent inactivated state has "updateStateParams" transition type, then
7477
// all descendant states must also be exit/entered, thus the first line of this function.
75-
function getEnterTransition(state, stateParams, ancestorParamsChanged) {
78+
function getEnterTransition(state, stateParams, reloadStateTree, ancestorParamsChanged) {
7679
if (ancestorParamsChanged) return "updateStateParams";
7780
var inactiveState = inactiveStates[state.self.name];
7881
if (!inactiveState) return "enter";
82+
if (state.self === reloadStateTree) return "updateStateParams";
7983
// if (inactiveState.locals == null || inactiveState.locals.globals == null) debugger;
8084
var paramsMatch = equalForKeys(stateParams, inactiveState.locals.globals.$stateParams, state.ownParams);
8185
// if (DEBUG) $log.debug("getEnterTransition: " + state.name + (paramsMatch ? ": reactivate" : ": updateStateParams"));
@@ -94,7 +98,7 @@ function $StickyStateProvider($stateProvider) {
9498
// Duplicates logic in $state.transitionTo, primarily to find the pivot state (i.e., the "keep" value)
9599
function equalForKeys(a, b, keys) {
96100
if (angular.isObject(keys)) {
97-
keys = protoKeys(keys, ["$$keys", "$$values", "$$equals", "$$validates"]);
101+
keys = protoKeys(keys, ["$$keys", "$$values", "$$equals", "$$validates", "$$new", "$$parent"]);
98102
}
99103
if (!keys) {
100104
keys = [];
@@ -133,6 +137,7 @@ function $StickyStateProvider($stateProvider) {
133137
fromParams = transition.fromParams,
134138
toPath = transition.toState.path,
135139
toParams = transition.toParams,
140+
reloadStateTree = transition.reloadStateTree,
136141
options = transition.options;
137142
var keep = 0, state = toPath[keep];
138143

@@ -141,28 +146,29 @@ function $StickyStateProvider($stateProvider) {
141146
}
142147

143148
while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) {
149+
// We're "keeping" this state. bump keep var and get the next state in toPath for the next iteration.
144150
state = toPath[++keep];
145151
}
146152

147153
result.keep = keep;
148154

149-
var idx, deepestUpdatedParams, deepestReactivate, reactivatedStatesByName = {}, pType = getStickyTransitionType(fromPath, toPath, keep);
150-
var ancestorUpdated = false; // When ancestor params change, treat reactivation as exit/enter
155+
var idx, deepestUpdatedParams, deepestReactivate, noLongerInactiveStates = {}, pType = getStickyTransitionType(fromPath, toPath, keep);
156+
var ancestorUpdated = !!options.reload; // When ancestor params change, treat reactivation as exit/enter
151157

152158
// Calculate the "enter" transitions for new states in toPath
153159
// Enter transitions will be either "enter", "reactivate", or "updateStateParams" where
154160
// enter: full resolve, no special logic
155161
// reactivate: use previous locals
156162
// updateStateParams: like 'enter', except exit the inactive state before entering it.
157163
for (idx = keep; idx < toPath.length; idx++) {
158-
var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], transition.toParams, ancestorUpdated);
164+
var enterTrans = !pType.to ? "enter" : getEnterTransition(toPath[idx], toParams, reloadStateTree, ancestorUpdated);
159165
ancestorUpdated = (ancestorUpdated || enterTrans == 'updateStateParams');
160166
result.enter[idx] = enterTrans;
161167
// If we're reactivating a state, make a note of it, so we can remove that state from the "inactive" list
162168
if (enterTrans == 'reactivate')
163-
deepestReactivate = reactivatedStatesByName[toPath[idx].name] = toPath[idx];
169+
deepestReactivate = noLongerInactiveStates[toPath[idx].name] = toPath[idx];
164170
if (enterTrans == 'updateStateParams')
165-
deepestUpdatedParams = toPath[idx];
171+
deepestUpdatedParams = noLongerInactiveStates[toPath[idx].name] = toPath[idx];
166172
}
167173
deepestReactivate = deepestReactivate ? deepestReactivate.self.name + "." : "";
168174
deepestUpdatedParams = deepestUpdatedParams ? deepestUpdatedParams.self.name + "." : "";
@@ -181,7 +187,7 @@ function $StickyStateProvider($stateProvider) {
181187
for (var i = 0; inactiveChildren && i < inactiveChildren.length; i++) {
182188
var child = inactiveChildren[i];
183189
// Don't organize state as inactive if we're about to reactivate it.
184-
if (!reactivatedStatesByName[child.name] &&
190+
if (!noLongerInactiveStates[child.name] &&
185191
(!deepestReactivate || (child.self.name.indexOf(deepestReactivate) !== 0)) &&
186192
(!deepestUpdatedParams || (child.self.name.indexOf(deepestUpdatedParams) !== 0)))
187193
result.inactives.push(child);
@@ -260,9 +266,9 @@ function $StickyStateProvider($stateProvider) {
260266
},
261267

262268
// Removes a previously inactivated state from the inactive sticky state registry
263-
stateEntering: function (entering, params, onEnter) {
269+
stateEntering: function (entering, params, onEnter, updateParams) {
264270
var inactivatedState = getInactivatedState(entering);
265-
if (inactivatedState && !getInactivatedState(entering, params)) {
271+
if (inactivatedState && (updateParams || !getInactivatedState(entering, params))) {
266272
var savedLocals = entering.locals;
267273
this.stateExiting(inactivatedState);
268274
entering.locals = savedLocals;

test/stickySpec.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('stickyState', function () {
2020
newStates['A'] = { template: '<div ui-view="_1"></div><div ui-view="_2"></div><div ui-view="_3"></div>'};
2121
newStates['A._1'] = {sticky: true, views: { '_1@A': {} } };
2222
newStates['A._2'] = {sticky: true, views: { '_2@A': {} } };
23+
newStates['A._2.__1'] = { };
2324
newStates['A._3'] = {
2425
sticky: true,
2526
views: { '_3@A': { controller: function ($scope, X) {
@@ -234,6 +235,38 @@ describe('stickyState', function () {
234235
});
235236
});
236237

238+
function getParameterizedStates() {
239+
return {
240+
'main': {},
241+
'main.other': { sticky: true, views: { 'other@main': {} } },
242+
'main.product': { sticky: true, views: { 'product@main': {} }, params: { 'product_id': 15 } },
243+
'main.product.something': {}
244+
};
245+
}
246+
247+
describe('with params in sticky state', function() {
248+
beforeEach(function() {
249+
ssReset(getParameterizedStates(), _stateProvider);
250+
});
251+
252+
it("should reload when params change", function() {
253+
testGo('main');
254+
var options = { params: { 'product_id': 12345 } };
255+
testGo('main.product.something', { entered: pathFrom('main', 'main.product.something') }, options);
256+
testGo('main.other', { entered: 'main.other', inactivated: [ 'main.product.something', 'main.product'] });
257+
testGo('main.product.something', { reactivated: ['main.product', 'main.product.something'], inactivated: 'main.other' }, options);
258+
_stickyStateProvider.enableDebug(true);
259+
testGo('main.other', { reactivated: 'main.other', inactivated: [ 'main.product.something', 'main.product'] });
260+
options.params.product_id = 54321;
261+
resetTransitionLog();
262+
testGo('main.product.something', {
263+
exited: ['main.product.something', 'main.product'],
264+
entered: ['main.product', 'main.product.something'],
265+
inactivated: 'main.other' }, options);
266+
_stickyStateProvider.enableDebug(false);
267+
});
268+
});
269+
237270
describe('nested sticky .go() transitions', function () {
238271
beforeEach(function() {
239272
ssReset(getNestedStickyStates(), _stateProvider);
@@ -360,6 +393,46 @@ describe('stickyState', function () {
360393
testGo('aside', { exited: ['__2', 'A._1.__1', 'A._1', '_2', 'A'], entered: ['aside'] });
361394
});
362395
});
396+
397+
// test cases for issue #139
398+
describe('ui-router option reload: true', function() {
399+
beforeEach(function() {
400+
ssReset(getSimpleStates(), _stateProvider);
401+
});
402+
403+
it('should be respected', function() {
404+
testGo('A._1', { entered: ['A', 'A._1' ] });
405+
testGo('A._2', { inactivated: [ 'A._1' ], entered: 'A._2' });
406+
testGo('A._1', { reactivated: 'A._1', inactivated: 'A._2' });
407+
resetTransitionLog();
408+
testGo('A._2', { exited: [ 'A._1', 'A._2', 'A' ], entered: [ 'A', 'A._2' ] }, { reload: true });
409+
});
410+
});
411+
412+
describe('ui-router option reload: [state ref]', function() {
413+
beforeEach(function() {
414+
ssReset(getSimpleStates(), _stateProvider);
415+
});
416+
417+
it('should reload a partial tree of sticky states', function() {
418+
testGo('A._1', { entered: ['A', 'A._1' ] });
419+
testGo('A._2', { inactivated: [ 'A._1' ], entered: 'A._2' });
420+
testGo('A._1', { reactivated: 'A._1', inactivated: 'A._2' });
421+
resetTransitionLog();
422+
testGo('A._2', { inactivated: 'A._1', exited: 'A._2', entered: 'A._2' }, { reload: "A._2" });
423+
});
424+
425+
it('should reload a partial tree of non-sticky states', function() {
426+
testGo('A._1', { entered: ['A', 'A._1' ] });
427+
testGo('A._2.__1', { inactivated: 'A._1', entered: [ 'A._2', 'A._2.__1' ] });
428+
testGo('A._1', { reactivated: 'A._1', inactivated: [ 'A._2.__1', 'A._2' ] });
429+
testGo('A._2.__1', {
430+
inactivated: 'A._1', reactivated: 'A._2',
431+
exited: [ 'A._2.__1' ], entered: [ 'A._2.__1' ]
432+
}, { reload: "A._2.__1" });
433+
testGo('A._2.__1', { exited: [ 'A._2.__1' ], entered: [ 'A._2.__1' ] }, { reload: "A._2.__1" });
434+
});
435+
});
363436
});
364437

365438
describe('stickyState+ui-sref-active', function () {

test/testUtil.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function pathFrom(start, end) {
155155
* { redirect: redirectstatename }
156156
*/
157157
function testGo(state, tAdditional, options) {
158-
$state.go(state);
158+
$state.go(state, options && options.params, options);
159159
$q.flush();
160160
var expectRedirect = options && options.redirect;
161161
if (!expectRedirect)

0 commit comments

Comments
 (0)