diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index d86a3d0678240..bc34d5efd684e 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -19,7 +19,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Type} from '../type'; import {assertLessThan, assertNotNull} from './assert'; -import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate} from './instructions'; +import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; import {ComponentTemplate, DirectiveDef} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node'; @@ -404,8 +404,15 @@ export function getOrCreateInjectable( } } - // If we *didn't* find the directive for the token from the candidate injector, we had a false - // positive. Traverse up the tree and continue. + // If we *didn't* find the directive for the token and we are searching the current node's + // injector, it's possible the directive is on this node and hasn't been created yet. + let instance: T|null; + if (injector === di && (instance = searchMatchesQueuedForCreation(node, token))) { + return instance; + } + + // The def wasn't found anywhere on this node, so it might be a false positive. + // Traverse up the tree and continue searching. injector = injector.parent; } } @@ -415,6 +422,19 @@ export function getOrCreateInjectable( throw createInjectionError('Not found', token); } +function searchMatchesQueuedForCreation(node: LNode, token: any): T|null { + const matches = node.view.tView.currentMatches; + if (matches) { + for (let i = 0; i < matches.length; i += 2) { + const def = matches[i] as DirectiveDef; + if (def.type === token) { + return resolveDirective(def, i + 1, matches, node.view.tView); + } + } + } + return null; +} + /** * Given a directive type, this function returns the bit in an injector's bloom filter * that should be used to determine whether or not the directive is present. diff --git a/packages/core/src/render3/errors.ts b/packages/core/src/render3/errors.ts new file mode 100644 index 0000000000000..9c37210d6efb6 --- /dev/null +++ b/packages/core/src/render3/errors.ts @@ -0,0 +1,35 @@ + +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {TNode} from './interfaces/node'; + +/** Called when directives inject each other (creating a circular dependency) */ +export function throwCyclicDependencyError(token: any): never { + throw new Error(`Cannot instantiate cyclic dependency! ${token}`); +} + +/** Called when there are multiple component selectors that match a given node */ +export function throwMultipleComponentError(tNode: TNode): never { + throw new Error(`Multiple components match node with tagname ${tNode.tagName}`); +} + +/** Throws an ExpressionChangedAfterChecked error if checkNoChanges mode is on. */ +export function throwErrorIfNoChangesMode( + creationMode: boolean, checkNoChangesMode: boolean, oldValue: any, currValue: any): never|void { + if (checkNoChangesMode) { + let msg = + `ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; + if (creationMode) { + msg += + ` It seems like the view has been created after its parent and its children have been dirty checked.` + + ` Has it been created in a change detection hook ?`; + } + // TODO: include debug context + throw new Error(msg); + } +} diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 127a34721bd1d..0964dd45e4a4b 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -13,7 +13,7 @@ import {LContainer, TContainer} from './interfaces/container'; import {LInjector} from './interfaces/injector'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; -import {LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view'; +import {CurrentMatchesList, LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view'; import {LContainerNode, LElementNode, LNode, LNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node'; import {assertNodeType} from './node_assert'; @@ -24,6 +24,7 @@ import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, Objec import {isDifferent, stringify} from './util'; import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; import {ViewRef} from './view_ref'; +import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; /** * Directive (D) sets a property on all component instances using this constant as a key and the @@ -50,6 +51,13 @@ export type Sanitizer = (value: any) => string; */ export const _ROOT_DIRECTIVE_INDICES = [0, 0]; +/** + * Token set in currentMatches while dependencies are being resolved. + * + * If we visit a directive that has a value set to CIRCULAR, we know we've + * already seen it, and thus have a circular dependency. + */ +export const CIRCULAR = '__CIRCULAR__'; /** * This property gets set before entering a template. @@ -523,66 +531,93 @@ export function elementStart( if (attrs) setUpAttributes(native, attrs); appendChild(node.parent !, native, currentView); + createDirectivesAndLocals(index, name, attrs, localRefs, null); + return native; +} +function createDirectivesAndLocals( + index: number, name: string | null, attrs: string[] | null | undefined, + localRefs: string[] | null | undefined, containerData: TView[] | null) { + const node = previousOrParentNode; if (firstTemplatePass) { - const tNode = createTNode(name, attrs || null, null); - cacheMatchingDirectivesForNode(tNode); - ngDevMode && assertDataInRange(index - 1); - node.tNode = tData[index] = tNode; + node.tNode = tData[index] = createTNode(name, attrs || null, containerData); + cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null); + } else { + instantiateDirectivesDirectly(); } - - hack_declareDirectives(index, localRefs || null); - return native; + saveResolvedLocalsInData(); } -function cacheMatchingDirectivesForNode(tNode: TNode): void { - const tView = currentView.tView; - const registry = tView.directiveRegistry; +/** + * On first template pass, we match each node against available directive selectors and save + * the resulting defs in the correct instantiation order for subsequent change detection runs + * (so dependencies are always created before the directives that inject them). + */ +function cacheMatchingDirectivesForNode( + tNode: TNode, tView: TView, localRefs: string[] | null): void { + const exportsMap = localRefs ? {'': -1} : null; + const matches = tView.currentMatches = findDirectiveMatches(tNode); + if (matches) { + for (let i = 0; i < matches.length; i += 2) { + const def = matches[i] as DirectiveDef; + const valueIndex = i + 1; + resolveDirective(def, valueIndex, matches, tView); + saveNameToExportMap(matches[valueIndex] as number, def, exportsMap); + } + } + if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); +} +/** Matches the current node against all available selectors. */ +function findDirectiveMatches(tNode: TNode): CurrentMatchesList|null { + const registry = currentView.tView.directiveRegistry; + let matches: any[]|null = null; if (registry) { - let componentFlag = 0; - let size = 0; - for (let i = 0; i < registry.length; i++) { const def = registry[i]; if (isNodeMatchingSelectorList(tNode, def.selectors !)) { if ((def as ComponentDef).template) { - if (componentFlag) throwMultipleComponentError(tNode); - componentFlag |= TNodeFlags.Component; + if (tNode.flags & TNodeFlags.Component) throwMultipleComponentError(tNode); + tNode.flags = TNodeFlags.Component; } - (tView.directives || (tView.directives = [])).push(def); - size++; + if (def.diPublic) def.diPublic(def); + (matches || (matches = [])).push(def, null); } } - if (size > 0) { - const startIndex = directives ? directives.length : 0; - buildTNodeFlags(tNode, startIndex, size, componentFlag); - } } + return matches as CurrentMatchesList; } -function buildTNodeFlags(tNode: TNode, index: number, size: number, component: number): void { - tNode.flags = (index << TNodeFlags.INDX_SHIFT) | (size << TNodeFlags.SIZE_SHIFT) | component; -} - -function throwMultipleComponentError(tNode: TNode): never { - throw new Error(`Multiple components match node with tagname ${tNode.tagName}`); +export function resolveDirective( + def: DirectiveDef, valueIndex: number, matches: CurrentMatchesList, tView: TView): any { + if (matches[valueIndex] === null) { + matches[valueIndex] = CIRCULAR; + const instance = def.factory(); + (tView.directives || (tView.directives = [])).push(def); + return directiveCreate(matches[valueIndex] = tView.directives !.length - 1, instance, def); + } else if (matches[valueIndex] === CIRCULAR) { + // If we revisit this directive before it's resolved, we know it's circular + throwCyclicDependencyError(def.type); + } + return null; } /** Stores index of component's host element so it will be queued for view refresh during CD. */ -function queueComponentIndexForCheck(dirIndex: number, elIndex: number): void { +function queueComponentIndexForCheck(dirIndex: number): void { if (firstTemplatePass) { - (currentView.tView.components || (currentView.tView.components = [])).push(dirIndex, elIndex); + (currentView.tView.components || (currentView.tView.components = [ + ])).push(dirIndex, data.length - 1); } } /** Stores index of directive and host element so it will be queued for binding refresh during CD. */ -function queueHostBindingForCheck(dirIndex: number, elIndex: number): void { +function queueHostBindingForCheck(dirIndex: number): void { ngDevMode && assertEqual(firstTemplatePass, true, 'Should only be called in first template pass.'); - (currentView.tView.hostBindings || (currentView.tView.hostBindings = [])).push(dirIndex, elIndex); + (currentView.tView.hostBindings || (currentView.tView.hostBindings = [ + ])).push(dirIndex, data.length - 1); } /** Sets the context for a ChangeDetectorRef to the given instance. */ @@ -598,32 +633,21 @@ export function isComponent(tNode: TNode): boolean { } /** - * This function instantiates the given directives. It is a hack since it assumes the directives - * come in the correct order for DI. + * This function instantiates the given directives. */ -function hack_declareDirectives(elementIndex: number, localRefs: string[] | null) { +function instantiateDirectivesDirectly() { const tNode = previousOrParentNode.tNode !; const size = (tNode.flags & TNodeFlags.SIZE_MASK) >> TNodeFlags.SIZE_SHIFT; - const exportsMap: {[key: string]: number}|null = firstTemplatePass && localRefs ? {'': -1} : null; - if (size > 0) { - let startIndex = tNode.flags >> TNodeFlags.INDX_SHIFT; - const endIndex = startIndex + size; + const startIndex = tNode.flags >> TNodeFlags.INDX_SHIFT; const tDirectives = currentView.tView.directives !; - // TODO(mhevery): This assumes that the directives come in correct order, which - // is not guaranteed. Must be refactored to take it into account. - for (let i = startIndex; i < endIndex; i++) { + for (let i = startIndex; i < startIndex + size; i++) { const def = tDirectives[i] as DirectiveDef; - directiveCreate(elementIndex, def.factory(), def); - saveNameToExportMap(startIndex, def, exportsMap); - startIndex++; + directiveCreate(i, def.factory(), def); } } - - if (firstTemplatePass) cacheMatchingLocalNames(tNode, localRefs, exportsMap !); - saveResolvedLocalsInData(); } /** Caches local names and their matching directive indices for query and template lookups. */ @@ -705,7 +729,8 @@ export function createTView( hostBindings: null, components: null, directiveRegistry: typeof defs === 'function' ? defs() : defs, - pipeRegistry: typeof pipes === 'function' ? pipes() : pipes + pipeRegistry: typeof pipes === 'function' ? pipes() : pipes, + currentMatches: null }; } @@ -772,8 +797,8 @@ export function hostElement( if (firstTemplatePass) { node.tNode = createTNode(tag as string, null, null); - // Root directive is stored at index 0, size 1 - buildTNodeFlags(node.tNode, 0, 1, TNodeFlags.Component); + node.tNode.flags = TNodeFlags.Component; + if (def.diPublic) def.diPublic(def); currentView.tView.directives = [def]; } @@ -1162,14 +1187,11 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { * NOTE: directives can be created in order other than the index order. They can also * be retrieved before they are created in which case the value will be null. * - * @param elementIndex Index of the host element in the data array * @param directive The directive instance. * @param directiveDef DirectiveDef object which contains information about the template. - * @param localRefs Names under which a query can retrieve the directive instance */ export function directiveCreate( - elementIndex: number, directive: T, directiveDef: DirectiveDef| ComponentDef): T { - const index = directives ? directives.length : 0; + index: number, directive: T, directiveDef: DirectiveDef| ComponentDef): T { const instance = baseDirectiveCreate(index, directive, directiveDef); ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); @@ -1177,7 +1199,7 @@ export function directiveCreate( const isComponent = (directiveDef as ComponentDef).template; if (isComponent) { - addComponentLogic(index, elementIndex, directive, directiveDef as ComponentDef); + addComponentLogic(index, directive, directiveDef as ComponentDef); } if (firstTemplatePass) { @@ -1185,7 +1207,7 @@ export function directiveCreate( // any projected components. queueInitHooks(index, directiveDef.onInit, directiveDef.doCheck, currentView.tView); - if (directiveDef.hostBindings) queueHostBindingForCheck(index, elementIndex); + if (directiveDef.hostBindings) queueHostBindingForCheck(index); } if (tNode && tNode.attrs) { @@ -1195,8 +1217,7 @@ export function directiveCreate( return instance; } -function addComponentLogic( - index: number, elementIndex: number, instance: T, def: ComponentDef): void { +function addComponentLogic(index: number, instance: T, def: ComponentDef): void { const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs); // Only component views should be added to the view tree directly. Embedded views are @@ -1212,7 +1233,7 @@ function addComponentLogic( initChangeDetectorIfExisting(previousOrParentNode.nodeInjector, instance, hostView); - if (firstTemplatePass) queueComponentIndexForCheck(index, elementIndex); + if (firstTemplatePass) queueComponentIndexForCheck(index); } /** @@ -1235,9 +1256,14 @@ export function baseDirectiveCreate( ngDevMode && assertDataNext(index, directives); directives[index] = directive; - const diPublic = directiveDef !.diPublic; - if (diPublic) { - diPublic(directiveDef !); + if (firstTemplatePass) { + const flags = previousOrParentNode.tNode !.flags; + previousOrParentNode.tNode !.flags = (flags & TNodeFlags.SIZE_MASK) === 0 ? + (index << TNodeFlags.INDX_SHIFT) | TNodeFlags.SIZE_SKIP | flags & TNodeFlags.Component : + flags + TNodeFlags.SIZE_SKIP; + } else { + const diPublic = directiveDef !.diPublic; + if (diPublic) diPublic(directiveDef !); } if (directiveDef !.attributes != null && previousOrParentNode.type == LNodeType.Element) { @@ -1352,18 +1378,10 @@ export function container( const node = createLNode(index, LNodeType.Container, undefined, lContainer); - if (node.tNode == null) { - node.tNode = tData[index] = createTNode(tagName || null, attrs || null, []); - } - // Containers are added to the current view tree instead of their embedded views // because views can be removed and re-inserted. addToViewTree(currentView, node.data); - - if (firstTemplatePass) cacheMatchingDirectivesForNode(node.tNode); - - // TODO: handle TemplateRef! - hack_declareDirectives(index, localRefs || null); + createDirectivesAndLocals(index, tagName || null, attrs, localRefs, []); isParent = false; ngDevMode && assertNodeType(previousOrParentNode, LNodeType.Container); @@ -1889,22 +1907,6 @@ export function checkNoChanges(component: T): void { } } -/** Throws an ExpressionChangedAfterChecked error if checkNoChanges mode is on. */ -function throwErrorIfNoChangesMode(oldValue: any, currValue: any): never|void { - if (checkNoChangesMode) { - let msg = - `ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; - if (creationMode) { - msg += - ` It seems like the view has been created after its parent and its children have been dirty checked.` + - ` Has it been created in a change detection hook ?`; - } - // TODO: include debug context - throw new Error(msg); - } -} - - /** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */ export function detectChangesInternal( hostView: LView, hostNode: LElementNode, def: ComponentDef, component: T) { @@ -1982,7 +1984,7 @@ export function bind(value: T | NO_CHANGE): T|NO_CHANGE { const changed: boolean = value !== NO_CHANGE && isDifferent(data[bindingIndex], value); if (changed) { - throwErrorIfNoChangesMode(data[bindingIndex], value); + throwErrorIfNoChangesMode(creationMode, checkNoChangesMode, data[bindingIndex], value); data[bindingIndex] = value; } bindingIndex++; @@ -2162,7 +2164,7 @@ export function bindingUpdated(value: any): boolean { if (creationMode) { initBindings(); } else if (isDifferent(data[bindingIndex], value)) { - throwErrorIfNoChangesMode(data[bindingIndex], value); + throwErrorIfNoChangesMode(creationMode, checkNoChangesMode, data[bindingIndex], value); } else { bindingIndex++; return false; diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 338e984dcc633..d4fbf6244052d 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -42,6 +42,9 @@ export const enum TNodeFlags { /** How far to shift the flags to get the number of directives on this node */ SIZE_SHIFT = 1, + /** The amount to add to flags to increment size when each directive is added */ + SIZE_SKIP = 2, + /** Mask to get the number of directives on this node */ SIZE_MASK = 0b00000000000000000001111111111110 } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 51435da2e4156..603234445ba23 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -7,7 +7,7 @@ */ import {LContainer} from './container'; -import {ComponentTemplate, DirectiveDefList, PipeDef, PipeDefList} from './definition'; +import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDef, PipeDefList} from './definition'; import {LElementNode, LViewNode, TNode} from './node'; import {LQueries} from './query'; import {Renderer3} from './renderer'; @@ -225,6 +225,24 @@ export interface TView { /** Static data equivalent of LView.data[]. Contains TNodes. */ data: TData; + /** + * Selector matches for a node are temporarily cached on the TView so the + * DI system can eagerly instantiate directives on the same node if they are + * created out of order. They are overwritten after each node. + * + *
+ * + * e.g. DirA injects DirB, but DirA is created first. DI should instantiate + * DirB when it finds that it's on the same node, but not yet created. + * + * Even indices: Directive defs + * Odd indices: + * - Null if the associated directive hasn't been instantiated yet + * - Directive index, if associated directive has been created + * - String, temporary 'CIRCULAR' token set while dependencies are being resolved + */ + currentMatches: CurrentMatchesList|null; + /** * Directive and component defs that have already been matched to nodes on * this view. @@ -397,6 +415,9 @@ export const enum LifecycleStage { */ export type TData = (TNode | PipeDef| null)[]; +/** Type for TView.currentMatches */ +export type CurrentMatchesList = [DirectiveDef, (string | number | null)]; + // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. export const unusedValueExportToPlacateAjd = 1; diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index a60d330ed3214..97f2e4852e365 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -35,9 +35,6 @@ { "name": "baseDirectiveCreate" }, - { - "name": "buildTNodeFlags" - }, { "name": "callHooks" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 9fbcd5c66b9f3..2f18bf9b3e5b4 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -11,6 +11,9 @@ { "name": "CIRCULAR$1" }, + { + "name": "CIRCULAR$2" + }, { "name": "CLEAN_PROMISE" }, @@ -260,9 +263,6 @@ { "name": "bindingUpdated" }, - { - "name": "buildTNodeFlags" - }, { "name": "cacheMatchingDirectivesForNode" }, @@ -296,6 +296,9 @@ { "name": "couldBeInjectableType" }, + { + "name": "createDirectivesAndLocals" + }, { "name": "createInjector" }, @@ -404,6 +407,9 @@ { "name": "findAttrIndexInNode" }, + { + "name": "findDirectiveMatches" + }, { "name": "findFirstRNode" }, @@ -485,9 +491,6 @@ { "name": "getTypeNameForDebugging$1" }, - { - "name": "hack_declareDirectives" - }, { "name": "hasDeps" }, @@ -527,6 +530,9 @@ { "name": "insertView" }, + { + "name": "instantiateDirectivesDirectly" + }, { "name": "interpolation1" }, @@ -680,6 +686,9 @@ { "name": "resetApplicationState" }, + { + "name": "resolveDirective" + }, { "name": "resolveForwardRef" }, @@ -728,6 +737,9 @@ { "name": "textBinding" }, + { + "name": "throwCyclicDependencyError" + }, { "name": "throwErrorIfNoChangesMode" }, @@ -761,4 +773,4 @@ { "name": "ɵ0" } -] +] \ No newline at end of file diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 0721806a6a4bd..16fe3803745ec 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -11,13 +11,13 @@ import {ChangeDetectorRef, ElementRef, TemplateRef, ViewContainerRef} from '@ang import {defineComponent} from '../../src/render3/definition'; import {InjectFlags, bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di'; import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; +import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {LInjector} from '../../src/render3/interfaces/injector'; import {LNodeType} from '../../src/render3/interfaces/node'; import {LViewFlags} from '../../src/render3/interfaces/view'; import {ViewRef} from '../../src/render3/view_ref'; -import {createComponent, createDirective, renderComponent, renderToHtml, toHtml} from './render_util'; +import {ComponentFixture, createComponent, createDirective, renderComponent, renderToHtml, toHtml} from './render_util'; describe('di', () => { describe('no dependencies', () => { @@ -47,35 +47,41 @@ describe('di', () => { }); }); - describe('view dependencies', () => { - it('should create directive with inter view dependencies', () => { - class DirectiveA { - value: string = 'A'; - static ngDirectiveDef = defineDirective({ - type: DirectiveA, - selectors: [['', 'dirA', '']], - factory: () => new DirectiveA, - features: [PublicFeature] - }); - } + describe('directive injection', () => { + let log: string[] = []; + + class DirB { + value = 'DirB'; + constructor() { log.push(this.value); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirB', '']], + type: DirB, + factory: () => new DirB(), + features: [PublicFeature] + }); + } + + beforeEach(() => log = []); - class DirectiveB { - value: string = 'B'; + it('should create directive with intra view dependencies', () => { + class DirA { + value: string = 'DirA'; static ngDirectiveDef = defineDirective({ - type: DirectiveB, - selectors: [['', 'dirB', '']], - factory: () => new DirectiveB, + type: DirA, + selectors: [['', 'dirA', '']], + factory: () => new DirA, features: [PublicFeature] }); } - class DirectiveC { + class DirC { value: string; - constructor(a: DirectiveA, b: DirectiveB) { this.value = a.value + b.value; } + constructor(a: DirA, b: DirB) { this.value = a.value + b.value; } static ngDirectiveDef = defineDirective({ - type: DirectiveC, + type: DirC, selectors: [['', 'dirC', '']], - factory: () => new DirectiveC(directiveInject(DirectiveA), directiveInject(DirectiveB)), + factory: () => new DirC(directiveInject(DirA), directiveInject(DirB)), exportAs: 'dirC' }); } @@ -99,10 +105,381 @@ describe('di', () => { textBinding(3, bind(tmp.value)); } - const defs = [DirectiveA, DirectiveB, DirectiveC]; + const defs = [DirA, DirB, DirC]; expect(renderToHtml(Template, {}, defs)) - .toEqual('
AB
'); + .toEqual('
DirADirB
'); + }); + + it('should instantiate injected directives first', () => { + class DirA { + constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirB)), + }); + } + + /**
*/ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dirA', '', 'dirB', '']); + elementEnd(); + } + }, [DirA, DirB]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual(['DirB', 'DirA (dep: DirB)']); + }); + + it('should instantiate injected directives before components', () => { + class Comp { + constructor(dir: DirB) { log.push(`Comp (dep: ${dir.value})`); } + + static ngComponentDef = defineComponent({ + selectors: [['comp']], + type: Comp, + factory: () => new Comp(directiveInject(DirB)), + template: (ctx: any, fm: boolean) => {} + }); + } + + /** */ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'comp', ['dirB', '']); + elementEnd(); + } + }, [Comp, DirB]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual(['DirB', 'Comp (dep: DirB)']); + }); + + it('should inject directives in the correct order in a for loop', () => { + class DirA { + constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirB)) + }); + } + + /** + * % for(let i = 0; i < 3; i++) { + *
+ * % } + */ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + container(0); + } + containerRefreshStart(0); + { + for (let i = 0; i < 3; i++) { + if (embeddedViewStart(0)) { + elementStart(0, 'div', ['dirA', '', 'dirB', '']); + elementEnd(); + } + embeddedViewEnd(); + } + } + containerRefreshEnd(); + }, [DirA, DirB]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual( + ['DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)']); + }); + + it('should instantiate directives with multiple out-of-order dependencies', () => { + class DirA { + value = 'DirA'; + constructor() { log.push(this.value); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(), + features: [PublicFeature] + }); + } + + class DirB { + constructor(dirA: DirA, dirC: DirC) { + log.push(`DirB (deps: ${dirA.value} and ${dirC.value})`); + } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirB', '']], + type: DirB, + factory: () => new DirB(directiveInject(DirA), directiveInject(DirC)) + }); + } + + class DirC { + value = 'DirC'; + constructor() { log.push(this.value); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirC', '']], + type: DirC, + factory: () => new DirC(), + features: [PublicFeature] + }); + } + + /**
*/ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dirA', '', 'dirB', '', 'dirC', '']); + elementEnd(); + } + }, [DirA, DirB, DirC]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual(['DirA', 'DirC', 'DirB (deps: DirA and DirC)']); + }); + + it('should instantiate in the correct order for complex case', () => { + class Comp { + constructor(dir: DirD) { log.push(`Comp (dep: ${dir.value})`); } + + static ngComponentDef = defineComponent({ + selectors: [['comp']], + type: Comp, + factory: () => new Comp(directiveInject(DirD)), + template: (ctx: any, fm: boolean) => {} + }); + } + + class DirA { + value = 'DirA'; + constructor(dir: DirC) { log.push(`DirA (dep: ${dir.value})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirC)), + features: [PublicFeature] + }); + } + + class DirC { + value = 'DirC'; + constructor(dir: DirB) { log.push(`DirC (dep: ${dir.value})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirC', '']], + type: DirC, + factory: () => new DirC(directiveInject(DirB)), + features: [PublicFeature] + }); + } + + class DirD { + value = 'DirD'; + constructor(dir: DirA) { log.push(`DirD (dep: ${dir.value})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirD', '']], + type: DirD, + factory: () => new DirD(directiveInject(DirA)), + features: [PublicFeature] + }); + } + + /** */ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'comp', ['dirA', '', 'dirB', '', 'dirC', '', 'dirD', '']); + elementEnd(); + } + }, [Comp, DirA, DirB, DirC, DirD]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual( + ['DirB', 'DirC (dep: DirB)', 'DirA (dep: DirC)', 'DirD (dep: DirA)', 'Comp (dep: DirD)']); + }); + + it('should instantiate in correct order with mixed parent and peer dependencies', () => { + class DirA { + constructor(dirB: DirB, app: App) { + log.push(`DirA (deps: ${dirB.value} and ${app.value})`); + } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirB), directiveInject(App)), + }); + } + + class App { + value = 'App'; + + static ngComponentDef = defineComponent({ + selectors: [['app']], + type: App, + factory: () => new App(), + /**
*/ + template: (ctx: any, cm: boolean) => { + if (cm) { + elementStart(0, 'div', ['dirA', '', 'dirB', '', 'dirC', 'dirC']); + elementEnd(); + } + }, + directives: [DirA, DirB], + features: [PublicFeature], + }); + } + + const fixture = new ComponentFixture(App); + expect(log).toEqual(['DirB', 'DirA (deps: DirB and App)']); + }); + + it('should not use a parent when peer dep is available', () => { + let count = 1; + + class DirA { + constructor(dirB: DirB) { log.push(`DirA (dep: DirB - ${dirB.count})`); } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirB)), + }); + } + + class DirB { + count: number; + + constructor() { + log.push(`DirB`); + this.count = count++; + } + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirB', '']], + type: DirB, + factory: () => new DirB(), + features: [PublicFeature], + }); + } + + /**
*/ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dirA', '', 'dirB', '']); + elementEnd(); + } + }, [DirA, DirB]); + + /** */ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'parent', ['dirB', '']); + elementEnd(); + } + }, [Parent, DirB]); + + const fixture = new ComponentFixture(App); + expect(log).toEqual(['DirB', 'DirB', 'DirA (dep: DirB - 2)']); + }); + + it('should throw if directive is not found', () => { + class Dir { + constructor(siblingDir: OtherDir) {} + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dir', '']], + type: Dir, + factory: () => new Dir(directiveInject(OtherDir)), + features: [PublicFeature] + }); + } + + class OtherDir { + static ngDirectiveDef = defineDirective({ + selectors: [['', 'other', '']], + type: OtherDir, + factory: () => new OtherDir(), + features: [PublicFeature] + }); + } + + /**
*/ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dir', '']); + elementEnd(); + } + }, [Dir, OtherDir]); + + expect(() => new ComponentFixture(App)) + .toThrowError(/ElementInjector: NotFound \[OtherDir\]/); }); + + it('should throw if directives try to inject each other', () => { + class DirA { + constructor(dir: DirB) {} + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirA', '']], + type: DirA, + factory: () => new DirA(directiveInject(DirB)), + features: [PublicFeature] + }); + } + + class DirB { + constructor(dir: DirA) {} + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dirB', '']], + type: DirB, + factory: () => new DirB(directiveInject(DirA)), + features: [PublicFeature] + }); + } + + /**
*/ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dirA', '', 'dirB', '']); + elementEnd(); + } + }, [DirA, DirB]); + + expect(() => new ComponentFixture(App)).toThrowError(/Cannot instantiate cyclic dependency!/); + }); + + it('should throw if directive tries to inject itself', () => { + class Dir { + constructor(dir: Dir) {} + + static ngDirectiveDef = defineDirective({ + selectors: [['', 'dir', '']], + type: Dir, + factory: () => new Dir(directiveInject(Dir)), + features: [PublicFeature] + }); + } + + /**
*/ + const App = createComponent('app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['dir', '']); + elementEnd(); + } + }, [Dir]); + + expect(() => new ComponentFixture(App)).toThrowError(/Cannot instantiate cyclic dependency!/); + }); + }); describe('ElementRef', () => { @@ -283,7 +660,9 @@ describe('di', () => { class Directive { value: string; + constructor(public cdr: ChangeDetectorRef) { this.value = (cdr.constructor as any).name; } + static ngDirectiveDef = defineDirective({ type: Directive, selectors: [['', 'dir', '']], @@ -504,24 +883,23 @@ describe('di', () => { expect(dir !.cdr).toBe(app.cdr); expect(dir !.cdr).toBe(dirSameInstance !.cdr); }); + }); - it('should injectAttribute', () => { - let exist: string|undefined = 'wrong'; - let nonExist: string|undefined = 'wrong'; + it('should injectAttribute', () => { + let exist: string|undefined = 'wrong'; + let nonExist: string|undefined = 'wrong'; - const MyApp = createComponent('my-app', function(ctx: any, cm: boolean) { - if (cm) { - elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']); - exist = injectAttribute('exist'); - nonExist = injectAttribute('nonExist'); - } - }); - - const app = renderComponent(MyApp); - expect(exist).toEqual('existValue'); - expect(nonExist).toEqual(undefined); + const MyApp = createComponent('my-app', function(ctx: any, cm: boolean) { + if (cm) { + elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']); + exist = injectAttribute('exist'); + nonExist = injectAttribute('nonExist'); + } }); + const app = renderComponent(MyApp); + expect(exist).toEqual('existValue'); + expect(nonExist).toEqual(undefined); }); describe('inject', () => {