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 {CompileStep} from './pipeline/compile_step';
/**
* Cache that stores the ProtoView of the template of a component.
* 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>
_compileTemplate(template: Template, tplElement: Element, component: Type) {
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;
// 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 {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 {Decorator, Component, Viewport} from '../../annotations/annotations';
import {ElementBinder} from '../element_binder';
@ -38,8 +38,9 @@ export class CompileElement {
distanceToParentInjector:number;
compileChildren: 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._attrs = null;
this._classList = null;
@ -66,6 +67,14 @@ export class CompileElement {
this.compileChildren = true;
// set to true to ignore all the bindings on the element
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() {
@ -165,3 +174,36 @@ export class CompileElement {
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);
}
process(rootElement:Element):List {
process(rootElement:Element, compilationCtxtDescription:string = ''):List {
var results = ListWrapper.create();
this._process(results, null, new CompileElement(rootElement));
this._process(results, null, new CompileElement(rootElement, compilationCtxtDescription), compilationCtxtDescription);
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);
if (current.compileChildren) {
@ -31,7 +31,7 @@ export class CompilePipeline {
// next sibling before recursing.
var nextNode = DOM.nextSibling(node);
if (DOM.isElementNode(node)) {
this._process(results, current, new CompileElement(node));
this._process(results, current, new CompileElement(node, compilationCtxtDescription));
}
node = nextNode;
}

View File

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

View File

@ -1,5 +1,5 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {List, MapWrapper} from 'angular2/src/facade/collection';
import {isPresent, isBlank, BaseException, assertionsEnabled, RegExpWrapper} from 'angular2/src/facade/lang';
import {List, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/facade/dom';
import {SelectorMatcher} from '../selector';
import {CssSelector} from '../selector';
@ -10,6 +10,10 @@ import {CompileStep} from './compile_step';
import {CompileElement} from './compile_element';
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
* <template> elements for template directives.
@ -29,13 +33,13 @@ export class DirectiveParser extends CompileStep {
_selectorMatcher:SelectorMatcher;
constructor(directives:List<DirectiveMetadata>) {
super();
var selector;
this._selectorMatcher = new SelectorMatcher();
for (var i=0; i<directives.length; i++) {
var directiveMetadata = directives[i];
this._selectorMatcher.addSelectable(
CssSelector.parse(directiveMetadata.annotation.selector),
directiveMetadata
);
selector=CssSelector.parse(directiveMetadata.annotation.selector);
this._selectorMatcher.addSelectable(selector, 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
// only be present on <template> elements any more!
var isTemplateElement = DOM.isTemplateElement(current.element);
this._selectorMatcher.match(cssSelector, (directive) => {
if (directive.annotation instanceof Viewport) {
if (!isTemplateElement) {
throw new BaseException('Viewport directives need to be placed on <template> elements or elements with template attribute!');
} else if (isPresent(current.viewportDirective)) {
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!');
}
var matchedProperties; // StringMap - used in dev mode to store all properties that have been matched
this._selectorMatcher.match(cssSelector, (selector, directive) => {
matchedProperties = updateMatchedProperties(matchedProperties, selector, directive);
checkDirectiveValidity(directive, current, isTemplateElement);
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;
}
const CLASS_ATTR = 'class';
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
@ -54,6 +55,7 @@ function classSetterFactory(className:string) {
return setterFn;
}
const STYLE_ATTR = 'style';
const STYLE_PREFIX = 'style.';
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
* ProtoChangeDetector.
@ -115,11 +124,9 @@ function roleSetter(element:Element, value) {
*/
export class ElementBinderBuilder extends CompileStep {
_parser:Parser;
_compilationUnit:any;
constructor(parser:Parser, compilationUnit:any) {
constructor(parser:Parser) {
super();
this._parser = parser;
this._compilationUnit = compilationUnit;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
@ -207,7 +214,7 @@ export class ElementBinderBuilder extends CompileStep {
if (isBlank(bindingAst)) {
var attributeValue = MapWrapper.get(compileElement.attrs(), elProp);
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("\\|"));
return ListWrapper.map(parts, (s) => s.trim());
}
}
}

View File

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

View File

@ -15,11 +15,9 @@ import {CompileControl} from './compile_control';
*/
export class TextInterpolationParser extends CompileStep {
_parser:Parser;
_compilationUnit:any;
constructor(parser:Parser, compilationUnit:any) {
constructor(parser:Parser) {
super();
this._parser = parser;
this._compilationUnit = compilationUnit;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
@ -37,7 +35,7 @@ export class TextInterpolationParser extends CompileStep {
}
_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)) {
DOM.setText(node, ' ');
pipelineElement.addTextNodeBinding(nodeIndex, ast);

View File

@ -9,6 +9,8 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
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:
* For `<template>` elements:
@ -32,14 +34,35 @@ import {StringWrapper} from 'angular2/src/facade/lang';
*/
export class ViewSplitter extends CompileStep {
_parser:Parser;
_compilationUnit:any;
constructor(parser:Parser, compilationUnit:any) {
constructor(parser:Parser) {
super();
this._parser = parser;
this._compilationUnit = compilationUnit;
}
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)) {
current.isViewRoot = true;
} else {
@ -49,6 +72,9 @@ export class ViewSplitter extends CompileStep {
var currentElement:TemplateElement = current.element;
var viewRootElement:TemplateElement = viewRoot.element;
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;
control.addChild(viewRoot);
}
@ -64,7 +90,8 @@ export class ViewSplitter extends CompileStep {
if (hasTemplateBinding) {
// 2nd template binding detected
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 {
templateBindings = (attrValue.length == 0) ? key : key + ' ' + attrValue;
hasTemplateBinding = true;
@ -74,6 +101,9 @@ export class ViewSplitter extends CompileStep {
if (hasTemplateBinding) {
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;
this._parseTemplateBindings(templateBindings, newParent);
this._addParentElement(current.element, newParent.element);
@ -99,7 +129,7 @@ export class ViewSplitter extends CompileStep {
}
_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++) {
var binding = bindings[i];
if (binding.keyIsVar) {

View File

@ -114,13 +114,15 @@ export class SelectorMatcher {
/**
* Add an object that can be found later on by calling `match`.
* @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 element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;
var selectable = new SelectorContext(cssSelector, callbackCtxt);
if (isPresent(element)) {
var isTerminal = attrs.length === 0 && classNames.length === 0;
@ -228,8 +230,10 @@ export class SelectorMatcher {
if (isBlank(selectables)) {
return;
}
var selectable;
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);
}
}
// 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 {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 {Lexer, Parser, ChangeDetector, dynamicChangeDetection,
@ -50,7 +52,6 @@ export function main() {
it('should consume text node changes', (done) => {
tplResolver.setTemplate(MyComp, new Template({inline: '<div>{{ctxProp}}</div>'}));
compiler.compile(MyComp).then((pv) => {
createView(pv);
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({
selector: '[some-viewport]'
})

View File

@ -1,5 +1,5 @@
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 {DirectiveParser} from 'angular2/src/core/compiler/pipeline/directive_parser';
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', () => {
expect( () => {
createPipeline().process(
el('<div some-comp some-comp2></div>')
);
}).toThrowError('Only one component directive per element is allowed!');
expect( () => {
createPipeline().process(
el('<div some-comp some-comp2></div>')
);
}).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', () => {
expect( () => {
createPipeline().process(
el('<template some-comp></template>')
);
}).toThrowError('Only template directives are allowed on <template> elements!');
});
expect( () => {
createPipeline().process(
el('<template some-comp></template>')
);
}).toThrowError('Only template directives are allowed on template elements - check <template some-comp>');
});
});
describe('viewport directives', () => {
@ -128,7 +128,7 @@ export function main() {
createPipeline().process(
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', () => {
@ -136,7 +136,8 @@ export function main() {
createPipeline().process(
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)]);
});
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', () => {
var pipeline = createPipeline({propertyBindings: {
'some-decor-with-binding': 'someExpr'

View File

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

View File

@ -12,7 +12,7 @@ export function main() {
function createPipeline(ignoreBindings = false) {
return new CompilePipeline([
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', () => {

View File

@ -14,7 +14,7 @@ export function main() {
return new CompilePipeline([
new MockStep((parent, current, control) => { current.ignoreBindings = ignoreBindings; }),
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', () => {
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', () => {
@ -160,14 +160,14 @@ export function main() {
expect( () => {
var rootElement = el('<div><div *foo *bar="blah"></div></div>');
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( () => {
var rootElement = el('<div><div *foo template="blah"></div></div>');
var rootElement = el('<div><div *foo template="bar"></div></div>');
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() {
describe('SelectorMatcher', () => {
var matcher, matched, selectableCollector;
var matcher, matched, selectableCollector, s1, s2, s3, s4;
function reset() {
matched = ListWrapper.create();
@ -13,79 +13,81 @@ export function main() {
beforeEach(() => {
reset();
selectableCollector = (selectable) => {
ListWrapper.push(matched, selectable);
s1 = s2 = s3 = s4 = null;
selectableCollector = (selector, context) => {
ListWrapper.push(matched, selector);
ListWrapper.push(matched, context);
}
matcher = new SelectorMatcher();
});
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);
expect(matched).toEqual([]);
matcher.match(CssSelector.parse('SOMETAG'), selectableCollector);
expect(matched).toEqual([1]);
expect(matched).toEqual([s1,1]);
});
it('should select by class name case insensitive', () => {
matcher.addSelectable(CssSelector.parse('.someClass'), 1);
matcher.addSelectable(CssSelector.parse('.someClass.class2'), 2);
matcher.addSelectable(s1 = CssSelector.parse('.someClass'), 1);
matcher.addSelectable(s2 = CssSelector.parse('.someClass.class2'), 2);
matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector);
expect(matched).toEqual([]);
matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector);
expect(matched).toEqual([1]);
expect(matched).toEqual([s1,1]);
reset();
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', () => {
matcher.addSelectable(CssSelector.parse('[someAttr]'), 1);
matcher.addSelectable(CssSelector.parse('[someAttr][someAttr2]'), 2);
matcher.addSelectable(s1 = CssSelector.parse('[someAttr]'), 1);
matcher.addSelectable(s2 = CssSelector.parse('[someAttr][someAttr2]'), 2);
matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector);
expect(matched).toEqual([]);
matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector);
expect(matched).toEqual([1]);
expect(matched).toEqual([s1,1]);
reset();
matcher.match(CssSelector.parse('[SOMEATTR=someValue]'), selectableCollector);
expect(matched).toEqual([1]);
expect(matched).toEqual([s1,1]);
reset();
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', () => {
matcher.addSelectable(CssSelector.parse('[some-decor]'), 1);
matcher.addSelectable(s1 = CssSelector.parse('[some-decor]'), 1);
var elementSelector = new CssSelector();
var element = el('<div attr></div>');
var empty = element.getAttribute('attr');
elementSelector.addAttribute('some-decor', empty);
matcher.match(elementSelector, selectableCollector);
expect(matched).toEqual([1]);
expect(matched).toEqual([s1,1]);
});
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);
expect(matched).toEqual([]);
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', () => {
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);
expect(matched).toEqual([]);
@ -100,29 +102,29 @@ export function main() {
expect(matched).toEqual([]);
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', () => {
matcher.addSelectable(CssSelector.parse('[someAttr].someClass'), 1);
matcher.addSelectable(CssSelector.parse('.someClass[someAttr]'), 2);
matcher.addSelectable(CssSelector.parse('.class1.class2'), 3);
matcher.addSelectable(CssSelector.parse('.class2.class1'), 4);
matcher.addSelectable(s1 = CssSelector.parse('[someAttr].someClass'), 1);
matcher.addSelectable(s2 = CssSelector.parse('.someClass[someAttr]'), 2);
matcher.addSelectable(s3 = CssSelector.parse('.class1.class2'), 3);
matcher.addSelectable(s4 = CssSelector.parse('.class2.class1'), 4);
matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector);
expect(matched).toEqual([1,2]);
expect(matched).toEqual([s1,1,s2,2]);
reset();
matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector);
expect(matched).toEqual([1,2]);
expect(matched).toEqual([s1,1,s2,2]);
reset();
matcher.match(CssSelector.parse('.class1.class2'), selectableCollector);
expect(matched).toEqual([3,4]);
expect(matched).toEqual([s3,3,s4,4]);
reset();
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() {
var matchCount = 0;
for (var i=0; i<count; i++) {
fixedMatcher.match(fixedSelectors[i], (selected) => {
fixedMatcher.match(fixedSelectors[i], (selector, selected) => {
matchCount += selected;
});
}