fix(compiler): query `<template>` elements before their children. (#13677)

Fixes #13118
Closes #13167
This commit is contained in:
Tobias Bosch 2016-12-27 16:28:54 -08:00 committed by Hans
parent 07e0fce8fc
commit 7c210645a3
7 changed files with 107 additions and 48 deletions

View File

@ -210,12 +210,7 @@ export class CompileElement extends CompileNode {
const directiveInstance = this.instances.get(tokenReference(identifierToken(directive.type)));
directive.queries.forEach((queryMeta) => { this._addQuery(queryMeta, directiveInstance); });
}
const queriesWithReads: _QueryWithRead[] = [];
Array.from(this._resolvedProviders.values()).forEach((resolvedProvider) => {
const queriesForProvider = this._getQueriesFor(resolvedProvider.token);
queriesWithReads.push(
...queriesForProvider.map(query => new _QueryWithRead(query, resolvedProvider.token)));
});
Object.keys(this.referenceTokens).forEach(varName => {
const token = this.referenceTokens[varName];
let varValue: o.Expression;
@ -225,27 +220,6 @@ export class CompileElement extends CompileNode {
varValue = this.renderNode;
}
this.view.locals.set(varName, varValue);
const varToken = {value: varName};
queriesWithReads.push(
...this._getQueriesFor(varToken).map(query => new _QueryWithRead(query, varToken)));
});
queriesWithReads.forEach((queryWithRead) => {
let value: o.Expression;
if (isPresent(queryWithRead.read.identifier)) {
// query for an identifier
value = this.instances.get(tokenReference(queryWithRead.read));
} else {
// query for a reference
const token = this.referenceTokens[queryWithRead.read.value];
if (isPresent(token)) {
value = this.instances.get(tokenReference(token));
} else {
value = this.elementRef;
}
}
if (isPresent(value)) {
queryWithRead.query.addValue(value, this.view);
}
});
}
@ -264,12 +238,14 @@ export class CompileElement extends CompileNode {
this.view.injectorGetMethod.addStmt(createInjectInternalCondition(
this.nodeIndex, providerChildNodeCount, resolvedProvider, providerExpr));
});
}
finish() {
Array.from(this._queries.values())
.forEach(
queries => queries.forEach(
q =>
q.afterChildren(this.view.createMethod, this.view.updateContentQueriesMethod)));
q => q.generateStatements(
this.view.createMethod, this.view.updateContentQueriesMethod)));
}
addContentNode(ngContentIndex: number, nodeExpr: CompileViewRootNode) {
@ -282,12 +258,11 @@ export class CompileElement extends CompileNode {
null;
}
getProviderTokens(): o.Expression[] {
return Array.from(this._resolvedProviders.values())
.map((resolvedProvider) => createDiTokenExpression(resolvedProvider.token));
getProviderTokens(): CompileTokenMetadata[] {
return Array.from(this._resolvedProviders.values()).map(provider => provider.token);
}
private _getQueriesFor(token: CompileTokenMetadata): CompileQuery[] {
getQueriesFor(token: CompileTokenMetadata): CompileQuery[] {
const result: CompileQuery[] = [];
let currentEl: CompileElement = this;
let distance = 0;
@ -425,10 +400,3 @@ function createProviderProperty(
}
return o.THIS_EXPR.prop(propName);
}
class _QueryWithRead {
public read: CompileTokenMetadata;
constructor(public query: CompileQuery, match: CompileTokenMetadata) {
this.read = query.meta.read || match;
}
}

View File

@ -64,7 +64,7 @@ export class CompileQuery {
return !this._values.values.some(value => value instanceof ViewQueryValues);
}
afterChildren(targetStaticMethod: CompileMethod, targetDynamicMethod: CompileMethod) {
generateStatements(targetStaticMethod: CompileMethod, targetDynamicMethod: CompileMethod) {
const values = createQueryValues(this._values);
const updateStmts = [this.queryList.callMethod('reset', [o.literalArr(values)]).toStmt()];
if (isPresent(this.ownerDirectiveExpression)) {

View File

@ -154,11 +154,11 @@ export class CompileView implements NameResolver {
}
}
afterNodes() {
finish() {
Array.from(this.viewQueries.values())
.forEach(
queries => queries.forEach(
q => q.afterChildren(this.createMethod, this.updateViewQueriesMethod)));
q => q.generateStatements(this.createMethod, this.updateViewQueriesMethod)));
}
}

View File

@ -0,0 +1,55 @@
/**
* @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 {CompileQueryMetadata, CompileTokenMetadata, tokenReference} from '../compile_metadata';
import * as o from '../output/output_ast';
import {CompileElement} from './compile_element';
import {CompileQuery} from './compile_query';
// Note: We can't do this when we create the CompileElements already,
// as we create embedded views before the <template> elements themselves.
export function bindQueryValues(ce: CompileElement) {
const queriesWithReads: _QueryWithRead[] = [];
ce.getProviderTokens().forEach((token) => {
const queriesForProvider = ce.getQueriesFor(token);
queriesWithReads.push(...queriesForProvider.map(query => new _QueryWithRead(query, token)));
});
Object.keys(ce.referenceTokens).forEach(varName => {
const token = ce.referenceTokens[varName];
const varToken = {value: varName};
queriesWithReads.push(
...ce.getQueriesFor(varToken).map(query => new _QueryWithRead(query, varToken)));
});
queriesWithReads.forEach((queryWithRead) => {
let value: o.Expression;
if (queryWithRead.read.identifier) {
// query for an identifier
value = ce.instances.get(tokenReference(queryWithRead.read));
} else {
// query for a reference
const token = ce.referenceTokens[queryWithRead.read.value];
if (token) {
value = ce.instances.get(tokenReference(token));
} else {
value = ce.elementRef;
}
}
if (value) {
queryWithRead.query.addValue(value, ce.view);
}
});
}
class _QueryWithRead {
public read: CompileTokenMetadata;
constructor(public query: CompileQuery, match: CompileTokenMetadata) {
this.read = query.meta.read || match;
}
}

View File

@ -15,6 +15,7 @@ import {CompileView} from './compile_view';
import {bindOutputs} from './event_binder';
import {bindDirectiveAfterContentLifecycleCallbacks, bindDirectiveAfterViewLifecycleCallbacks, bindDirectiveWrapperLifecycleCallbacks, bindInjectableDestroyLifecycleCallbacks, bindPipeDestroyLifecycleCallbacks} from './lifecycle_binder';
import {bindDirectiveHostProps, bindDirectiveInputs, bindRenderInputs, bindRenderText} from './property_binder';
import {bindQueryValues} from './query_binder';
export function bindView(
view: CompileView, parsedTemplate: TemplateAst[], schemaRegistry: ElementSchemaRegistry): void {
@ -43,6 +44,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
visitElement(ast: ElementAst, parent: CompileElement): any {
const compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
bindQueryValues(compileElement);
const hasEvents = bindOutputs(ast.outputs, ast.directives, compileElement, true);
bindRenderInputs(ast.inputs, ast.outputs, hasEvents, compileElement);
ast.directives.forEach((directiveAst, dirIndex) => {
@ -75,6 +77,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, parent: CompileElement): any {
const compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
bindQueryValues(compileElement);
bindOutputs(ast.outputs, ast.directives, compileElement, false);
ast.directives.forEach((directiveAst, dirIndex) => {
const directiveInstance = compileElement.instances.get(directiveAst.directive.type.reference);

View File

@ -48,13 +48,16 @@ export function buildView(
}
export function finishView(view: CompileView, targetStatements: o.Statement[]) {
view.afterNodes();
createViewTopLevelStmts(view, targetStatements);
view.nodes.forEach((node) => {
if (node instanceof CompileElement && node.hasEmbeddedView) {
finishView(node.embeddedView, targetStatements);
if (node instanceof CompileElement) {
node.finish();
if (node.hasEmbeddedView) {
finishView(node.embeddedView, targetStatements);
}
}
});
view.finish();
createViewTopLevelStmts(view, targetStatements);
}
class ViewBuilderVisitor implements TemplateAstVisitor {
@ -416,7 +419,9 @@ function createStaticNodeDebugInfo(node: CompileNode): o.Expression {
let componentToken: o.Expression = o.NULL_EXPR;
const varTokenEntries: any[] = [];
if (isPresent(compileElement)) {
providerTokens = compileElement.getProviderTokens();
providerTokens =
compileElement.getProviderTokens().map((token) => createDiTokenExpression(token));
if (isPresent(compileElement.component)) {
componentToken = createDiTokenExpression(identifierToken(compileElement.component.type));
}

View File

@ -43,6 +43,8 @@ export function main() {
NeedsContentChildWithRead,
NeedsViewChildrenWithRead,
NeedsViewChildWithRead,
NeedsContentChildTemplateRef,
NeedsContentChildTemplateRefApp,
NeedsViewContainerWithRead,
ManualProjecting
]
@ -262,6 +264,15 @@ export function main() {
expect(comp.textDirChild.text).toEqual('ca');
});
it('should contain the first descendant content child templateRef', () => {
const template = '<needs-content-child-template-ref-app>' +
'</needs-content-child-template-ref-app>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
view.detectChanges();
expect(view.nativeElement).toHaveText('OUTER');
});
it('should contain the first view child', () => {
const template = '<needs-view-child-read></needs-view-child-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
@ -730,6 +741,23 @@ class NeedsContentChildWithRead {
@ContentChild('nonExisting', {read: TextDirective}) nonExistingVar: TextDirective;
}
@Component({
selector: 'needs-content-child-template-ref',
template: '<div [ngTemplateOutlet]="templateRef"></div>'
})
class NeedsContentChildTemplateRef {
@ContentChild(TemplateRef) templateRef: TemplateRef<any>;
}
@Component({
selector: 'needs-content-child-template-ref-app',
template: '<needs-content-child-template-ref>' +
'<template>OUTER<template>INNER</template></template>' +
'</needs-content-child-template-ref>'
})
class NeedsContentChildTemplateRefApp {
}
@Component({
selector: 'needs-view-children-read',
template: '<div #q text="va"></div><div #w text="vb"></div>',