Skip to content

Commit 9544ae5

Browse files
feat(view): Add onSync callback API to plugin API
Closes angular-ui/ui-router#3573
1 parent 042a950 commit 9544ae5

File tree

3 files changed

+110
-42
lines changed

3 files changed

+110
-42
lines changed

src/common/trace.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,18 @@
3333
* @coreapi
3434
* @module trace
3535
*/ /** for typedoc */
36-
import {parse} from "../common/hof";
37-
import {isFunction, isNumber} from "../common/predicates";
38-
import {Transition} from "../transition/transition";
39-
import {ActiveUIView, ViewConfig, ViewContext} from "../view/interface";
40-
import {stringify, functionToString, maxLength, padString} from "./strings";
41-
import {Resolvable} from "../resolve/resolvable";
42-
import {PathNode} from "../path/pathNode";
43-
import {PolicyWhen} from "../resolve/interface";
44-
import {TransitionHook} from "../transition/transitionHook";
45-
import {HookResult} from "../transition/interface";
46-
import {StateObject} from "../state/stateObject";
36+
import { parse } from "../common/hof";
37+
import { isFunction, isNumber } from "../common/predicates";
38+
import { Transition } from "../transition/transition";
39+
import { ViewTuple } from '../view';
40+
import { ActiveUIView, ViewConfig, ViewContext } from "../view/interface";
41+
import { stringify, functionToString, maxLength, padString } from "./strings";
42+
import { Resolvable } from "../resolve/resolvable";
43+
import { PathNode } from "../path/pathNode";
44+
import { PolicyWhen } from "../resolve/interface";
45+
import { TransitionHook } from "../transition/transitionHook";
46+
import { HookResult } from "../transition/interface";
47+
import { StateObject } from "../state/stateObject";
4748

4849
/** @hidden */
4950
function uiViewString (uiview: ActiveUIView) {
@@ -226,13 +227,13 @@ export class Trace {
226227
}
227228

228229
/** @internalapi called by ui-router code */
229-
traceViewSync(pairs: any[]) {
230+
traceViewSync(pairs: ViewTuple[]) {
230231
if (!this.enabled(Category.VIEWCONFIG)) return;
231-
const mapping = pairs.map(([ uiViewData, config ]) => {
232-
const uiView = `${uiViewData.$type}:${uiViewData.fqn}`;
233-
const view = config && `${config.viewDecl.$context.name}: ${config.viewDecl.$name} (${config.viewDecl.$type})`;
232+
const mapping = pairs.map(({ uiView, viewConfig }) => {
233+
const uiv = uiView && uiView.fqn;
234+
const cfg = viewConfig && `${viewConfig.viewDecl.$context.name}: ${viewConfig.viewDecl.$name}`;
234235

235-
return { 'ui-view fqn': uiView, 'state: view name': view };
236+
return { 'ui-view fqn': uiv, 'state: view name': cfg };
236237
}).sort((a, b) => a['ui-view fqn'].localeCompare(b['ui-view fqn']));
237238

238239
consoletable(mapping);

src/view/view.ts

+40-18
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
* @coreapi
33
* @module view
44
*/ /** for typedoc */
5-
import {equals, applyPairs, removeFrom, TypedMap} from "../common/common";
6-
import {curry, prop} from "../common/hof";
7-
import {isString, isArray} from "../common/predicates";
8-
import {trace} from "../common/trace";
9-
import {PathNode} from "../path/pathNode";
10-
11-
import {ActiveUIView, ViewContext, ViewConfig} from "./interface";
12-
import {_ViewDeclaration} from "../state/interface";
5+
import { equals, applyPairs, removeFrom, TypedMap, inArray } from "../common/common";
6+
import { curry, prop } from "../common/hof";
7+
import { isString, isArray } from "../common/predicates";
8+
import { trace } from "../common/trace";
9+
import { PathNode } from "../path/pathNode";
10+
import { ActiveUIView, ViewContext, ViewConfig } from "./interface";
11+
import { _ViewDeclaration } from "../state/interface";
1312

1413
export type ViewConfigFactory = (path: PathNode[], decl: _ViewDeclaration) => ViewConfig|ViewConfig[];
1514

@@ -18,6 +17,17 @@ export interface ViewServicePluginAPI {
1817
_viewConfigFactory(viewType: string, factory: ViewConfigFactory);
1918
_registeredUIViews(): ActiveUIView[];
2019
_activeViewConfigs(): ViewConfig[];
20+
_onSync(listener: ViewSyncListener): Function;
21+
}
22+
23+
// A uiView and its matching viewConfig
24+
export interface ViewTuple {
25+
uiView: ActiveUIView;
26+
viewConfig: ViewConfig;
27+
}
28+
29+
export interface ViewSyncListener {
30+
(viewTuples: ViewTuple[]): void;
2131
}
2232

2333
/**
@@ -41,6 +51,7 @@ export class ViewService {
4151
private _viewConfigs: ViewConfig[] = [];
4252
private _rootContext: ViewContext;
4353
private _viewConfigFactories: { [key: string]: ViewConfigFactory } = {};
54+
private _listeners: ViewSyncListener[] = [];
4455

4556
constructor() { }
4657

@@ -49,6 +60,10 @@ export class ViewService {
4960
_viewConfigFactory: this._viewConfigFactory.bind(this),
5061
_registeredUIViews: () => this._uiViews,
5162
_activeViewConfigs: () => this._viewConfigs,
63+
_onSync: (listener: ViewSyncListener) => {
64+
this._listeners.push(listener);
65+
return () => removeFrom(this._listeners, listener);
66+
},
5267
};
5368

5469
private _rootViewContext(context?: ViewContext): ViewContext {
@@ -65,7 +80,7 @@ export class ViewService {
6580
let cfgs = cfgFactory(path, decl);
6681
return isArray(cfgs) ? cfgs : [cfgs];
6782
}
68-
83+
6984
/**
7085
* Deactivates a ViewConfig.
7186
*
@@ -186,30 +201,37 @@ export class ViewService {
186201
// Given a depth function, returns a compare function which can return either ascending or descending order
187202
const depthCompare = curry((depthFn, posNeg, left, right) => posNeg * (depthFn(left) - depthFn(right)));
188203

189-
const matchingConfigPair = (uiView: ActiveUIView) => {
204+
const matchingConfigPair = (uiView: ActiveUIView): ViewTuple => {
190205
let matchingConfigs = this._viewConfigs.filter(ViewService.matches(uiViewsByFqn, uiView));
191206
if (matchingConfigs.length > 1) {
192207
// This is OK. Child states can target a ui-view that the parent state also targets (the child wins)
193208
// Sort by depth and return the match from the deepest child
194209
// console.log(`Multiple matching view configs for ${uiView.fqn}`, matchingConfigs);
195210
matchingConfigs.sort(depthCompare(viewConfigDepth, -1)); // descending
196211
}
197-
return [uiView, matchingConfigs[0]];
212+
return { uiView, viewConfig: matchingConfigs[0] };
198213
};
199214

200-
const configureUIView = ([uiView, viewConfig]) => {
215+
const configureUIView = (tuple: ViewTuple) => {
201216
// If a parent ui-view is reconfigured, it could destroy child ui-views.
202217
// Before configuring a child ui-view, make sure it's still in the active uiViews array.
203-
if (this._uiViews.indexOf(uiView) !== -1)
204-
uiView.configUpdated(viewConfig);
218+
if (this._uiViews.indexOf(tuple.uiView) !== -1)
219+
tuple.uiView.configUpdated(tuple.viewConfig);
205220
};
206221

207222
// Sort views by FQN and state depth. Process uiviews nearest the root first.
208-
const pairs = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair);
223+
const uiViewTuples = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair);
224+
const matchedViewConfigs = uiViewTuples.map(tuple => tuple.viewConfig);
225+
const unmatchedConfigTuples = this._viewConfigs
226+
.filter(config => inArray(matchedViewConfigs, config))
227+
.map(viewConfig => ({ uiView: undefined, viewConfig }));
209228

210-
trace.traceViewSync(pairs);
229+
const allTuples: ViewTuple[] = uiViewTuples.concat(unmatchedConfigTuples);
211230

212-
pairs.forEach(configureUIView);
231+
uiViewTuples.forEach(configureUIView);
232+
233+
this._listeners.forEach(cb => cb(allTuples));
234+
trace.traceViewSync(allTuples);
213235
};
214236

215237
/**
@@ -310,4 +332,4 @@ export class ViewService {
310332

311333
return {uiViewName, uiViewContextAnchor};
312334
}
313-
}
335+
}

test/viewServiceSpec.ts

+53-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UIRouter } from "../src/router";
2+
import { ViewSyncListener, ViewTuple } from '../src/view';
23
import { tree2Array } from "./_testUtils";
34
import { StateRegistry } from "../src/state/stateRegistry";
45
import { ViewService } from "../src/view/view";
@@ -13,21 +14,21 @@ let statetree = {
1314
C: {
1415
D: {
1516

16-
}
17-
}
18-
}
19-
}
17+
},
18+
},
19+
},
20+
},
2021
};
2122

2223
let count = 0;
23-
const makeUIView = (): ActiveUIView => ({
24+
const makeUIView = (state?): ActiveUIView => ({
2425
$type: 'test',
2526
id: count++,
2627
name: '$default',
2728
fqn: '$default',
2829
config: null,
29-
creationContext: null,
30-
configUpdated: function() {}
30+
creationContext: state,
31+
configUpdated: function() {},
3132
});
3233

3334
describe("View Service", () => {
@@ -54,4 +55,48 @@ describe("View Service", () => {
5455
expect($view.available().length).toBe(0);
5556
});
5657
});
57-
});
58+
59+
describe('onSync', () => {
60+
it('registers view sync listeners', () => {
61+
function listener(tuples: ViewTuple[]) {}
62+
const listeners: ViewSyncListener[] = ($view as any)._listeners;
63+
expect(listeners).not.toContain(listener);
64+
65+
$view._pluginapi._onSync(listener);
66+
67+
expect(listeners).toContain(listener);
68+
});
69+
70+
it('returns a deregistration function', () => {
71+
function listener(tuples: ViewTuple[]) {}
72+
const listeners: ViewSyncListener[] = ($view as any)._listeners;
73+
const deregister = $view._pluginapi._onSync(listener);
74+
expect(listeners).toContain(listener);
75+
76+
deregister();
77+
expect(listeners).not.toContain(listener);
78+
});
79+
80+
it('calls the listener during sync()', () => {
81+
const listener = jasmine.createSpy('listener');
82+
$view._pluginapi._onSync(listener);
83+
$view.sync();
84+
expect(listener).toHaveBeenCalledWith([]);
85+
});
86+
87+
it('ViewSyncListeners receive tuples for all registered uiviews', () => {
88+
const listener = jasmine.createSpy('listener');
89+
const uiView1 = makeUIView();
90+
const uiView2 = makeUIView();
91+
$view.registerUIView(uiView1);
92+
$view.registerUIView(uiView2);
93+
94+
$view._pluginapi._onSync(listener);
95+
$view.sync();
96+
97+
const tuple1 = { uiView: uiView1, viewConfig: undefined };
98+
const tuple2 = { uiView: uiView2, viewConfig: undefined };
99+
expect(listener).toHaveBeenCalledWith([tuple1, tuple2]);
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)