UNPKG

@uirouter/core

Version:

UI-Router Core: Framework agnostic, State-based routing for JavaScript Single Page Apps

290 lines 15.5 kB
import { equals, applyPairs, removeFrom, inArray, find } from '../common/common'; import { curry, prop } from '../common/hof'; import { isString, isArray } from '../common/predicates'; import { trace } from '../common/trace'; /** * The View service * * This service pairs existing `ui-view` components (which live in the DOM) * with view configs (from the state declaration objects: [[StateDeclaration.views]]). * * - After a successful Transition, the views from the newly entered states are activated via [[activateViewConfig]]. * The views from exited states are deactivated via [[deactivateViewConfig]]. * (See: the [[registerActivateViews]] Transition Hook) * * - As `ui-view` components pop in and out of existence, they register themselves using [[registerUIView]]. * * - When the [[sync]] function is called, the registered `ui-view`(s) ([[ActiveUIView]]) * are configured with the matching [[ViewConfig]](s) * */ var ViewService = /** @class */ (function () { /** @internal */ function ViewService(/** @internal */ router) { var _this = this; this.router = router; /** @internal */ this._uiViews = []; /** @internal */ this._viewConfigs = []; /** @internal */ this._viewConfigFactories = {}; /** @internal */ this._listeners = []; /** @internal */ this._pluginapi = { _rootViewContext: this._rootViewContext.bind(this), _viewConfigFactory: this._viewConfigFactory.bind(this), _registeredUIView: function (id) { return find(_this._uiViews, function (view) { return _this.router.$id + "." + view.id === id; }); }, _registeredUIViews: function () { return _this._uiViews; }, _activeViewConfigs: function () { return _this._viewConfigs; }, _onSync: function (listener) { _this._listeners.push(listener); return function () { return removeFrom(_this._listeners, listener); }; }, }; } /** * Normalizes a view's name from a state.views configuration block. * * This should be used by a framework implementation to calculate the values for * [[_ViewDeclaration.$uiViewName]] and [[_ViewDeclaration.$uiViewContextAnchor]]. * * @param context the context object (state declaration) that the view belongs to * @param rawViewName the name of the view, as declared in the [[StateDeclaration.views]] * * @returns the normalized uiViewName and uiViewContextAnchor that the view targets */ ViewService.normalizeUIViewTarget = function (context, rawViewName) { if (rawViewName === void 0) { rawViewName = ''; } // TODO: Validate incoming view name with a regexp to allow: // ex: "view.name@foo.bar" , "^.^.view.name" , "view.name@^.^" , "" , // "@" , "$default@^" , "!$default.$default" , "!foo.bar" var viewAtContext = rawViewName.split('@'); var uiViewName = viewAtContext[0] || '$default'; // default to unnamed view var uiViewContextAnchor = isString(viewAtContext[1]) ? viewAtContext[1] : '^'; // default to parent context // Handle relative view-name sugar syntax. // Matches rawViewName "^.^.^.foo.bar" into array: ["^.^.^.foo.bar", "^.^.^", "foo.bar"], var relativeViewNameSugar = /^(\^(?:\.\^)*)\.(.*$)/.exec(uiViewName); if (relativeViewNameSugar) { // Clobbers existing contextAnchor (rawViewName validation will fix this) uiViewContextAnchor = relativeViewNameSugar[1]; // set anchor to "^.^.^" uiViewName = relativeViewNameSugar[2]; // set view-name to "foo.bar" } if (uiViewName.charAt(0) === '!') { uiViewName = uiViewName.substr(1); uiViewContextAnchor = ''; // target absolutely from root } // handle parent relative targeting "^.^.^" var relativeMatch = /^(\^(?:\.\^)*)$/; if (relativeMatch.exec(uiViewContextAnchor)) { var anchorState = uiViewContextAnchor.split('.').reduce(function (anchor, x) { return anchor.parent; }, context); uiViewContextAnchor = anchorState.name; } else if (uiViewContextAnchor === '.') { uiViewContextAnchor = context.name; } return { uiViewName: uiViewName, uiViewContextAnchor: uiViewContextAnchor }; }; /** @internal */ ViewService.prototype._rootViewContext = function (context) { return (this._rootContext = context || this._rootContext); }; /** @internal */ ViewService.prototype._viewConfigFactory = function (viewType, factory) { this._viewConfigFactories[viewType] = factory; }; ViewService.prototype.createViewConfig = function (path, decl) { var cfgFactory = this._viewConfigFactories[decl.$type]; if (!cfgFactory) throw new Error('ViewService: No view config factory registered for type ' + decl.$type); var cfgs = cfgFactory(path, decl); return isArray(cfgs) ? cfgs : [cfgs]; }; /** * Deactivates a ViewConfig. * * This function deactivates a `ViewConfig`. * After calling [[sync]], it will un-pair from any `ui-view` with which it is currently paired. * * @param viewConfig The ViewConfig view to deregister. */ ViewService.prototype.deactivateViewConfig = function (viewConfig) { trace.traceViewServiceEvent('<- Removing', viewConfig); removeFrom(this._viewConfigs, viewConfig); }; ViewService.prototype.activateViewConfig = function (viewConfig) { trace.traceViewServiceEvent('-> Registering', viewConfig); this._viewConfigs.push(viewConfig); }; ViewService.prototype.sync = function () { var _this = this; var uiViewsByFqn = this._uiViews.map(function (uiv) { return [uiv.fqn, uiv]; }).reduce(applyPairs, {}); // Return a weighted depth value for a uiView. // The depth is the nesting depth of ui-views (based on FQN; times 10,000) // plus the depth of the state that is populating the uiView function uiViewDepth(uiView) { var stateDepth = function (context) { return (context && context.parent ? stateDepth(context.parent) + 1 : 1); }; return uiView.fqn.split('.').length * 10000 + stateDepth(uiView.creationContext); } // Return the ViewConfig's context's depth in the context tree. function viewConfigDepth(config) { var context = config.viewDecl.$context, count = 0; while (++count && context.parent) context = context.parent; return count; } // Given a depth function, returns a compare function which can return either ascending or descending order var depthCompare = curry(function (depthFn, posNeg, left, right) { return posNeg * (depthFn(left) - depthFn(right)); }); var matchingConfigPair = function (uiView) { var 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: uiView, viewConfig: matchingConfigs[0] }; }; var configureUIView = function (tuple) { // 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(tuple.uiView) !== -1) tuple.uiView.configUpdated(tuple.viewConfig); }; // Sort views by FQN and state depth. Process uiviews nearest the root first. var uiViewTuples = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair); var matchedViewConfigs = uiViewTuples.map(function (tuple) { return tuple.viewConfig; }); var unmatchedConfigTuples = this._viewConfigs .filter(function (config) { return !inArray(matchedViewConfigs, config); }) .map(function (viewConfig) { return ({ uiView: undefined, viewConfig: viewConfig }); }); uiViewTuples.forEach(configureUIView); var allTuples = uiViewTuples.concat(unmatchedConfigTuples); this._listeners.forEach(function (cb) { return cb(allTuples); }); trace.traceViewSync(allTuples); }; /** * Registers a `ui-view` component * * When a `ui-view` component is created, it uses this method to register itself. * After registration the [[sync]] method is used to ensure all `ui-view` are configured with the proper [[ViewConfig]]. * * Note: the `ui-view` component uses the `ViewConfig` to determine what view should be loaded inside the `ui-view`, * and what the view's state context is. * * Note: There is no corresponding `deregisterUIView`. * A `ui-view` should hang on to the return value of `registerUIView` and invoke it to deregister itself. * * @param uiView The metadata for a UIView * @return a de-registration function used when the view is destroyed. */ ViewService.prototype.registerUIView = function (uiView) { trace.traceViewServiceUIViewEvent('-> Registering', uiView); var uiViews = this._uiViews; var fqnAndTypeMatches = function (uiv) { return uiv.fqn === uiView.fqn && uiv.$type === uiView.$type; }; if (uiViews.filter(fqnAndTypeMatches).length) trace.traceViewServiceUIViewEvent('!!!! duplicate uiView named:', uiView); uiViews.push(uiView); this.sync(); return function () { var idx = uiViews.indexOf(uiView); if (idx === -1) { trace.traceViewServiceUIViewEvent('Tried removing non-registered uiView', uiView); return; } trace.traceViewServiceUIViewEvent('<- Deregistering', uiView); removeFrom(uiViews)(uiView); }; }; /** * Returns the list of views currently available on the page, by fully-qualified name. * * @return {Array} Returns an array of fully-qualified view names. */ ViewService.prototype.available = function () { return this._uiViews.map(prop('fqn')); }; /** * Returns the list of views on the page containing loaded content. * * @return {Array} Returns an array of fully-qualified view names. */ ViewService.prototype.active = function () { return this._uiViews.filter(prop('$config')).map(prop('name')); }; /** * Given a ui-view and a ViewConfig, determines if they "match". * * A ui-view has a fully qualified name (fqn) and a context object. The fqn is built from its overall location in * the DOM, describing its nesting relationship to any parent ui-view tags it is nested inside of. * * A ViewConfig has a target ui-view name and a context anchor. The ui-view name can be a simple name, or * can be a segmented ui-view path, describing a portion of a ui-view fqn. * * In order for a ui-view to match ViewConfig, ui-view's $type must match the ViewConfig's $type * * If the ViewConfig's target ui-view name is a simple name (no dots), then a ui-view matches if: * - the ui-view's name matches the ViewConfig's target name * - the ui-view's context matches the ViewConfig's anchor * * If the ViewConfig's target ui-view name is a segmented name (with dots), then a ui-view matches if: * - There exists a parent ui-view where: * - the parent ui-view's name matches the first segment (index 0) of the ViewConfig's target name * - the parent ui-view's context matches the ViewConfig's anchor * - And the remaining segments (index 1..n) of the ViewConfig's target name match the tail of the ui-view's fqn * * Example: * * DOM: * <ui-view> <!-- created in the root context (name: "") --> * <ui-view name="foo"> <!-- created in the context named: "A" --> * <ui-view> <!-- created in the context named: "A.B" --> * <ui-view name="bar"> <!-- created in the context named: "A.B.C" --> * </ui-view> * </ui-view> * </ui-view> * </ui-view> * * uiViews: [ * { fqn: "$default", creationContext: { name: "" } }, * { fqn: "$default.foo", creationContext: { name: "A" } }, * { fqn: "$default.foo.$default", creationContext: { name: "A.B" } } * { fqn: "$default.foo.$default.bar", creationContext: { name: "A.B.C" } } * ] * * These four view configs all match the ui-view with the fqn: "$default.foo.$default.bar": * * - ViewConfig1: { uiViewName: "bar", uiViewContextAnchor: "A.B.C" } * - ViewConfig2: { uiViewName: "$default.bar", uiViewContextAnchor: "A.B" } * - ViewConfig3: { uiViewName: "foo.$default.bar", uiViewContextAnchor: "A" } * - ViewConfig4: { uiViewName: "$default.foo.$default.bar", uiViewContextAnchor: "" } * * Using ViewConfig3 as an example, it matches the ui-view with fqn "$default.foo.$default.bar" because: * - The ViewConfig's segmented target name is: [ "foo", "$default", "bar" ] * - There exists a parent ui-view (which has fqn: "$default.foo") where: * - the parent ui-view's name "foo" matches the first segment "foo" of the ViewConfig's target name * - the parent ui-view's context "A" matches the ViewConfig's anchor context "A" * - And the remaining segments [ "$default", "bar" ].join("."_ of the ViewConfig's target name match * the tail of the ui-view's fqn "default.bar" * * @internal */ ViewService.matches = function (uiViewsByFqn, uiView) { return function (viewConfig) { // Don't supply an ng1 ui-view with an ng2 ViewConfig, etc if (uiView.$type !== viewConfig.viewDecl.$type) return false; // Split names apart from both viewConfig and uiView into segments var vc = viewConfig.viewDecl; var vcSegments = vc.$uiViewName.split('.'); var uivSegments = uiView.fqn.split('.'); // Check if the tails of the segment arrays match. ex, these arrays' tails match: // vc: ["foo", "bar"], uiv fqn: ["$default", "foo", "bar"] if (!equals(vcSegments, uivSegments.slice(0 - vcSegments.length))) return false; // Now check if the fqn ending at the first segment of the viewConfig matches the context: // ["$default", "foo"].join(".") == "$default.foo", does the ui-view $default.foo context match? var negOffset = 1 - vcSegments.length || undefined; var fqnToFirstSegment = uivSegments.slice(0, negOffset).join('.'); var uiViewContext = uiViewsByFqn[fqnToFirstSegment].creationContext; return vc.$uiViewContextAnchor === (uiViewContext && uiViewContext.name); }; }; return ViewService; }()); export { ViewService }; //# sourceMappingURL=view.js.map