Skip to content

Commit b7754c6

Browse files
refactor(StateBuilder): convert StateBuilder to ES6 class
1 parent 8b83128 commit b7754c6

File tree

6 files changed

+178
-119
lines changed

6 files changed

+178
-119
lines changed

src/params/stateParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @module params */ /** for typedoc */
12
import {IServiceProviderFactory} from "angular";
23
import {forEach, ancestors, extend, copy} from "../common/common";
34

src/state/interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,11 @@ export interface StateDeclaration extends ViewDeclaration {
382382
onEnter?: Function;
383383
onRetain?: Function;
384384
onExit?: Function;
385+
386+
/**
387+
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
388+
*/
389+
reloadOnSearch: boolean;
385390
}
386391

387392
export interface StateParams {

src/state/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {StateService, StateDeclaration, StateOrName, HrefOptions, ViewDeclaratio
1010
import {ITransitionService, TransitionOptions, TreeChanges} from "../transition/interface";
1111
import {RawParams, ParamsOrArray} from "../params/interface";
1212

13-
import {Glob, StateQueueManager, StateBuilder, StateMatcher, State, TargetState} from "./module";
13+
import {Glob, StateQueueManager, StateBuilder, StateMatcher, State, TargetState, BuilderFunction} from "./module";
1414
import {Transition, RejectFactory, defaultTransOpts} from "../transition/module";
1515
import {PathFactory, Node} from "../path/module";
1616
import {TransitionManager} from "./hooks/transitionManager";
@@ -144,7 +144,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactoryProvider) {
144144
* @return {object} $stateProvider - $stateProvider instance
145145
*/
146146
this.decorator = decorator;
147-
function decorator(name: string, func: Function) {
147+
function decorator(name: string, func: BuilderFunction) {
148148
/*jshint validthis: true */
149149
return builder.builder(name, func) || this;
150150
}

src/state/stateBuilder.ts

Lines changed: 168 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/** @module state */ /** for typedoc */
22
import {map, noop, extend, pick, omit, values, applyPairs, prop, isArray, isDefined, isFunction, isString, forEach} from "../common/common";
3+
import {StateDeclaration} from "./interface";
4+
5+
import {State, StateMatcher} from "./module";
36
import {Param} from "../params/module";
47

58
const parseUrl = (url: string): any => {
@@ -8,124 +11,172 @@ const parseUrl = (url: string): any => {
811
return { val: root ? url.substring(1) : url, root };
912
};
1013

11-
// Builds state properties from definition passed to StateQueueManager.register()
12-
export function StateBuilder(root, matcher, $urlMatcherFactoryProvider) {
13-
14-
let self = this, builders = {
15-
16-
parent: [function(state) {
17-
if (state === root()) return null;
18-
return matcher.find(self.parentName(state)) || root();
19-
}],
20-
21-
data: [function(state) {
22-
if (state.parent && state.parent.data) {
23-
state.data = state.self.data = extend({}, state.parent.data, state.data);
24-
}
25-
return state.data;
26-
}],
27-
28-
// Build a URLMatcher if necessary, either via a relative or absolute URL
29-
url: [function(state) {
30-
const parsed = parseUrl(state.url), parent = state.parent;
31-
const url = !parsed ? state.url : $urlMatcherFactoryProvider.compile(parsed.val, {
32-
params: state.params || {},
33-
paramMap: function(paramConfig, isSearch) {
34-
if (state.reloadOnSearch === false && isSearch) paramConfig = extend(paramConfig || {}, { dynamic: true });
35-
return paramConfig;
14+
export type BuilderFunction = (state: State, parent?) => any;
15+
16+
interface Builders {
17+
[key: string]: BuilderFunction[];
18+
19+
parent: BuilderFunction[];
20+
data: BuilderFunction[];
21+
url: BuilderFunction[];
22+
navigable: BuilderFunction[];
23+
params: BuilderFunction[];
24+
views: BuilderFunction[];
25+
path: BuilderFunction[];
26+
includes: BuilderFunction[];
27+
}
28+
29+
/**
30+
* @internalapi A internal global service
31+
*
32+
* StateBuilder is a factory for the internal [[State]] objects.
33+
*
34+
* When you register a state with the [[StateRegistry]], you register a plain old javascript object which
35+
* conforms to the [[StateDeclaration]] interface. This factory takes that object and builds the corresponding
36+
* [[State]] object, which has an API and is used internally.
37+
*
38+
* Custom properties or API may be added to the internal [[State]] object by registering a decorator function
39+
* using the [[builder]] method.
40+
*/
41+
export class StateBuilder {
42+
/** An object that contains all the BuilderFunctions registered, key'd by the name of the State property they build */
43+
private builders: Builders;
44+
45+
constructor(root: () => State, private matcher: StateMatcher, $urlMatcherFactoryProvider) {
46+
let self = this;
47+
48+
this.builders = {
49+
parent: [function (state: State) {
50+
if (state === root()) return null;
51+
return matcher.find(self.parentName(state)) || root();
52+
}],
53+
54+
data: [function (state: State) {
55+
if (state.parent && state.parent.data) {
56+
state.data = state.self.data = extend({}, state.parent.data, state.data);
3657
}
37-
});
38-
39-
if (!url) return null;
40-
if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${state}'`);
41-
return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root()).url.append(url);
42-
}],
43-
44-
// Keep track of the closest ancestor state that has a URL (i.e. is navigable)
45-
navigable: [function(state) {
46-
return (state !== root()) && state.url ? state : (state.parent ? state.parent.navigable : null);
47-
}],
48-
49-
params: [function(state): { [key: string]: Param } {
50-
const makeConfigParam = (config: any, id: string) => Param.fromConfig(id, null, config);
51-
let urlParams: Param[] = (state.url && state.url.parameters({ inherit: false })) || [];
52-
let nonUrlParams: Param[] = values(map(omit(state.params || {}, urlParams.map(prop('id'))), makeConfigParam));
53-
return urlParams.concat(nonUrlParams).map(p => [p.id, p]).reduce(applyPairs, {});
54-
}],
55-
56-
// If there is no explicit multi-view configuration, make one up so we don't have
57-
// to handle both cases in the view directive later. Note that having an explicit
58-
// 'views' property will mean the default unnamed view properties are ignored. This
59-
// is also a good time to resolve view names to absolute names, so everything is a
60-
// straight lookup at link time.
61-
views: [function(state) {
62-
let views = {},
63-
tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'],
64-
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs'];
65-
let allKeys = tplKeys.concat(ctrlKeys);
66-
67-
forEach(state.views || { "$default": pick(state, allKeys) }, function (config, name) {
68-
name = name || "$default"; // Account for views: { "": { template... } }
69-
// Allow controller settings to be defined at the state level for all views
70-
forEach(ctrlKeys, (key) => {
71-
if (state[key] && !config[key]) config[key] = state[key];
58+
return state.data;
59+
}],
60+
61+
// Build a URLMatcher if necessary, either via a relative or absolute URL
62+
url: [function (state: State) {
63+
let stateDec: StateDeclaration = <any> state;
64+
const parsed = parseUrl(stateDec.url), parent = state.parent;
65+
const url = !parsed ? stateDec.url : $urlMatcherFactoryProvider.compile(parsed.val, {
66+
params: state.params || {},
67+
paramMap: function (paramConfig, isSearch) {
68+
if (stateDec.reloadOnSearch === false && isSearch) paramConfig = extend(paramConfig || {}, {dynamic: true});
69+
return paramConfig;
70+
}
71+
});
72+
73+
if (!url) return null;
74+
if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${state}'`);
75+
return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root()).url.append(url);
76+
}],
77+
78+
// Keep track of the closest ancestor state that has a URL (i.e. is navigable)
79+
navigable: [function (state: State) {
80+
return (state !== root()) && state.url ? state : (state.parent ? state.parent.navigable : null);
81+
}],
82+
83+
params: [function (state: State): { [key: string]: Param } {
84+
const makeConfigParam = (config:any, id:string) => Param.fromConfig(id, null, config);
85+
let urlParams:Param[] = (state.url && state.url.parameters({inherit: false})) || [];
86+
let nonUrlParams:Param[] = values(map(omit(state.params || {}, urlParams.map(prop('id'))), makeConfigParam));
87+
return urlParams.concat(nonUrlParams).map(p => [p.id, p]).reduce(applyPairs, {});
88+
}],
89+
90+
// If there is no explicit multi-view configuration, make one up so we don't have
91+
// to handle both cases in the view directive later. Note that having an explicit
92+
// 'views' property will mean the default unnamed view properties are ignored. This
93+
// is also a good time to resolve view names to absolute names, so everything is a
94+
// straight lookup at link time.
95+
views: [function (state: State) {
96+
let views = {},
97+
tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'],
98+
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs'];
99+
let allKeys = tplKeys.concat(ctrlKeys);
100+
101+
forEach(state.views || {"$default": pick(state, allKeys)}, function (config, name) {
102+
name = name || "$default"; // Account for views: { "": { template... } }
103+
// Allow controller settings to be defined at the state level for all views
104+
forEach(ctrlKeys, (key) => {
105+
if (state[key] && !config[key]) config[key] = state[key];
106+
});
107+
if (Object.keys(config).length > 0) views[name] = config;
72108
});
73-
if (Object.keys(config).length > 0) views[name] = config;
74-
});
75-
return views;
76-
}],
77-
78-
// Keep a full path from the root down to this state as this is needed for state activation.
79-
path: [function(state) {
80-
return state.parent ? state.parent.path.concat(state) : /*root*/ [state];
81-
}],
82-
83-
// Speed up $state.includes() as it's used a lot
84-
includes: [function(state) {
85-
let includes = state.parent ? extend({}, state.parent.includes) : {};
86-
includes[state.name] = true;
87-
return includes;
88-
}]
89-
};
90-
91-
extend(this, {
92-
builder: function(name, fn) {
93-
let array: Function[] = builders[name] || [];
94-
// Backwards compat: if only one builder exists, return it, else return whole arary.
95-
if (isString(name) && !isDefined(fn)) return array.length > 1 ? array : array[0];
96-
if (!isString(name) || !isFunction(fn)) return;
97-
98-
builders[name] = array;
99-
builders[name].push(fn);
100-
return () => builders[name].splice(builders[name].indexOf(fn, 1))
101-
},
102-
103-
build: function(state) {
104-
let parent = this.parentName(state);
105-
if (parent && !matcher.find(parent)) return null;
106-
107-
for (let key in builders) {
108-
if (!builders.hasOwnProperty(key)) continue;
109-
let steps = isArray(builders[key]) ? builders[key] : [builders[key]];
110-
let chain = steps.reduce((parentFn, step) => (state) => step(state, parentFn), noop);
111-
state[key] = chain(state);
112-
}
113-
return state;
114-
},
115-
116-
parentName: function(state) {
117-
let name = state.name || "";
118-
if (name.indexOf('.') !== -1) return name.substring(0, name.lastIndexOf('.'));
119-
if (!state.parent) return "";
120-
return isString(state.parent) ? state.parent : state.parent.name;
121-
},
122-
123-
name: function(state) {
124-
let name = state.name;
125-
if (name.indexOf('.') !== -1 || !state.parent) return name;
126-
127-
let parentName = isString(state.parent) ? state.parent : state.parent.name;
128-
return parentName ? parentName + "." + name : name;
109+
return views;
110+
}],
111+
112+
// Keep a full path from the root down to this state as this is needed for state activation.
113+
path: [function (state: State) {
114+
return state.parent ? state.parent.path.concat(state) : /*root*/ [state];
115+
}],
116+
117+
// Speed up $state.includes() as it's used a lot
118+
includes: [function (state: State) {
119+
let includes = state.parent ? extend({}, state.parent.includes) : {};
120+
includes[state.name] = true;
121+
return includes;
122+
}]
123+
};
124+
}
125+
126+
/**
127+
* Registers a [[BuilderFunction]] for a specific [[State]] property (e.g., `parent`, `url`, or `path`).
128+
* More than one BuilderFunction can be registered for a given property.
129+
*
130+
* The BuilderFunction(s) will be used to define the property on any subsequently built [[State]] objects.
131+
*
132+
* @param name The name of the State property being registered for.
133+
* @param fn The BuilderFunction which will be used to build the State property
134+
* @returns a function which deregisters the BuilderFunction
135+
*/
136+
builder(name: string, fn: BuilderFunction) {
137+
let builders = this.builders;
138+
let array = builders[name] || [];
139+
// Backwards compat: if only one builder exists, return it, else return whole arary.
140+
if (isString(name) && !isDefined(fn)) return array.length > 1 ? array : array[0];
141+
if (!isString(name) || !isFunction(fn)) return;
142+
143+
builders[name] = array;
144+
builders[name].push(fn);
145+
return () => builders[name].splice(builders[name].indexOf(fn, 1)) && null;
146+
}
147+
148+
/**
149+
* Builds all of the properties on an essentially blank State object, returning a State object which has all its
150+
* properties and API built.
151+
*
152+
* @param state an uninitialized State object
153+
* @returns the built State object
154+
*/
155+
build(state: State): State {
156+
let {matcher, builders} = this;
157+
let parent = this.parentName(state);
158+
if (parent && !matcher.find(parent)) return null;
159+
160+
for (let key in builders) {
161+
if (!builders.hasOwnProperty(key)) continue;
162+
let chain = builders[key].reduce((parentFn, step: BuilderFunction) => (state) => step(state, parentFn), noop);
163+
state[key] = chain(state);
129164
}
130-
});
165+
return state;
166+
}
167+
168+
parentName(state) {
169+
let name = state.name || "";
170+
if (name.indexOf('.') !== -1) return name.substring(0, name.lastIndexOf('.'));
171+
if (!state.parent) return "";
172+
return isString(state.parent) ? state.parent : state.parent.name;
173+
}
174+
175+
name(state) {
176+
let name = state.name;
177+
if (name.indexOf('.') !== -1 || !state.parent) return name;
178+
179+
let parentName = isString(state.parent) ? state.parent : state.parent.name;
180+
return parentName ? parentName + "." + name : name;
181+
}
131182
}

src/state/stateObject.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @module state */ /** for typedoc */
12

23
import {StateDeclaration, ViewDeclaration} from "./interface";
34
import {extend, defaults, values, find, propEq} from "../common/module";

src/state/stateRegistry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @module state */ /** for typedoc */
12
export class StateRegistry {
23
constructor() {
34

0 commit comments

Comments
 (0)