fix(ivy): fix inline template bindings parsing (#25272)
PR Close #25272
This commit is contained in:
parent
1000fb8406
commit
2f4abbf5a1
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ParsedEvent, ParsedProperty, ParsedVariable, ParserError} from '../expression_parser/ast';
|
import {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
|
||||||
import * as html from '../ml_parser/ast';
|
import * as html from '../ml_parser/ast';
|
||||||
import {replaceNgsp} from '../ml_parser/html_whitespaces';
|
import {replaceNgsp} from '../ml_parser/html_whitespaces';
|
||||||
import {isNgTemplate} from '../ml_parser/tags';
|
import {isNgTemplate} from '../ml_parser/tags';
|
||||||
|
@ -15,6 +15,7 @@ import {isStyleUrlResolvable} from '../style_url_resolver';
|
||||||
import {BindingParser} from '../template_parser/binding_parser';
|
import {BindingParser} from '../template_parser/binding_parser';
|
||||||
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
|
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
|
||||||
import {syntaxError} from '../util';
|
import {syntaxError} from '../util';
|
||||||
|
|
||||||
import * as t from './r3_ast';
|
import * as t from './r3_ast';
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +44,6 @@ const IDENT_PROPERTY_IDX = 9;
|
||||||
const IDENT_EVENT_IDX = 10;
|
const IDENT_EVENT_IDX = 10;
|
||||||
|
|
||||||
const TEMPLATE_ATTR_PREFIX = '*';
|
const TEMPLATE_ATTR_PREFIX = '*';
|
||||||
const CLASS_ATTR = 'class';
|
|
||||||
// Default selector used by `<ng-content>` if none specified
|
// Default selector used by `<ng-content>` if none specified
|
||||||
const DEFAULT_CONTENT_SELECTOR = '*';
|
const DEFAULT_CONTENT_SELECTOR = '*';
|
||||||
|
|
||||||
|
@ -107,15 +107,12 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
// Whether the element is a `<ng-template>`
|
// Whether the element is a `<ng-template>`
|
||||||
const isTemplateElement = isNgTemplate(element.name);
|
const isTemplateElement = isNgTemplate(element.name);
|
||||||
|
|
||||||
const matchableAttributes: [string, string][] = [];
|
|
||||||
const parsedProperties: ParsedProperty[] = [];
|
const parsedProperties: ParsedProperty[] = [];
|
||||||
const boundEvents: t.BoundEvent[] = [];
|
const boundEvents: t.BoundEvent[] = [];
|
||||||
const variables: t.Variable[] = [];
|
const variables: t.Variable[] = [];
|
||||||
const references: t.Reference[] = [];
|
const references: t.Reference[] = [];
|
||||||
const attributes: t.TextAttribute[] = [];
|
const attributes: t.TextAttribute[] = [];
|
||||||
|
|
||||||
const templateMatchableAttributes: [string, string][] = [];
|
|
||||||
let inlineTemplateSourceSpan: ParseSourceSpan;
|
|
||||||
const templateParsedProperties: ParsedProperty[] = [];
|
const templateParsedProperties: ParsedProperty[] = [];
|
||||||
const templateVariables: t.Variable[] = [];
|
const templateVariables: t.Variable[] = [];
|
||||||
|
|
||||||
|
@ -130,6 +127,7 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
let isTemplateBinding = false;
|
let isTemplateBinding = false;
|
||||||
|
|
||||||
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
||||||
|
// *-attributes
|
||||||
if (elementHasInlineTemplate) {
|
if (elementHasInlineTemplate) {
|
||||||
this.reportError(
|
this.reportError(
|
||||||
`Can't have multiple template bindings on one element. Use only one attribute prefixed with *`,
|
`Can't have multiple template bindings on one element. Use only one attribute prefixed with *`,
|
||||||
|
@ -140,25 +138,21 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
const templateValue = attribute.value;
|
const templateValue = attribute.value;
|
||||||
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
|
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
|
||||||
|
|
||||||
inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan;
|
|
||||||
|
|
||||||
const parsedVariables: ParsedVariable[] = [];
|
const parsedVariables: ParsedVariable[] = [];
|
||||||
this.bindingParser.parseInlineTemplateBinding(
|
this.bindingParser.parseInlineTemplateBinding(
|
||||||
templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes,
|
templateKey, templateValue, attribute.sourceSpan, [], templateParsedProperties,
|
||||||
templateParsedProperties, parsedVariables);
|
parsedVariables);
|
||||||
templateVariables.push(
|
templateVariables.push(
|
||||||
...parsedVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));
|
...parsedVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));
|
||||||
} else {
|
} else {
|
||||||
// Check for variables, events, property bindings, interpolation
|
// Check for variables, events, property bindings, interpolation
|
||||||
hasBinding = this.parseAttribute(
|
hasBinding = this.parseAttribute(
|
||||||
isTemplateElement, attribute, matchableAttributes, parsedProperties, boundEvents,
|
isTemplateElement, attribute, [], parsedProperties, boundEvents, variables, references);
|
||||||
variables, references);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasBinding && !isTemplateBinding) {
|
if (!hasBinding && !isTemplateBinding) {
|
||||||
// don't include the bindings as attributes as well in the AST
|
// don't include the bindings as attributes as well in the AST
|
||||||
attributes.push(this.visitAttribute(attribute) as t.TextAttribute);
|
attributes.push(this.visitAttribute(attribute) as t.TextAttribute);
|
||||||
matchableAttributes.push([attribute.name, attribute.value]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,44 +170,37 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
|
|
||||||
const selector = preparsedElement.selectAttr;
|
const selector = preparsedElement.selectAttr;
|
||||||
|
|
||||||
let attributes: t.TextAttribute[] = element.attrs.map(attribute => {
|
let attributes: t.TextAttribute[] =
|
||||||
return new t.TextAttribute(
|
element.attrs.map(attribute => this.visitAttribute(attribute));
|
||||||
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectorIndex =
|
const selectorIndex =
|
||||||
selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector);
|
selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector);
|
||||||
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan);
|
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan);
|
||||||
} else if (isTemplateElement) {
|
} else if (isTemplateElement) {
|
||||||
// `<ng-template>`
|
// `<ng-template>`
|
||||||
const boundAttributes = this.createBoundAttributes(element.name, parsedProperties);
|
const attrs = this.extractAttributes(element.name, parsedProperties);
|
||||||
|
|
||||||
parsedElement = new t.Template(
|
parsedElement = new t.Template(
|
||||||
attributes, boundAttributes, children, references, variables, element.sourceSpan,
|
attributes, attrs.bound, children, references, variables, element.sourceSpan,
|
||||||
element.startSourceSpan, element.endSourceSpan);
|
element.startSourceSpan, element.endSourceSpan);
|
||||||
} else {
|
} else {
|
||||||
const boundAttributes = this.createBoundAttributes(element.name, parsedProperties);
|
const attrs = this.extractAttributes(element.name, parsedProperties);
|
||||||
|
|
||||||
parsedElement = new t.Element(
|
parsedElement = new t.Element(
|
||||||
element.name, attributes, boundAttributes, boundEvents, children, references,
|
element.name, attributes, attrs.bound, boundEvents, children, references,
|
||||||
element.sourceSpan, element.startSourceSpan, element.endSourceSpan);
|
element.sourceSpan, element.startSourceSpan, element.endSourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementHasInlineTemplate) {
|
if (elementHasInlineTemplate) {
|
||||||
const attributes: t.TextAttribute[] = [];
|
const attrs = this.extractAttributes('ng-template', templateParsedProperties);
|
||||||
|
|
||||||
templateMatchableAttributes.forEach(
|
|
||||||
([name, value]) =>
|
|
||||||
attributes.push(new t.TextAttribute(name, value, inlineTemplateSourceSpan)));
|
|
||||||
|
|
||||||
const boundAttributes = this.createBoundAttributes('ng-template', templateParsedProperties);
|
|
||||||
parsedElement = new t.Template(
|
parsedElement = new t.Template(
|
||||||
attributes, boundAttributes, [parsedElement], [], templateVariables, element.sourceSpan,
|
attrs.literal, attrs.bound, [parsedElement], [], templateVariables, element.sourceSpan,
|
||||||
element.startSourceSpan, element.endSourceSpan);
|
element.startSourceSpan, element.endSourceSpan);
|
||||||
}
|
}
|
||||||
return parsedElement;
|
return parsedElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitAttribute(attribute: html.Attribute): t.Node {
|
visitAttribute(attribute: html.Attribute): t.TextAttribute {
|
||||||
return new t.TextAttribute(
|
return new t.TextAttribute(
|
||||||
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
|
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
|
||||||
}
|
}
|
||||||
|
@ -230,11 +217,22 @@ class HtmlAstToIvyAst implements html.Visitor {
|
||||||
|
|
||||||
visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; }
|
visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; }
|
||||||
|
|
||||||
private createBoundAttributes(elementName: string, properties: ParsedProperty[]):
|
// convert view engine `ParsedProperty` to a format suitable for IVY
|
||||||
t.BoundAttribute[] {
|
private extractAttributes(elementName: string, properties: ParsedProperty[]):
|
||||||
return properties.filter(prop => !prop.isLiteral)
|
{bound: t.BoundAttribute[], literal: t.TextAttribute[]} {
|
||||||
.map(prop => this.bindingParser.createBoundElementProperty(elementName, prop))
|
const bound: t.BoundAttribute[] = [];
|
||||||
.map(prop => t.BoundAttribute.fromBoundElementProperty(prop));
|
const literal: t.TextAttribute[] = [];
|
||||||
|
|
||||||
|
properties.forEach(prop => {
|
||||||
|
if (prop.isLiteral) {
|
||||||
|
literal.push(new t.TextAttribute(prop.name, prop.expression.source || '', prop.sourceSpan));
|
||||||
|
} else {
|
||||||
|
const bep = this.bindingParser.createBoundElementProperty(elementName, prop);
|
||||||
|
bound.push(t.BoundAttribute.fromBoundElementProperty(bep));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {bound, literal};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseAttribute(
|
private parseAttribute(
|
||||||
|
|
|
@ -300,6 +300,15 @@ describe('R3 template transform', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('inline templates', () => {
|
describe('inline templates', () => {
|
||||||
|
it('should support attribute and bound attributes', () => {
|
||||||
|
expectFromHtml('<div *ngFor="item of items"></div>').toEqual([
|
||||||
|
['Template'],
|
||||||
|
['BoundAttribute', BindingType.Property, 'ngFor', 'item'],
|
||||||
|
['BoundAttribute', BindingType.Property, 'ngForOf', 'items'],
|
||||||
|
['Element', 'div'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should parse variables via let ...', () => {
|
it('should parse variables via let ...', () => {
|
||||||
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
|
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
|
||||||
['Template'],
|
['Template'],
|
||||||
|
@ -312,7 +321,6 @@ describe('R3 template transform', () => {
|
||||||
it('should parse variables via as ...', () => {
|
it('should parse variables via as ...', () => {
|
||||||
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
|
||||||
['Template'],
|
['Template'],
|
||||||
['TextAttribute', 'ngIf', 'expr '],
|
|
||||||
['BoundAttribute', BindingType.Property, 'ngIf', 'expr'],
|
['BoundAttribute', BindingType.Property, 'ngIf', 'expr'],
|
||||||
['Variable', 'local', 'ngIf'],
|
['Variable', 'local', 'ngIf'],
|
||||||
['Element', 'div'],
|
['Element', 'div'],
|
||||||
|
@ -439,7 +447,6 @@ describe('R3 template transform', () => {
|
||||||
|
|
||||||
it('should parse ngProjectAs as an attribute', () => {
|
it('should parse ngProjectAs as an attribute', () => {
|
||||||
const res = parse('<ng-content ngProjectAs="a"></ng-content>');
|
const res = parse('<ng-content ngProjectAs="a"></ng-content>');
|
||||||
const selectors = [''];
|
|
||||||
expect(res.hasNgContent).toEqual(true);
|
expect(res.hasNgContent).toEqual(true);
|
||||||
expect(res.ngContentSelectors).toEqual([]);
|
expect(res.ngContentSelectors).toEqual([]);
|
||||||
expectFromR3Nodes(res.nodes).toEqual([
|
expectFromR3Nodes(res.nodes).toEqual([
|
||||||
|
|
Loading…
Reference in New Issue