Skip to content

Commit 961c96d

Browse files
feat(params): Add uiOnParamsChanged controller callback
closes #2608 closes #2470 closes #2391 closes #1967
1 parent 1541b90 commit 961c96d

File tree

7 files changed

+407
-331
lines changed

7 files changed

+407
-331
lines changed

src/ng1/services.ts

-4
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,6 @@ const resolveFactory = () => ({
229229
});
230230

231231
function $stateParamsFactory(ng1UIRouter, $rootScope) {
232-
$rootScope.$watch(function() {
233-
router.stateParams.$digest();
234-
});
235-
236232
return router.stateParams;
237233
}
238234

src/ng1/viewDirective.ts

+55-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @module view */ /** for typedoc */
22
"use strict";
3-
import {extend, map} from "../common/common";
3+
import {extend, map, unnestR, filter} from "../common/common";
44
import {isDefined, isFunction} from "../common/predicates";
55
import {trace} from "../common/trace";
66
import {ActiveUIView} from "../view/interface";
@@ -9,6 +9,9 @@ import {RejectType} from "../transition/rejectFactory";
99
import {TransitionService} from "../transition/transitionService";
1010
import {parse} from "../common/hof";
1111
import {ResolveContext} from "../resolve/resolveContext";
12+
import {Transition} from "../transition/transition";
13+
import {Node} from "../path/node";
14+
import {Param} from "../params/param";
1215

1316
export type UIViewData = {
1417
$cfg: Ng1ViewConfig;
@@ -340,9 +343,11 @@ function $ViewDirectiveFill ( $compile, $controller, $transitions, $view,
340343
scope[controllerAs] = controllerInstance;
341344
scope[controllerAs][resolveAs] = locals;
342345
}
343-
if (isFunction(controllerInstance.$onInit)) controllerInstance.$onInit();
346+
344347
$element.data('$ngControllerController', controllerInstance);
345348
$element.children().data('$ngControllerController', controllerInstance);
349+
350+
registerControllerCallbacks($transitions, controllerInstance, scope, cfg);
346351
}
347352

348353
link(scope);
@@ -351,5 +356,53 @@ function $ViewDirectiveFill ( $compile, $controller, $transitions, $view,
351356
};
352357
}
353358

359+
// TODO: move these callbacks to $view and/or `/hooks/components.ts` or something
360+
function registerControllerCallbacks($transitions: TransitionService, controllerInstance, $scope, cfg: Ng1ViewConfig) {
361+
// Call $onInit() ASAP
362+
if (isFunction(controllerInstance.$onInit)) controllerInstance.$onInit();
363+
364+
// Add component-level hook for onParamsChange
365+
if (isFunction(controllerInstance.uiOnParamsChanged)) {
366+
// Fire callback on any successful transition
367+
const paramsUpdated = ($transition$: Transition) => {
368+
let ctx: ResolveContext = cfg.node.resolveContext;
369+
let viewCreationTrans = ctx.getResolvables()['$transition$'].data;
370+
// Exit early if the $transition$ is the same as the view was created within.
371+
// Exit early if the $transition$ will exit the state the view is for.
372+
if ($transition$ === viewCreationTrans || $transition$.exiting().indexOf(cfg.node.state.self) !== -1) return;
373+
374+
let toParams = $transition$.params("to");
375+
let fromParams = $transition$.params("from");
376+
let toSchema: Param[] = $transition$.treeChanges().to.map((node: Node) => node.paramSchema).reduce(unnestR, []);
377+
let fromSchema: Param[] = $transition$.treeChanges().from.map((node: Node) => node.paramSchema).reduce(unnestR, []);
378+
379+
// Find the to params that have different values than the from params
380+
let changedToParams = toSchema.filter((param: Param) => {
381+
let idx = fromSchema.indexOf(param);
382+
return idx === -1 || !fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id]);
383+
});
384+
385+
// Only trigger callback if a to param has changed or is new
386+
if (changedToParams.length) {
387+
let changedKeys = changedToParams.map(x => x.id);
388+
// Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
389+
controllerInstance.uiOnParamsChanged(filter(toParams, (val, key) => changedKeys.indexOf(key) !== -1), $transition$);
390+
}
391+
};
392+
$scope.$on('$destroy', $transitions.onSuccess({}, ['$transition$', paramsUpdated]));
393+
394+
// Fire callback on any IGNORED transition
395+
let onDynamic = ($error$, $transition$) => {
396+
if ($error$.type === RejectType.IGNORED) paramsUpdated($transition$);
397+
};
398+
$scope.$on('$destroy', $transitions.onError({}, ['$error$', '$transition$', onDynamic]));
399+
}
400+
401+
// Add component-level hook for canDeactivate
402+
if (isFunction(controllerInstance.canDeactivate)) {
403+
$scope.$on('$destroy', $transitions.onBefore({exiting: cfg.node.state.name}, controllerInstance.canDeactivate.bind(controllerInstance)));
404+
}
405+
}
406+
354407
angular.module('ui.router.state').directive('uiView', $ViewDirective);
355408
angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill);

src/params/param.ts

+34-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @module params */ /** for typedoc */
2-
import {extend, filter, map, applyPairs} from "../common/common";
2+
import {extend, filter, map, applyPairs, allTrueR} from "../common/common";
33
import {prop, propEq} from "../common/hof";
44
import {isInjectable, isDefined, isString, isArray} from "../common/predicates";
55
import {RawParams} from "../params/interface";
@@ -135,31 +135,55 @@ export class Param {
135135
return `{Param:${this.id} ${this.type} squash: '${this.squash}' optional: ${this.isOptional}}`;
136136
}
137137

138+
/** Creates a new [[Param]] from a CONFIG block */
138139
static fromConfig(id: string, type: Type, config: any): Param {
139140
return new Param(id, type, config, DefType.CONFIG);
140141
}
141142

143+
/** Creates a new [[Param]] from a url PATH */
142144
static fromPath(id: string, type: Type, config: any): Param {
143145
return new Param(id, type, config, DefType.PATH);
144146
}
145147

148+
/** Creates a new [[Param]] from a url SEARCH */
146149
static fromSearch(id: string, type: Type, config: any): Param {
147150
return new Param(id, type, config, DefType.SEARCH);
148151
}
149152

150-
static values(params: Param[], values): RawParams {
151-
values = values || {};
153+
static values(params: Param[], values = {}): RawParams {
152154
return <RawParams> params.map(param => [param.id, param.value(values[param.id])]).reduce(applyPairs, {});
153155
}
154156

155-
static equals(params: Param[], values1, values2): boolean {
156-
values1 = values1 || {};
157-
values2 = values2 || {};
158-
return params.map(param => param.type.equals(values1[param.id], values2[param.id])).indexOf(false) === -1;
157+
/**
158+
* Finds [[Param]] objects which have different param values
159+
*
160+
* Filters a list of [[Param]] objects to only those whose parameter values differ in two param value objects
161+
*
162+
* @param params: The list of Param objects to filter
163+
* @param values1: The first set of parameter values
164+
* @param values2: the second set of parameter values
165+
*
166+
* @returns any Param objects whose values were different between values1 and values2
167+
*/
168+
static changed(params: Param[], values1 = {}, values2 = {}): Param[] {
169+
return params.filter(param => !param.type.equals(values1[param.id], values2[param.id]));
170+
}
171+
172+
/**
173+
* Checks if two param value objects are equal (for a set of [[Param]] objects)
174+
*
175+
* @param params The list of [[Param]] objects to check
176+
* @param values1 The first set of param values
177+
* @param values2 The second set of param values
178+
*
179+
* @returns true if the param values in values1 and values2 are equal
180+
*/
181+
static equals(params: Param[], values1 = {}, values2 = {}): boolean {
182+
return Param.changed(params, values1, values2).length === 0;
159183
}
160184

161-
static validates(params: Param[], values): boolean {
162-
values = values || {};
163-
return params.map(param => param.validates(values[param.id])).indexOf(false) === -1;
185+
/** Returns true if a the parameter values are valid, according to the Param definitions */
186+
static validates(params: Param[], values = {}): boolean {
187+
return params.map(param => param.validates(values[param.id])).reduce(allTrueR, true);
164188
}
165189
}

src/params/stateParams.ts

+28-108
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,37 @@
11
/** @module params */ /** for typedoc */
2-
import {forEach, ancestors, extend, copy, pick, omit} from "../common/common";
2+
import {extend, ancestors} from "../common/common";
33

44
export class StateParams {
55
constructor(params: Object = {}) {
66
extend(this, params);
77
}
88

9-
$digest() {}
10-
$inherit(newParams, $current, $to) {}
11-
$set(params, url) {}
12-
$sync() {}
13-
$off() {}
14-
$raw() {}
15-
$localize(state, params) {}
16-
$observe(key: string, fn: Function) {}
17-
}
18-
19-
20-
export function stateParamsFactory() {
21-
let observers = {}, current = {};
22-
23-
function unhook(key, func) {
24-
return () => {
25-
forEach(key.split(" "), k => observers[k].splice(observers[k].indexOf(func), 1));
26-
};
27-
}
28-
29-
function observeChange(key, val?: any) {
30-
if (!observers[key] || !observers[key].length) return;
31-
forEach(observers[key], func => func(val));
32-
}
33-
34-
35-
StateParams.prototype.$digest = function() {
36-
forEach(this, (val, key) => {
37-
if (val === current[key] || !this.hasOwnProperty(key)) return;
38-
current[key] = val;
39-
observeChange(key, val);
40-
});
41-
};
42-
43-
/**
44-
* Merges a set of parameters with all parameters inherited between the common parents of the
45-
* current state and a given destination state.
46-
*
47-
* @param {Object} newParams The set of parameters which will be composited with inherited params.
48-
* @param {Object} $current Internal definition of object representing the current state.
49-
* @param {Object} $to Internal definition of object representing state to transition to.
50-
*/
51-
StateParams.prototype.$inherit = function(newParams, $current, $to) {
52-
let parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
53-
54-
for (let i in parents) {
55-
if (!parents[i] || !parents[i].params) continue;
56-
parentParams = Object.keys(parents[i].params);
57-
if (!parentParams.length) continue;
58-
59-
for (let j in parentParams) {
60-
if (inheritList.indexOf(parentParams[j]) >= 0) continue;
61-
inheritList.push(parentParams[j]);
62-
inherited[parentParams[j]] = this[parentParams[j]];
63-
}
64-
}
65-
return extend({}, inherited, newParams);
66-
};
67-
68-
StateParams.prototype.$set = function(params, url) {
69-
let hasChanged = false, abort = false;
70-
71-
if (url) {
72-
forEach(params, function(val, key) {
73-
if ((url.parameter(key) || {}).dynamic !== true) abort = true;
74-
});
9+
/**
10+
* Merges a set of parameters with all parameters inherited between the common parents of the
11+
* current state and a given destination state.
12+
*
13+
* @param {Object} newParams The set of parameters which will be composited with inherited params.
14+
* @param {Object} $current Internal definition of object representing the current state.
15+
* @param {Object} $to Internal definition of object representing state to transition to.
16+
*/
17+
$inherit(newParams, $current, $to) {
18+
let parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];
19+
20+
for (let i in parents) {
21+
if (!parents[i] || !parents[i].params) continue;
22+
parentParams = Object.keys(parents[i].params);
23+
if (!parentParams.length) continue;
24+
25+
for (let j in parentParams) {
26+
if (inheritList.indexOf(parentParams[j]) >= 0) continue;
27+
inheritList.push(parentParams[j]);
28+
inherited[parentParams[j]] = this[parentParams[j]];
7529
}
76-
if (abort) return false;
77-
78-
forEach(params, (val, key) => {
79-
if (val !== this[key]) {
80-
this[key] = val;
81-
observeChange(key);
82-
hasChanged = true;
83-
}
84-
});
85-
86-
this.$sync();
87-
return hasChanged;
88-
};
89-
90-
StateParams.prototype.$sync = function() {
91-
copy(this, current);
92-
return this;
93-
};
94-
95-
StateParams.prototype.$off = function() {
96-
observers = {};
97-
return this;
98-
};
99-
100-
StateParams.prototype.$raw = function() {
101-
return omit(
102-
this,
103-
Object.keys(this).filter(StateParams.prototype.hasOwnProperty.bind(StateParams.prototype))
104-
);
105-
};
106-
107-
StateParams.prototype.$localize = function(state, params) {
108-
return new StateParams(pick(params || this, Object.keys(state.params)));
109-
};
110-
111-
StateParams.prototype.$observe = function(key: string, fn: Function) {
112-
forEach(key.split(" "), k => (observers[k] || (observers[k] = [])).push(fn));
113-
return unhook(key, fn);
114-
};
30+
}
31+
return extend({}, inherited, newParams);
32+
};
33+
}
11534

116-
return new StateParams();
117-
}
35+
export function stateParamsFactory() {
36+
return new StateParams();
37+
}

src/state/hooks/transitionManager.ts

-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export class TransitionManager {
119119
let options = transition.options();
120120
$state.params = transition.params();
121121
copy($state.params, $stateParams);
122-
$stateParams.$sync().$off();
123122

124123
if (options.location && $state.$current.navigable) {
125124
$urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' });

0 commit comments

Comments
 (0)