diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 46c4bbcebf..08977f7251 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ - +import {SecurityContext} from '../core'; +import {ParseSourceSpan} from '../parse_util'; export class ParserError { public message: string; @@ -663,3 +664,63 @@ export function visitAstChildren(ast: AST, visitor: AstVisitor, context?: any) { visitSafePropertyRead(ast) { visit(ast.receiver); }, }); } + + +// Bindings + +export class ParsedProperty { + public readonly isLiteral: boolean; + public readonly isAnimation: boolean; + + constructor( + public name: string, public expression: ASTWithSource, public type: ParsedPropertyType, + public sourceSpan: ParseSourceSpan) { + this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR; + this.isAnimation = this.type === ParsedPropertyType.ANIMATION; + } +} + +export enum ParsedPropertyType { + DEFAULT, + LITERAL_ATTR, + ANIMATION +} + +export const enum ParsedEventType { + // DOM or Directive event + Regular, + // Animation specific event + Animation, +} + +export class ParsedEvent { + // Regular events have a target + // Animation events have a phase + constructor( + public name: string, public targetOrPhase: string, public type: ParsedEventType, + public handler: AST, public sourceSpan: ParseSourceSpan) {} +} + +export class ParsedVariable { + constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} +} + +export const enum BoundElementBindingType { + // A regular binding to a property (e.g. `[property]="expression"`). + Property, + // A binding to an element attribute (e.g. `[attr.name]="expression"`). + Attribute, + // A binding to a CSS class (e.g. `[class.name]="condition"`). + Class, + // A binding to a style rule (e.g. `[style.rule]="expression"`). + Style, + // A binding to an animation reference (e.g. `[animate.key]="expression"`). + Animation, +} + +export class BoundElementProperty { + constructor( + public name: string, public type: BoundElementBindingType, + public securityContext: SecurityContext, public value: AST, public unit: string|null, + public sourceSpan: ParseSourceSpan) {} +} diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index c35a78dd53..029516eb98 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -7,7 +7,7 @@ */ import {SecurityContext} from '../core'; -import {AST} from '../expression_parser/ast'; +import {AST, BoundElementBindingType, BoundElementProperty, ParsedEvent, ParsedEventType} from '../expression_parser/ast'; import {ParseSourceSpan} from '../parse_util'; export interface Node { @@ -32,42 +32,17 @@ export class TextAttribute implements Node { visit(visitor: Visitor): Result { return visitor.visitAttribute(this); } } -/** - * Enumeration of types of property bindings. - */ -export enum PropertyBindingType { - - /** - * A normal binding to a property (e.g. `[property]="expression"`). - */ - Property, - - /** - * A binding to an element attribute (e.g. `[attr.name]="expression"`). - */ - Attribute, - - /** - * A binding to a CSS class (e.g. `[class.name]="condition"`). - */ - Class, - - /** - * A binding to a style rule (e.g. `[style.rule]="expression"`). - */ - Style, - - /** - * A binding to an animation reference (e.g. `[animate.key]="expression"`). - */ - Animation -} - export class BoundAttribute implements Node { constructor( - public name: string, public type: PropertyBindingType, + public name: string, public type: BoundElementBindingType, public securityContext: SecurityContext, public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {} + + static fromBoundElementProperty(prop: BoundElementProperty) { + return new BoundAttribute( + prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan); + } + visit(visitor: Visitor): Result { return visitor.visitBoundAttribute(this); } } @@ -75,6 +50,14 @@ export class BoundEvent implements Node { constructor( public name: string, public handler: AST, public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan) {} + + static fromParsedEvent(event: ParsedEvent) { + const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null; + const phase: string|null = + event.type === ParsedEventType.Animation ? event.targetOrPhase : null; + return new BoundEvent(event.name, event.handler, target, phase, event.sourceSpan); + } + visit(visitor: Visitor): Result { return visitor.visitBoundEvent(this); } } diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index 138975c59c..b5679db9d0 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -6,14 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +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'; import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; import {isStyleUrlResolvable} from '../style_url_resolver'; -import {BindingParser, BoundProperty} from '../template_parser/binding_parser'; -// TODO(chuckj): Refactor binding parser to not have a dependency on template_ast. -import {BoundEventAst, VariableAst} from '../template_parser/template_ast'; +import {BindingParser} from '../template_parser/binding_parser'; import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser'; import * as t from './r3_ast'; @@ -78,7 +77,7 @@ export class HtmlToTemplateTransform implements html.Visitor { const isTemplateElement = isNgTemplate(element.name); const matchableAttributes: [string, string][] = []; - const boundProperties: BoundProperty[] = []; + const parsedProperties: ParsedProperty[] = []; const boundEvents: t.BoundEvent[] = []; const variables: t.Variable[] = []; const references: t.Reference[] = []; @@ -86,7 +85,7 @@ export class HtmlToTemplateTransform implements html.Visitor { const templateMatchableAttributes: [string, string][] = []; let inlineTemplateSourceSpan: ParseSourceSpan; - const templateBoundProperties: BoundProperty[] = []; + const templateParsedProperties: ParsedProperty[] = []; const templateVariables: t.Variable[] = []; // Whether the element has any *-attribute @@ -110,20 +109,15 @@ export class HtmlToTemplateTransform implements html.Visitor { const templateValue = attribute.value; const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length); - const oldVariables: VariableAst[] = []; - inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan; this.bindingParser.parseInlineTemplateBinding( templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes, - templateBoundProperties, oldVariables); - - templateVariables.push( - ...oldVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan))); + templateParsedProperties, templateVariables); } else { // Check for variables, events, property bindings, interpolation hasBinding = this.parseAttribute( - isTemplateElement, attribute, matchableAttributes, boundProperties, boundEvents, + isTemplateElement, attribute, matchableAttributes, parsedProperties, boundEvents, variables, references); } @@ -158,12 +152,12 @@ export class HtmlToTemplateTransform implements html.Visitor { parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan); } else if (isTemplateElement) { // `` - const boundAttributes = this.createBoundAttributes(element.name, boundProperties); + const boundAttributes = this.createBoundAttributes(element.name, parsedProperties); parsedElement = new t.Template( attributes, boundAttributes, children, references, variables, element.sourceSpan, element.startSourceSpan, element.endSourceSpan); } else { - const boundAttributes = this.createBoundAttributes(element.name, boundProperties); + const boundAttributes = this.createBoundAttributes(element.name, parsedProperties); parsedElement = new t.Element( element.name, attributes, boundAttributes, boundEvents, children, references, @@ -177,7 +171,7 @@ export class HtmlToTemplateTransform implements html.Visitor { ([name, value]) => attributes.push(new t.TextAttribute(name, value, inlineTemplateSourceSpan))); - const boundAttributes = this.createBoundAttributes('ng-template', templateBoundProperties); + const boundAttributes = this.createBoundAttributes('ng-template', templateParsedProperties); parsedElement = new t.Template( attributes, boundAttributes, [parsedElement], [], templateVariables, element.sourceSpan, element.startSourceSpan, element.endSourceSpan); @@ -202,22 +196,16 @@ export class HtmlToTemplateTransform implements html.Visitor { visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; } - private createBoundAttributes(elementName: string, boundProperties: BoundProperty[]): + private createBoundAttributes(elementName: string, properties: ParsedProperty[]): t.BoundAttribute[] { - const literalProperties = boundProperties.filter(prop => !prop.isLiteral); - - return literalProperties.map(property => { - // TODO(vicb): get ride of the boundProperty (from TemplateAst) - const boundProp = this.bindingParser.createElementPropertyAst(elementName, property); - return new t.BoundAttribute( - boundProp.name, boundProp.type as any as t.PropertyBindingType, boundProp.securityContext, - boundProp.value, boundProp.unit, boundProp.sourceSpan); - }); + return properties.filter(prop => !prop.isLiteral) + .map(prop => this.bindingParser.createBoundElementProperty(elementName, prop)) + .map(prop => t.BoundAttribute.fromBoundElementProperty(prop)); } private parseAttribute( isTemplateElement: boolean, attribute: html.Attribute, matchableAttributes: string[][], - boundProperties: BoundProperty[], boundEvents: t.BoundEvent[], variables: t.Variable[], + parsedProperties: ParsedProperty[], boundEvents: t.BoundEvent[], variables: t.Variable[], references: t.Reference[]) { const name = normalizeAttributeName(attribute.name); const value = attribute.value; @@ -230,7 +218,7 @@ export class HtmlToTemplateTransform implements html.Visitor { hasBinding = true; if (bindParts[KW_BIND_IDX] != null) { this.bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, boundProperties); + bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, parsedProperties); } else if (bindParts[KW_LET_IDX]) { if (isTemplateElement) { @@ -245,40 +233,40 @@ export class HtmlToTemplateTransform implements html.Visitor { this.parseReference(identifier, value, srcSpan, references); } else if (bindParts[KW_ON_IDX]) { - const events: BoundEventAst[] = []; + const events: ParsedEvent[] = []; this.bindingParser.parseEvent( bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, events); addEvents(events, boundEvents); } else if (bindParts[KW_BINDON_IDX]) { this.bindingParser.parsePropertyBinding( - bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, boundProperties); + bindParts[IDENT_KW_IDX], value, false, srcSpan, matchableAttributes, parsedProperties); this.parseAssignmentEvent( bindParts[IDENT_KW_IDX], value, srcSpan, matchableAttributes, boundEvents); } else if (bindParts[KW_AT_IDX]) { this.bindingParser.parseLiteralAttr( - name, value, srcSpan, matchableAttributes, boundProperties); + name, value, srcSpan, matchableAttributes, parsedProperties); } else if (bindParts[IDENT_BANANA_BOX_IDX]) { this.bindingParser.parsePropertyBinding( bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, matchableAttributes, - boundProperties); + parsedProperties); this.parseAssignmentEvent( bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, matchableAttributes, boundEvents); } else if (bindParts[IDENT_PROPERTY_IDX]) { this.bindingParser.parsePropertyBinding( bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, matchableAttributes, - boundProperties); + parsedProperties); } else if (bindParts[IDENT_EVENT_IDX]) { - const events: BoundEventAst[] = []; + const events: ParsedEvent[] = []; this.bindingParser.parseEvent( bindParts[IDENT_EVENT_IDX], value, srcSpan, matchableAttributes, events); addEvents(events, boundEvents); } } else { hasBinding = this.bindingParser.parsePropertyInterpolation( - name, value, srcSpan, matchableAttributes, boundProperties); + name, value, srcSpan, matchableAttributes, parsedProperties); } return hasBinding; @@ -305,7 +293,7 @@ export class HtmlToTemplateTransform implements html.Visitor { private parseAssignmentEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], boundEvents: t.BoundEvent[]) { - const events: BoundEventAst[] = []; + const events: ParsedEvent[] = []; this.bindingParser.parseEvent( `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, events); addEvents(events, boundEvents); @@ -356,9 +344,8 @@ function normalizeAttributeName(attrName: string): string { return /^data-/i.test(attrName) ? attrName.substring(5) : attrName; } -function addEvents(events: BoundEventAst[], boundEvents: t.BoundEvent[]) { - boundEvents.push( - ...events.map(e => new t.BoundEvent(e.name, e.handler, e.target, e.phase, e.sourceSpan))); +function addEvents(events: ParsedEvent[], boundEvents: t.BoundEvent[]) { + boundEvents.push(...events.map(e => t.BoundEvent.fromParsedEvent(e))); } function isEmptyTextNode(node: html.Node): boolean { diff --git a/packages/compiler/src/render3/r3_view_compiler_local.ts b/packages/compiler/src/render3/r3_view_compiler_local.ts index cab255e72e..0b4f211cb2 100644 --- a/packages/compiler/src/render3/r3_view_compiler_local.ts +++ b/packages/compiler/src/render3/r3_view_compiler_local.ts @@ -12,6 +12,7 @@ import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, c import {ConstantPool, DefinitionKind} from '../constant_pool'; import * as core from '../core'; import {AST, AstMemoryEfficientTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../expression_parser/ast'; +import {BoundElementBindingType} from '../expression_parser/ast'; import {Identifiers} from '../identifiers'; import {LifecycleHooks} from '../lifecycle_reflector'; import * as o from '../output/output_ast'; @@ -24,6 +25,7 @@ import * as t from './r3_ast'; import {Identifiers as R3} from './r3_identifiers'; + /** Name of the context parameter passed into a template function */ const CONTEXT_NAME = 'ctx'; @@ -213,10 +215,10 @@ function unsupported(feature: string): never { } const BINDING_INSTRUCTION_MAP: {[type: number]: o.ExternalReference} = { - [t.PropertyBindingType.Property]: R3.elementProperty, - [t.PropertyBindingType.Attribute]: R3.elementAttribute, - [t.PropertyBindingType.Class]: R3.elementClassNamed, - [t.PropertyBindingType.Style]: R3.elementStyleNamed, + [BoundElementBindingType.Property]: R3.elementProperty, + [BoundElementBindingType.Attribute]: R3.elementAttribute, + [BoundElementBindingType.Class]: R3.elementClassNamed, + [BoundElementBindingType.Style]: R3.elementStyleNamed, }; function interpolate(args: o.Expression[]): o.Expression { @@ -679,7 +681,7 @@ class TemplateDefinitionBuilder implements t.Visitor, LocalResolver { // Generate element input bindings element.inputs.forEach((input: t.BoundAttribute) => { - if (input.type === t.PropertyBindingType.Animation) { + if (input.type === BoundElementBindingType.Animation) { this._unsupported('animations'); } const convertedBinding = this.convertPropertyBinding(implicit, input.value); @@ -691,7 +693,7 @@ class TemplateDefinitionBuilder implements t.Visitor, LocalResolver { this._bindingCode, input.sourceSpan, instruction, o.literal(elementIndex), o.literal(input.name), value); } else { - this._unsupported(`binding ${t.PropertyBindingType[input.type]}`); + this._unsupported(`binding type ${input.type}`); } }); diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index 66168a429c..ea493cf3c2 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -9,6 +9,7 @@ import {CompileDirectiveSummary, CompilePipeSummary} from '../compile_metadata'; import {SecurityContext} from '../core'; import {ASTWithSource, BindingPipe, EmptyExpr, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; +import {BoundElementBindingType, BoundElementProperty, ParsedEvent, ParsedEventType, ParsedProperty, ParsedPropertyType, ParsedVariable} from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {mergeNsAndName} from '../ml_parser/tags'; @@ -17,7 +18,7 @@ import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {CssSelector} from '../selector'; import {splitAtColon, splitAtPeriod} from '../util'; -import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast'; +import {BoundElementPropertyAst, PropertyBindingType} from './template_ast'; const PROPERTY_PARTS_SEPARATOR = '.'; const ATTRIBUTE_PREFIX = 'attr'; @@ -26,27 +27,6 @@ const STYLE_PREFIX = 'style'; const ANIMATE_PROP_PREFIX = 'animate-'; -export enum BoundPropertyType { - DEFAULT, - LITERAL_ATTR, - ANIMATION -} - -/** - * Represents a parsed property. - */ -export class BoundProperty { - public readonly isLiteral: boolean; - public readonly isAnimation: boolean; - - constructor( - public name: string, public expression: ASTWithSource, public type: BoundPropertyType, - public sourceSpan: ParseSourceSpan) { - this.isLiteral = this.type === BoundPropertyType.LITERAL_ATTR; - this.isAnimation = this.type === BoundPropertyType.ANIMATION; - } -} - /** * Parses bindings in templates and in the directive host area. */ @@ -70,9 +50,9 @@ export class BindingParser { getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); } createBoundHostProperties(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan): - BoundProperty[]|null { + ParsedProperty[]|null { if (dirMeta.hostProperties) { - const boundProps: BoundProperty[] = []; + const boundProps: ParsedProperty[] = []; Object.keys(dirMeta.hostProperties).forEach(propName => { const expression = dirMeta.hostProperties[propName]; if (typeof expression === 'string') { @@ -90,27 +70,27 @@ export class BindingParser { createDirectiveHostPropertyAsts( dirMeta: CompileDirectiveSummary, elementSelector: string, - sourceSpan: ParseSourceSpan): BoundElementPropertyAst[]|null { + sourceSpan: ParseSourceSpan): BoundElementProperty[]|null { const boundProps = this.createBoundHostProperties(dirMeta, sourceSpan); return boundProps && - boundProps.map((prop) => this.createElementPropertyAst(elementSelector, prop)); + boundProps.map((prop) => this.createBoundElementProperty(elementSelector, prop)); } createDirectiveHostEventAsts(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan): - BoundEventAst[]|null { + ParsedEvent[]|null { if (dirMeta.hostListeners) { - const targetEventAsts: BoundEventAst[] = []; + const targetEvents: ParsedEvent[] = []; Object.keys(dirMeta.hostListeners).forEach(propName => { const expression = dirMeta.hostListeners[propName]; if (typeof expression === 'string') { - this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts); + this.parseEvent(propName, expression, sourceSpan, [], targetEvents); } else { this._reportError( `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, sourceSpan); } }); - return targetEventAsts; + return targetEvents; } return null; } @@ -133,13 +113,14 @@ export class BindingParser { // Parse an inline template binding. ie `` parseInlineTemplateBinding( tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetVars: VariableAst[]) { + targetMatchableAttrs: string[][], targetProps: ParsedProperty[], + targetVars: ParsedVariable[]) { const bindings = this._parseTemplateBindings(tplKey, tplValue, sourceSpan); for (let i = 0; i < bindings.length; i++) { const binding = bindings[i]; if (binding.keyIsVar) { - targetVars.push(new VariableAst(binding.key, binding.name, sourceSpan)); + targetVars.push(new ParsedVariable(binding.key, binding.name, sourceSpan)); } else if (binding.expression) { this._parsePropertyAst( binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps); @@ -173,8 +154,8 @@ export class BindingParser { parseLiteralAttr( name: string, value: string|null, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { - if (_isAnimationLabel(name)) { + targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { + if (isAnimationLabel(name)) { name = name.substring(1); if (value) { this._reportError( @@ -184,20 +165,20 @@ export class BindingParser { } this._parseAnimation(name, value, sourceSpan, targetMatchableAttrs, targetProps); } else { - targetProps.push(new BoundProperty( - name, this._exprParser.wrapLiteralPrimitive(value, ''), BoundPropertyType.LITERAL_ATTR, + targetProps.push(new ParsedProperty( + name, this._exprParser.wrapLiteralPrimitive(value, ''), ParsedPropertyType.LITERAL_ATTR, sourceSpan)); } } parsePropertyBinding( name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { let isAnimationProp = false; if (name.startsWith(ANIMATE_PROP_PREFIX)) { isAnimationProp = true; name = name.substring(ANIMATE_PROP_PREFIX.length); - } else if (_isAnimationLabel(name)) { + } else if (isAnimationLabel(name)) { isAnimationProp = true; name = name.substring(1); } @@ -213,7 +194,7 @@ export class BindingParser { parsePropertyInterpolation( name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], - targetProps: BoundProperty[]): boolean { + targetProps: ParsedProperty[]): boolean { const expr = this.parseInterpolation(value, sourceSpan); if (expr) { this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps); @@ -224,20 +205,20 @@ export class BindingParser { private _parsePropertyAst( name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { targetMatchableAttrs.push([name, ast.source !]); - targetProps.push(new BoundProperty(name, ast, BoundPropertyType.DEFAULT, sourceSpan)); + targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.DEFAULT, sourceSpan)); } private _parseAnimation( name: string, expression: string|null, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + targetMatchableAttrs: string[][], targetProps: ParsedProperty[]) { // This will occur when a @trigger is not paired with an expression. // For animations it is valid to not have an expression since */void // states will be applied by angular when the element is attached/detached const ast = this._parseBinding(expression || 'undefined', false, sourceSpan); targetMatchableAttrs.push([name, ast.source !]); - targetProps.push(new BoundProperty(name, ast, BoundPropertyType.ANIMATION, sourceSpan)); + targetProps.push(new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan)); } private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan): @@ -257,16 +238,16 @@ export class BindingParser { } } - createElementPropertyAst(elementSelector: string, boundProp: BoundProperty): - BoundElementPropertyAst { + createBoundElementProperty(elementSelector: string, boundProp: ParsedProperty): + BoundElementProperty { if (boundProp.isAnimation) { - return new BoundElementPropertyAst( - boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression, - null, boundProp.sourceSpan); + return new BoundElementProperty( + boundProp.name, BoundElementBindingType.Animation, SecurityContext.NONE, + boundProp.expression, null, boundProp.sourceSpan); } let unit: string|null = null; - let bindingType: PropertyBindingType = undefined !; + let bindingType: BoundElementBindingType = undefined !; let boundPropertyName: string|null = null; const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); let securityContexts: SecurityContext[] = undefined !; @@ -286,15 +267,15 @@ export class BindingParser { boundPropertyName = mergeNsAndName(ns, name); } - bindingType = PropertyBindingType.Attribute; + bindingType = BoundElementBindingType.Attribute; } else if (parts[0] == CLASS_PREFIX) { boundPropertyName = parts[1]; - bindingType = PropertyBindingType.Class; + bindingType = BoundElementBindingType.Class; securityContexts = [SecurityContext.NONE]; } else if (parts[0] == STYLE_PREFIX) { unit = parts.length > 2 ? parts[2] : null; boundPropertyName = parts[1]; - bindingType = PropertyBindingType.Style; + bindingType = BoundElementBindingType.Style; securityContexts = [SecurityContext.STYLE]; } } @@ -304,29 +285,28 @@ export class BindingParser { boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name); securityContexts = calcPossibleSecurityContexts( this._schemaRegistry, elementSelector, boundPropertyName, false); - bindingType = PropertyBindingType.Property; + bindingType = BoundElementBindingType.Property; this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); } - return new BoundElementPropertyAst( + return new BoundElementProperty( boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit, boundProp.sourceSpan); } parseEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { - if (_isAnimationLabel(name)) { + targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { + if (isAnimationLabel(name)) { name = name.substr(1); this._parseAnimationEvent(name, expression, sourceSpan, targetEvents); } else { - this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); + this._parseRegularEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); } } private _parseAnimationEvent( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetEvents: BoundEventAst[]) { + name: string, expression: string, sourceSpan: ParseSourceSpan, targetEvents: ParsedEvent[]) { const matches = splitAtPeriod(name, [name, '']); const eventName = matches[0]; const phase = matches[1].toLowerCase(); @@ -335,7 +315,8 @@ export class BindingParser { case 'start': case 'done': const ast = this._parseAction(expression, sourceSpan); - targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan)); + targetEvents.push( + new ParsedEvent(eventName, phase, ParsedEventType.Animation, ast, sourceSpan)); break; default: @@ -351,14 +332,14 @@ export class BindingParser { } } - private _parseEvent( + private _parseRegularEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { // long format: 'target: eventName' const [target, eventName] = splitAtColon(name, [null !, name]); const ast = this._parseAction(expression, sourceSpan); targetMatchableAttrs.push([name !, ast.source !]); - targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan)); + targetEvents.push(new ParsedEvent(eventName, target, ParsedEventType.Regular, ast, sourceSpan)); // Don't detect directives for event names for now, // so don't add the event name to the matchableAttrs } @@ -439,7 +420,7 @@ export class PipeCollector extends RecursiveAstVisitor { } } -function _isAnimationLabel(name: string): boolean { +function isAnimationLabel(name: string): boolean { return name[0] == '@'; } diff --git a/packages/compiler/src/template_parser/template_ast.ts b/packages/compiler/src/template_parser/template_ast.ts index c09fb8ed09..438a8b491a 100644 --- a/packages/compiler/src/template_parser/template_ast.ts +++ b/packages/compiler/src/template_parser/template_ast.ts @@ -9,7 +9,7 @@ import {AstPath} from '../ast_path'; import {CompileDirectiveSummary, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata'; import {SecurityContext} from '../core'; -import {AST} from '../expression_parser/ast'; +import {AST, BoundElementBindingType, BoundElementProperty, ParsedEvent, ParsedEventType, ParsedVariable} from '../expression_parser/ast'; import {LifecycleHooks} from '../lifecycle_reflector'; import {ParseSourceSpan} from '../parse_util'; @@ -58,12 +58,33 @@ export class AttrAst implements TemplateAst { visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); } } +export enum PropertyBindingType { + // A normal binding to a property (e.g. `[property]="expression"`). + Property, + // A binding to an element attribute (e.g. `[attr.name]="expression"`). + Attribute, + // A binding to a CSS class (e.g. `[class.name]="condition"`). + Class, + // A binding to a style rule (e.g. `[style.rule]="expression"`). + Style, + // A binding to an animation reference (e.g. `[animate.key]="expression"`). + Animation, +} + +const BoundPropertyMapping = { + [BoundElementBindingType.Animation]: PropertyBindingType.Animation, + [BoundElementBindingType.Attribute]: PropertyBindingType.Attribute, + [BoundElementBindingType.Class]: PropertyBindingType.Class, + [BoundElementBindingType.Property]: PropertyBindingType.Property, + [BoundElementBindingType.Style]: PropertyBindingType.Style, +}; + /** * A binding for an element property (e.g. `[property]="expression"`) or an animation trigger (e.g. * `[@trigger]="stateExp"`) */ export class BoundElementPropertyAst implements TemplateAst { - public readonly isAnimation: boolean; + readonly isAnimation: boolean; constructor( public name: string, public type: PropertyBindingType, @@ -71,6 +92,13 @@ export class BoundElementPropertyAst implements TemplateAst { public sourceSpan: ParseSourceSpan) { this.isAnimation = this.type === PropertyBindingType.Animation; } + + static fromBoundProperty(prop: BoundElementProperty) { + const type = BoundPropertyMapping[prop.type]; + return new BoundElementPropertyAst( + prop.name, type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan); + } + visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitElementProperty(this, context); } @@ -81,18 +109,8 @@ export class BoundElementPropertyAst implements TemplateAst { * `(@trigger.phase)="callback($event)"`). */ export class BoundEventAst implements TemplateAst { - static calcFullName(name: string, target: string|null, phase: string|null): string { - if (target) { - return `${target}:${name}`; - } else if (phase) { - return `@${name}.${phase}`; - } else { - return name; - } - } - - public readonly fullName: string; - public readonly isAnimation: boolean; + readonly fullName: string; + readonly isAnimation: boolean; constructor( public name: string, public target: string|null, public phase: string|null, @@ -100,6 +118,25 @@ export class BoundEventAst implements TemplateAst { this.fullName = BoundEventAst.calcFullName(this.name, this.target, this.phase); this.isAnimation = !!this.phase; } + + static calcFullName(name: string, target: string|null, phase: string|null): string { + if (target) { + return `${target}:${name}`; + } + if (phase) { + return `@${name}.${phase}`; + } + + return name; + } + + static fromParsedEvent(event: ParsedEvent) { + const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null; + const phase: string|null = + event.type === ParsedEventType.Animation ? event.targetOrPhase : null; + return new BoundEventAst(event.name, target, phase, event.handler, event.sourceSpan); + } + visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitEvent(this, context); } @@ -122,6 +159,11 @@ export class ReferenceAst implements TemplateAst { */ export class VariableAst implements TemplateAst { constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} + + static fromParsedVariable(v: ParsedVariable) { + return new VariableAst(v.name, v.value, v.sourceSpan); + } + visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitVariable(this, context); } @@ -220,37 +262,6 @@ export class NgContentAst implements TemplateAst { } } -/** - * Enumeration of types of property bindings. - */ -export enum PropertyBindingType { - - /** - * A normal binding to a property (e.g. `[property]="expression"`). - */ - Property, - - /** - * A binding to an element attribute (e.g. `[attr.name]="expression"`). - */ - Attribute, - - /** - * A binding to a CSS class (e.g. `[class.name]="condition"`). - */ - Class, - - /** - * A binding to a style rule (e.g. `[style.rule]="expression"`). - */ - Style, - - /** - * A binding to an animation reference (e.g. `[animate.key]="expression"`). - */ - Animation -} - export interface QueryMatch { queryId: number; value: CompileTokenMetadata; diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 8880fe4037..e3d18553ae 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -10,7 +10,7 @@ import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, C import {CompileReflector} from '../compile_reflector'; import {CompilerConfig} from '../config'; import {SchemaMetadata} from '../core'; -import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast'; +import {AST, ASTWithSource, EmptyExpr, ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../identifiers'; import * as html from '../ml_parser/ast'; @@ -26,8 +26,8 @@ import {CssSelector, SelectorMatcher} from '../selector'; import {isStyleUrlResolvable} from '../style_url_resolver'; import {Console, syntaxError} from '../util'; -import {BindingParser, BoundProperty} from './binding_parser'; -import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast'; +import {BindingParser} from './binding_parser'; +import * as t from './template_ast'; import {PreparsedElementType, preparseElement} from './template_preparser'; const BIND_NAME_REGEXP = @@ -79,7 +79,7 @@ export class TemplateParseError extends ParseError { export class TemplateParseResult { constructor( - public templateAst?: TemplateAst[], public usedPipes?: CompilePipeSummary[], + public templateAst?: t.TemplateAst[], public usedPipes?: CompilePipeSummary[], public errors?: ParseError[]) {} } @@ -88,7 +88,7 @@ export class TemplateParser { private _config: CompilerConfig, private _reflector: CompileReflector, private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry, private _htmlParser: HtmlParser, private _console: Console, - public transforms: TemplateAstVisitor[]) {} + public transforms: t.TemplateAstVisitor[]) {} public get expressionParser() { return this._exprParser; } @@ -96,7 +96,7 @@ export class TemplateParser { component: CompileDirectiveMetadata, template: string|ParseTreeResult, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string, - preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} { + preserveWhitespaces: boolean): {template: t.TemplateAst[], pipes: CompilePipeSummary[]} { const result = this.tryParse( component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces); const warnings = result.errors !.filter(error => error.level === ParseErrorLevel.WARNING); @@ -136,7 +136,7 @@ export class TemplateParser { htmlAstWithErrors: ParseTreeResult, component: CompileDirectiveMetadata, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[]): TemplateParseResult { - let result: TemplateAst[]; + let result: t.TemplateAst[]; const errors = htmlAstWithErrors.errors; const usedPipes: CompilePipeSummary[] = []; if (htmlAstWithErrors.rootNodes.length > 0) { @@ -169,7 +169,7 @@ export class TemplateParser { if (this.transforms) { this.transforms.forEach( - (transform: TemplateAstVisitor) => { result = templateVisitAll(transform, result); }); + (transform: t.TemplateAstVisitor) => { result = t.templateVisitAll(transform, result); }); } return new TemplateParseResult(result, usedPipes, errors); @@ -195,12 +195,12 @@ export class TemplateParser { } /** @internal */ - _assertNoReferenceDuplicationOnTemplate(result: TemplateAst[], errors: TemplateParseError[]): + _assertNoReferenceDuplicationOnTemplate(result: t.TemplateAst[], errors: TemplateParseError[]): void { const existingReferences: string[] = []; result.filter(element => !!(element).references) - .forEach(element => (element).references.forEach((reference: ReferenceAst) => { + .forEach(element => (element).references.forEach((reference: t.ReferenceAst) => { const name = reference.name; if (existingReferences.indexOf(name) < 0) { existingReferences.push(name); @@ -242,12 +242,12 @@ class TemplateParseVisitor implements html.Visitor { const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR) !; const valueNoNgsp = replaceNgsp(text.value); const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan !); - return expr ? new BoundTextAst(expr, ngContentIndex, text.sourceSpan !) : - new TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan !); + return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan !) : + new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan !); } visitAttribute(attribute: html.Attribute, context: any): any { - return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan); + return new t.AttrAst(attribute.name, attribute.value, attribute.sourceSpan); } visitComment(comment: html.Comment, context: any): any { return null; } @@ -271,23 +271,25 @@ class TemplateParseVisitor implements html.Visitor { } const matchableAttrs: [string, string][] = []; - const elementOrDirectiveProps: BoundProperty[] = []; + const elementOrDirectiveProps: ParsedProperty[] = []; const elementOrDirectiveRefs: ElementOrDirectiveRef[] = []; - const elementVars: VariableAst[] = []; - const events: BoundEventAst[] = []; + const elementVars: t.VariableAst[] = []; + const events: t.BoundEventAst[] = []; - const templateElementOrDirectiveProps: BoundProperty[] = []; + const templateElementOrDirectiveProps: ParsedProperty[] = []; const templateMatchableAttrs: [string, string][] = []; - const templateElementVars: VariableAst[] = []; + const templateElementVars: t.VariableAst[] = []; let hasInlineTemplates = false; - const attrs: AttrAst[] = []; + const attrs: t.AttrAst[] = []; const isTemplateElement = isNgTemplate(element.name); element.attrs.forEach(attr => { + const parsedVariables: ParsedVariable[] = []; const hasBinding = this._parseAttr( isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events, elementOrDirectiveRefs, elementVars); + elementVars.push(...parsedVariables.map(v => t.VariableAst.fromParsedVariable(v))); let templateValue: string|undefined; let templateKey: string|undefined; @@ -306,9 +308,11 @@ class TemplateParseVisitor implements html.Visitor { attr.sourceSpan); } hasInlineTemplates = true; + const parsedVariables: ParsedVariable[] = []; this._bindingParser.parseInlineTemplateBinding( templateKey !, templateValue !, attr.sourceSpan, templateMatchableAttrs, - templateElementOrDirectiveProps, templateElementVars); + templateElementOrDirectiveProps, parsedVariables); + templateElementVars.push(...parsedVariables.map(v => t.VariableAst.fromParsedVariable(v))); } if (!hasBinding && !hasTemplateBinding) { @@ -321,12 +325,12 @@ class TemplateParseVisitor implements html.Visitor { const elementCssSelector = createElementCssSelector(elName, matchableAttrs); const {directives: directiveMetas, matchElement} = this._parseDirectives(this.selectorMatcher, elementCssSelector); - const references: ReferenceAst[] = []; + const references: t.ReferenceAst[] = []; const boundDirectivePropNames = new Set(); const directiveAsts = this._createDirectiveAsts( isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps, elementOrDirectiveRefs, element.sourceSpan !, references, boundDirectivePropNames); - const elementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts( + const elementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts( element.name, elementOrDirectiveProps, boundDirectivePropNames); const isViewRoot = parent.isTemplateElement || hasInlineTemplates; @@ -334,7 +338,7 @@ class TemplateParseVisitor implements html.Visitor { this.providerViewContext, parent.providerContext !, isViewRoot, directiveAsts, attrs, references, isTemplateElement, queryStartIndex, element.sourceSpan !); - const children: TemplateAst[] = html.visitAll( + const children: t.TemplateAst[] = html.visitAll( preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children, ElementContext.create( isTemplateElement, directiveAsts, @@ -345,7 +349,7 @@ class TemplateParseVisitor implements html.Visitor { CssSelector.parse(preparsedElement.projectAs)[0] : elementCssSelector; const ngContentIndex = parent.findNgContentIndex(projectionSelector) !; - let parsedElement: TemplateAst; + let parsedElement: t.TemplateAst; if (preparsedElement.type === PreparsedElementType.NG_CONTENT) { // `` element @@ -353,7 +357,7 @@ class TemplateParseVisitor implements html.Visitor { this._reportError(` element cannot have content.`, element.sourceSpan !); } - parsedElement = new NgContentAst( + parsedElement = new t.NgContentAst( this.ngContentCount++, hasInlineTemplates ? null ! : ngContentIndex, element.sourceSpan !); } else if (isTemplateElement) { @@ -362,7 +366,7 @@ class TemplateParseVisitor implements html.Visitor { this._assertNoComponentsNorElementBindingsOnTemplate( directiveAsts, elementProps, element.sourceSpan !); - parsedElement = new EmbeddedTemplateAst( + parsedElement = new t.EmbeddedTemplateAst( attrs, events, references, elementVars, providerContext.transformedDirectiveAsts, providerContext.transformProviders, providerContext.transformedHasViewContainer, providerContext.queryMatches, children, hasInlineTemplates ? null ! : ngContentIndex, @@ -374,7 +378,7 @@ class TemplateParseVisitor implements html.Visitor { const ngContentIndex = hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector); - parsedElement = new ElementAst( + parsedElement = new t.ElementAst( elName, attrs, elementProps, events, references, providerContext.transformedDirectiveAsts, providerContext.transformProviders, providerContext.transformedHasViewContainer, providerContext.queryMatches, children, hasInlineTemplates ? null : ngContentIndex, @@ -390,7 +394,7 @@ class TemplateParseVisitor implements html.Visitor { const templateDirectiveAsts = this._createDirectiveAsts( true, elName, directives, templateElementOrDirectiveProps, [], element.sourceSpan !, [], templateBoundDirectivePropNames); - const templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts( + const templateElementProps: t.BoundElementPropertyAst[] = this._createElementPropertyAsts( elName, templateElementOrDirectiveProps, templateBoundDirectivePropNames); this._assertNoComponentsNorElementBindingsOnTemplate( templateDirectiveAsts, templateElementProps, element.sourceSpan !); @@ -399,7 +403,7 @@ class TemplateParseVisitor implements html.Visitor { templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan !); templateProviderContext.afterElement(); - parsedElement = new EmbeddedTemplateAst( + parsedElement = new t.EmbeddedTemplateAst( [], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts, templateProviderContext.transformProviders, templateProviderContext.transformedHasViewContainer, templateProviderContext.queryMatches, @@ -411,15 +415,15 @@ class TemplateParseVisitor implements html.Visitor { private _parseAttr( isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][], - targetProps: BoundProperty[], targetEvents: BoundEventAst[], - targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean { + targetProps: ParsedProperty[], targetEvents: t.BoundEventAst[], + targetRefs: ElementOrDirectiveRef[], targetVars: t.VariableAst[]): boolean { const name = this._normalizeAttributeName(attr.name); const value = attr.value; const srcSpan = attr.sourceSpan; + const boundEvents: ParsedEvent[] = []; const bindParts = name.match(BIND_NAME_REGEXP); let hasBinding = false; - const boundEvents: BoundEventAst[] = []; if (bindParts !== null) { hasBinding = true; @@ -441,13 +445,13 @@ class TemplateParseVisitor implements html.Visitor { } else if (bindParts[KW_ON_IDX]) { this._bindingParser.parseEvent( - bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); + bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); } else if (bindParts[KW_BINDON_IDX]) { this._bindingParser.parsePropertyBinding( bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps); this._parseAssignmentEvent( - bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); + bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); } else if (bindParts[KW_AT_IDX]) { this._bindingParser.parseLiteralAttr( @@ -458,7 +462,7 @@ class TemplateParseVisitor implements html.Visitor { bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps); this._parseAssignmentEvent( - bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); + bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); } else if (bindParts[IDENT_PROPERTY_IDX]) { this._bindingParser.parsePropertyBinding( @@ -467,7 +471,7 @@ class TemplateParseVisitor implements html.Visitor { } else if (bindParts[IDENT_EVENT_IDX]) { this._bindingParser.parseEvent( - bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); + bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, boundEvents); } } else { hasBinding = this._bindingParser.parsePropertyInterpolation( @@ -478,6 +482,8 @@ class TemplateParseVisitor implements html.Visitor { this._bindingParser.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps); } + targetEvents.push(...boundEvents.map(e => t.BoundEventAst.fromParsedEvent(e))); + return hasBinding; } @@ -486,12 +492,12 @@ class TemplateParseVisitor implements html.Visitor { } private _parseVariable( - identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) { + identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: t.VariableAst[]) { if (identifier.indexOf('-') > -1) { this._reportError(`"-" is not allowed in variable names`, sourceSpan); } - targetVars.push(new VariableAst(identifier, value, sourceSpan)); + targetVars.push(new t.VariableAst(identifier, value, sourceSpan)); } private _parseReference( @@ -506,7 +512,7 @@ class TemplateParseVisitor implements html.Visitor { private _parseAssignmentEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + targetMatchableAttrs: string[][], targetEvents: ParsedEvent[]) { this._bindingParser.parseEvent( `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents); } @@ -533,9 +539,9 @@ class TemplateParseVisitor implements html.Visitor { private _createDirectiveAsts( isTemplateElement: boolean, elementName: string, directives: CompileDirectiveSummary[], - props: BoundProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[], - elementSourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[], - targetBoundDirectivePropNames: Set): DirectiveAst[] { + props: ParsedProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[], + elementSourceSpan: ParseSourceSpan, targetReferences: t.ReferenceAst[], + targetBoundDirectivePropNames: Set): t.DirectiveAst[] { const matchedReferences = new Set(); let component: CompileDirectiveSummary = null !; @@ -547,27 +553,32 @@ class TemplateParseVisitor implements html.Visitor { if (directive.isComponent) { component = directive; } - const directiveProperties: BoundDirectivePropertyAst[] = []; - let hostProperties = + const directiveProperties: t.BoundDirectivePropertyAst[] = []; + const boundProperties = this._bindingParser.createDirectiveHostPropertyAsts(directive, elementName, sourceSpan) !; + + let hostProperties = + boundProperties.map(prop => t.BoundElementPropertyAst.fromBoundProperty(prop)); // Note: We need to check the host properties here as well, // as we don't know the element name in the DirectiveWrapperCompiler yet. hostProperties = this._checkPropertiesInSchema(elementName, hostProperties); - const hostEvents = this._bindingParser.createDirectiveHostEventAsts(directive, sourceSpan) !; + const parsedEvents = + this._bindingParser.createDirectiveHostEventAsts(directive, sourceSpan) !; this._createDirectivePropertyAsts( directive.inputs, props, directiveProperties, targetBoundDirectivePropNames); elementOrDirectiveRefs.forEach((elOrDirRef) => { if ((elOrDirRef.value.length === 0 && directive.isComponent) || (elOrDirRef.isReferenceToDirective(directive))) { - targetReferences.push(new ReferenceAst( + targetReferences.push(new t.ReferenceAst( elOrDirRef.name, createTokenForReference(directive.type.reference), elOrDirRef.value, elOrDirRef.sourceSpan)); matchedReferences.add(elOrDirRef.name); } }); + const hostEvents = parsedEvents.map(e => t.BoundEventAst.fromParsedEvent(e)); const contentQueryStartId = this.contentQueryStartId; this.contentQueryStartId += directive.queries.length; - return new DirectiveAst( + return new t.DirectiveAst( directive, directiveProperties, hostProperties, hostEvents, contentQueryStartId, sourceSpan); }); @@ -585,18 +596,18 @@ class TemplateParseVisitor implements html.Visitor { refToken = createTokenForExternalReference(this.reflector, Identifiers.TemplateRef); } targetReferences.push( - new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.value, elOrDirRef.sourceSpan)); + new t.ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.value, elOrDirRef.sourceSpan)); } }); return directiveAsts; } private _createDirectivePropertyAsts( - directiveProperties: {[key: string]: string}, boundProps: BoundProperty[], - targetBoundDirectiveProps: BoundDirectivePropertyAst[], + directiveProperties: {[key: string]: string}, boundProps: ParsedProperty[], + targetBoundDirectiveProps: t.BoundDirectivePropertyAst[], targetBoundDirectivePropNames: Set) { if (directiveProperties) { - const boundPropsByName = new Map(); + const boundPropsByName = new Map(); boundProps.forEach(boundProp => { const prevValue = boundPropsByName.get(boundProp.name); if (!prevValue || prevValue.isLiteral) { @@ -613,7 +624,7 @@ class TemplateParseVisitor implements html.Visitor { if (boundProp) { targetBoundDirectivePropNames.add(boundProp.name); if (!isEmptyExpression(boundProp.expression)) { - targetBoundDirectiveProps.push(new BoundDirectivePropertyAst( + targetBoundDirectiveProps.push(new t.BoundDirectivePropertyAst( dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan)); } } @@ -622,28 +633,29 @@ class TemplateParseVisitor implements html.Visitor { } private _createElementPropertyAsts( - elementName: string, props: BoundProperty[], - boundDirectivePropNames: Set): BoundElementPropertyAst[] { - const boundElementProps: BoundElementPropertyAst[] = []; + elementName: string, props: ParsedProperty[], + boundDirectivePropNames: Set): t.BoundElementPropertyAst[] { + const boundElementProps: t.BoundElementPropertyAst[] = []; - props.forEach((prop: BoundProperty) => { + props.forEach((prop: ParsedProperty) => { if (!prop.isLiteral && !boundDirectivePropNames.has(prop.name)) { - boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop)); + const boundProp = this._bindingParser.createBoundElementProperty(elementName, prop); + boundElementProps.push(t.BoundElementPropertyAst.fromBoundProperty(boundProp)); } }); return this._checkPropertiesInSchema(elementName, boundElementProps); } - private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] { + private _findComponentDirectives(directives: t.DirectiveAst[]): t.DirectiveAst[] { return directives.filter(directive => directive.directive.isComponent); } - private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] { + private _findComponentDirectiveNames(directives: t.DirectiveAst[]): string[] { return this._findComponentDirectives(directives) .map(directive => identifierName(directive.directive.type) !); } - private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) { + private _assertOnlyOneComponent(directives: t.DirectiveAst[], sourceSpan: ParseSourceSpan) { const componentTypeNames = this._findComponentDirectiveNames(directives); if (componentTypeNames.length > 1) { this._reportError( @@ -682,7 +694,7 @@ class TemplateParseVisitor implements html.Visitor { } private _assertNoComponentsNorElementBindingsOnTemplate( - directives: DirectiveAst[], elementProps: BoundElementPropertyAst[], + directives: t.DirectiveAst[], elementProps: t.BoundElementPropertyAst[], sourceSpan: ParseSourceSpan) { const componentTypeNames: string[] = this._findComponentDirectiveNames(directives); if (componentTypeNames.length > 0) { @@ -697,7 +709,7 @@ class TemplateParseVisitor implements html.Visitor { } private _assertAllEventsPublishedByDirectives( - directives: DirectiveAst[], events: BoundEventAst[]) { + directives: t.DirectiveAst[], events: t.BoundEventAst[]) { const allDirectiveEvents = new Set(); directives.forEach(directive => { @@ -716,12 +728,12 @@ class TemplateParseVisitor implements html.Visitor { }); } - private _checkPropertiesInSchema(elementName: string, boundProps: BoundElementPropertyAst[]): - BoundElementPropertyAst[] { + private _checkPropertiesInSchema(elementName: string, boundProps: t.BoundElementPropertyAst[]): + t.BoundElementPropertyAst[] { // Note: We can't filter out empty expressions before this method, // as we still want to validate them! return boundProps.filter((boundProp) => { - if (boundProp.type === PropertyBindingType.Property && + if (boundProp.type === t.PropertyBindingType.Property && !this._schemaRegistry.hasProperty(elementName, boundProp.name, this._schemas)) { let errorMsg = `Can't bind to '${boundProp.name}' since it isn't a known property of '${elementName}'.`; @@ -749,7 +761,7 @@ class TemplateParseVisitor implements html.Visitor { } class NonBindableVisitor implements html.Visitor { - visitElement(ast: html.Element, parent: ElementContext): ElementAst|null { + visitElement(ast: html.Element, parent: ElementContext): t.ElementAst|null { const preparsedElement = preparseElement(ast); if (preparsedElement.type === PreparsedElementType.SCRIPT || preparsedElement.type === PreparsedElementType.STYLE || @@ -763,20 +775,20 @@ class NonBindableVisitor implements html.Visitor { const attrNameAndValues = ast.attrs.map((attr): [string, string] => [attr.name, attr.value]); const selector = createElementCssSelector(ast.name, attrNameAndValues); const ngContentIndex = parent.findNgContentIndex(selector); - const children: TemplateAst[] = html.visitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT); - return new ElementAst( + const children: t.TemplateAst[] = html.visitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT); + return new t.ElementAst( ast.name, html.visitAll(this, ast.attrs), [], [], [], [], [], false, [], children, ngContentIndex, ast.sourceSpan, ast.endSourceSpan); } visitComment(comment: html.Comment, context: any): any { return null; } - visitAttribute(attribute: html.Attribute, context: any): AttrAst { - return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan); + visitAttribute(attribute: html.Attribute, context: any): t.AttrAst { + return new t.AttrAst(attribute.name, attribute.value, attribute.sourceSpan); } - visitText(text: html.Text, parent: ElementContext): TextAst { + visitText(text: html.Text, parent: ElementContext): t.TextAst { const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR) !; - return new TextAst(text.value, ngContentIndex, text.sourceSpan !); + return new t.TextAst(text.value, ngContentIndex, text.sourceSpan !); } visitExpansion(expansion: html.Expansion, context: any): any { return expansion; } @@ -811,7 +823,7 @@ export function splitClasses(classAttrValue: string): string[] { class ElementContext { static create( - isTemplateElement: boolean, directives: DirectiveAst[], + isTemplateElement: boolean, directives: t.DirectiveAst[], providerContext: ProviderElementContext): ElementContext { const matcher = new SelectorMatcher(); let wildcardNgContentIndex: number = null !; diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index a46e9008de..1c925c3984 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -278,6 +278,9 @@ { "name": "BoundDirectivePropertyAst" }, + { + "name": "BoundElementProperty" + }, { "name": "BoundElementPropertyAst" }, @@ -285,10 +288,7 @@ "name": "BoundEventAst" }, { - "name": "BoundProperty" - }, - { - "name": "BoundPropertyType" + "name": "BoundPropertyMapping" }, { "name": "BoundTextAst" @@ -1235,6 +1235,18 @@ { "name": "ParseTreeResult" }, + { + "name": "ParsedEvent" + }, + { + "name": "ParsedProperty" + }, + { + "name": "ParsedPropertyType" + }, + { + "name": "ParsedVariable" + }, { "name": "Parser" }, @@ -2195,9 +2207,6 @@ { "name": "_global" }, - { - "name": "_isAnimationLabel" - }, { "name": "_isClosingComment" }, @@ -3221,6 +3230,9 @@ { "name": "invalid" }, + { + "name": "isAnimationLabel" + }, { "name": "isArray" },