1
1
/** @module state */ /** for typedoc */
2
2
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" ;
3
6
import { Param } from "../params/module" ;
4
7
5
8
const parseUrl = ( url : string ) : any => {
@@ -8,124 +11,172 @@ const parseUrl = (url: string): any => {
8
11
return { val : root ? url . substring ( 1 ) : url , root } ;
9
12
} ;
10
13
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 ) ;
36
57
}
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 ;
72
108
} ) ;
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 ) ;
129
164
}
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
+ }
131
182
}
0 commit comments