fix(compiler-cli): Handle ng-template with structural directive in indexer (#44788)

An `ng-template` with an inline template (i.e. has a structural
directive) would previously not get an `undefined` `tagName` because the
logic assumed the element would be `t.Element` or `t.Content` and read
the tag name from the `name` property. For a `t.Template`, this exists
instead on the `t.tagName`. The final result would be an `tagName` of `undefined`
for the parent `t.Template`, causing failures in the indexer downstream.

This `undefined` value is actually expected in the renderer code, even
though the type does not specify this possibility. This change updates
the type of `tagName` to be `string|null` and explicitly handles the
case where there is a structural directive on an `ng-template`. You can
see how the two are differentiated in the compliance code that was
modified in this commit.

PR Close #44788
This commit is contained in:
Andrew Scott
2022-01-21 11:51:40 -08:00
committed by Andrew Kushnir
parent 6cb7c3ebe3
commit fe3e4d6865
10 changed files with 55 additions and 17 deletions
@@ -229,7 +229,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor {
let name: string;
let kind: IdentifierKind.Element|IdentifierKind.Template;
if (node instanceof TmplAstTemplate) {
name = node.tagName;
name = node.tagName ?? 'ng-template';
kind = IdentifierKind.Template;
} else {
name = node.name;
@@ -75,6 +75,20 @@ runInEachFileSystem(() => {
});
});
it('works when structural directives are on templates', () => {
const template = '<ng-template *ngIf="true">';
const refs = getTemplateIdentifiers(bind(template));
const [ref] = Array.from(refs);
expect(ref).toEqual({
kind: IdentifierKind.Template,
name: 'ng-template',
span: new AbsoluteSourceSpan(1, 12),
usedDirectives: new Set(),
attributes: new Set(),
});
});
it('should generate nothing in empty template', () => {
const template = '';
const refs = getTemplateIdentifiers(bind(template));
@@ -20,7 +20,7 @@ consts: function() {
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 1, undefined, 0);
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 1, null, 0);
}
if (rf & 2) {
$r3$.ɵɵproperty("ngIf", true);
@@ -19,7 +19,7 @@ consts: function() {
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 1, 0, undefined, 0);
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 1, 0, null, 0);
}
if (rf & 2) {
$r3$.ɵɵproperty("ngIf", ctx.visible);
@@ -31,7 +31,7 @@ consts: function() {
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 1, 0, undefined, 0);
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 1, 0, null, 0);
$r3$.ɵɵtemplate(1, MyComponent_ng_container_1_Template, 2, 0, "ng-container", 0);
}
if (rf & 2) {
@@ -1,7 +1,7 @@
consts: [[4, "ngIf"], [3, "dir"]],
template: function TestComp_Template(rf, ctx) {
if (rf & 1) {
$i0$.ɵɵtemplate(0, $TestComp_0_Template$, 1, 1, undefined, 0);
$i0$.ɵɵtemplate(0, $TestComp_0_Template$, 1, 1, null, 0);
}
if (rf & 2) {
$i0$.ɵɵproperty("ngIf", true);
+17 -5
View File
@@ -119,11 +119,23 @@ export class Element implements Node {
export class Template implements Node {
constructor(
public tagName: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
public outputs: BoundEvent[], public templateAttrs: (BoundAttribute|TextAttribute)[],
public children: Node[], public references: Reference[], public variables: Variable[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nMeta) {}
// tagName is the name of the container element, if applicable.
// `null` is a special case for when there is a structural directive on an `ng-template` so
// the renderer can differentiate between the synthetic template and the one written in the
// file.
public tagName: string|null,
public attributes: TextAttribute[],
public inputs: BoundAttribute[],
public outputs: BoundEvent[],
public templateAttrs: (BoundAttribute|TextAttribute)[],
public children: Node[],
public references: Reference[],
public variables: Variable[],
public sourceSpan: ParseSourceSpan,
public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan|null,
public i18n?: I18nMeta,
) {}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitTemplate(this);
}
@@ -186,7 +186,7 @@ class HtmlAstToIvyAst implements html.Visitor {
const children: t.Node[] =
html.visitAll(preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children);
let parsedElement: t.Node|undefined;
let parsedElement: t.Content|t.Template|t.Element|undefined;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
// `<ng-content>`
if (element.children &&
@@ -235,13 +235,12 @@ class HtmlAstToIvyAst implements html.Visitor {
// the wrapping template to prevent unnecessary i18n instructions from being generated. The
// necessary i18n meta information will be extracted from child elements.
const i18n = isTemplateElement && isI18nRootElement ? undefined : element.i18n;
const name = parsedElement instanceof t.Template ? null : parsedElement.name;
// TODO(pk): test for this case
parsedElement = new t.Template(
(parsedElement as t.Element | t.Content).name, hoistedAttrs.attributes,
hoistedAttrs.inputs, hoistedAttrs.outputs, templateAttrs, [parsedElement],
[/* no references */], templateVariables, element.sourceSpan, element.startSourceSpan,
element.endSourceSpan, i18n);
name, hoistedAttrs.attributes, hoistedAttrs.inputs, hoistedAttrs.outputs, templateAttrs,
[parsedElement], [/* no references */], templateVariables, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan, i18n);
}
if (isI18nRootElement) {
this.inI18nBlock = false;
@@ -9,6 +9,7 @@
import {BindingType} from '../../src/expression_parser/ast';
import * as t from '../../src/render3/r3_ast';
import {unparse} from '../expression_parser/utils/unparser';
import {parseR3 as parse} from './view/util';
@@ -272,6 +273,18 @@ describe('R3 template transform', () => {
]);
});
it('should support <ng-template> with structural directive', () => {
expectFromHtml('<ng-template *ngIf="true"></ng-template>').toEqual([
['Template'],
['BoundAttribute', 0, 'ngIf', 'true'],
['Template'],
]);
const res = parse('<ng-template *ngIf="true"></ng-template>', {ignoreError: false});
expect((res.nodes[0] as t.Template).tagName).toEqual(null);
expect(((res.nodes[0] as t.Template).children[0] as t.Template).tagName)
.toEqual('ng-template');
});
it('should support reference via #...', () => {
expectFromHtml('<ng-template #a></ng-template>').toEqual([
['Template'],
+1 -1
View File
@@ -183,7 +183,7 @@ function toAttributeCssSelector(attribute: t.TextAttribute|t.BoundAttribute|t.Bo
}
function getNodeName(node: t.Template|t.Element): string {
return node instanceof t.Template ? node.tagName : node.name;
return node instanceof t.Template ? (node.tagName ?? 'ng-template') : node.name;
}
/**