diff --git a/modules/angular2/src/compiler/api.ts b/modules/angular2/src/compiler/api.ts index 972fdb046b..509add5563 100644 --- a/modules/angular2/src/compiler/api.ts +++ b/modules/angular2/src/compiler/api.ts @@ -1,7 +1,8 @@ -import {isPresent} from 'angular2/src/core/facade/lang'; +import {isPresent, normalizeBool} from 'angular2/src/core/facade/lang'; import {HtmlAst} from './html_ast'; +import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection/change_detection'; -export class TypeMeta { +export class TypeMetadata { type: any; typeName: string; typeUrl: string; @@ -13,7 +14,28 @@ export class TypeMeta { } } -export class TemplateMeta { +export class ChangeDetectionMetadata { + changeDetection: ChangeDetectionStrategy; + properties: string[]; + events: string[]; + hostListeners: StringMap; + hostProperties: StringMap; + constructor({changeDetection, properties, events, hostListeners, hostProperties}: { + changeDetection?: ChangeDetectionStrategy, + properties?: string[], + events?: string[], + hostListeners?: StringMap, + hostProperties?: StringMap + }) { + this.changeDetection = changeDetection; + this.properties = properties; + this.events = events; + this.hostListeners = hostListeners; + this.hostProperties = hostProperties; + } +} + +export class TemplateMetadata { encapsulation: ViewEncapsulation; nodes: HtmlAst[]; styles: string[]; @@ -54,19 +76,25 @@ export enum ViewEncapsulation { } export class DirectiveMetadata { - type: TypeMeta; + type: TypeMetadata; isComponent: boolean; selector: string; - template: TemplateMeta; - constructor({type, isComponent, selector, template}: { - type?: TypeMeta, + hostAttributes: Map; + changeDetection: ChangeDetectionMetadata; + template: TemplateMetadata; + constructor({type, isComponent, selector, hostAttributes, changeDetection, template}: { + type?: TypeMetadata, isComponent?: boolean, selector?: string, - template?: TemplateMeta + hostAttributes?: Map, + changeDetection?: ChangeDetectionMetadata, + template?: TemplateMetadata } = {}) { this.type = type; - this.isComponent = isPresent(isComponent) ? isComponent : false; + this.isComponent = normalizeBool(isComponent); this.selector = selector; + this.hostAttributes = hostAttributes; + this.changeDetection = changeDetection; this.template = template; } } diff --git a/modules/angular2/src/compiler/template_ast.ts b/modules/angular2/src/compiler/template_ast.ts index 2c7e51875e..3292a441dc 100644 --- a/modules/angular2/src/compiler/template_ast.ts +++ b/modules/angular2/src/compiler/template_ast.ts @@ -22,13 +22,15 @@ export class AttrAst implements TemplateAst { visit(visitor: TemplateAstVisitor): any { return visitor.visitAttr(this); } } -export class BoundPropertyAst implements TemplateAst { - constructor(public name: string, public value: AST, public sourceInfo: string) {} - visit(visitor: TemplateAstVisitor): any { return visitor.visitProperty(this); } +export class BoundElementPropertyAst implements TemplateAst { + constructor(public name: string, public type: PropertyBindingType, public value: AST, + public unit: string, public sourceInfo: string) {} + visit(visitor: TemplateAstVisitor): any { return visitor.visitElementProperty(this); } } export class BoundEventAst implements TemplateAst { - constructor(public name: string, public handler: AST, public sourceInfo: string) {} + constructor(public name: string, public target: string, public handler: AST, + public sourceInfo: string) {} visit(visitor: TemplateAstVisitor): any { return visitor.visitEvent(this); } } @@ -38,35 +40,57 @@ export class VariableAst implements TemplateAst { } export class ElementAst implements TemplateAst { - constructor(public attrs: AttrAst[], public properties: BoundPropertyAst[], + constructor(public attrs: AttrAst[], public properties: BoundElementPropertyAst[], public events: BoundEventAst[], public vars: VariableAst[], - public directives: DirectiveMetadata[], public children: TemplateAst[], + public directives: DirectiveAst[], public children: TemplateAst[], public sourceInfo: string) {} visit(visitor: TemplateAstVisitor): any { return visitor.visitElement(this); } } export class EmbeddedTemplateAst implements TemplateAst { - constructor(public attrs: AttrAst[], public properties: BoundPropertyAst[], - public vars: VariableAst[], public directives: DirectiveMetadata[], - public children: TemplateAst[], public sourceInfo: string) {} + constructor(public attrs: AttrAst[], public vars: VariableAst[], + public directives: DirectiveAst[], public children: TemplateAst[], + public sourceInfo: string) {} visit(visitor: TemplateAstVisitor): any { return visitor.visitEmbeddedTemplate(this); } } +export class BoundDirectivePropertyAst implements TemplateAst { + constructor(public directiveName: string, public templateName: string, public value: AST, + public sourceInfo: string) {} + visit(visitor: TemplateAstVisitor): any { return visitor.visitDirectiveProperty(this); } +} + +export class DirectiveAst implements TemplateAst { + constructor(public directive: DirectiveMetadata, public properties: BoundDirectivePropertyAst[], + public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[], + public sourceInfo: string) {} + visit(visitor: TemplateAstVisitor): any { return visitor.visitDirective(this); } +} + export class NgContentAst implements TemplateAst { constructor(public select: string, public sourceInfo: string) {} visit(visitor: TemplateAstVisitor): any { return visitor.visitNgContent(this); } } +export enum PropertyBindingType { + Property, + Attribute, + Class, + Style +} + export interface TemplateAstVisitor { visitNgContent(ast: NgContentAst): any; visitEmbeddedTemplate(ast: EmbeddedTemplateAst): any; visitElement(ast: ElementAst): any; visitVariable(ast: VariableAst): any; visitEvent(ast: BoundEventAst): any; - visitProperty(ast: BoundPropertyAst): any; + visitElementProperty(ast: BoundElementPropertyAst): any; visitAttr(ast: AttrAst): any; visitBoundText(ast: BoundTextAst): any; visitText(ast: TextAst): any; + visitDirective(ast: DirectiveAst): any; + visitDirectiveProperty(ast: BoundDirectivePropertyAst): any; } diff --git a/modules/angular2/src/compiler/template_loader.ts b/modules/angular2/src/compiler/template_loader.ts index 2f7a0448d4..47f2a25684 100644 --- a/modules/angular2/src/compiler/template_loader.ts +++ b/modules/angular2/src/compiler/template_loader.ts @@ -1,4 +1,4 @@ -import {TypeMeta, TemplateMeta, ViewEncapsulation} from './api'; +import {TypeMetadata, TemplateMetadata, ViewEncapsulation} from './api'; import {isPresent} from 'angular2/src/core/facade/lang'; import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async'; @@ -28,8 +28,9 @@ export class TemplateLoader { constructor(private _xhr: XHR, private _urlResolver: UrlResolver, private _styleUrlResolver: StyleUrlResolver, private _domParser: HtmlParser) {} - loadTemplate(directiveType: TypeMeta, encapsulation: ViewEncapsulation, template: string, - templateUrl: string, styles: string[], styleUrls: string[]): Promise { + loadTemplate(directiveType: TypeMetadata, encapsulation: ViewEncapsulation, template: string, + templateUrl: string, styles: string[], + styleUrls: string[]): Promise { if (isPresent(template)) { return PromiseWrapper.resolve(this.createTemplateFromString( directiveType, encapsulation, template, directiveType.typeUrl, styles, styleUrls)); @@ -42,9 +43,9 @@ export class TemplateLoader { } } - createTemplateFromString(directiveType: TypeMeta, encapsulation: ViewEncapsulation, + createTemplateFromString(directiveType: TypeMetadata, encapsulation: ViewEncapsulation, template: string, templateSourceUrl: string, styles: string[], - styleUrls: string[]): TemplateMeta { + styleUrls: string[]): TemplateMetadata { var domNodes = this._domParser.parse(template, directiveType.typeName); var visitor = new TemplatePreparseVisitor(); var remainingNodes = htmlVisitAll(visitor, domNodes); @@ -60,7 +61,7 @@ export class TemplateLoader { allStyles.map(style => this._styleUrlResolver.resolveUrls(style, templateSourceUrl)); var allStyleAbsUrls = allStyleUrls.map(styleUrl => this._urlResolver.resolve(templateSourceUrl, styleUrl)); - return new TemplateMeta({ + return new TemplateMetadata({ encapsulation: encapsulation, nodes: remainingNodes, styles: allResolvedStyles, diff --git a/modules/angular2/src/compiler/template_parser.ts b/modules/angular2/src/compiler/template_parser.ts index dfc4998cd1..8717333fb8 100644 --- a/modules/angular2/src/compiler/template_parser.ts +++ b/modules/angular2/src/compiler/template_parser.ts @@ -1,4 +1,4 @@ -import {MapWrapper, ListWrapper} from 'angular2/src/core/facade/collection'; +import {MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection'; import { RegExpWrapper, isPresent, @@ -11,11 +11,12 @@ import { } from 'angular2/src/core/facade/lang'; import {Parser, AST, ASTWithSource} from 'angular2/src/core/change_detection/change_detection'; +import {TemplateBinding} from 'angular2/src/core/change_detection/parser/ast'; import {DirectiveMetadata} from './api'; import { ElementAst, - BoundPropertyAst, + BoundElementPropertyAst, BoundEventAst, VariableAst, TemplateAst, @@ -23,10 +24,15 @@ import { BoundTextAst, EmbeddedTemplateAst, AttrAst, - NgContentAst + NgContentAst, + PropertyBindingType, + DirectiveAst, + BoundDirectivePropertyAst } from './template_ast'; import {CssSelector, SelectorMatcher} from 'angular2/src/core/render/dom/compiler/selector'; +import {ElementSchemaRegistry} from 'angular2/src/core/render/dom/schema/element_schema_registry'; + import { HtmlAstVisitor, HtmlAst, @@ -36,7 +42,7 @@ import { htmlVisitAll } from './html_ast'; -import {dashCaseToCamelCase} from './util'; +import {dashCaseToCamelCase, camelCaseToDashCase} from './util'; // Group 1 = "bind-" // Group 2 = "var-" or "#" @@ -57,18 +63,30 @@ const TEMPLATE_ATTR_PREFIX = '*'; const CLASS_ATTR = 'class'; const IMPLICIT_VAR_NAME = '$implicit'; +var PROPERTY_PARTS_SEPARATOR = new RegExp('\\.'); +const ATTRIBUTE_PREFIX = 'attr'; +const CLASS_PREFIX = 'class'; +const STYLE_PREFIX = 'style'; + export class TemplateParser { - constructor(private _exprParser: Parser) {} + constructor(private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry) {} parse(domNodes: HtmlAst[], directives: DirectiveMetadata[]): TemplateAst[] { - var parseVisitor = new TemplateParseVisitor(directives, this._exprParser); - return htmlVisitAll(parseVisitor, domNodes); + var parseVisitor = new TemplateParseVisitor(directives, this._exprParser, this._schemaRegistry); + var result = htmlVisitAll(parseVisitor, domNodes); + if (parseVisitor.errors.length > 0) { + var errorString = parseVisitor.errors.join('\n'); + throw new BaseException(`Template parse errors:\n${errorString}`); + } + return result; } } class TemplateParseVisitor implements HtmlAstVisitor { selectorMatcher: SelectorMatcher; - constructor(directives: DirectiveMetadata[], private _exprParser: Parser) { + errors: string[] = []; + constructor(directives: DirectiveMetadata[], private _exprParser: Parser, + private _schemaRegistry: ElementSchemaRegistry) { this.selectorMatcher = new SelectorMatcher(); directives.forEach(directive => { var selector = CssSelector.parse(directive.selector); @@ -76,8 +94,46 @@ class TemplateParseVisitor implements HtmlAstVisitor { }); } + private _reportError(message: string) { this.errors.push(message); } + + private _parseInterpolation(value: string, sourceInfo: string): ASTWithSource { + try { + return this._exprParser.parseInterpolation(value, sourceInfo); + } catch (e) { + this._reportError(`${e}`); // sourceInfo is already contained in the AST + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + private _parseAction(value: string, sourceInfo: string): ASTWithSource { + try { + return this._exprParser.parseAction(value, sourceInfo); + } catch (e) { + this._reportError(`${e}`); // sourceInfo is already contained in the AST + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + private _parseBinding(value: string, sourceInfo: string): ASTWithSource { + try { + return this._exprParser.parseBinding(value, sourceInfo); + } catch (e) { + this._reportError(`${e}`); // sourceInfo is already contained in the AST + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + private _parseTemplateBindings(value: string, sourceInfo: string): TemplateBinding[] { + try { + return this._exprParser.parseTemplateBindings(value, sourceInfo); + } catch (e) { + this._reportError(`${e}`); // sourceInfo is already contained in the AST + return []; + } + } + visitText(ast: HtmlTextAst): any { - var expr = this._exprParser.parseInterpolation(ast.value, ast.sourceInfo); + var expr = this._parseInterpolation(ast.value, ast.sourceInfo); if (isPresent(expr)) { return new BoundTextAst(expr, ast.sourceInfo); } else { @@ -90,11 +146,11 @@ class TemplateParseVisitor implements HtmlAstVisitor { visitElement(element: HtmlElementAst): any { var nodeName = element.name; var matchableAttrs: string[][] = []; - var props: BoundPropertyAst[] = []; + var elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; var vars: VariableAst[] = []; var events: BoundEventAst[] = []; - var templateProps: BoundPropertyAst[] = []; + var templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; var templateVars: VariableAst[] = []; var templateMatchableAttrs: string[][] = []; var hasInlineTemplates = false; @@ -105,9 +161,9 @@ class TemplateParseVisitor implements HtmlAstVisitor { if (attr.name == NG_CONTENT_SELECT_ATTR) { selectAttr = attr.value; } - var hasBinding = this._parseAttr(attr, matchableAttrs, props, events, vars); - var hasTemplateBinding = this._parseInlineTemplateBinding(attr, templateMatchableAttrs, - templateProps, templateVars); + var hasBinding = this._parseAttr(attr, matchableAttrs, elementOrDirectiveProps, events, vars); + var hasTemplateBinding = this._parseInlineTemplateBinding( + attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateVars); if (!hasBinding && !hasTemplateBinding) { // don't include the bindings as attributes as well in the AST attrs.push(this.visitAttr(attr)); @@ -116,29 +172,43 @@ class TemplateParseVisitor implements HtmlAstVisitor { hasInlineTemplates = true; } }); - var directives = this._parseDirectives(this.selectorMatcher, nodeName, matchableAttrs); + var directives = this._createDirectiveAsts( + element.name, this._parseDirectives(this.selectorMatcher, nodeName, matchableAttrs), + elementOrDirectiveProps, element.sourceInfo); + var elementProps: BoundElementPropertyAst[] = + this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives); var children = htmlVisitAll(this, element.children); var parsedElement; if (nodeName == NG_CONTENT_ELEMENT) { parsedElement = new NgContentAst(selectAttr, element.sourceInfo); } else if (nodeName == TEMPLATE_ELEMENT) { + this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events, + element.sourceInfo); parsedElement = - new EmbeddedTemplateAst(attrs, props, vars, directives, children, element.sourceInfo); + new EmbeddedTemplateAst(attrs, vars, directives, children, element.sourceInfo); } else { - parsedElement = - new ElementAst(attrs, props, events, vars, directives, children, element.sourceInfo); + this._assertOnlyOneComponent(directives, element.sourceInfo); + parsedElement = new ElementAst(attrs, elementProps, events, vars, directives, children, + element.sourceInfo); } if (hasInlineTemplates) { - var templateDirectives = - this._parseDirectives(this.selectorMatcher, TEMPLATE_ELEMENT, templateMatchableAttrs); - parsedElement = new EmbeddedTemplateAst([], templateProps, templateVars, templateDirectives, - [parsedElement], element.sourceInfo); + var templateDirectives = this._createDirectiveAsts( + element.name, + this._parseDirectives(this.selectorMatcher, TEMPLATE_ELEMENT, templateMatchableAttrs), + templateElementOrDirectiveProps, element.sourceInfo); + var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts( + element.name, templateElementOrDirectiveProps, templateDirectives); + this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps, + [], element.sourceInfo); + parsedElement = new EmbeddedTemplateAst([], templateVars, templateDirectives, [parsedElement], + element.sourceInfo); } return parsedElement; } - private _parseInlineTemplateBinding(attr: HtmlAttrAst, matchableAttrs: string[][], - props: BoundPropertyAst[], vars: VariableAst[]): boolean { + private _parseInlineTemplateBinding(attr: HtmlAttrAst, targetMatchableAttrs: string[][], + targetProps: BoundElementOrDirectiveProperty[], + targetVars: VariableAst[]): boolean { var templateBindingsSource = null; if (attr.name == TEMPLATE_ATTR) { templateBindingsSource = attr.value; @@ -147,20 +217,19 @@ class TemplateParseVisitor implements HtmlAstVisitor { templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value; } if (isPresent(templateBindingsSource)) { - var bindings = - this._exprParser.parseTemplateBindings(templateBindingsSource, attr.sourceInfo); + var bindings = this._parseTemplateBindings(templateBindingsSource, attr.sourceInfo); for (var i = 0; i < bindings.length; i++) { var binding = bindings[i]; + var dashCaseKey = camelCaseToDashCase(binding.key); if (binding.keyIsVar) { - vars.push( + targetVars.push( new VariableAst(dashCaseToCamelCase(binding.key), binding.name, attr.sourceInfo)); - matchableAttrs.push([binding.key, binding.name]); + targetMatchableAttrs.push([dashCaseKey, binding.name]); } else if (isPresent(binding.expression)) { - props.push(new BoundPropertyAst(dashCaseToCamelCase(binding.key), binding.expression, - attr.sourceInfo)); - matchableAttrs.push([binding.key, binding.expression.source]); + this._parsePropertyAst(dashCaseKey, binding.expression, attr.sourceInfo, + targetMatchableAttrs, targetProps); } else { - matchableAttrs.push([binding.key, '']); + targetMatchableAttrs.push([dashCaseKey, '']); } } return true; @@ -168,8 +237,9 @@ class TemplateParseVisitor implements HtmlAstVisitor { return false; } - private _parseAttr(attr: HtmlAttrAst, matchableAttrs: string[][], props: BoundPropertyAst[], - events: BoundEventAst[], vars: VariableAst[]): boolean { + private _parseAttr(attr: HtmlAttrAst, targetMatchableAttrs: string[][], + targetProps: BoundElementOrDirectiveProperty[], targetEvents: BoundEventAst[], + targetVars: VariableAst[]): boolean { var attrName = this._normalizeAttributeName(attr.name); var attrValue = attr.value; var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName); @@ -177,36 +247,45 @@ class TemplateParseVisitor implements HtmlAstVisitor { if (isPresent(bindParts)) { hasBinding = true; if (isPresent(bindParts[1])) { // match: bind-prop - this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, props); + this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetProps); } else if (isPresent( bindParts[2])) { // match: var-name / var-name="iden" / #name / #name="iden" var identifier = bindParts[5]; var value = attrValue.length === 0 ? IMPLICIT_VAR_NAME : attrValue; - this._parseVariable(identifier, value, attr.sourceInfo, matchableAttrs, vars); + this._parseVariable(identifier, value, attr.sourceInfo, targetMatchableAttrs, targetVars); } else if (isPresent(bindParts[3])) { // match: on-event - this._parseEvent(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, events); + this._parseEvent(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetEvents); } else if (isPresent(bindParts[4])) { // match: bindon-prop - this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, props); - this._parseAssignmentEvent(bindParts[5], attrValue, attr.sourceInfo, matchableAttrs, - events); + this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetProps); + this._parseAssignmentEvent(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetEvents); } else if (isPresent(bindParts[6])) { // match: [(expr)] - this._parseProperty(bindParts[6], attrValue, attr.sourceInfo, matchableAttrs, props); - this._parseAssignmentEvent(bindParts[6], attrValue, attr.sourceInfo, matchableAttrs, - events); + this._parseProperty(bindParts[6], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetProps); + this._parseAssignmentEvent(bindParts[6], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetEvents); } else if (isPresent(bindParts[7])) { // match: [expr] - this._parseProperty(bindParts[7], attrValue, attr.sourceInfo, matchableAttrs, props); + this._parseProperty(bindParts[7], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetProps); } else if (isPresent(bindParts[8])) { // match: (event) - this._parseEvent(bindParts[8], attrValue, attr.sourceInfo, matchableAttrs, events); + this._parseEvent(bindParts[8], attrValue, attr.sourceInfo, targetMatchableAttrs, + targetEvents); } } else { hasBinding = this._parsePropertyInterpolation(attrName, attrValue, attr.sourceInfo, - matchableAttrs, props); + targetMatchableAttrs, targetProps); + } + if (!hasBinding) { + this._parseLiteralAttr(attrName, attrValue, attr.sourceInfo, targetProps); } return hasBinding; } @@ -217,48 +296,60 @@ class TemplateParseVisitor implements HtmlAstVisitor { } private _parseVariable(identifier: string, value: string, sourceInfo: any, - matchableAttrs: string[][], vars: VariableAst[]) { - vars.push(new VariableAst(dashCaseToCamelCase(identifier), value, sourceInfo)); - matchableAttrs.push([identifier, value]); + targetMatchableAttrs: string[][], targetVars: VariableAst[]) { + targetVars.push(new VariableAst(dashCaseToCamelCase(identifier), value, sourceInfo)); + targetMatchableAttrs.push([identifier, value]); } private _parseProperty(name: string, expression: string, sourceInfo: any, - matchableAttrs: string[][], props: BoundPropertyAst[]) { - this._parsePropertyAst(name, this._exprParser.parseBinding(expression, sourceInfo), sourceInfo, - matchableAttrs, props); + targetMatchableAttrs: string[][], + targetProps: BoundElementOrDirectiveProperty[]) { + this._parsePropertyAst(name, this._parseBinding(expression, sourceInfo), sourceInfo, + targetMatchableAttrs, targetProps); } private _parsePropertyInterpolation(name: string, value: string, sourceInfo: any, - matchableAttrs: string[][], - props: BoundPropertyAst[]): boolean { - var expr = this._exprParser.parseInterpolation(value, sourceInfo); + targetMatchableAttrs: string[][], + targetProps: BoundElementOrDirectiveProperty[]): boolean { + var expr = this._parseInterpolation(value, sourceInfo); if (isPresent(expr)) { - this._parsePropertyAst(name, expr, sourceInfo, matchableAttrs, props); + this._parsePropertyAst(name, expr, sourceInfo, targetMatchableAttrs, targetProps); return true; } return false; } private _parsePropertyAst(name: string, ast: ASTWithSource, sourceInfo: any, - matchableAttrs: string[][], props: BoundPropertyAst[]) { - props.push(new BoundPropertyAst(dashCaseToCamelCase(name), ast, sourceInfo)); - matchableAttrs.push([name, ast.source]); + targetMatchableAttrs: string[][], + targetProps: BoundElementOrDirectiveProperty[]) { + targetMatchableAttrs.push([name, ast.source]); + targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceInfo)); } private _parseAssignmentEvent(name: string, expression: string, sourceInfo: string, - matchableAttrs: string[][], events: BoundEventAst[]) { - this._parseEvent(name, `${expression}=$event`, sourceInfo, matchableAttrs, events); + targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + this._parseEvent(name, `${expression}=$event`, sourceInfo, targetMatchableAttrs, targetEvents); } private _parseEvent(name: string, expression: string, sourceInfo: string, - matchableAttrs: string[][], events: BoundEventAst[]) { - events.push(new BoundEventAst(dashCaseToCamelCase(name), - this._exprParser.parseAction(expression, sourceInfo), - sourceInfo)); + targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + // long format: 'target: eventName' + var parts = splitAtColon(name, [null, name]); + var target = parts[0]; + var eventName = parts[1]; + targetEvents.push(new BoundEventAst(dashCaseToCamelCase(eventName), target, + this._parseAction(expression, sourceInfo), sourceInfo)); // Don't detect directives for event names for now, // so don't add the event name to the matchableAttrs } + private _parseLiteralAttr(name: string, value: string, sourceInfo: string, + targetProps: BoundElementOrDirectiveProperty[]) { + targetProps.push(new BoundElementOrDirectiveProperty( + dashCaseToCamelCase(name), this._exprParser.wrapLiteralPrimitive(value, sourceInfo), true, + sourceInfo)); + } + private _parseDirectives(selectorMatcher: SelectorMatcher, elementName: string, matchableAttrs: string[][]): DirectiveMetadata[] { var cssSelector = new CssSelector(); @@ -291,8 +382,178 @@ class TemplateParseVisitor implements HtmlAstVisitor { }); return directives; } + + private _createDirectiveAsts(elementName: string, directives: DirectiveMetadata[], + props: BoundElementOrDirectiveProperty[], + sourceInfo: string): DirectiveAst[] { + return directives.map((directive: DirectiveMetadata) => { + var hostProperties: BoundElementPropertyAst[] = []; + var hostEvents: BoundEventAst[] = []; + var directiveProperties: BoundDirectivePropertyAst[] = []; + var changeDetection = directive.changeDetection; + if (isPresent(changeDetection)) { + this._createDirectiveHostPropertyAsts(elementName, changeDetection.hostProperties, + sourceInfo, hostProperties); + this._createDirectiveHostEventAsts(changeDetection.hostListeners, sourceInfo, hostEvents); + this._createDirectivePropertyAsts(changeDetection.properties, props, directiveProperties); + } + return new DirectiveAst(directive, directiveProperties, hostProperties, hostEvents, + sourceInfo); + }); + } + + private _createDirectiveHostPropertyAsts(elementName: string, + hostProps: StringMap, sourceInfo: string, + targetPropertyAsts: BoundElementPropertyAst[]) { + if (isPresent(hostProps)) { + StringMapWrapper.forEach(hostProps, (expression, propName) => { + var exprAst = this._parseBinding(expression, sourceInfo); + targetPropertyAsts.push( + this._createElementPropertyAst(elementName, propName, exprAst, sourceInfo)); + }); + } + } + + private _createDirectiveHostEventAsts(hostListeners: StringMap, + sourceInfo: string, targetEventAsts: BoundEventAst[]) { + if (isPresent(hostListeners)) { + StringMapWrapper.forEach(hostListeners, (expression, propName) => { + this._parseEvent(propName, expression, sourceInfo, [], targetEventAsts); + }); + } + } + + private _createDirectivePropertyAsts(directiveProperties: string[], + boundProps: BoundElementOrDirectiveProperty[], + targetBoundDirectiveProps: BoundDirectivePropertyAst[]) { + if (isPresent(directiveProperties)) { + var boundPropsByName: Map = new Map(); + boundProps.forEach(boundProp => + boundPropsByName.set(dashCaseToCamelCase(boundProp.name), boundProp)); + + directiveProperties.forEach((bindConfig: string) => { + // canonical syntax: `dirProp: elProp` + // if there is no `:`, use dirProp = elProp + var parts = splitAtColon(bindConfig, [bindConfig, bindConfig]); + var dirProp = parts[0]; + var elProp = dashCaseToCamelCase(parts[1]); + var boundProp = boundPropsByName.get(elProp); + + // Bindings are optional, so this binding only needs to be set up if an expression is given. + if (isPresent(boundProp)) { + targetBoundDirectiveProps.push(new BoundDirectivePropertyAst( + dirProp, boundProp.name, boundProp.expression, boundProp.sourceInfo)); + } + }); + } + } + + private _createElementPropertyAsts(elementName: string, props: BoundElementOrDirectiveProperty[], + directives: DirectiveAst[]): BoundElementPropertyAst[] { + var boundElementProps: BoundElementPropertyAst[] = []; + var boundDirectivePropsIndex: Map = new Map(); + directives.forEach((directive: DirectiveAst) => { + directive.properties.forEach((prop: BoundDirectivePropertyAst) => { + boundDirectivePropsIndex.set(prop.templateName, prop); + }); + }); + props.forEach((prop: BoundElementOrDirectiveProperty) => { + if (!prop.isLiteral && isBlank(boundDirectivePropsIndex.get(prop.name))) { + boundElementProps.push(this._createElementPropertyAst(elementName, prop.name, + prop.expression, prop.sourceInfo)); + } + }); + return boundElementProps; + } + + private _createElementPropertyAst(elementName: string, name: string, ast: AST, + sourceInfo: any): BoundElementPropertyAst { + var unit = null; + var bindingType; + var boundPropertyName; + var parts = StringWrapper.split(name, PROPERTY_PARTS_SEPARATOR); + if (parts.length === 1) { + boundPropertyName = this._schemaRegistry.getMappedPropName(dashCaseToCamelCase(parts[0])); + bindingType = PropertyBindingType.Property; + if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) { + this._reportError( + `Can't bind to '${boundPropertyName}' since it isn't a known native property in ${sourceInfo}`); + } + } else if (parts[0] == ATTRIBUTE_PREFIX) { + boundPropertyName = dashCaseToCamelCase(parts[1]); + bindingType = PropertyBindingType.Attribute; + } else if (parts[0] == CLASS_PREFIX) { + // keep original case! + boundPropertyName = parts[1]; + bindingType = PropertyBindingType.Class; + } else if (parts[0] == STYLE_PREFIX) { + unit = parts.length > 2 ? parts[2] : null; + boundPropertyName = dashCaseToCamelCase(parts[1]); + bindingType = PropertyBindingType.Style; + } else { + this._reportError(`Invalid property name ${name} in ${sourceInfo}`); + bindingType = null; + } + return new BoundElementPropertyAst(boundPropertyName, bindingType, ast, unit, sourceInfo); + } + + + private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] { + var componentTypeNames: string[] = []; + directives.forEach(directive => { + var typeName = directive.directive.type.typeName; + if (directive.directive.isComponent) { + componentTypeNames.push(typeName); + } + }); + return componentTypeNames; + } + + private _assertOnlyOneComponent(directives: DirectiveAst[], sourceInfo: string) { + var componentTypeNames = this._findComponentDirectiveNames(directives); + if (componentTypeNames.length > 1) { + this._reportError( + `More than one component: ${componentTypeNames.join(',')} in ${sourceInfo}`); + } + } + + _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[], + elementProps: BoundElementPropertyAst[], + events: BoundEventAst[], sourceInfo: string) { + var componentTypeNames: string[] = this._findComponentDirectiveNames(directives); + if (componentTypeNames.length > 0) { + this._reportError( + `Components on an embedded template: ${componentTypeNames.join(',')} in ${sourceInfo}`); + } + elementProps.forEach(prop => { + this._reportError( + `Property binding ${prop.name} not used by any directive on an embedded template in ${prop.sourceInfo}`); + }); + events.forEach(event => { + this._reportError( + `Event binding ${event.name} on an embedded template in ${event.sourceInfo}`); + }); + } +} + +class BoundElementOrDirectiveProperty { + constructor(public name: string, public expression: AST, public isLiteral: boolean, + public sourceInfo: string) {} +} + +class ParseError { + constructor(public message: string, public sourceInfo: string) {} } export function splitClasses(classAttrValue: string): string[] { return StringWrapper.split(classAttrValue.trim(), /\s+/g); } + +export function splitAtColon(input: string, defaultValues: string[]): string[] { + var parts = StringWrapper.split(input.trim(), /\s*:\s*/g); + if (parts.length > 1) { + return parts; + } else { + return defaultValues; + } +} \ No newline at end of file diff --git a/modules/angular2/src/compiler/util.ts b/modules/angular2/src/compiler/util.ts index e0963ead53..1ba3c4fdad 100644 --- a/modules/angular2/src/compiler/util.ts +++ b/modules/angular2/src/compiler/util.ts @@ -1,7 +1,14 @@ import {StringWrapper} from 'angular2/src/core/facade/lang'; +var CAMEL_CASE_REGEXP = /([A-Z])/g; var DASH_CASE_REGEXP = /-([a-z])/g; + +export function camelCaseToDashCase(input: string): string { + return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP, + (m) => { return '-' + m[1].toLowerCase(); }); +} + export function dashCaseToCamelCase(input: string): string { return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP, (m) => { return m[1].toUpperCase(); }); diff --git a/modules/angular2/src/core/render/dom/schema/dom_element_schema_registry.ts b/modules/angular2/src/core/render/dom/schema/dom_element_schema_registry.ts index 346cd143d2..f5a9b97d81 100644 --- a/modules/angular2/src/core/render/dom/schema/dom_element_schema_registry.ts +++ b/modules/angular2/src/core/render/dom/schema/dom_element_schema_registry.ts @@ -1,17 +1,28 @@ -import {isPresent} from 'angular2/src/core/facade/lang'; +import {isPresent, isBlank} from 'angular2/src/core/facade/lang'; import {StringMapWrapper} from 'angular2/src/core/facade/collection'; import {DOM} from 'angular2/src/core/dom/dom_adapter'; import {ElementSchemaRegistry} from './element_schema_registry'; export class DomElementSchemaRegistry extends ElementSchemaRegistry { - hasProperty(elm: any, propName: string): boolean { - var tagName = DOM.tagName(elm); + private _protoElements: Map = new Map(); + + private _getProtoElement(tagName: string): Element { + var element = this._protoElements.get(tagName); + if (isBlank(element)) { + element = DOM.createElement(tagName); + this._protoElements.set(tagName, element); + } + return element; + } + + hasProperty(tagName: string, propName: string): boolean { if (tagName.indexOf('-') !== -1) { // can't tell now as we don't know which properties a custom element will get // once it is instantiated return true; } else { + var elm = this._getProtoElement(tagName); return DOM.hasProperty(elm, propName); } } diff --git a/modules/angular2/src/core/render/dom/schema/element_schema_registry.ts b/modules/angular2/src/core/render/dom/schema/element_schema_registry.ts index b0a35d2243..bfbd5db161 100644 --- a/modules/angular2/src/core/render/dom/schema/element_schema_registry.ts +++ b/modules/angular2/src/core/render/dom/schema/element_schema_registry.ts @@ -1,4 +1,4 @@ export class ElementSchemaRegistry { - hasProperty(elm: any, propName: string): boolean { return true; } + hasProperty(tagName: string, propName: string): boolean { return true; } getMappedPropName(propName: string): string { return propName; } } diff --git a/modules/angular2/src/core/render/dom/view/proto_view_builder.ts b/modules/angular2/src/core/render/dom/view/proto_view_builder.ts index f9cb9cd3e8..09db14f478 100644 --- a/modules/angular2/src/core/render/dom/view/proto_view_builder.ts +++ b/modules/angular2/src/core/render/dom/view/proto_view_builder.ts @@ -369,7 +369,7 @@ function isValidElementPropertyBinding(schemaRegistry: ElementSchemaRegistry, binding: ElementPropertyBinding): boolean { if (binding.type === PropertyBindingType.PROPERTY) { if (!isNgComponent) { - return schemaRegistry.hasProperty(protoElement, binding.property); + return schemaRegistry.hasProperty(DOM.tagName(protoElement), binding.property); } else { // TODO(pk): change this logic as soon as we can properly detect custom elements return DOM.hasProperty(protoElement, binding.property); diff --git a/modules/angular2/test/compiler/template_loader_spec.ts b/modules/angular2/test/compiler/template_loader_spec.ts index 8db90937b6..47bba55abf 100644 --- a/modules/angular2/test/compiler/template_loader_spec.ts +++ b/modules/angular2/test/compiler/template_loader_spec.ts @@ -15,7 +15,7 @@ import { } from 'angular2/test_lib'; import {HtmlParser} from 'angular2/src/compiler/html_parser'; -import {TypeMeta, ViewEncapsulation, TemplateMeta} from 'angular2/src/compiler/api'; +import {TypeMetadata, ViewEncapsulation, TemplateMetadata} from 'angular2/src/compiler/api'; import {TemplateLoader} from 'angular2/src/compiler/template_loader'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; @@ -28,21 +28,21 @@ import {MockXHR} from 'angular2/src/core/render/xhr_mock'; export function main() { describe('TemplateLoader', () => { var loader: TemplateLoader; - var dirType: TypeMeta; + var dirType: TypeMetadata; var xhr: MockXHR; beforeEach(inject([XHR], (mockXhr) => { xhr = mockXhr; var urlResolver = new UrlResolver(); loader = new TemplateLoader(xhr, urlResolver, new StyleUrlResolver(urlResolver), new HtmlParser()); - dirType = new TypeMeta({typeUrl: 'http://sometypeurl', typeName: 'SomeComp'}); + dirType = new TypeMetadata({typeUrl: 'http://sometypeurl', typeName: 'SomeComp'}); })); describe('loadTemplate', () => { describe('inline template', () => { it('should parse the template', inject([AsyncTestCompleter], (async) => { loader.loadTemplate(dirType, null, 'a', null, [], ['test.css']) - .then((template: TemplateMeta) => { + .then((template: TemplateMetadata) => { expect(humanizeDom(template.nodes)) .toEqual([[HtmlTextAst, 'a', 'SomeComp > #text(a):nth-child(0)']]) async.done(); @@ -51,7 +51,7 @@ export function main() { it('should resolve styles against the typeUrl', inject([AsyncTestCompleter], (async) => { loader.loadTemplate(dirType, null, 'a', null, [], ['test.css']) - .then((template: TemplateMeta) => { + .then((template: TemplateMetadata) => { expect(template.styleAbsUrls).toEqual(['http://sometypeurl/test.css']); async.done(); }); @@ -64,7 +64,7 @@ export function main() { inject([AsyncTestCompleter], (async) => { xhr.expect('http://sometypeurl/sometplurl', 'a'); loader.loadTemplate(dirType, null, null, 'sometplurl', [], ['test.css']) - .then((template: TemplateMeta) => { + .then((template: TemplateMetadata) => { expect(humanizeDom(template.nodes)) .toEqual([[HtmlTextAst, 'a', 'SomeComp > #text(a):nth-child(0)']]) async.done(); @@ -76,7 +76,7 @@ export function main() { inject([AsyncTestCompleter], (async) => { xhr.expect('http://sometypeurl/tpl/sometplurl', 'a'); loader.loadTemplate(dirType, null, null, 'tpl/sometplurl', [], ['test.css']) - .then((template: TemplateMeta) => { + .then((template: TemplateMetadata) => { expect(template.styleAbsUrls).toEqual(['http://sometypeurl/tpl/test.css']); async.done(); }); diff --git a/modules/angular2/test/compiler/template_parser_spec.ts b/modules/angular2/test/compiler/template_parser_spec.ts index fb5717ff2b..72829a5793 100644 --- a/modules/angular2/test/compiler/template_parser_spec.ts +++ b/modules/angular2/test/compiler/template_parser_spec.ts @@ -1,9 +1,10 @@ import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib'; +import {isPresent} from 'angular2/src/core/facade/lang'; import {Parser, Lexer} from 'angular2/src/core/change_detection/change_detection'; import {TemplateParser, splitClasses} from 'angular2/src/compiler/template_parser'; import {HtmlParser} from 'angular2/src/compiler/html_parser'; -import {DirectiveMetadata, TypeMeta} from 'angular2/src/compiler/api'; +import {DirectiveMetadata, TypeMetadata, ChangeDetectionMetadata} from 'angular2/src/compiler/api'; import { templateVisitAll, TemplateAstVisitor, @@ -13,12 +14,17 @@ import { ElementAst, VariableAst, BoundEventAst, - BoundPropertyAst, + BoundElementPropertyAst, + BoundDirectivePropertyAst, AttrAst, BoundTextAst, - TextAst + TextAst, + PropertyBindingType, + DirectiveAst } from 'angular2/src/compiler/template_ast'; +import {ElementSchemaRegistry} from 'angular2/src/core/render/dom/schema/element_schema_registry'; + import {Unparser} from '../core/change_detection/parser/unparser'; var expressionUnparser = new Unparser(); @@ -27,10 +33,18 @@ export function main() { describe('TemplateParser', () => { var domParser: HtmlParser; var parser: TemplateParser; + var ngIf; beforeEach(() => { domParser = new HtmlParser(); - parser = new TemplateParser(new Parser(new Lexer())); + parser = new TemplateParser( + new Parser(new Lexer()), + new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'})); + ngIf = new DirectiveMetadata({ + selector: '[ng-if]', + type: new TypeMetadata({typeName: 'NgIf'}), + changeDetection: new ChangeDetectionMetadata({properties: ['ngIf']}) + }); }); function parse(template: string, directives: DirectiveMetadata[]): TemplateAst[] { @@ -48,7 +62,7 @@ export function main() { it('should parse elements with attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], + [ElementAst, 'TestComp > div:nth-child(0)'], [AttrAst, 'a', 'b', 'TestComp > div:nth-child(0)[a=b]'] ]); }); @@ -65,73 +79,196 @@ export function main() { .toEqual([[BoundTextAst, '{{ a }}', 'TestComp > #text({{a}}):nth-child(0)']]); }); - describe('property, event and variable bindings', () => { + describe('bound properties', () => { + + it('should parse and camel case bound properties', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'someProp', + 'v', + null, + 'TestComp > div:nth-child(0)[[some-prop]=v]' + ] + ]); + }); + + it('should normalize property names via the element schema', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'mappedProp', + 'v', + null, + 'TestComp > div:nth-child(0)[[mapped-attr]=v]' + ] + ]); + }); + + it('should parse and camel case bound attributes', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Attribute, + 'someAttr', + 'v', + null, + 'TestComp > div:nth-child(0)[[attr.some-attr]=v]' + ] + ]); + }); + + it('should parse and dash case bound classes', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Class, + 'some-class', + 'v', + null, + 'TestComp > div:nth-child(0)[[class.some-class]=v]' + ] + ]); + }); + + it('should parse and camel case bound styles', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Style, + 'someStyle', + 'v', + null, + 'TestComp > div:nth-child(0)[[style.some-style]=v]' + ] + ]); + }); it('should parse bound properties via [...] and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'prop', 'v', 'TestComp > div:nth-child(0)[[prop]=v]'] - ]); - }); - - it('should camel case bound properties', () => { - expect(humanizeTemplateAsts(parse('
', []))) - .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'someProp', 'v', 'TestComp > div:nth-child(0)[[some-prop]=v]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'prop', + 'v', + null, + 'TestComp > div:nth-child(0)[[prop]=v]' + ] ]); }); it('should parse bound properties via bind- and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'prop', 'v', 'TestComp > div:nth-child(0)[bind-prop=v]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'prop', + 'v', + null, + 'TestComp > div:nth-child(0)[bind-prop=v]' + ] ]); }); it('should parse bound properties via {{...}} and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'prop', '{{ v }}', 'TestComp > div:nth-child(0)[prop={{v}}]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'prop', + '{{ v }}', + null, + 'TestComp > div:nth-child(0)[prop={{v}}]' + ] + ]); + }); + + }); + + describe('events', () => { + + it('should parse bound events with a target', () => { + expect(humanizeTemplateAsts(parse('
', []))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundEventAst, + 'event', + 'window', + 'v', + 'TestComp > div:nth-child(0)[(window:event)=v]' + ] ]); }); it('should parse bound events via (...) and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundEventAst, 'event', 'v', 'TestComp > div:nth-child(0)[(event)=v]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [BoundEventAst, 'event', null, 'v', 'TestComp > div:nth-child(0)[(event)=v]'] ]); }); it('should camel case event names', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundEventAst, 'someEvent', 'v', 'TestComp > div:nth-child(0)[(some-event)=v]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundEventAst, + 'someEvent', + null, + 'v', + 'TestComp > div:nth-child(0)[(some-event)=v]' + ] ]); }); it('should parse bound events via on- and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundEventAst, 'event', 'v', 'TestComp > div:nth-child(0)[on-event=v]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [BoundEventAst, 'event', null, 'v', 'TestComp > div:nth-child(0)[on-event=v]'] ]); }); + }); + + describe('bindon', () => { it('should parse bound events and properties via [(...)] and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'prop', 'v', 'TestComp > div:nth-child(0)[[(prop)]=v]'], + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'prop', + 'v', + null, + 'TestComp > div:nth-child(0)[[(prop)]=v]' + ], [ BoundEventAst, 'prop', + null, 'v = $event', 'TestComp > div:nth-child(0)[[(prop)]=v]' ] @@ -142,21 +279,33 @@ export function main() { () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'prop', 'v', 'TestComp > div:nth-child(0)[bindon-prop=v]'], + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'prop', + 'v', + null, + 'TestComp > div:nth-child(0)[bindon-prop=v]' + ], [ BoundEventAst, 'prop', + null, 'v = $event', 'TestComp > div:nth-child(0)[bindon-prop=v]' ] ]); }); + }); + + describe('variables', () => { + it('should parse variables via #... and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], + [ElementAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[#a=b]'] ]); }); @@ -164,7 +313,7 @@ export function main() { it('should parse variables via var-... and not report them as attributes', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], + [ElementAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[var-a=b]'] ]); }); @@ -172,7 +321,7 @@ export function main() { it('should camel case variables', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], + [ElementAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'someA', 'b', 'TestComp > div:nth-child(0)[var-some-a=b]'] ]); }); @@ -180,7 +329,7 @@ export function main() { it('should use $implicit as variable name if none was specified', () => { expect(humanizeTemplateAsts(parse('
', []))) .toEqual([ - [ElementAst, [], 'TestComp > div:nth-child(0)'], + [ElementAst, 'TestComp > div:nth-child(0)'], [VariableAst, 'a', '$implicit', 'TestComp > div:nth-child(0)[var-a=]'] ]); }); @@ -188,48 +337,162 @@ export function main() { describe('directives', () => { it('should locate directives ordered by typeName and components first', () => { - var dirA = - new DirectiveMetadata({selector: '[a=b]', type: new TypeMeta({typeName: 'DirA'})}); + var dirA = new DirectiveMetadata( + {selector: '[a=b]', type: new TypeMetadata({typeName: 'DirA'})}); var dirB = - new DirectiveMetadata({selector: '[a]', type: new TypeMeta({typeName: 'DirB'})}); + new DirectiveMetadata({selector: '[a]', type: new TypeMetadata({typeName: 'DirB'})}); var comp = new DirectiveMetadata( - {selector: 'div', isComponent: true, type: new TypeMeta({typeName: 'ZComp'})}); + {selector: 'div', isComponent: true, type: new TypeMetadata({typeName: 'ZComp'})}); expect(humanizeTemplateAsts(parse('
', [dirB, dirA, comp]))) .toEqual([ - [ElementAst, [comp, dirA, dirB], 'TestComp > div:nth-child(0)'], - [AttrAst, 'a', 'b', 'TestComp > div:nth-child(0)[a=b]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [AttrAst, 'a', 'b', 'TestComp > div:nth-child(0)[a=b]'], + [DirectiveAst, comp, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirB, 'TestComp > div:nth-child(0)'] ]); }); it('should locate directives in property bindings', () => { - var dirA = - new DirectiveMetadata({selector: '[a=b]', type: new TypeMeta({typeName: 'DirA'})}); + var dirA = new DirectiveMetadata( + {selector: '[a=b]', type: new TypeMetadata({typeName: 'DirA'})}); var dirB = - new DirectiveMetadata({selector: '[b]', type: new TypeMeta({typeName: 'DirB'})}); + new DirectiveMetadata({selector: '[b]', type: new TypeMetadata({typeName: 'DirB'})}); expect(humanizeTemplateAsts(parse('
', [dirA, dirB]))) .toEqual([ - [ElementAst, [dirA], 'TestComp > div:nth-child(0)'], - [BoundPropertyAst, 'a', 'b', 'TestComp > div:nth-child(0)[[a]=b]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'a', + 'b', + null, + 'TestComp > div:nth-child(0)[[a]=b]' + ], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'] ]); }); it('should locate directives in variable bindings', () => { - var dirA = - new DirectiveMetadata({selector: '[a=b]', type: new TypeMeta({typeName: 'DirA'})}); + var dirA = new DirectiveMetadata( + {selector: '[a=b]', type: new TypeMetadata({typeName: 'DirA'})}); var dirB = - new DirectiveMetadata({selector: '[b]', type: new TypeMeta({typeName: 'DirB'})}); + new DirectiveMetadata({selector: '[b]', type: new TypeMetadata({typeName: 'DirB'})}); expect(humanizeTemplateAsts(parse('
', [dirA, dirB]))) .toEqual([ - [ElementAst, [dirA], 'TestComp > div:nth-child(0)'], - [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[#a=b]'] + [ElementAst, 'TestComp > div:nth-child(0)'], + [VariableAst, 'a', 'b', 'TestComp > div:nth-child(0)[#a=b]'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'] ]); }); + + it('should parse directive host properties', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({hostProperties: {'a': 'expr'}}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [ + BoundElementPropertyAst, + PropertyBindingType.Property, + 'a', + 'expr', + null, + 'TestComp > div:nth-child(0)' + ] + ]); + }); + + it('should parse directive host listeners', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({hostListeners: {'a': 'expr'}}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [BoundEventAst, 'a', null, 'expr', 'TestComp > div:nth-child(0)'] + ]); + }); + + it('should parse directive properties', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({properties: ['aProp']}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [ + BoundDirectivePropertyAst, + 'aProp', + 'expr', + 'TestComp > div:nth-child(0)[[a-prop]=expr]' + ] + ]); + }); + + it('should parse renamed directive properties', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({properties: ['b:a']}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [BoundDirectivePropertyAst, 'b', 'expr', 'TestComp > div:nth-child(0)[[a]=expr]'] + ]); + }); + + it('should parse literal directive properties', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({properties: ['a']}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [AttrAst, 'a', 'literal', 'TestComp > div:nth-child(0)[a=literal]'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'], + [ + BoundDirectivePropertyAst, + 'a', + '"literal"', + 'TestComp > div:nth-child(0)[a=literal]' + ] + ]); + }); + + it('should support optional directive properties', () => { + var dirA = new DirectiveMetadata({ + selector: 'div', + type: new TypeMetadata({typeName: 'DirA'}), + changeDetection: new ChangeDetectionMetadata({properties: ['a']}) + }); + expect(humanizeTemplateAsts(parse('
', [dirA]))) + .toEqual([ + [ElementAst, 'TestComp > div:nth-child(0)'], + [DirectiveAst, dirA, 'TestComp > div:nth-child(0)'] + ]); + }); + }); describe('explicit templates', () => { it('should create embedded templates for