feat(DirectiveParser): throw errors when expected directives are not present

closes #527
Closes #570
This commit is contained in:
Bertrand Laporte 2015-02-06 15:41:02 -08:00 committed by Misko Hevery
parent 715ee14ced
commit 94e203b9df
18 changed files with 354 additions and 123 deletions

View File

@ -17,6 +17,7 @@ import {Template} from '../annotations/template';
import {ShadowDomStrategy} from './shadow_dom_strategy'; import {ShadowDomStrategy} from './shadow_dom_strategy';
import {CompileStep} from './pipeline/compile_step'; import {CompileStep} from './pipeline/compile_step';
/** /**
* Cache that stores the ProtoView of the template of a component. * Cache that stores the ProtoView of the template of a component.
* Used to prevent duplicate work and resolve cyclic dependencies. * Used to prevent duplicate work and resolve cyclic dependencies.
@ -134,7 +135,15 @@ export class Compiler {
// TODO(vicb): union type return ProtoView or Promise<ProtoView> // TODO(vicb): union type return ProtoView or Promise<ProtoView>
_compileTemplate(template: Template, tplElement: Element, component: Type) { _compileTemplate(template: Template, tplElement: Element, component: Type) {
var pipeline = new CompilePipeline(this.createSteps(component, template)); var pipeline = new CompilePipeline(this.createSteps(component, template));
var compileElements = pipeline.process(tplElement); var compilationCtxtDescription = stringify(this._reader.read(component).type);
var compileElements;
try {
compileElements = pipeline.process(tplElement, compilationCtxtDescription);
} catch(ex) {
return PromiseWrapper.reject(ex);
}
var protoView = compileElements[0].inheritedProtoView; var protoView = compileElements[0].inheritedProtoView;
// Populate the cache before compiling the nested components, // Populate the cache before compiling the nested components,

View File

@ -1,6 +1,6 @@
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {Element, DOM} from 'angular2/src/facade/dom'; import {Element, DOM} from 'angular2/src/facade/dom';
import {int, isBlank, isPresent, Type} from 'angular2/src/facade/lang'; import {int, isBlank, isPresent, Type, StringJoiner, assertionsEnabled} from 'angular2/src/facade/lang';
import {DirectiveMetadata} from '../directive_metadata'; import {DirectiveMetadata} from '../directive_metadata';
import {Decorator, Component, Viewport} from '../../annotations/annotations'; import {Decorator, Component, Viewport} from '../../annotations/annotations';
import {ElementBinder} from '../element_binder'; import {ElementBinder} from '../element_binder';
@ -38,8 +38,9 @@ export class CompileElement {
distanceToParentInjector:number; distanceToParentInjector:number;
compileChildren: boolean; compileChildren: boolean;
ignoreBindings: boolean; ignoreBindings: boolean;
elementDescription: string; // e.g. '<div [class]="foo">' : used to provide context in case of error
constructor(element:Element) { constructor(element:Element, compilationUnit = '') {
this.element = element; this.element = element;
this._attrs = null; this._attrs = null;
this._classList = null; this._classList = null;
@ -66,6 +67,14 @@ export class CompileElement {
this.compileChildren = true; this.compileChildren = true;
// set to true to ignore all the bindings on the element // set to true to ignore all the bindings on the element
this.ignoreBindings = false; this.ignoreBindings = false;
// description is calculated here as compilation steps may change the element
var tplDesc = assertionsEnabled()? getElementDescription(element) : null;
if (compilationUnit !== '') {
this.elementDescription = compilationUnit;
if (isPresent(tplDesc)) this.elementDescription += ": " + tplDesc;
} else {
this.elementDescription = tplDesc;
}
} }
refreshAttrs() { refreshAttrs() {
@ -165,3 +174,36 @@ export class CompileElement {
return this._allDirectives; return this._allDirectives;
} }
} }
// return an HTML representation of an element start tag - without its content
// this is used to give contextual information in case of errors
function getElementDescription(domElement:Element):string {
var buf = new StringJoiner();
var atts = DOM.attributeMap(domElement);
buf.add("<");
buf.add(DOM.tagName(domElement).toLowerCase());
// show id and class first to ease element identification
addDescriptionAttribute(buf, "id", MapWrapper.get(atts, "id"));
addDescriptionAttribute(buf, "class", MapWrapper.get(atts, "class"));
MapWrapper.forEach(atts, (attValue, attName) => {
if (attName !== "id" && attName !== "class") {
addDescriptionAttribute(buf, attName, attValue);
}
});
buf.add(">");
return buf.toString();
}
function addDescriptionAttribute(buffer:StringJoiner, attName:string, attValue) {
if (isPresent(attValue)) {
if (attValue.length === 0) {
buffer.add(' ' + attName);
} else {
buffer.add(' ' + attName + '="' + attValue + '"');
}
}
}

View File

@ -15,13 +15,13 @@ export class CompilePipeline {
this._control = new CompileControl(steps); this._control = new CompileControl(steps);
} }
process(rootElement:Element):List { process(rootElement:Element, compilationCtxtDescription:string = ''):List {
var results = ListWrapper.create(); var results = ListWrapper.create();
this._process(results, null, new CompileElement(rootElement)); this._process(results, null, new CompileElement(rootElement, compilationCtxtDescription), compilationCtxtDescription);
return results; return results;
} }
_process(results, parent:CompileElement, current:CompileElement) { _process(results, parent:CompileElement, current:CompileElement, compilationCtxtDescription:string = '') {
var additionalChildren = this._control.internalProcess(results, 0, parent, current); var additionalChildren = this._control.internalProcess(results, 0, parent, current);
if (current.compileChildren) { if (current.compileChildren) {
@ -31,7 +31,7 @@ export class CompilePipeline {
// next sibling before recursing. // next sibling before recursing.
var nextNode = DOM.nextSibling(node); var nextNode = DOM.nextSibling(node);
if (DOM.isElementNode(node)) { if (DOM.isElementNode(node)) {
this._process(results, current, new CompileElement(node)); this._process(results, current, new CompileElement(node, compilationCtxtDescription));
} }
node = nextNode; node = nextNode;
} }

View File

@ -13,7 +13,6 @@ import {ShimShadowCss} from './shim_shadow_css';
import {ShimShadowDom} from './shim_shadow_dom'; import {ShimShadowDom} from './shim_shadow_dom';
import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata'; import {DirectiveMetadata} from 'angular2/src/core/compiler/directive_metadata';
import {ShadowDomStrategy, EmulatedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {ShadowDomStrategy, EmulatedShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy';
import {stringify} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/facade/dom'; import {DOM} from 'angular2/src/facade/dom';
/** /**
@ -28,9 +27,7 @@ export function createDefaultSteps(
directives: List<DirectiveMetadata>, directives: List<DirectiveMetadata>,
shadowDomStrategy: ShadowDomStrategy) { shadowDomStrategy: ShadowDomStrategy) {
var compilationUnit = stringify(compiledComponent.type); var steps = [new ViewSplitter(parser)];
var steps = [new ViewSplitter(parser, compilationUnit)];
if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) { if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) {
var step = new ShimShadowCss(compiledComponent, shadowDomStrategy, DOM.defaultDoc().head); var step = new ShimShadowCss(compiledComponent, shadowDomStrategy, DOM.defaultDoc().head);
@ -38,13 +35,13 @@ export function createDefaultSteps(
} }
steps = ListWrapper.concat(steps,[ steps = ListWrapper.concat(steps,[
new PropertyBindingParser(parser, compilationUnit), new PropertyBindingParser(parser),
new DirectiveParser(directives), new DirectiveParser(directives),
new TextInterpolationParser(parser, compilationUnit), new TextInterpolationParser(parser),
new ElementBindingMarker(), new ElementBindingMarker(),
new ProtoViewBuilder(changeDetection, shadowDomStrategy), new ProtoViewBuilder(changeDetection, shadowDomStrategy),
new ProtoElementInjectorBuilder(), new ProtoElementInjectorBuilder(),
new ElementBinderBuilder(parser, compilationUnit) new ElementBinderBuilder(parser)
]); ]);
if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) { if (shadowDomStrategy instanceof EmulatedShadowDomStrategy) {

View File

@ -1,5 +1,5 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang'; import {isPresent, isBlank, BaseException, assertionsEnabled, RegExpWrapper} from 'angular2/src/facade/lang';
import {List, MapWrapper} from 'angular2/src/facade/collection'; import {List, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/facade/dom'; import {DOM} from 'angular2/src/facade/dom';
import {SelectorMatcher} from '../selector'; import {SelectorMatcher} from '../selector';
import {CssSelector} from '../selector'; import {CssSelector} from '../selector';
@ -10,6 +10,10 @@ import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element'; import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control'; import {CompileControl} from './compile_control';
import {isSpecialProperty} from './element_binder_builder';;
var PROPERTY_BINDING_REGEXP = RegExpWrapper.create('^ *([^\\s\\|]+)');
/** /**
* Parses the directives on a single element. Assumes ViewSplitter has already created * Parses the directives on a single element. Assumes ViewSplitter has already created
* <template> elements for template directives. * <template> elements for template directives.
@ -29,13 +33,13 @@ export class DirectiveParser extends CompileStep {
_selectorMatcher:SelectorMatcher; _selectorMatcher:SelectorMatcher;
constructor(directives:List<DirectiveMetadata>) { constructor(directives:List<DirectiveMetadata>) {
super(); super();
var selector;
this._selectorMatcher = new SelectorMatcher(); this._selectorMatcher = new SelectorMatcher();
for (var i=0; i<directives.length; i++) { for (var i=0; i<directives.length; i++) {
var directiveMetadata = directives[i]; var directiveMetadata = directives[i];
this._selectorMatcher.addSelectable( selector=CssSelector.parse(directiveMetadata.annotation.selector);
CssSelector.parse(directiveMetadata.annotation.selector), this._selectorMatcher.addSelectable(selector, directiveMetadata);
directiveMetadata
);
} }
} }
@ -67,19 +71,85 @@ export class DirectiveParser extends CompileStep {
// Note: We assume that the ViewSplitter already did its work, i.e. template directive should // Note: We assume that the ViewSplitter already did its work, i.e. template directive should
// only be present on <template> elements any more! // only be present on <template> elements any more!
var isTemplateElement = DOM.isTemplateElement(current.element); var isTemplateElement = DOM.isTemplateElement(current.element);
this._selectorMatcher.match(cssSelector, (directive) => { var matchedProperties; // StringMap - used in dev mode to store all properties that have been matched
if (directive.annotation instanceof Viewport) {
if (!isTemplateElement) { this._selectorMatcher.match(cssSelector, (selector, directive) => {
throw new BaseException('Viewport directives need to be placed on <template> elements or elements with template attribute!'); matchedProperties = updateMatchedProperties(matchedProperties, selector, directive);
} else if (isPresent(current.viewportDirective)) { checkDirectiveValidity(directive, current, isTemplateElement);
throw new BaseException('Only one template directive per element is allowed!');
}
} else if (isTemplateElement) {
throw new BaseException('Only template directives are allowed on <template> elements!');
} else if ((directive.annotation instanceof Component) && isPresent(current.componentDirective)) {
throw new BaseException('Only one component directive per element is allowed!');
}
current.addDirective(directive); current.addDirective(directive);
}); });
// raise error if some directives are missing
checkMissingDirectives(current, matchedProperties, isTemplateElement);
}
}
// calculate all the properties that are used or interpreted by all directives
// those properties correspond to the directive selectors and the directive bindings
function updateMatchedProperties(matchedProperties, selector, directive) {
if (assertionsEnabled()) {
var attrs = selector.attrs;
if (!isPresent(matchedProperties)) {
matchedProperties = StringMapWrapper.create();
}
if (isPresent(attrs)) {
for (var idx = 0; idx<attrs.length; idx+=2) {
// attribute name is stored on even indexes
StringMapWrapper.set(matchedProperties, attrs[idx], true);
}
}
// some properties can be used by the directive, so we need to register them
if (isPresent(directive.annotation) && isPresent(directive.annotation.bind)) {
var bindMap = directive.annotation.bind;
StringMapWrapper.forEach(bindMap, (value, key) => {
// value is the name of the property that is intepreted
// e.g. 'myprop' or 'myprop | double' when a pipe is used to transform the property
// keep the property name and remove the pipe
var bindProp = RegExpWrapper.firstMatch(PROPERTY_BINDING_REGEXP, value);
if (isPresent(bindProp) && isPresent(bindProp[1])) {
StringMapWrapper.set(matchedProperties, bindProp[1], true);
}
});
}
}
return matchedProperties;
}
// check if the directive is compatible with the current element
function checkDirectiveValidity(directive, current, isTemplateElement) {
if (directive.annotation instanceof Viewport) {
if (!isTemplateElement) {
throw new BaseException(`Viewport directives need to be placed on <template> elements or elements ` +
`with template attribute - check ${current.elementDescription}`);
} else if (isPresent(current.viewportDirective)) {
throw new BaseException(`Only one viewport directive can be used per element - check ${current.elementDescription}`);
}
} else if (isTemplateElement) {
throw new BaseException(`Only template directives are allowed on template elements - check ${current.elementDescription}`);
} else if ((directive.annotation instanceof Component) && isPresent(current.componentDirective)) {
throw new BaseException(`Multiple component directives not allowed on the same element - check ${current.elementDescription}`);
}
}
// validates that there is no missing directive - dev mode only
function checkMissingDirectives(current, matchedProperties, isTemplateElement) {
if (assertionsEnabled()) {
var ppBindings=current.propertyBindings;
if (isPresent(ppBindings)) {
// check that each property corresponds to a real property or has been matched by a directive
MapWrapper.forEach(ppBindings, (expression, prop) => {
if (!DOM.hasProperty(current.element, prop) && !isSpecialProperty(prop)) {
if (!isPresent(matchedProperties) || !isPresent(StringMapWrapper.get(matchedProperties, prop))) {
throw new BaseException(`Missing directive to handle '${prop}' in ${current.elementDescription}`);
}
}
});
}
// template only store directives as attribute when they are not bound to expressions
// so we have to validate the expression case too (e.g. !if="condition")
if (isTemplateElement && !current.isViewRoot && !isPresent(current.viewportDirective)) {
throw new BaseException(`Missing directive to handle: ${current.elementDescription}`);
}
} }
} }

View File

@ -34,6 +34,7 @@ function ariaSetterFactory(attrName:string) {
return setterFn; return setterFn;
} }
const CLASS_ATTR = 'class';
const CLASS_PREFIX = 'class.'; const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create(); var classSettersCache = StringMapWrapper.create();
@ -54,6 +55,7 @@ function classSetterFactory(className:string) {
return setterFn; return setterFn;
} }
const STYLE_ATTR = 'style';
const STYLE_PREFIX = 'style.'; const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create(); var styleSettersCache = StringMapWrapper.create();
@ -89,6 +91,13 @@ function roleSetter(element:Element, value) {
} }
} }
// tells if an attribute is handled by the ElementBinderBuilder step
export function isSpecialProperty(propName:string) {
return StringWrapper.startsWith(propName, ARIA_PREFIX)
|| StringWrapper.startsWith(propName, CLASS_PREFIX)
|| StringWrapper.startsWith(propName, STYLE_PREFIX);
}
/** /**
* Creates the ElementBinders and adds watches to the * Creates the ElementBinders and adds watches to the
* ProtoChangeDetector. * ProtoChangeDetector.
@ -115,11 +124,9 @@ function roleSetter(element:Element, value) {
*/ */
export class ElementBinderBuilder extends CompileStep { export class ElementBinderBuilder extends CompileStep {
_parser:Parser; _parser:Parser;
_compilationUnit:any; constructor(parser:Parser) {
constructor(parser:Parser, compilationUnit:any) {
super(); super();
this._parser = parser; this._parser = parser;
this._compilationUnit = compilationUnit;
} }
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
@ -207,7 +214,7 @@ export class ElementBinderBuilder extends CompileStep {
if (isBlank(bindingAst)) { if (isBlank(bindingAst)) {
var attributeValue = MapWrapper.get(compileElement.attrs(), elProp); var attributeValue = MapWrapper.get(compileElement.attrs(), elProp);
if (isPresent(attributeValue)) { if (isPresent(attributeValue)) {
bindingAst = this._parser.wrapLiteralPrimitive(attributeValue, this._compilationUnit); bindingAst = this._parser.wrapLiteralPrimitive(attributeValue, compileElement.elementDescription);
} }
} }
@ -229,4 +236,4 @@ export class ElementBinderBuilder extends CompileStep {
var parts = StringWrapper.split(bindConfig, RegExpWrapper.create("\\|")); var parts = StringWrapper.split(bindConfig, RegExpWrapper.create("\\|"));
return ListWrapper.map(parts, (s) => s.trim()); return ListWrapper.map(parts, (s) => s.trim());
} }
} }

View File

@ -29,11 +29,9 @@ var BIND_NAME_REGEXP = RegExpWrapper.create(
*/ */
export class PropertyBindingParser extends CompileStep { export class PropertyBindingParser extends CompileStep {
_parser:Parser; _parser:Parser;
_compilationUnit:any; constructor(parser:Parser) {
constructor(parser:Parser, compilationUnit:any) {
super(); super();
this._parser = parser; this._parser = parser;
this._compilationUnit = compilationUnit;
} }
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
@ -42,12 +40,13 @@ export class PropertyBindingParser extends CompileStep {
} }
var attrs = current.attrs(); var attrs = current.attrs();
var desc = current.elementDescription;
MapWrapper.forEach(attrs, (attrValue, attrName) => { MapWrapper.forEach(attrs, (attrValue, attrName) => {
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName); var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
if (isPresent(bindParts)) { if (isPresent(bindParts)) {
if (isPresent(bindParts[1])) { if (isPresent(bindParts[1])) {
// match: bind-prop // match: bind-prop
current.addPropertyBinding(bindParts[4], this._parseBinding(attrValue)); current.addPropertyBinding(bindParts[4], this._parseBinding(attrValue, desc));
} else if (isPresent(bindParts[2]) || isPresent(bindParts[7])) { } else if (isPresent(bindParts[2]) || isPresent(bindParts[7])) {
// match: var-name / var-name="iden" / #name / #name="iden" // match: var-name / var-name="iden" / #name / #name="iden"
var identifier = (isPresent(bindParts[4]) && bindParts[4] !== '') ? var identifier = (isPresent(bindParts[4]) && bindParts[4] !== '') ?
@ -56,16 +55,16 @@ export class PropertyBindingParser extends CompileStep {
current.addVariableBinding(identifier, value); current.addVariableBinding(identifier, value);
} else if (isPresent(bindParts[3])) { } else if (isPresent(bindParts[3])) {
// match: on-prop // match: on-prop
current.addEventBinding(bindParts[4], this._parseAction(attrValue)); current.addEventBinding(bindParts[4], this._parseAction(attrValue, desc));
} else if (isPresent(bindParts[5])) { } else if (isPresent(bindParts[5])) {
// match: [prop] // match: [prop]
current.addPropertyBinding(bindParts[5], this._parseBinding(attrValue)); current.addPropertyBinding(bindParts[5], this._parseBinding(attrValue, desc));
} else if (isPresent(bindParts[6])) { } else if (isPresent(bindParts[6])) {
// match: (prop) // match: (prop)
current.addEventBinding(bindParts[6], this._parseBinding(attrValue)); current.addEventBinding(bindParts[6], this._parseBinding(attrValue, desc));
} }
} else { } else {
var ast = this._parseInterpolation(attrValue); var ast = this._parseInterpolation(attrValue, desc);
if (isPresent(ast)) { if (isPresent(ast)) {
current.addPropertyBinding(attrName, ast); current.addPropertyBinding(attrName, ast);
} }
@ -73,15 +72,15 @@ export class PropertyBindingParser extends CompileStep {
}); });
} }
_parseInterpolation(input:string):AST { _parseInterpolation(input:string, location:string):AST {
return this._parser.parseInterpolation(input, this._compilationUnit); return this._parser.parseInterpolation(input, location);
} }
_parseBinding(input:string):AST { _parseBinding(input:string, location:string):AST {
return this._parser.parseBinding(input, this._compilationUnit); return this._parser.parseBinding(input, location);
} }
_parseAction(input:string):AST { _parseAction(input:string, location:string):AST {
return this._parser.parseAction(input, this._compilationUnit); return this._parser.parseAction(input, location);
} }
} }

View File

@ -15,11 +15,9 @@ import {CompileControl} from './compile_control';
*/ */
export class TextInterpolationParser extends CompileStep { export class TextInterpolationParser extends CompileStep {
_parser:Parser; _parser:Parser;
_compilationUnit:any; constructor(parser:Parser) {
constructor(parser:Parser, compilationUnit:any) {
super(); super();
this._parser = parser; this._parser = parser;
this._compilationUnit = compilationUnit;
} }
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
@ -37,7 +35,7 @@ export class TextInterpolationParser extends CompileStep {
} }
_parseTextNode(pipelineElement, node, nodeIndex) { _parseTextNode(pipelineElement, node, nodeIndex) {
var ast = this._parser.parseInterpolation(DOM.nodeValue(node), this._compilationUnit); var ast = this._parser.parseInterpolation(DOM.nodeValue(node), pipelineElement.elementDescription);
if (isPresent(ast)) { if (isPresent(ast)) {
DOM.setText(node, ' '); DOM.setText(node, ' ');
pipelineElement.addTextNodeBinding(nodeIndex, ast); pipelineElement.addTextNodeBinding(nodeIndex, ast);

View File

@ -9,6 +9,8 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control'; import {CompileControl} from './compile_control';
import {StringWrapper} from 'angular2/src/facade/lang'; import {StringWrapper} from 'angular2/src/facade/lang';
import {$BANG} from 'angular2/src/change_detection/parser/lexer';
/** /**
* Splits views at `<template>` elements or elements with `template` attribute: * Splits views at `<template>` elements or elements with `template` attribute:
* For `<template>` elements: * For `<template>` elements:
@ -32,14 +34,35 @@ import {StringWrapper} from 'angular2/src/facade/lang';
*/ */
export class ViewSplitter extends CompileStep { export class ViewSplitter extends CompileStep {
_parser:Parser; _parser:Parser;
_compilationUnit:any; constructor(parser:Parser) {
constructor(parser:Parser, compilationUnit:any) {
super(); super();
this._parser = parser; this._parser = parser;
this._compilationUnit = compilationUnit;
} }
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var attrs = current.attrs();
var templateBindings = MapWrapper.get(attrs, 'template');
var hasTemplateBinding = isPresent(templateBindings);
// look for template shortcuts such as !if="condition" and treat them as template="if condition"
MapWrapper.forEach(attrs, (attrValue, attrName) => {
if (StringWrapper.charCodeAt(attrName, 0) == $BANG) {
var key = StringWrapper.substring(attrName, 1); // remove the bang
if (hasTemplateBinding) {
// 2nd template binding detected
throw new BaseException(`Only one template directive per element is allowed: ` +
`${templateBindings} and ${key} cannot be used simultaneously ` +
`in ${current.elementDescription}`);
} else {
if (isBlank(parent)) {
throw new BaseException(`Template directives cannot be used on root components in ${current.elementDescription}`);
}
templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue;
hasTemplateBinding = true;
}
}
});
if (isBlank(parent)) { if (isBlank(parent)) {
current.isViewRoot = true; current.isViewRoot = true;
} else { } else {
@ -49,6 +72,9 @@ export class ViewSplitter extends CompileStep {
var currentElement:TemplateElement = current.element; var currentElement:TemplateElement = current.element;
var viewRootElement:TemplateElement = viewRoot.element; var viewRootElement:TemplateElement = viewRoot.element;
this._moveChildNodes(DOM.content(currentElement), DOM.content(viewRootElement)); this._moveChildNodes(DOM.content(currentElement), DOM.content(viewRootElement));
// viewRoot is a doesn't appear in the original template, so we associate
// the current element description to get a more meaninful message in case of error
viewRoot.elementDescription = current.elementDescription;
viewRoot.isViewRoot = true; viewRoot.isViewRoot = true;
control.addChild(viewRoot); control.addChild(viewRoot);
} }
@ -64,7 +90,8 @@ export class ViewSplitter extends CompileStep {
if (hasTemplateBinding) { if (hasTemplateBinding) {
// 2nd template binding detected // 2nd template binding detected
throw new BaseException(`Only one template directive per element is allowed: ` + throw new BaseException(`Only one template directive per element is allowed: ` +
`${templateBindings} and ${key} cannot be used simultaneously!`); `${templateBindings} and ${key} cannot be used simultaneously ` +
`in ${current.elementDescription}`);
} else { } else {
templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue; templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue;
hasTemplateBinding = true; hasTemplateBinding = true;
@ -74,6 +101,9 @@ export class ViewSplitter extends CompileStep {
if (hasTemplateBinding) { if (hasTemplateBinding) {
var newParent = new CompileElement(DOM.createTemplate('')); var newParent = new CompileElement(DOM.createTemplate(''));
// newParent doesn't appear in the original template, so we associate
// the current element description to get a more meaninful message in case of error
newParent.elementDescription = current.elementDescription;
current.isViewRoot = true; current.isViewRoot = true;
this._parseTemplateBindings(templateBindings, newParent); this._parseTemplateBindings(templateBindings, newParent);
this._addParentElement(current.element, newParent.element); this._addParentElement(current.element, newParent.element);
@ -99,7 +129,7 @@ export class ViewSplitter extends CompileStep {
} }
_parseTemplateBindings(templateBindings:string, compileElement:CompileElement) { _parseTemplateBindings(templateBindings:string, compileElement:CompileElement) {
var bindings = this._parser.parseTemplateBindings(templateBindings, this._compilationUnit); var bindings = this._parser.parseTemplateBindings(templateBindings, compileElement.elementDescription);
for (var i=0; i<bindings.length; i++) { for (var i=0; i<bindings.length; i++) {
var binding = bindings[i]; var binding = bindings[i];
if (binding.keyIsVar) { if (binding.keyIsVar) {

View File

@ -114,13 +114,15 @@ export class SelectorMatcher {
/** /**
* Add an object that can be found later on by calling `match`. * Add an object that can be found later on by calling `match`.
* @param cssSelector A css selector * @param cssSelector A css selector
* @param selectable An opaque object that will be given to the callback of the `match` function * @param callbackCtxt An opaque object that will be given to the callback of the `match` function
*/ */
addSelectable(cssSelector:CssSelector, selectable) { addSelectable(cssSelector:CssSelector, callbackCtxt) {
var matcher = this; var matcher = this;
var element = cssSelector.element; var element = cssSelector.element;
var classNames = cssSelector.classNames; var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs; var attrs = cssSelector.attrs;
var selectable = new SelectorContext(cssSelector, callbackCtxt);
if (isPresent(element)) { if (isPresent(element)) {
var isTerminal = attrs.length === 0 && classNames.length === 0; var isTerminal = attrs.length === 0 && classNames.length === 0;
@ -228,8 +230,10 @@ export class SelectorMatcher {
if (isBlank(selectables)) { if (isBlank(selectables)) {
return; return;
} }
var selectable;
for (var index=0; index<selectables.length; index++) { for (var index=0; index<selectables.length; index++) {
matchedCallback(selectables[index]); selectable = selectables[index];
matchedCallback(selectable.selector, selectable.cbContext);
} }
} }
@ -247,3 +251,15 @@ export class SelectorMatcher {
nestedSelector.match(cssSelector, matchedCallback); nestedSelector.match(cssSelector, matchedCallback);
} }
} }
// Store context to pass back selector and context when a selector is matched
class SelectorContext {
selector:CssSelector;
cbContext; // callback context
constructor(selector:CssSelector, cbContext) {
this.selector = selector;
this.cbContext = cbContext;
}
}

View File

@ -1,6 +1,8 @@
import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'angular2/test_lib'; import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'angular2/test_lib';
import {DOM} from 'angular2/src/facade/dom'; import {DOM} from 'angular2/src/facade/dom';
import {Type, isPresent, BaseException} from 'angular2/src/facade/lang';
import {assertionsEnabled, isJsObject} from 'angular2/src/facade/lang';
import {Injector} from 'angular2/di'; import {Injector} from 'angular2/di';
import {Lexer, Parser, ChangeDetector, dynamicChangeDetection, import {Lexer, Parser, ChangeDetector, dynamicChangeDetection,
@ -50,7 +52,6 @@ export function main() {
it('should consume text node changes', (done) => { it('should consume text node changes', (done) => {
tplResolver.setTemplate(MyComp, new Template({inline: '<div>{{ctxProp}}</div>'})); tplResolver.setTemplate(MyComp, new Template({inline: '<div>{{ctxProp}}</div>'}));
compiler.compile(MyComp).then((pv) => { compiler.compile(MyComp).then((pv) => {
createView(pv); createView(pv);
ctx.ctxProp = 'Hello World!'; ctx.ctxProp = 'Hello World!';
@ -365,6 +366,60 @@ export function main() {
}) })
}); });
}); });
// TODO support these tests with DART e.g. with Promise.catch (JS) transpiled to Future.catchError (DART)
if (assertionsEnabled() && isJsObject({})) {
function expectCompileError(inlineTpl, errMessage, done) {
tplResolver.setTemplate(MyComp, new Template({inline: inlineTpl}));
compiler.compile(MyComp).then(() => {
throw new BaseException("Test failure: should not have come here as an exception was expected");
},(err) => {
expect(err.message).toBe(errMessage);
done();
});
}
it('should raise an error if no directive is registered for an unsupported DOM property', (done) => {
expectCompileError(
'<div [some-prop]="foo"></div>',
'Missing directive to handle \'some-prop\' in MyComp: <div [some-prop]="foo">',
done
);
});
it('should raise an error if no directive is registered for a template with template bindings', (done) => {
expectCompileError(
'<div><div template="if: foo"></div></div>',
'Missing directive to handle \'if\' in <div template="if: foo">',
done
);
});
it('should raise an error for missing template directive (1)', (done) => {
expectCompileError(
'<div><template foo></template></div>',
'Missing directive to handle: <template foo>',
done
);
});
it('should raise an error for missing template directive (2)', (done) => {
expectCompileError(
'<div><template *if="condition"></template></div>',
'Missing directive to handle: <template *if="condition">',
done
);
});
it('should raise an error for missing template directive (3)', (done) => {
expectCompileError(
'<div *if="condition"></div>',
'Missing directive to handle \'if\' in MyComp: <div *if="condition">',
done
);
});
}
}); });
} }
@ -473,6 +528,19 @@ class CompWithAncestor {
} }
} }
@Component({
selector: '[child-cmp2]',
componentServices: [MyService]
})
class ChildComp2 {
ctxProp:string;
dirProp:string;
constructor(service: MyService) {
this.ctxProp = service.greeting;
this.dirProp = null;
}
}
@Viewport({ @Viewport({
selector: '[some-viewport]' selector: '[some-viewport]'
}) })

View File

@ -1,5 +1,5 @@
import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib'; import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'angular2/test_lib';
import {isPresent} from 'angular2/src/facade/lang'; import {isPresent, assertionsEnabled} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DirectiveParser} from 'angular2/src/core/compiler/pipeline/directive_parser'; import {DirectiveParser} from 'angular2/src/core/compiler/pipeline/directive_parser';
import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline'; import {CompilePipeline} from 'angular2/src/core/compiler/pipeline/compile_pipeline';
@ -85,20 +85,20 @@ export function main() {
}); });
it('should not allow multiple component directives on the same element', () => { it('should not allow multiple component directives on the same element', () => {
expect( () => { expect( () => {
createPipeline().process( createPipeline().process(
el('<div some-comp some-comp2></div>') el('<div some-comp some-comp2></div>')
); );
}).toThrowError('Only one component directive per element is allowed!'); }).toThrowError('Multiple component directives not allowed on the same element - check <div some-comp some-comp2>');
}); });
it('should not allow component directives on <template> elements', () => { it('should not allow component directives on <template> elements', () => {
expect( () => { expect( () => {
createPipeline().process( createPipeline().process(
el('<template some-comp></template>') el('<template some-comp></template>')
); );
}).toThrowError('Only template directives are allowed on <template> elements!'); }).toThrowError('Only template directives are allowed on template elements - check <template some-comp>');
}); });
}); });
describe('viewport directives', () => { describe('viewport directives', () => {
@ -128,7 +128,7 @@ export function main() {
createPipeline().process( createPipeline().process(
el('<template some-templ some-templ2></template>') el('<template some-templ some-templ2></template>')
); );
}).toThrowError('Only one template directive per element is allowed!'); }).toThrowError('Only one viewport directive can be used per element - check <template some-templ some-templ2>');
}); });
it('should not allow viewport directives on non <template> elements', () => { it('should not allow viewport directives on non <template> elements', () => {
@ -136,7 +136,8 @@ export function main() {
createPipeline().process( createPipeline().process(
el('<div some-templ></div>') el('<div some-templ></div>')
); );
}).toThrowError('Viewport directives need to be placed on <template> elements or elements with template attribute!');
}).toThrowError('Viewport directives need to be placed on <template> elements or elements with template attribute - check <div some-templ>');
}); });
}); });
@ -172,14 +173,6 @@ export function main() {
expect(results[0].decoratorDirectives).toEqual([reader.read(SomeDecorator)]); expect(results[0].decoratorDirectives).toEqual([reader.read(SomeDecorator)]);
}); });
it('should not allow decorator directives on <template> elements', () => {
expect( () => {
createPipeline().process(
el('<template some-decor></template>')
);
}).toThrowError('Only template directives are allowed on <template> elements!');
});
it('should not instantiate decorator directive twice', () => { it('should not instantiate decorator directive twice', () => {
var pipeline = createPipeline({propertyBindings: { var pipeline = createPipeline({propertyBindings: {
'some-decor-with-binding': 'someExpr' 'some-decor-with-binding': 'someExpr'

View File

@ -76,7 +76,7 @@ export function main() {
} else if (isPresent(parent)) { } else if (isPresent(parent)) {
current.inheritedProtoView = parent.inheritedProtoView; current.inheritedProtoView = parent.inheritedProtoView;
} }
}), new ElementBinderBuilder(parser, null) }), new ElementBinderBuilder(parser)
]); ]);
} }

View File

@ -12,7 +12,7 @@ export function main() {
function createPipeline(ignoreBindings = false) { function createPipeline(ignoreBindings = false) {
return new CompilePipeline([ return new CompilePipeline([
new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }), new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }),
new PropertyBindingParser(new Parser(new Lexer()), null)]); new PropertyBindingParser(new Parser(new Lexer()))]);
} }
it('should not parse bindings when ignoreBindings is true', () => { it('should not parse bindings when ignoreBindings is true', () => {

View File

@ -14,7 +14,7 @@ export function main() {
return new CompilePipeline([ return new CompilePipeline([
new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }), new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }),
new IgnoreChildrenStep(), new IgnoreChildrenStep(),
new TextInterpolationParser(new Parser(new Lexer()), null) new TextInterpolationParser(new Parser(new Lexer()))
]); ]);
} }

View File

@ -11,7 +11,7 @@ export function main() {
describe('ViewSplitter', () => { describe('ViewSplitter', () => {
function createPipeline() { function createPipeline() {
return new CompilePipeline([new ViewSplitter(new Parser(new Lexer()), null)]); return new CompilePipeline([new ViewSplitter(new Parser(new Lexer()))]);
} }
it('should mark root elements as viewRoot', () => { it('should mark root elements as viewRoot', () => {
@ -160,14 +160,14 @@ export function main() {
expect( () => { expect( () => {
var rootElement = el('<div><div *foo *bar="blah"></div></div>'); var rootElement = el('<div><div *foo *bar="blah"></div></div>');
createPipeline().process(rootElement); createPipeline().process(rootElement);
}).toThrowError('Only one template directive per element is allowed: foo and bar cannot be used simultaneously!'); }).toThrowError('Only one template directive per element is allowed: foo and bar cannot be used simultaneously in <div *foo *bar="blah">');
}); });
it('should not allow template and bang directives on the same element', () => { it('should not allow template and star directives on the same element', () => {
expect( () => { expect( () => {
var rootElement = el('<div><div *foo template="blah"></div></div>'); var rootElement = el('<div><div *foo template="bar"></div></div>');
createPipeline().process(rootElement); createPipeline().process(rootElement);
}).toThrowError('Only one template directive per element is allowed: blah and foo cannot be used simultaneously!'); }).toThrowError('Only one template directive per element is allowed: bar and foo cannot be used simultaneously in <div *foo template="bar">');
}); });
}); });

View File

@ -5,7 +5,7 @@ import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
export function main() { export function main() {
describe('SelectorMatcher', () => { describe('SelectorMatcher', () => {
var matcher, matched, selectableCollector; var matcher, matched, selectableCollector, s1, s2, s3, s4;
function reset() { function reset() {
matched = ListWrapper.create(); matched = ListWrapper.create();
@ -13,79 +13,81 @@ export function main() {
beforeEach(() => { beforeEach(() => {
reset(); reset();
selectableCollector = (selectable) => { s1 = s2 = s3 = s4 = null;
ListWrapper.push(matched, selectable); selectableCollector = (selector, context) => {
ListWrapper.push(matched, selector);
ListWrapper.push(matched, context);
} }
matcher = new SelectorMatcher(); matcher = new SelectorMatcher();
}); });
it('should select by element name case insensitive', () => { it('should select by element name case insensitive', () => {
matcher.addSelectable(CssSelector.parse('someTag'), 1); matcher.addSelectable(s1 = CssSelector.parse('someTag'), 1);
matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector); matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector);
expect(matched).toEqual([]); expect(matched).toEqual([]);
matcher.match(CssSelector.parse('SOMETAG'), selectableCollector); matcher.match(CssSelector.parse('SOMETAG'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
}); });
it('should select by class name case insensitive', () => { it('should select by class name case insensitive', () => {
matcher.addSelectable(CssSelector.parse('.someClass'), 1); matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1);
matcher.addSelectable(CssSelector.parse('.someClass.class2'), 2); matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2);
matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector); matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector);
expect(matched).toEqual([]); expect(matched).toEqual([]);
matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector); matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
reset(); reset();
matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector); matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector);
expect(matched).toEqual([1,2]); expect(matched).toEqual([s1,1,s2,2]);
}); });
it('should select by attr name case insensitive independent of the value', () => { it('should select by attr name case insensitive independent of the value', () => {
matcher.addSelectable(CssSelector.parse('[someAttr]'), 1); matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1);
matcher.addSelectable(CssSelector.parse('[someAttr][someAttr2]'), 2); matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector); matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector);
expect(matched).toEqual([]); expect(matched).toEqual([]);
matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector); matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
reset(); reset();
matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector); matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
reset(); reset();
matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector); matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector);
expect(matched).toEqual([1,2]); expect(matched).toEqual([s1,1,s2,2]);
}); });
it('should select by attr name only once if the value is from the DOM', () => { it('should select by attr name only once if the value is from the DOM', () => {
matcher.addSelectable(CssSelector.parse('[some-decor]'), 1); matcher.addSelectable(s1 = CssSelector.parse('[some-decor]'), 1);
var elementSelector = new CssSelector(); var elementSelector = new CssSelector();
var element = el('<div attr></div>'); var element = el('<div attr></div>');
var empty = element.getAttribute('attr'); var empty = element.getAttribute('attr');
elementSelector.addAttribute('some-decor', empty); elementSelector.addAttribute('some-decor', empty);
matcher.match(elementSelector, selectableCollector); matcher.match(elementSelector, selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
}); });
it('should select by attr name and value case insensitive', () => { it('should select by attr name and value case insensitive', () => {
matcher.addSelectable(CssSelector.parse('[someAttr=someValue]'), 1); matcher.addSelectable(s1 = CssSelector.parse('[someAttr=someValue]'), 1);
matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector); matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector);
expect(matched).toEqual([]); expect(matched).toEqual([]);
matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector); matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
}); });
it('should select by element name, class name and attribute name with value', () => { it('should select by element name, class name and attribute name with value', () => {
matcher.addSelectable(CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1); matcher.addSelectable(s1 = CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1);
matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector); matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector);
expect(matched).toEqual([]); expect(matched).toEqual([]);
@ -100,29 +102,29 @@ export function main() {
expect(matched).toEqual([]); expect(matched).toEqual([]);
matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector); matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector);
expect(matched).toEqual([1]); expect(matched).toEqual([s1,1]);
}); });
it('should select independent of the order in the css selector', () => { it('should select independent of the order in the css selector', () => {
matcher.addSelectable(CssSelector.parse('[someAttr].someClass'), 1); matcher.addSelectable(s1 = CssSelector.parse('[someAttr].someClass'), 1);
matcher.addSelectable(CssSelector.parse('.someClass[someAttr]'), 2); matcher.addSelectable(s2 = CssSelector.parse('.someClass[someAttr]'), 2);
matcher.addSelectable(CssSelector.parse('.class1.class2'), 3); matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3);
matcher.addSelectable(CssSelector.parse('.class2.class1'), 4); matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4);
matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector); matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector);
expect(matched).toEqual([1,2]); expect(matched).toEqual([s1,1,s2,2]);
reset(); reset();
matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector); matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector);
expect(matched).toEqual([1,2]); expect(matched).toEqual([s1,1,s2,2]);
reset(); reset();
matcher.match(CssSelector.parse('.class1.class2'), selectableCollector); matcher.match(CssSelector.parse('.class1.class2'), selectableCollector);
expect(matched).toEqual([3,4]); expect(matched).toEqual([s3,3,s4,4]);
reset(); reset();
matcher.match(CssSelector.parse('.class2.class1'), selectableCollector); matcher.match(CssSelector.parse('.class2.class1'), selectableCollector);
expect(matched).toEqual([4,3]); expect(matched).toEqual([s4,4,s3,3]);
}); });
}); });

View File

@ -40,7 +40,7 @@ export function main() {
function match() { function match() {
var matchCount = 0; var matchCount = 0;
for (var i=0; i<count; i++) { for (var i=0; i<count; i++) {
fixedMatcher.match(fixedSelectors[i], (selected) => { fixedMatcher.match(fixedSelectors[i], (selector, selected) => {
matchCount += selected; matchCount += selected;
}); });
} }