feat(DirectiveParser): throw errors when expected directives are not present
closes #527 Closes #570
This commit is contained in:
parent
715ee14ced
commit
94e203b9df
|
@ -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,
|
||||
|
|
|
@ -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 + '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]'
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -76,7 +76,7 @@ export function main() {
|
|||
} else if (isPresent(parent)) {
|
||||
current.inheritedProtoView = parent.inheritedProtoView;
|
||||
}
|
||||
}), new ElementBinderBuilder(parser, null)
|
||||
}), new ElementBinderBuilder(parser)
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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()))
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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">');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue