fix(ivy): fix inline template bindings parsing (#25272)

PR Close #25272
This commit is contained in:
Victor Berchet 2018-08-02 11:32:04 -07:00 committed by Kara Erickson
parent 1000fb8406
commit 2f4abbf5a1
2 changed files with 41 additions and 36 deletions

View File

@ -6,7 +6,7 @@
* 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 {replaceNgsp} from '../ml_parser/html_whitespaces';
import {isNgTemplate} from '../ml_parser/tags';
@ -15,6 +15,7 @@ import {isStyleUrlResolvable} from '../style_url_resolver';
import {BindingParser} from '../template_parser/binding_parser';
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
import {syntaxError} from '../util';
import * as t from './r3_ast';
@ -43,7 +44,6 @@ const IDENT_PROPERTY_IDX = 9;
const IDENT_EVENT_IDX = 10;
const TEMPLATE_ATTR_PREFIX = '*';
const CLASS_ATTR = 'class';
// Default selector used by `<ng-content>` if none specified
const DEFAULT_CONTENT_SELECTOR = '*';
@ -107,15 +107,12 @@ class HtmlAstToIvyAst implements html.Visitor {
// Whether the element is a `<ng-template>`
const isTemplateElement = isNgTemplate(element.name);
const matchableAttributes: [string, string][] = [];
const parsedProperties: ParsedProperty[] = [];
const boundEvents: t.BoundEvent[] = [];
const variables: t.Variable[] = [];
const references: t.Reference[] = [];
const attributes: t.TextAttribute[] = [];
const templateMatchableAttributes: [string, string][] = [];
let inlineTemplateSourceSpan: ParseSourceSpan;
const templateParsedProperties: ParsedProperty[] = [];
const templateVariables: t.Variable[] = [];
@ -130,6 +127,7 @@ class HtmlAstToIvyAst implements html.Visitor {
let isTemplateBinding = false;
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
// *-attributes
if (elementHasInlineTemplate) {
this.reportError(
`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 templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan;
const parsedVariables: ParsedVariable[] = [];
this.bindingParser.parseInlineTemplateBinding(
templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes,
templateParsedProperties, parsedVariables);
templateKey, templateValue, attribute.sourceSpan, [], templateParsedProperties,
parsedVariables);
templateVariables.push(
...parsedVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));
} else {
// Check for variables, events, property bindings, interpolation
hasBinding = this.parseAttribute(
isTemplateElement, attribute, matchableAttributes, parsedProperties, boundEvents,
variables, references);
isTemplateElement, attribute, [], parsedProperties, boundEvents, variables, references);
}
if (!hasBinding && !isTemplateBinding) {
// don't include the bindings as attributes as well in the AST
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;
let attributes: t.TextAttribute[] = element.attrs.map(attribute => {
return new t.TextAttribute(
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
});
let attributes: t.TextAttribute[] =
element.attrs.map(attribute => this.visitAttribute(attribute));
const selectorIndex =
selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector);
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan);
} else if (isTemplateElement) {
// `<ng-template>`
const boundAttributes = this.createBoundAttributes(element.name, parsedProperties);
const attrs = this.extractAttributes(element.name, parsedProperties);
parsedElement = new t.Template(
attributes, boundAttributes, children, references, variables, element.sourceSpan,
attributes, attrs.bound, children, references, variables, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
} else {
const boundAttributes = this.createBoundAttributes(element.name, parsedProperties);
const attrs = this.extractAttributes(element.name, parsedProperties);
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);
}
if (elementHasInlineTemplate) {
const attributes: t.TextAttribute[] = [];
templateMatchableAttributes.forEach(
([name, value]) =>
attributes.push(new t.TextAttribute(name, value, inlineTemplateSourceSpan)));
const boundAttributes = this.createBoundAttributes('ng-template', templateParsedProperties);
const attrs = this.extractAttributes('ng-template', templateParsedProperties);
parsedElement = new t.Template(
attributes, boundAttributes, [parsedElement], [], templateVariables, element.sourceSpan,
attrs.literal, attrs.bound, [parsedElement], [], templateVariables, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
return parsedElement;
}
visitAttribute(attribute: html.Attribute): t.Node {
visitAttribute(attribute: html.Attribute): t.TextAttribute {
return new t.TextAttribute(
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
}
@ -230,11 +217,22 @@ class HtmlAstToIvyAst implements html.Visitor {
visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; }
private createBoundAttributes(elementName: string, properties: ParsedProperty[]):
t.BoundAttribute[] {
return properties.filter(prop => !prop.isLiteral)
.map(prop => this.bindingParser.createBoundElementProperty(elementName, prop))
.map(prop => t.BoundAttribute.fromBoundElementProperty(prop));
// convert view engine `ParsedProperty` to a format suitable for IVY
private extractAttributes(elementName: string, properties: ParsedProperty[]):
{bound: t.BoundAttribute[], literal: t.TextAttribute[]} {
const bound: t.BoundAttribute[] = [];
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(

View File

@ -300,6 +300,15 @@ describe('R3 template transform', () => {
});
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 ...', () => {
expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([
['Template'],
@ -312,7 +321,6 @@ describe('R3 template transform', () => {
it('should parse variables via as ...', () => {
expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([
['Template'],
['TextAttribute', 'ngIf', 'expr '],
['BoundAttribute', BindingType.Property, 'ngIf', 'expr'],
['Variable', 'local', 'ngIf'],
['Element', 'div'],
@ -439,7 +447,6 @@ describe('R3 template transform', () => {
it('should parse ngProjectAs as an attribute', () => {
const res = parse('<ng-content ngProjectAs="a"></ng-content>');
const selectors = [''];
expect(res.hasNgContent).toEqual(true);
expect(res.ngContentSelectors).toEqual([]);
expectFromR3Nodes(res.nodes).toEqual([