1 | ;
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.ViewService = void 0;
|
4 | var common_1 = require("../common/common");
|
5 | var hof_1 = require("../common/hof");
|
6 | var predicates_1 = require("../common/predicates");
|
7 | var 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 | */
|
24 | var 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 | }());
|
292 | exports.ViewService = ViewService;
|
293 | //# sourceMappingURL=view.js.map |
\ | No newline at end of file |