Skip to content

Commit 45aa2aa

Browse files
feat(uiView): Add uiOnParamsChanged support for routed components
Closes #234
1 parent 1535d22 commit 45aa2aa

File tree

3 files changed

+262
-22
lines changed

3 files changed

+262
-22
lines changed

src/directives/uiView.ts

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,41 @@
22
/** */
33
import {
44
Component,
5+
ComponentFactory,
56
ComponentFactoryResolver,
6-
ViewContainerRef,
7-
Input,
87
ComponentRef,
9-
Type,
8+
Inject,
9+
Injector,
10+
Input,
11+
OnDestroy,
12+
OnInit,
1013
ReflectiveInjector,
1114
ViewChild,
12-
Injector,
13-
Inject,
14-
ComponentFactory,
15+
ViewContainerRef,
1516
} from '@angular/core';
1617

1718
import {
18-
UIRouter,
19+
ActiveUIView,
20+
filter,
21+
inArray,
1922
isFunction,
20-
Transition,
23+
NATIVE_INJECTOR_TOKEN,
24+
Param,
2125
parse,
22-
TransitionHookFn,
26+
PathNode,
27+
ResolveContext,
2328
StateDeclaration,
24-
inArray,
2529
trace,
26-
ViewContext,
30+
Transition,
31+
TransitionHookFn,
32+
UIRouter,
33+
unnestR,
2734
ViewConfig,
28-
ActiveUIView,
29-
ResolveContext,
30-
NATIVE_INJECTOR_TOKEN,
31-
flattenR,
35+
ViewContext,
3236
} from '@uirouter/core';
3337
import { Ng2ViewConfig } from '../statebuilders/views';
3438
import { MergeInjector } from '../mergeInjector';
39+
import { Ng2Component } from '../interface';
3540

3641
/** @hidden */
3742
let id = 0;
@@ -114,22 +119,26 @@ const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
114119
<ng-content *ngIf="!_componentRef"></ng-content>
115120
`,
116121
})
117-
export class UIView {
122+
export class UIView implements OnInit, OnDestroy {
118123
static PARENT_INJECT = 'UIView.PARENT_INJECT';
119124

120125
@ViewChild('componentTarget', { read: ViewContainerRef })
121126
_componentTarget: ViewContainerRef;
122127
@Input('name') name: string;
128+
123129
@Input('ui-view')
124130
set _name(val: string) {
125131
this.name = val;
126132
}
133+
127134
/** The reference to the component currently inside the viewport */
128135
_componentRef: ComponentRef<any>;
129136
/** Deregisters the ui-view from the view service */
130137
private _deregisterUIView: Function;
131138
/** Deregisters the master uiCanExit transition hook */
132-
private _deregisterHook: Function;
139+
private _deregisterUiCanExitHook: Function;
140+
/** Deregisters the master uiOnParamsChanged transition hook */
141+
private _deregisterUiOnParamsChangedHook: Function;
133142
/** Data about the this UIView */
134143
private _uiViewData: ActiveUIView = <any>{};
135144
private _parent: ParentUIViewInject;
@@ -164,7 +173,14 @@ export class UIView {
164173
config: undefined,
165174
};
166175

167-
this._deregisterHook = router.transitionService.onBefore({}, trans => this._applyUiCanExitHook(trans));
176+
this._deregisterUiCanExitHook = router.transitionService.onBefore({}, trans => {
177+
return this._invokeUiCanExitHook(trans);
178+
});
179+
180+
this._deregisterUiOnParamsChangedHook = router.transitionService.onSuccess({}, trans =>
181+
this._invokeUiOnParamsChangedHook(trans),
182+
);
183+
168184
this._deregisterUIView = router.viewService.registerUIView(this._uiViewData);
169185
}
170186

@@ -176,7 +192,7 @@ export class UIView {
176192
*
177193
* If both are true, adds the uiCanExit component function as a hook to that singular Transition.
178194
*/
179-
private _applyUiCanExitHook(trans: Transition) {
195+
private _invokeUiCanExitHook(trans: Transition) {
180196
const instance = this._componentRef && this._componentRef.instance;
181197
const uiCanExitFn: TransitionHookFn = instance && instance.uiCanExit;
182198

@@ -191,14 +207,61 @@ export class UIView {
191207
}
192208
}
193209

210+
/**
211+
* For each transition, checks if any param values changed and notify component
212+
*/
213+
private _invokeUiOnParamsChangedHook($transition$: Transition) {
214+
const instance: Ng2Component = this._componentRef && this._componentRef.instance;
215+
const uiOnParamsChanged: TransitionHookFn = instance && instance.uiOnParamsChanged;
216+
217+
if (isFunction(uiOnParamsChanged)) {
218+
const viewState: StateDeclaration = this.state;
219+
const resolveContext: ResolveContext = new ResolveContext(this._uiViewData.config.path);
220+
const viewCreationTrans = resolveContext.getResolvable('$transition$').data;
221+
222+
// Exit early if the $transition$ is the same as the view was created within.
223+
// Exit early if the $transition$ will exit the state the view is for.
224+
if ($transition$ === viewCreationTrans || $transition$.exiting().indexOf(viewState as StateDeclaration) !== -1)
225+
return;
226+
227+
const toParams: { [paramName: string]: any } = $transition$.params('to');
228+
const fromParams: { [paramName: string]: any } = $transition$.params('from');
229+
const getNodeSchema = (node: PathNode) => node.paramSchema;
230+
const toSchema: Param[] = $transition$
231+
.treeChanges('to')
232+
.map(getNodeSchema)
233+
.reduce(unnestR, []);
234+
const fromSchema: Param[] = $transition$
235+
.treeChanges('from')
236+
.map(getNodeSchema)
237+
.reduce(unnestR, []);
238+
239+
// Find the to params that have different values than the from params
240+
const changedToParams = toSchema.filter((param: Param) => {
241+
const idx = fromSchema.indexOf(param);
242+
return idx === -1 || !fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id]);
243+
});
244+
245+
// Only trigger callback if a to param has changed or is new
246+
if (changedToParams.length) {
247+
const changedKeys: string[] = changedToParams.map(x => x.id);
248+
// Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
249+
const newValues = filter(toParams, (val, key) => changedKeys.indexOf(key) !== -1);
250+
instance.uiOnParamsChanged(newValues, $transition$);
251+
}
252+
}
253+
}
254+
194255
private _disposeLast() {
195256
if (this._componentRef) this._componentRef.destroy();
196257
this._componentRef = null;
197258
}
198259

199260
ngOnDestroy() {
200261
if (this._deregisterUIView) this._deregisterUIView();
201-
if (this._deregisterHook) this._deregisterHook();
262+
if (this._deregisterUiCanExitHook) this._deregisterUiCanExitHook();
263+
if (this._deregisterUiOnParamsChangedHook) this._deregisterUiOnParamsChangedHook();
264+
this._deregisterUIView = this._deregisterUiCanExitHook = this._deregisterUiOnParamsChangedHook = null;
202265
this._disposeLast();
203266
}
204267

src/interface.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,36 @@ export interface Ng2ViewDeclaration extends _ViewDeclaration {
340340
*/
341341
export interface Ng2Component extends Component {
342342
/**
343-
* This callback is called when the a routed component's state is about to be exited.
343+
* This callback is called when parameter values change
344+
*
345+
* This callback is used to respond dynamic parameter values changing.
346+
* It is called when a transition changed one or more dynamic parameter values,
347+
* and the routed component was not destroyed.
348+
*
349+
* It receives two parameters:
350+
*
351+
* - An object with (only) changed parameter values.
352+
* The keys are the parameter names and the values are the new parameter values.
353+
* - The [[Transition]] which changed the parameter values.
354+
*
355+
* #### Example:
356+
* ```js
357+
* @Component({
358+
* template: '<input type="text">'
359+
* })
360+
* class MyComponent {
361+
* uiOnParamsChanged(newParams: { [paramName: string]: any }, trans: Transition) {
362+
* Object.keys(newParams).forEach(paramName => {
363+
* console.log(`${paramName} changed to ${newParams[paramName]}`)
364+
* });
365+
* }
366+
* }
367+
* ```
368+
*/
369+
uiOnParamsChanged?(newParams: { [paramName: string]: any }, trans?: Transition): void;
370+
371+
/**
372+
* This callback is called when the routed component's state is about to be exited.
344373
*
345374
* The callback can be used to cancel or alter the new Transition that would otherwise exit the component's state.
346375
*
@@ -379,5 +408,5 @@ export interface Ng2Component extends Component {
379408
*
380409
* @return a hook result which may cancel or alter the pending Transition (see [[HookResult]])
381410
*/
382-
uiCanExit(newTransition?: Transition): HookResult;
411+
uiCanExit?(newTransition?: Transition): HookResult;
383412
}

test/uiView/uiOnParamsChanged.spec.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { Component } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { Ng2StateDeclaration, UIRouterModule, UIView } from '../../src';
4+
import { memoryLocationPlugin, Transition, UIRouter } from '@uirouter/core';
5+
import { APP_BASE_HREF } from '@angular/common';
6+
7+
describe('uiView', () => {
8+
describe('should map resolve data to inputs', () => {
9+
type Params = { [paramName: string]: any };
10+
11+
let paramChanges: Params[];
12+
let lifecycle: string[];
13+
14+
let fixture: ComponentFixture<UIView>;
15+
let router: UIRouter;
16+
17+
let id = 0;
18+
19+
@Component({ template: `<h3>hey</h3> ` })
20+
class ParamWatcherComponent {
21+
$id = id++;
22+
23+
public uiOnParamsChanged(newParams: { [paramName: string]: any }, trans: Transition) {
24+
paramChanges.push(newParams);
25+
}
26+
27+
public ngOnInit() {
28+
lifecycle.push(`ngOnInit-${this.$id}`);
29+
}
30+
31+
public ngOnDestroy() {
32+
lifecycle.push(`ngOnDestroy-${this.$id}`);
33+
}
34+
}
35+
36+
beforeEach(done => {
37+
id = 0;
38+
paramChanges = [];
39+
lifecycle = [];
40+
41+
const hasDynamicParams: Ng2StateDeclaration = {
42+
name: 'hasDynamicParams',
43+
component: ParamWatcherComponent,
44+
url: '/url/:param1/:param2?param3',
45+
params: {
46+
param2: { dynamic: true },
47+
param3: { dynamic: true },
48+
},
49+
};
50+
51+
const config = (router: UIRouter) => router.plugin(memoryLocationPlugin);
52+
const routerModule = UIRouterModule.forRoot({ config, states: [hasDynamicParams] });
53+
54+
fixture = TestBed.configureTestingModule({
55+
declarations: [ParamWatcherComponent],
56+
imports: [routerModule],
57+
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
58+
}).createComponent(UIView);
59+
fixture.detectChanges();
60+
61+
router = fixture.debugElement.injector.get(UIRouter);
62+
63+
done();
64+
});
65+
66+
afterEach(() => {
67+
router.urlService.url('');
68+
router.dispose();
69+
fixture.destroy();
70+
});
71+
72+
////////////////////////////////////////////////////////////////////////////////
73+
74+
it('should not receive the initial parameter values', async done => {
75+
expect(lifecycle).toEqual([]);
76+
77+
await router.stateService.go('hasDynamicParams', { param1: 'foo1', param2: 'bar1' });
78+
fixture.detectChanges();
79+
80+
expect(lifecycle).toEqual(['ngOnInit-0']);
81+
expect(paramChanges).toEqual([]);
82+
83+
done();
84+
});
85+
86+
it('should receive dynamic parameter changes', async done => {
87+
expect(lifecycle).toEqual([]);
88+
89+
await router.stateService.go('hasDynamicParams', { param1: 'foo3', param2: 'bar3' });
90+
fixture.detectChanges();
91+
92+
expect(lifecycle).toEqual(['ngOnInit-0']);
93+
expect(paramChanges).toEqual([]);
94+
95+
done();
96+
});
97+
98+
it('should receive dynamic parameter changes', async done => {
99+
expect(lifecycle).toEqual([]);
100+
101+
await router.stateService.go('hasDynamicParams', { param1: 'foo', param2: 'bar' });
102+
fixture.detectChanges();
103+
104+
expect(lifecycle).toEqual(['ngOnInit-0']);
105+
106+
await router.stateService.go('.', { param2: 'notbar' });
107+
fixture.detectChanges();
108+
109+
expect(lifecycle).toEqual(['ngOnInit-0']);
110+
expect(paramChanges).toEqual([{ param2: 'notbar' }]);
111+
112+
done();
113+
});
114+
115+
it('should receive multiple parameter changes', async done => {
116+
expect(lifecycle).toEqual([]);
117+
118+
await router.stateService.go('hasDynamicParams', { param1: 'foo', param2: 'bar' });
119+
fixture.detectChanges();
120+
121+
expect(lifecycle).toEqual(['ngOnInit-0']);
122+
await router.stateService.go('.', { param2: 'notbar', param3: 'baz' });
123+
fixture.detectChanges();
124+
125+
expect(lifecycle).toEqual(['ngOnInit-0']);
126+
expect(paramChanges).toEqual([{ param2: 'notbar', param3: 'baz' }]);
127+
128+
done();
129+
});
130+
131+
it('should not receive non-dynamic parameter changes', async done => {
132+
expect(lifecycle).toEqual([]);
133+
134+
await router.stateService.go('hasDynamicParams', { param1: 'foo', param2: 'bar' });
135+
fixture.detectChanges();
136+
137+
expect(lifecycle).toEqual(['ngOnInit-0']);
138+
139+
await router.stateService.go('hasDynamicParams', { param1: 'notfoo' });
140+
fixture.detectChanges();
141+
142+
expect(lifecycle).toEqual(['ngOnInit-0', 'ngOnDestroy-0', 'ngOnInit-1']);
143+
expect(paramChanges).toEqual([]);
144+
145+
done();
146+
});
147+
});
148+
});

0 commit comments

Comments
 (0)