fix(ivy): instantiate dirs in correct order (#23178)

PR Close #23178
This commit is contained in:
Kara Erickson 2018-04-04 21:21:12 -07:00 committed by Igor Minar
parent d80e9304c6
commit 628303d2cb
8 changed files with 613 additions and 145 deletions

View File

@ -19,7 +19,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie
import {Type} from '../type'; import {Type} from '../type';
import {assertLessThan, assertNotNull} from './assert'; 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 {ComponentTemplate, DirectiveDef} from './interfaces/definition';
import {LInjector} from './interfaces/injector'; import {LInjector} from './interfaces/injector';
import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node'; import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node';
@ -404,8 +404,15 @@ export function getOrCreateInjectable<T>(
} }
} }
// If we *didn't* find the directive for the token from the candidate injector, we had a false // If we *didn't* find the directive for the token and we are searching the current node's
// positive. Traverse up the tree and continue. // 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<T>(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; injector = injector.parent;
} }
} }
@ -415,6 +422,19 @@ export function getOrCreateInjectable<T>(
throw createInjectionError('Not found', token); throw createInjectionError('Not found', token);
} }
function searchMatchesQueuedForCreation<T>(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<any>;
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 * 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. * that should be used to determine whether or not the directive is present.

View File

@ -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);
}
}

View File

@ -13,7 +13,7 @@ import {LContainer, TContainer} from './interfaces/container';
import {LInjector} from './interfaces/injector'; import {LInjector} from './interfaces/injector';
import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query'; 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 {LContainerNode, LElementNode, LNode, LNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node';
import {assertNodeType} from './node_assert'; import {assertNodeType} from './node_assert';
@ -24,6 +24,7 @@ import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, Objec
import {isDifferent, stringify} from './util'; import {isDifferent, stringify} from './util';
import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; import {executeHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks';
import {ViewRef} from './view_ref'; 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 * 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]; 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. * This property gets set before entering a template.
@ -528,66 +536,93 @@ export function elementStart(
if (attrs) setUpAttributes(native, attrs); if (attrs) setUpAttributes(native, attrs);
appendChild(node.parent !, native, currentView); appendChild(node.parent !, native, currentView);
createDirectivesAndLocals(index, name, attrs, localRefs, null);
if (firstTemplatePass) {
const tNode = createTNode(name, attrs || null, null);
cacheMatchingDirectivesForNode(tNode);
ngDevMode && assertDataInRange(index - 1);
node.tNode = tData[index] = tNode;
}
hack_declareDirectives(index, localRefs || null);
return native; return native;
} }
function cacheMatchingDirectivesForNode(tNode: TNode): void { function createDirectivesAndLocals(
const tView = currentView.tView; index: number, name: string | null, attrs: string[] | null | undefined,
const registry = tView.directiveRegistry; localRefs: string[] | null | undefined, containerData: TView[] | null) {
const node = previousOrParentNode;
if (firstTemplatePass) {
ngDevMode && assertDataInRange(index - 1);
node.tNode = tData[index] = createTNode(name, attrs || null, containerData);
cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null);
} else {
instantiateDirectivesDirectly();
}
saveResolvedLocalsInData();
}
/**
* 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<any>;
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) { if (registry) {
let componentFlag = 0;
let size = 0;
for (let i = 0; i < registry.length; i++) { for (let i = 0; i < registry.length; i++) {
const def = registry[i]; const def = registry[i];
if (isNodeMatchingSelectorList(tNode, def.selectors !)) { if (isNodeMatchingSelectorList(tNode, def.selectors !)) {
if ((def as ComponentDef<any>).template) { if ((def as ComponentDef<any>).template) {
if (componentFlag) throwMultipleComponentError(tNode); if (tNode.flags & TNodeFlags.Component) throwMultipleComponentError(tNode);
componentFlag |= TNodeFlags.Component; tNode.flags = TNodeFlags.Component;
} }
(tView.directives || (tView.directives = [])).push(def); if (def.diPublic) def.diPublic(def);
size++; (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 { export function resolveDirective(
tNode.flags = (index << TNodeFlags.INDX_SHIFT) | (size << TNodeFlags.SIZE_SHIFT) | component; def: DirectiveDef<any>, valueIndex: number, matches: CurrentMatchesList, tView: TView): any {
} if (matches[valueIndex] === null) {
matches[valueIndex] = CIRCULAR;
function throwMultipleComponentError(tNode: TNode): never { const instance = def.factory();
throw new Error(`Multiple components match node with tagname ${tNode.tagName}`); (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. */ /** 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) { 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. /** 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 && ngDevMode &&
assertEqual(firstTemplatePass, true, 'Should only be called in first template pass.'); 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. */ /** Sets the context for a ChangeDetectorRef to the given instance. */
@ -603,32 +638,21 @@ export function isComponent(tNode: TNode): boolean {
} }
/** /**
* This function instantiates the given directives. It is a hack since it assumes the directives * This function instantiates the given directives.
* come in the correct order for DI.
*/ */
function hack_declareDirectives(elementIndex: number, localRefs: string[] | null) { function instantiateDirectivesDirectly() {
const tNode = previousOrParentNode.tNode !; const tNode = previousOrParentNode.tNode !;
const size = (tNode.flags & TNodeFlags.SIZE_MASK) >> TNodeFlags.SIZE_SHIFT; const size = (tNode.flags & TNodeFlags.SIZE_MASK) >> TNodeFlags.SIZE_SHIFT;
const exportsMap: {[key: string]: number}|null = firstTemplatePass && localRefs ? {'': -1} : null;
if (size > 0) { if (size > 0) {
let startIndex = tNode.flags >> TNodeFlags.INDX_SHIFT; const startIndex = tNode.flags >> TNodeFlags.INDX_SHIFT;
const endIndex = startIndex + size;
const tDirectives = currentView.tView.directives !; const tDirectives = currentView.tView.directives !;
// TODO(mhevery): This assumes that the directives come in correct order, which for (let i = startIndex; i < startIndex + size; i++) {
// is not guaranteed. Must be refactored to take it into account.
for (let i = startIndex; i < endIndex; i++) {
const def = tDirectives[i] as DirectiveDef<any>; const def = tDirectives[i] as DirectiveDef<any>;
directiveCreate(elementIndex, def.factory(), def); directiveCreate(i, def.factory(), def);
saveNameToExportMap(startIndex, def, exportsMap);
startIndex++;
} }
} }
if (firstTemplatePass) cacheMatchingLocalNames(tNode, localRefs, exportsMap !);
saveResolvedLocalsInData();
} }
/** Caches local names and their matching directive indices for query and template lookups. */ /** Caches local names and their matching directive indices for query and template lookups. */
@ -710,7 +734,8 @@ export function createTView(
hostBindings: null, hostBindings: null,
components: null, components: null,
directiveRegistry: typeof defs === 'function' ? defs() : defs, directiveRegistry: typeof defs === 'function' ? defs() : defs,
pipeRegistry: typeof pipes === 'function' ? pipes() : pipes pipeRegistry: typeof pipes === 'function' ? pipes() : pipes,
currentMatches: null
}; };
} }
@ -777,8 +802,8 @@ export function hostElement(
if (firstTemplatePass) { if (firstTemplatePass) {
node.tNode = createTNode(tag as string, null, null); node.tNode = createTNode(tag as string, null, null);
// Root directive is stored at index 0, size 1 node.tNode.flags = TNodeFlags.Component;
buildTNodeFlags(node.tNode, 0, 1, TNodeFlags.Component); if (def.diPublic) def.diPublic(def);
currentView.tView.directives = [def]; currentView.tView.directives = [def];
} }
@ -1167,14 +1192,11 @@ export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
* NOTE: directives can be created in order other than the index order. They can also * 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. * 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 directive The directive instance.
* @param directiveDef DirectiveDef object which contains information about the template. * @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<T>( export function directiveCreate<T>(
elementIndex: number, directive: T, directiveDef: DirectiveDef<T>| ComponentDef<T>): T { index: number, directive: T, directiveDef: DirectiveDef<T>| ComponentDef<T>): T {
const index = directives ? directives.length : 0;
const instance = baseDirectiveCreate(index, directive, directiveDef); const instance = baseDirectiveCreate(index, directive, directiveDef);
ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode');
@ -1182,7 +1204,7 @@ export function directiveCreate<T>(
const isComponent = (directiveDef as ComponentDef<T>).template; const isComponent = (directiveDef as ComponentDef<T>).template;
if (isComponent) { if (isComponent) {
addComponentLogic(index, elementIndex, directive, directiveDef as ComponentDef<T>); addComponentLogic(index, directive, directiveDef as ComponentDef<T>);
} }
if (firstTemplatePass) { if (firstTemplatePass) {
@ -1190,7 +1212,7 @@ export function directiveCreate<T>(
// any projected components. // any projected components.
queueInitHooks(index, directiveDef.onInit, directiveDef.doCheck, currentView.tView); queueInitHooks(index, directiveDef.onInit, directiveDef.doCheck, currentView.tView);
if (directiveDef.hostBindings) queueHostBindingForCheck(index, elementIndex); if (directiveDef.hostBindings) queueHostBindingForCheck(index);
} }
if (tNode && tNode.attrs) { if (tNode && tNode.attrs) {
@ -1200,8 +1222,7 @@ export function directiveCreate<T>(
return instance; return instance;
} }
function addComponentLogic<T>( function addComponentLogic<T>(index: number, instance: T, def: ComponentDef<T>): void {
index: number, elementIndex: number, instance: T, def: ComponentDef<T>): void {
const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs); const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs);
// Only component views should be added to the view tree directly. Embedded views are // Only component views should be added to the view tree directly. Embedded views are
@ -1217,7 +1238,7 @@ function addComponentLogic<T>(
initChangeDetectorIfExisting(previousOrParentNode.nodeInjector, instance, hostView); initChangeDetectorIfExisting(previousOrParentNode.nodeInjector, instance, hostView);
if (firstTemplatePass) queueComponentIndexForCheck(index, elementIndex); if (firstTemplatePass) queueComponentIndexForCheck(index);
} }
/** /**
@ -1240,9 +1261,14 @@ export function baseDirectiveCreate<T>(
ngDevMode && assertDataNext(index, directives); ngDevMode && assertDataNext(index, directives);
directives[index] = directive; directives[index] = directive;
const diPublic = directiveDef !.diPublic; if (firstTemplatePass) {
if (diPublic) { const flags = previousOrParentNode.tNode !.flags;
diPublic(directiveDef !); 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) { if (directiveDef !.attributes != null && previousOrParentNode.type == LNodeType.Element) {
@ -1355,18 +1381,10 @@ export function container(
const node = createLNode(index, LNodeType.Container, undefined, lContainer); 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 // Containers are added to the current view tree instead of their embedded views
// because views can be removed and re-inserted. // because views can be removed and re-inserted.
addToViewTree(currentView, node.data); addToViewTree(currentView, node.data);
createDirectivesAndLocals(index, tagName || null, attrs, localRefs, []);
if (firstTemplatePass) cacheMatchingDirectivesForNode(node.tNode);
// TODO: handle TemplateRef!
hack_declareDirectives(index, localRefs || null);
isParent = false; isParent = false;
ngDevMode && assertNodeType(previousOrParentNode, LNodeType.Container); ngDevMode && assertNodeType(previousOrParentNode, LNodeType.Container);
@ -1892,22 +1910,6 @@ export function checkNoChanges<T>(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. */ /** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */
export function detectChangesInternal<T>( export function detectChangesInternal<T>(
hostView: LView, hostNode: LElementNode, def: ComponentDef<T>, component: T) { hostView: LView, hostNode: LElementNode, def: ComponentDef<T>, component: T) {
@ -1985,7 +1987,7 @@ export function bind<T>(value: T | NO_CHANGE): T|NO_CHANGE {
const changed: boolean = value !== NO_CHANGE && isDifferent(data[bindingIndex], value); const changed: boolean = value !== NO_CHANGE && isDifferent(data[bindingIndex], value);
if (changed) { if (changed) {
throwErrorIfNoChangesMode(data[bindingIndex], value); throwErrorIfNoChangesMode(creationMode, checkNoChangesMode, data[bindingIndex], value);
data[bindingIndex] = value; data[bindingIndex] = value;
} }
bindingIndex++; bindingIndex++;
@ -2165,7 +2167,7 @@ export function bindingUpdated(value: any): boolean {
if (creationMode) { if (creationMode) {
initBindings(); initBindings();
} else if (isDifferent(data[bindingIndex], value)) { } else if (isDifferent(data[bindingIndex], value)) {
throwErrorIfNoChangesMode(data[bindingIndex], value); throwErrorIfNoChangesMode(creationMode, checkNoChangesMode, data[bindingIndex], value);
} else { } else {
bindingIndex++; bindingIndex++;
return false; return false;

View File

@ -42,6 +42,9 @@ export const enum TNodeFlags {
/** How far to shift the flags to get the number of directives on this node */ /** How far to shift the flags to get the number of directives on this node */
SIZE_SHIFT = 1, 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 */ /** Mask to get the number of directives on this node */
SIZE_MASK = 0b00000000000000000001111111111110 SIZE_MASK = 0b00000000000000000001111111111110
} }

View File

@ -7,7 +7,7 @@
*/ */
import {LContainer} from './container'; 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 {LElementNode, LViewNode, TNode} from './node';
import {LQueries} from './query'; import {LQueries} from './query';
import {Renderer3} from './renderer'; import {Renderer3} from './renderer';
@ -225,6 +225,24 @@ export interface TView {
/** Static data equivalent of LView.data[]. Contains TNodes. */ /** Static data equivalent of LView.data[]. Contains TNodes. */
data: TData; 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.
*
* <div dirA dirB></div>
*
* 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 * Directive and component defs that have already been matched to nodes on
* this view. * this view.
@ -397,6 +415,9 @@ export const enum LifecycleStage {
*/ */
export type TData = (TNode | PipeDef<any>| null)[]; export type TData = (TNode | PipeDef<any>| null)[];
/** Type for TView.currentMatches */
export type CurrentMatchesList = [DirectiveDef<any>, (string | number | null)];
// Note: This hack is necessary so we don't erroneously get a circular dependency // Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types. // failure based on types.
export const unusedValueExportToPlacateAjd = 1; export const unusedValueExportToPlacateAjd = 1;

View File

@ -35,9 +35,6 @@
{ {
"name": "baseDirectiveCreate" "name": "baseDirectiveCreate"
}, },
{
"name": "buildTNodeFlags"
},
{ {
"name": "callHooks" "name": "callHooks"
}, },

View File

@ -11,6 +11,9 @@
{ {
"name": "CIRCULAR$1" "name": "CIRCULAR$1"
}, },
{
"name": "CIRCULAR$2"
},
{ {
"name": "CLEAN_PROMISE" "name": "CLEAN_PROMISE"
}, },
@ -260,9 +263,6 @@
{ {
"name": "bindingUpdated" "name": "bindingUpdated"
}, },
{
"name": "buildTNodeFlags"
},
{ {
"name": "cacheMatchingDirectivesForNode" "name": "cacheMatchingDirectivesForNode"
}, },
@ -296,6 +296,9 @@
{ {
"name": "couldBeInjectableType" "name": "couldBeInjectableType"
}, },
{
"name": "createDirectivesAndLocals"
},
{ {
"name": "createInjector" "name": "createInjector"
}, },
@ -404,6 +407,9 @@
{ {
"name": "findAttrIndexInNode" "name": "findAttrIndexInNode"
}, },
{
"name": "findDirectiveMatches"
},
{ {
"name": "findFirstRNode" "name": "findFirstRNode"
}, },
@ -485,9 +491,6 @@
{ {
"name": "getTypeNameForDebugging$1" "name": "getTypeNameForDebugging$1"
}, },
{
"name": "hack_declareDirectives"
},
{ {
"name": "hasDeps" "name": "hasDeps"
}, },
@ -527,6 +530,9 @@
{ {
"name": "insertView" "name": "insertView"
}, },
{
"name": "instantiateDirectivesDirectly"
},
{ {
"name": "interpolation1" "name": "interpolation1"
}, },
@ -680,6 +686,9 @@
{ {
"name": "resetApplicationState" "name": "resetApplicationState"
}, },
{
"name": "resolveDirective"
},
{ {
"name": "resolveForwardRef" "name": "resolveForwardRef"
}, },
@ -728,6 +737,9 @@
{ {
"name": "textBinding" "name": "textBinding"
}, },
{
"name": "throwCyclicDependencyError"
},
{ {
"name": "throwErrorIfNoChangesMode" "name": "throwErrorIfNoChangesMode"
}, },
@ -761,4 +773,4 @@
{ {
"name": "ɵ0" "name": "ɵ0"
} }
] ]

View File

@ -11,13 +11,13 @@ import {ChangeDetectorRef, ElementRef, TemplateRef, ViewContainerRef} from '@ang
import {defineComponent} from '../../src/render3/definition'; import {defineComponent} from '../../src/render3/definition';
import {InjectFlags, bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di'; import {InjectFlags, bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector, injectAttribute} from '../../src/render3/di';
import {NgOnChangesFeature, PublicFeature, defineDirective, directiveInject, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index'; 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 {LInjector} from '../../src/render3/interfaces/injector';
import {LNodeType} from '../../src/render3/interfaces/node'; import {LNodeType} from '../../src/render3/interfaces/node';
import {LViewFlags} from '../../src/render3/interfaces/view'; import {LViewFlags} from '../../src/render3/interfaces/view';
import {ViewRef} from '../../src/render3/view_ref'; 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('di', () => {
describe('no dependencies', () => { describe('no dependencies', () => {
@ -47,35 +47,41 @@ describe('di', () => {
}); });
}); });
describe('view dependencies', () => { describe('directive injection', () => {
it('should create directive with inter view dependencies', () => { let log: string[] = [];
class DirectiveA {
value: string = 'A'; class DirB {
value = 'DirB';
constructor() { log.push(this.value); }
static ngDirectiveDef = defineDirective({
selectors: [['', 'dirB', '']],
type: DirB,
factory: () => new DirB(),
features: [PublicFeature]
});
}
beforeEach(() => log = []);
it('should create directive with intra view dependencies', () => {
class DirA {
value: string = 'DirA';
static ngDirectiveDef = defineDirective({ static ngDirectiveDef = defineDirective({
type: DirectiveA, type: DirA,
selectors: [['', 'dirA', '']], selectors: [['', 'dirA', '']],
factory: () => new DirectiveA, factory: () => new DirA,
features: [PublicFeature] features: [PublicFeature]
}); });
} }
class DirectiveB { class DirC {
value: string = 'B';
static ngDirectiveDef = defineDirective({
type: DirectiveB,
selectors: [['', 'dirB', '']],
factory: () => new DirectiveB,
features: [PublicFeature]
});
}
class DirectiveC {
value: string; 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({ static ngDirectiveDef = defineDirective({
type: DirectiveC, type: DirC,
selectors: [['', 'dirC', '']], selectors: [['', 'dirC', '']],
factory: () => new DirectiveC(directiveInject(DirectiveA), directiveInject(DirectiveB)), factory: () => new DirC(directiveInject(DirA), directiveInject(DirB)),
exportAs: 'dirC' exportAs: 'dirC'
}); });
} }
@ -99,10 +105,381 @@ describe('di', () => {
textBinding(3, bind(tmp.value)); textBinding(3, bind(tmp.value));
} }
const defs = [DirectiveA, DirectiveB, DirectiveC]; const defs = [DirA, DirB, DirC];
expect(renderToHtml(Template, {}, defs)) expect(renderToHtml(Template, {}, defs))
.toEqual('<div dira=""><span dirb="" dirc="">AB</span></div>'); .toEqual('<div dira=""><span dirb="" dirc="">DirADirB</span></div>');
}); });
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)),
});
}
/** <div dirA dirB></div> */
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) => {}
});
}
/** <comp dirB></comp> */
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++) {
* <div dirA dirB></div>
* % }
*/
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]
});
}
/** <div dirA dirB dirC></div> */
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]
});
}
/** <comp dirA dirB dirC dirD></comp> */
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(),
/** <div dirA dirB dirC></div> */
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],
});
}
/** <div dirA dirB></div> */
const Parent = createComponent('parent', function(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, 'div', ['dirA', '', 'dirB', '']);
elementEnd();
}
}, [DirA, DirB]);
/** <parent dirB></parent> */
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]
});
}
/** <div dir></div> */
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]
});
}
/** <div dirA dirB></div> */
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]
});
}
/** <div dir></div> */
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', () => { describe('ElementRef', () => {
@ -283,7 +660,9 @@ describe('di', () => {
class Directive { class Directive {
value: string; value: string;
constructor(public cdr: ChangeDetectorRef) { this.value = (cdr.constructor as any).name; } constructor(public cdr: ChangeDetectorRef) { this.value = (cdr.constructor as any).name; }
static ngDirectiveDef = defineDirective({ static ngDirectiveDef = defineDirective({
type: Directive, type: Directive,
selectors: [['', 'dir', '']], selectors: [['', 'dir', '']],
@ -504,24 +883,23 @@ describe('di', () => {
expect(dir !.cdr).toBe(app.cdr); expect(dir !.cdr).toBe(app.cdr);
expect(dir !.cdr).toBe(dirSameInstance !.cdr); expect(dir !.cdr).toBe(dirSameInstance !.cdr);
}); });
});
it('should injectAttribute', () => { it('should injectAttribute', () => {
let exist: string|undefined = 'wrong'; let exist: string|undefined = 'wrong';
let nonExist: string|undefined = 'wrong'; let nonExist: string|undefined = 'wrong';
const MyApp = createComponent('my-app', function(ctx: any, cm: boolean) { const MyApp = createComponent('my-app', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']); elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']);
exist = injectAttribute('exist'); exist = injectAttribute('exist');
nonExist = injectAttribute('nonExist'); nonExist = injectAttribute('nonExist');
} }
});
const app = renderComponent(MyApp);
expect(exist).toEqual('existValue');
expect(nonExist).toEqual(undefined);
}); });
const app = renderComponent(MyApp);
expect(exist).toEqual('existValue');
expect(nonExist).toEqual(undefined);
}); });
describe('inject', () => { describe('inject', () => {