diff --git a/src/common/common.ts b/src/common/common.ts index 2781bba2..e3bca6fb 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -26,6 +26,33 @@ export const noop = () => undefined; export type Mapper = (x: X, key?: (string|number)) => T; export interface TypedMap { [key: string]: T; } export type Predicate = (x?: X) => boolean; +/** + * An ng1-style injectable + * + * This could be a (non-minified) function such as: + * ```js + * function injectableFunction(SomeDependency) { + * + * } + * ``` + * + * or an explicitly annotated function (minify safe) + * ```js + * injectableFunction.$inject = [ 'SomeDependency' ]; + * function injectableFunction(SomeDependency) { + * + * } + * ``` + * + * or an array style annotated function (minify safe) + * ```js + * ['SomeDependency', function injectableFunction(SomeDependency) { + * + * }]; + * ``` + * + * @publicapi + */ export type IInjectable = (Function|any[]); export interface Obj extends Object { diff --git a/src/view/view.ts b/src/view/view.ts index 12710009..fd149aa8 100644 --- a/src/view/view.ts +++ b/src/view/view.ts @@ -84,86 +84,88 @@ export class ViewService { this._viewConfigs.push(viewConfig); } - sync() { - let uiViewsByFqn: TypedMap = - this._uiViews.map(uiv => [uiv.fqn, uiv]).reduce(applyPairs, {}); + /** + * 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: + * + * + * + * + * + * + * + * + * + * 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" + * + * @internalapi + */ + static matches = (uiViewsByFqn: TypedMap, uiView: ActiveUIView) => (viewConfig: ViewConfig) => { + // Don't supply an ng1 ui-view with an ng2 ViewConfig, etc + if (uiView.$type !== viewConfig.viewDecl.$type) return false; - /** - * 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: - *
- *
- *
- *
- *
- *
- *
- *
- * - * 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" - */ - const matches = (uiView: ActiveUIView) => (viewConfig: 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 + let vc = viewConfig.viewDecl; + let vcSegments = vc.$uiViewName.split("."); + let uivSegments = uiView.fqn.split("."); - // Split names apart from both viewConfig and uiView into segments - let vc = viewConfig.viewDecl; - let vcSegments = vc.$uiViewName.split("."); - let 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; - // 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? + let negOffset = (1 - vcSegments.length) || undefined; + let fqnToFirstSegment = uivSegments.slice(0, negOffset).join("."); + let uiViewContext = uiViewsByFqn[fqnToFirstSegment].creationContext; + return vc.$uiViewContextAnchor === (uiViewContext && uiViewContext.name); + }; - // 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? - let negOffset = (1 - vcSegments.length) || undefined; - let fqnToFirstSegment = uivSegments.slice(0, negOffset).join("."); - let uiViewContext = uiViewsByFqn[fqnToFirstSegment].creationContext; - return vc.$uiViewContextAnchor === (uiViewContext && uiViewContext.name); - }; + sync() { + let uiViewsByFqn: TypedMap = + this._uiViews.map(uiv => [uiv.fqn, uiv]).reduce(applyPairs, {}); // Return the number of dots in the fully qualified name function uiViewDepth(uiView: ActiveUIView) { @@ -181,7 +183,7 @@ export class ViewService { const depthCompare = curry((depthFn, posNeg, left, right) => posNeg * (depthFn(left) - depthFn(right))); const matchingConfigPair = (uiView: ActiveUIView) => { - let matchingConfigs = this._viewConfigs.filter(matches(uiView)); + let 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 @@ -258,6 +260,9 @@ export class ViewService { /** * 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]] *