Skip to content

Commit

Permalink
feat(view): Add onSync callback API to plugin API
Browse files Browse the repository at this point in the history
  • Loading branch information
christopherthielen committed Dec 21, 2017
1 parent 042a950 commit 9544ae5
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 42 deletions.
33 changes: 17 additions & 16 deletions src/common/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@
* @coreapi
* @module trace
*/ /** for typedoc */
import {parse} from "../common/hof";
import {isFunction, isNumber} from "../common/predicates";
import {Transition} from "../transition/transition";
import {ActiveUIView, ViewConfig, ViewContext} from "../view/interface";
import {stringify, functionToString, maxLength, padString} from "./strings";
import {Resolvable} from "../resolve/resolvable";
import {PathNode} from "../path/pathNode";
import {PolicyWhen} from "../resolve/interface";
import {TransitionHook} from "../transition/transitionHook";
import {HookResult} from "../transition/interface";
import {StateObject} from "../state/stateObject";
import { parse } from "../common/hof";
import { isFunction, isNumber } from "../common/predicates";
import { Transition } from "../transition/transition";
import { ViewTuple } from '../view';
import { ActiveUIView, ViewConfig, ViewContext } from "../view/interface";
import { stringify, functionToString, maxLength, padString } from "./strings";
import { Resolvable } from "../resolve/resolvable";
import { PathNode } from "../path/pathNode";
import { PolicyWhen } from "../resolve/interface";
import { TransitionHook } from "../transition/transitionHook";
import { HookResult } from "../transition/interface";
import { StateObject } from "../state/stateObject";

/** @hidden */
function uiViewString (uiview: ActiveUIView) {
Expand Down Expand Up @@ -226,13 +227,13 @@ export class Trace {
}

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

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

consoletable(mapping);
Expand Down
58 changes: 40 additions & 18 deletions src/view/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
* @coreapi
* @module view
*/ /** for typedoc */
import {equals, applyPairs, removeFrom, TypedMap} from "../common/common";
import {curry, prop} from "../common/hof";
import {isString, isArray} from "../common/predicates";
import {trace} from "../common/trace";
import {PathNode} from "../path/pathNode";

import {ActiveUIView, ViewContext, ViewConfig} from "./interface";
import {_ViewDeclaration} from "../state/interface";
import { equals, applyPairs, removeFrom, TypedMap, inArray } from "../common/common";
import { curry, prop } from "../common/hof";
import { isString, isArray } from "../common/predicates";
import { trace } from "../common/trace";
import { PathNode } from "../path/pathNode";
import { ActiveUIView, ViewContext, ViewConfig } from "./interface";
import { _ViewDeclaration } from "../state/interface";

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

Expand All @@ -18,6 +17,17 @@ export interface ViewServicePluginAPI {
_viewConfigFactory(viewType: string, factory: ViewConfigFactory);
_registeredUIViews(): ActiveUIView[];
_activeViewConfigs(): ViewConfig[];
_onSync(listener: ViewSyncListener): Function;
}

// A uiView and its matching viewConfig
export interface ViewTuple {
uiView: ActiveUIView;
viewConfig: ViewConfig;
}

export interface ViewSyncListener {
(viewTuples: ViewTuple[]): void;
}

/**
Expand All @@ -41,6 +51,7 @@ export class ViewService {
private _viewConfigs: ViewConfig[] = [];
private _rootContext: ViewContext;
private _viewConfigFactories: { [key: string]: ViewConfigFactory } = {};
private _listeners: ViewSyncListener[] = [];

constructor() { }

Expand All @@ -49,6 +60,10 @@ export class ViewService {
_viewConfigFactory: this._viewConfigFactory.bind(this),
_registeredUIViews: () => this._uiViews,
_activeViewConfigs: () => this._viewConfigs,
_onSync: (listener: ViewSyncListener) => {
this._listeners.push(listener);
return () => removeFrom(this._listeners, listener);
},
};

private _rootViewContext(context?: ViewContext): ViewContext {
Expand All @@ -65,7 +80,7 @@ export class ViewService {
let cfgs = cfgFactory(path, decl);
return isArray(cfgs) ? cfgs : [cfgs];
}

/**
* Deactivates a ViewConfig.
*
Expand Down Expand Up @@ -186,30 +201,37 @@ export class ViewService {
// Given a depth function, returns a compare function which can return either ascending or descending order
const depthCompare = curry((depthFn, posNeg, left, right) => posNeg * (depthFn(left) - depthFn(right)));

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

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

// Sort views by FQN and state depth. Process uiviews nearest the root first.
const pairs = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair);
const uiViewTuples = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair);
const matchedViewConfigs = uiViewTuples.map(tuple => tuple.viewConfig);
const unmatchedConfigTuples = this._viewConfigs
.filter(config => inArray(matchedViewConfigs, config))

This comment has been minimized.

Copy link
@known-as-bmf

known-as-bmf Dec 22, 2017

First of all, sorry to bother you.

I don't really understand everything that is going on here but the variable naming makes me think there is a mistake.

Shouldn't it be:

.filter(config => !inArray(matchedViewConfigs, config))

?

Thank you for this great library.

This comment has been minimized.

Copy link
@christopherthielen

christopherthielen Dec 23, 2017

Author Member

I think you're right! How did you find this bug?

This comment has been minimized.

Copy link
@christopherthielen

christopherthielen Dec 23, 2017

Author Member

fixed for 5.0.14

This comment has been minimized.

Copy link
@known-as-bmf

known-as-bmf Dec 28, 2017

Sorry for the delay, I was away for christmas.

I was investigating an undefined error that appeared after I upgraded to @uirouter/angularjs 1.0.12 and stumbled upon this !

.map(viewConfig => ({ uiView: undefined, viewConfig }));

trace.traceViewSync(pairs);
const allTuples: ViewTuple[] = uiViewTuples.concat(unmatchedConfigTuples);

pairs.forEach(configureUIView);
uiViewTuples.forEach(configureUIView);

this._listeners.forEach(cb => cb(allTuples));
trace.traceViewSync(allTuples);
};

/**
Expand Down Expand Up @@ -310,4 +332,4 @@ export class ViewService {

return {uiViewName, uiViewContextAnchor};
}
}
}
61 changes: 53 additions & 8 deletions test/viewServiceSpec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UIRouter } from "../src/router";
import { ViewSyncListener, ViewTuple } from '../src/view';
import { tree2Array } from "./_testUtils";
import { StateRegistry } from "../src/state/stateRegistry";
import { ViewService } from "../src/view/view";
Expand All @@ -13,21 +14,21 @@ let statetree = {
C: {
D: {

}
}
}
}
},
},
},
},
};

let count = 0;
const makeUIView = (): ActiveUIView => ({
const makeUIView = (state?): ActiveUIView => ({
$type: 'test',
id: count++,
name: '$default',
fqn: '$default',
config: null,
creationContext: null,
configUpdated: function() {}
creationContext: state,
configUpdated: function() {},
});

describe("View Service", () => {
Expand All @@ -54,4 +55,48 @@ describe("View Service", () => {
expect($view.available().length).toBe(0);
});
});
});

describe('onSync', () => {
it('registers view sync listeners', () => {
function listener(tuples: ViewTuple[]) {}
const listeners: ViewSyncListener[] = ($view as any)._listeners;
expect(listeners).not.toContain(listener);

$view._pluginapi._onSync(listener);

expect(listeners).toContain(listener);
});

it('returns a deregistration function', () => {
function listener(tuples: ViewTuple[]) {}
const listeners: ViewSyncListener[] = ($view as any)._listeners;
const deregister = $view._pluginapi._onSync(listener);
expect(listeners).toContain(listener);

deregister();
expect(listeners).not.toContain(listener);
});

it('calls the listener during sync()', () => {
const listener = jasmine.createSpy('listener');
$view._pluginapi._onSync(listener);
$view.sync();
expect(listener).toHaveBeenCalledWith([]);
});

it('ViewSyncListeners receive tuples for all registered uiviews', () => {
const listener = jasmine.createSpy('listener');
const uiView1 = makeUIView();
const uiView2 = makeUIView();
$view.registerUIView(uiView1);
$view.registerUIView(uiView2);

$view._pluginapi._onSync(listener);
$view.sync();

const tuple1 = { uiView: uiView1, viewConfig: undefined };
const tuple2 = { uiView: uiView2, viewConfig: undefined };
expect(listener).toHaveBeenCalledWith([tuple1, tuple2]);
});
});
});

0 comments on commit 9544ae5

Please sign in to comment.