UNPKG

15.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.ViewService = void 0;
4var common_1 = require("../common/common");
5var hof_1 = require("../common/hof");
6var predicates_1 = require("../common/predicates");
7var trace_1 = require("../common/trace");
8/**
9 * The View service
10 *
11 * This service pairs existing `ui-view` components (which live in the DOM)
12 * with view configs (from the state declaration objects: [[StateDeclaration.views]]).
13 *
14 * - After a successful Transition, the views from the newly entered states are activated via [[activateViewConfig]].
15 * The views from exited states are deactivated via [[deactivateViewConfig]].
16 * (See: the [[registerActivateViews]] Transition Hook)
17 *
18 * - As `ui-view` components pop in and out of existence, they register themselves using [[registerUIView]].
19 *
20 * - When the [[sync]] function is called, the registered `ui-view`(s) ([[ActiveUIView]])
21 * are configured with the matching [[ViewConfig]](s)
22 *
23 */
24var ViewService = /** @class */ (function () {
25 /** @internal */
26 function ViewService(/** @internal */ router) {
27 var _this = this;
28 this.router = router;
29 /** @internal */ this._uiViews = [];
30 /** @internal */ this._viewConfigs = [];
31 /** @internal */ this._viewConfigFactories = {};
32 /** @internal */ this._listeners = [];
33 /** @internal */
34 this._pluginapi = {
35 _rootViewContext: this._rootViewContext.bind(this),
36 _viewConfigFactory: this._viewConfigFactory.bind(this),
37 _registeredUIView: function (id) { return common_1.find(_this._uiViews, function (view) { return _this.router.$id + "." + view.id === id; }); },
38 _registeredUIViews: function () { return _this._uiViews; },
39 _activeViewConfigs: function () { return _this._viewConfigs; },
40 _onSync: function (listener) {
41 _this._listeners.push(listener);
42 return function () { return common_1.removeFrom(_this._listeners, listener); };
43 },
44 };
45 }
46 /**
47 * Normalizes a view's name from a state.views configuration block.
48 *
49 * This should be used by a framework implementation to calculate the values for
50 * [[_ViewDeclaration.$uiViewName]] and [[_ViewDeclaration.$uiViewContextAnchor]].
51 *
52 * @param context the context object (state declaration) that the view belongs to
53 * @param rawViewName the name of the view, as declared in the [[StateDeclaration.views]]
54 *
55 * @returns the normalized uiViewName and uiViewContextAnchor that the view targets
56 */
57 ViewService.normalizeUIViewTarget = function (context, rawViewName) {
58 if (rawViewName === void 0) { rawViewName = ''; }
59 // TODO: Validate incoming view name with a regexp to allow:
60 // ex: "view.name@foo.bar" , "^.^.view.name" , "view.name@^.^" , "" ,
61 // "@" , "$default@^" , "!$default.$default" , "!foo.bar"
62 var viewAtContext = rawViewName.split('@');
63 var uiViewName = viewAtContext[0] || '$default'; // default to unnamed view
64 var uiViewContextAnchor = predicates_1.isString(viewAtContext[1]) ? viewAtContext[1] : '^'; // default to parent context
65 // Handle relative view-name sugar syntax.
66 // Matches rawViewName "^.^.^.foo.bar" into array: ["^.^.^.foo.bar", "^.^.^", "foo.bar"],
67 var relativeViewNameSugar = /^(\^(?:\.\^)*)\.(.*$)/.exec(uiViewName);
68 if (relativeViewNameSugar) {
69 // Clobbers existing contextAnchor (rawViewName validation will fix this)
70 uiViewContextAnchor = relativeViewNameSugar[1]; // set anchor to "^.^.^"
71 uiViewName = relativeViewNameSugar[2]; // set view-name to "foo.bar"
72 }
73 if (uiViewName.charAt(0) === '!') {
74 uiViewName = uiViewName.substr(1);
75 uiViewContextAnchor = ''; // target absolutely from root
76 }
77 // handle parent relative targeting "^.^.^"
78 var relativeMatch = /^(\^(?:\.\^)*)$/;
79 if (relativeMatch.exec(uiViewContextAnchor)) {
80 var anchorState = uiViewContextAnchor.split('.').reduce(function (anchor, x) { return anchor.parent; }, context);
81 uiViewContextAnchor = anchorState.name;
82 }
83 else if (uiViewContextAnchor === '.') {
84 uiViewContextAnchor = context.name;
85 }
86 return { uiViewName: uiViewName, uiViewContextAnchor: uiViewContextAnchor };
87 };
88 /** @internal */
89 ViewService.prototype._rootViewContext = function (context) {
90 return (this._rootContext = context || this._rootContext);
91 };
92 /** @internal */
93 ViewService.prototype._viewConfigFactory = function (viewType, factory) {
94 this._viewConfigFactories[viewType] = factory;
95 };
96 ViewService.prototype.createViewConfig = function (path, decl) {
97 var cfgFactory = this._viewConfigFactories[decl.$type];
98 if (!cfgFactory)
99 throw new Error('ViewService: No view config factory registered for type ' + decl.$type);
100 var cfgs = cfgFactory(path, decl);
101 return predicates_1.isArray(cfgs) ? cfgs : [cfgs];
102 };
103 /**
104 * Deactivates a ViewConfig.
105 *
106 * This function deactivates a `ViewConfig`.
107 * After calling [[sync]], it will un-pair from any `ui-view` with which it is currently paired.
108 *
109 * @param viewConfig The ViewConfig view to deregister.
110 */
111 ViewService.prototype.deactivateViewConfig = function (viewConfig) {
112 trace_1.trace.traceViewServiceEvent('<- Removing', viewConfig);
113 common_1.removeFrom(this._viewConfigs, viewConfig);
114 };
115 ViewService.prototype.activateViewConfig = function (viewConfig) {
116 trace_1.trace.traceViewServiceEvent('-> Registering', viewConfig);
117 this._viewConfigs.push(viewConfig);
118 };
119 ViewService.prototype.sync = function () {
120 var _this = this;
121 var uiViewsByFqn = this._uiViews.map(function (uiv) { return [uiv.fqn, uiv]; }).reduce(common_1.applyPairs, {});
122 // Return a weighted depth value for a uiView.
123 // The depth is the nesting depth of ui-views (based on FQN; times 10,000)
124 // plus the depth of the state that is populating the uiView
125 function uiViewDepth(uiView) {
126 var stateDepth = function (context) { return (context && context.parent ? stateDepth(context.parent) + 1 : 1); };
127 return uiView.fqn.split('.').length * 10000 + stateDepth(uiView.creationContext);
128 }
129 // Return the ViewConfig's context's depth in the context tree.
130 function viewConfigDepth(config) {
131 var context = config.viewDecl.$context, count = 0;
132 while (++count && context.parent)
133 context = context.parent;
134 return count;
135 }
136 // Given a depth function, returns a compare function which can return either ascending or descending order
137 var depthCompare = hof_1.curry(function (depthFn, posNeg, left, right) { return posNeg * (depthFn(left) - depthFn(right)); });
138 var matchingConfigPair = function (uiView) {
139 var matchingConfigs = _this._viewConfigs.filter(ViewService.matches(uiViewsByFqn, uiView));
140 if (matchingConfigs.length > 1) {
141 // This is OK. Child states can target a ui-view that the parent state also targets (the child wins)
142 // Sort by depth and return the match from the deepest child
143 // console.log(`Multiple matching view configs for ${uiView.fqn}`, matchingConfigs);
144 matchingConfigs.sort(depthCompare(viewConfigDepth, -1)); // descending
145 }
146 return { uiView: uiView, viewConfig: matchingConfigs[0] };
147 };
148 var configureUIView = function (tuple) {
149 // If a parent ui-view is reconfigured, it could destroy child ui-views.
150 // Before configuring a child ui-view, make sure it's still in the active uiViews array.
151 if (_this._uiViews.indexOf(tuple.uiView) !== -1)
152 tuple.uiView.configUpdated(tuple.viewConfig);
153 };
154 // Sort views by FQN and state depth. Process uiviews nearest the root first.
155 var uiViewTuples = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair);
156 var matchedViewConfigs = uiViewTuples.map(function (tuple) { return tuple.viewConfig; });
157 var unmatchedConfigTuples = this._viewConfigs
158 .filter(function (config) { return !common_1.inArray(matchedViewConfigs, config); })
159 .map(function (viewConfig) { return ({ uiView: undefined, viewConfig: viewConfig }); });
160 uiViewTuples.forEach(configureUIView);
161 var allTuples = uiViewTuples.concat(unmatchedConfigTuples);
162 this._listeners.forEach(function (cb) { return cb(allTuples); });
163 trace_1.trace.traceViewSync(allTuples);
164 };
165 /**
166 * Registers a `ui-view` component
167 *
168 * When a `ui-view` component is created, it uses this method to register itself.
169 * After registration the [[sync]] method is used to ensure all `ui-view` are configured with the proper [[ViewConfig]].
170 *
171 * Note: the `ui-view` component uses the `ViewConfig` to determine what view should be loaded inside the `ui-view`,
172 * and what the view's state context is.
173 *
174 * Note: There is no corresponding `deregisterUIView`.
175 * A `ui-view` should hang on to the return value of `registerUIView` and invoke it to deregister itself.
176 *
177 * @param uiView The metadata for a UIView
178 * @return a de-registration function used when the view is destroyed.
179 */
180 ViewService.prototype.registerUIView = function (uiView) {
181 trace_1.trace.traceViewServiceUIViewEvent('-> Registering', uiView);
182 var uiViews = this._uiViews;
183 var fqnAndTypeMatches = function (uiv) { return uiv.fqn === uiView.fqn && uiv.$type === uiView.$type; };
184 if (uiViews.filter(fqnAndTypeMatches).length)
185 trace_1.trace.traceViewServiceUIViewEvent('!!!! duplicate uiView named:', uiView);
186 uiViews.push(uiView);
187 this.sync();
188 return function () {
189 var idx = uiViews.indexOf(uiView);
190 if (idx === -1) {
191 trace_1.trace.traceViewServiceUIViewEvent('Tried removing non-registered uiView', uiView);
192 return;
193 }
194 trace_1.trace.traceViewServiceUIViewEvent('<- Deregistering', uiView);
195 common_1.removeFrom(uiViews)(uiView);
196 };
197 };
198 /**
199 * Returns the list of views currently available on the page, by fully-qualified name.
200 *
201 * @return {Array} Returns an array of fully-qualified view names.
202 */
203 ViewService.prototype.available = function () {
204 return this._uiViews.map(hof_1.prop('fqn'));
205 };
206 /**
207 * Returns the list of views on the page containing loaded content.
208 *
209 * @return {Array} Returns an array of fully-qualified view names.
210 */
211 ViewService.prototype.active = function () {
212 return this._uiViews.filter(hof_1.prop('$config')).map(hof_1.prop('name'));
213 };
214 /**
215 * Given a ui-view and a ViewConfig, determines if they "match".
216 *
217 * A ui-view has a fully qualified name (fqn) and a context object. The fqn is built from its overall location in
218 * the DOM, describing its nesting relationship to any parent ui-view tags it is nested inside of.
219 *
220 * A ViewConfig has a target ui-view name and a context anchor. The ui-view name can be a simple name, or
221 * can be a segmented ui-view path, describing a portion of a ui-view fqn.
222 *
223 * In order for a ui-view to match ViewConfig, ui-view's $type must match the ViewConfig's $type
224 *
225 * If the ViewConfig's target ui-view name is a simple name (no dots), then a ui-view matches if:
226 * - the ui-view's name matches the ViewConfig's target name
227 * - the ui-view's context matches the ViewConfig's anchor
228 *
229 * If the ViewConfig's target ui-view name is a segmented name (with dots), then a ui-view matches if:
230 * - There exists a parent ui-view where:
231 * - the parent ui-view's name matches the first segment (index 0) of the ViewConfig's target name
232 * - the parent ui-view's context matches the ViewConfig's anchor
233 * - And the remaining segments (index 1..n) of the ViewConfig's target name match the tail of the ui-view's fqn
234 *
235 * Example:
236 *
237 * DOM:
238 * <ui-view> <!-- created in the root context (name: "") -->
239 * <ui-view name="foo"> <!-- created in the context named: "A" -->
240 * <ui-view> <!-- created in the context named: "A.B" -->
241 * <ui-view name="bar"> <!-- created in the context named: "A.B.C" -->
242 * </ui-view>
243 * </ui-view>
244 * </ui-view>
245 * </ui-view>
246 *
247 * uiViews: [
248 * { fqn: "$default", creationContext: { name: "" } },
249 * { fqn: "$default.foo", creationContext: { name: "A" } },
250 * { fqn: "$default.foo.$default", creationContext: { name: "A.B" } }
251 * { fqn: "$default.foo.$default.bar", creationContext: { name: "A.B.C" } }
252 * ]
253 *
254 * These four view configs all match the ui-view with the fqn: "$default.foo.$default.bar":
255 *
256 * - ViewConfig1: { uiViewName: "bar", uiViewContextAnchor: "A.B.C" }
257 * - ViewConfig2: { uiViewName: "$default.bar", uiViewContextAnchor: "A.B" }
258 * - ViewConfig3: { uiViewName: "foo.$default.bar", uiViewContextAnchor: "A" }
259 * - ViewConfig4: { uiViewName: "$default.foo.$default.bar", uiViewContextAnchor: "" }
260 *
261 * Using ViewConfig3 as an example, it matches the ui-view with fqn "$default.foo.$default.bar" because:
262 * - The ViewConfig's segmented target name is: [ "foo", "$default", "bar" ]
263 * - There exists a parent ui-view (which has fqn: "$default.foo") where:
264 * - the parent ui-view's name "foo" matches the first segment "foo" of the ViewConfig's target name
265 * - the parent ui-view's context "A" matches the ViewConfig's anchor context "A"
266 * - And the remaining segments [ "$default", "bar" ].join("."_ of the ViewConfig's target name match
267 * the tail of the ui-view's fqn "default.bar"
268 *
269 * @internal
270 */
271 ViewService.matches = function (uiViewsByFqn, uiView) { return function (viewConfig) {
272 // Don't supply an ng1 ui-view with an ng2 ViewConfig, etc
273 if (uiView.$type !== viewConfig.viewDecl.$type)
274 return false;
275 // Split names apart from both viewConfig and uiView into segments
276 var vc = viewConfig.viewDecl;
277 var vcSegments = vc.$uiViewName.split('.');
278 var uivSegments = uiView.fqn.split('.');
279 // Check if the tails of the segment arrays match. ex, these arrays' tails match:
280 // vc: ["foo", "bar"], uiv fqn: ["$default", "foo", "bar"]
281 if (!common_1.equals(vcSegments, uivSegments.slice(0 - vcSegments.length)))
282 return false;
283 // Now check if the fqn ending at the first segment of the viewConfig matches the context:
284 // ["$default", "foo"].join(".") == "$default.foo", does the ui-view $default.foo context match?
285 var negOffset = 1 - vcSegments.length || undefined;
286 var fqnToFirstSegment = uivSegments.slice(0, negOffset).join('.');
287 var uiViewContext = uiViewsByFqn[fqnToFirstSegment].creationContext;
288 return vc.$uiViewContextAnchor === (uiViewContext && uiViewContext.name);
289 }; };
290 return ViewService;
291}());
292exports.ViewService = ViewService;
293//# sourceMappingURL=view.js.map
\No newline at end of file