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 * 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(

View File

@ -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([