feat(compiler): new semantics for `template` attributes and view variables.

- Supports `<div template=“…”>`, including parsing the expressions within
  the attribute.
- Supports `<template let-ng-repeat=“rows”>`
- Adds attribute interpolation (was missing previously)
This commit is contained in:
Tobias Bosch 2014-11-18 16:38:36 -08:00
parent f864aa1f8e
commit c6846f1163
25 changed files with 586 additions and 283 deletions

View File

@ -336,6 +336,22 @@ export class FunctionCall extends AST {
} }
} }
export class ASTWithSource {
constructor(ast:AST, source:string) {
this.source = source;
this.ast = ast;
}
}
export class TemplateBinding {
constructor(key:string, name:string, expression:ASTWithSource) {
this.key = key;
// only either name or expression will be filled.
this.name = name;
this.expression = expression;
}
}
//INTERFACE //INTERFACE
export class AstVisitor { export class AstVisitor {
visitChain(ast:Chain, args){} visitChain(ast:Chain, args){}

View File

@ -130,6 +130,7 @@ export const $CR = 13;
export const $SPACE = 32; export const $SPACE = 32;
export const $BANG = 33; export const $BANG = 33;
export const $DQ = 34; export const $DQ = 34;
export const $HASH = 35;
export const $$ = 36; export const $$ = 36;
export const $PERCENT = 37; export const $PERCENT = 37;
export const $AMPERSAND = 38; export const $AMPERSAND = 38;
@ -246,6 +247,8 @@ class _Scanner {
case $SQ: case $SQ:
case $DQ: case $DQ:
return this.scanString(); return this.scanString();
case $HASH:
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
case $PLUS: case $PLUS:
case $MINUS: case $MINUS:
case $STAR: case $STAR:
@ -459,7 +462,8 @@ var OPERATORS = SetWrapper.createFromList([
'&', '&',
'|', '|',
'!', '!',
'?' '?',
'#'
]); ]);

View File

@ -19,7 +19,10 @@ import {
LiteralArray, LiteralArray,
LiteralMap, LiteralMap,
MethodCall, MethodCall,
FunctionCall FunctionCall,
TemplateBindings,
TemplateBinding,
ASTWithSource
} from './ast'; } from './ast';
var _implicitReceiver = new ImplicitReceiver(); var _implicitReceiver = new ImplicitReceiver();
@ -32,14 +35,21 @@ export class Parser {
this._closureMap = closureMap; this._closureMap = closureMap;
} }
parseAction(input:string):AST { parseAction(input:string):ASTWithSource {
var tokens = this._lexer.tokenize(input); var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, tokens, this._closureMap, true).parseChain(); var ast = new _ParseAST(input, tokens, this._closureMap, true).parseChain();
return new ASTWithSource(ast, input);
} }
parseBinding(input:string):AST { parseBinding(input:string):ASTWithSource {
var tokens = this._lexer.tokenize(input); var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, tokens, this._closureMap, false).parseChain(); var ast = new _ParseAST(input, tokens, this._closureMap, false).parseChain();
return new ASTWithSource(ast, input);
}
parseTemplateBindings(input:string):List<TemplateBinding> {
var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, tokens, this._closureMap, false).parseTemplateBindings();
} }
} }
@ -407,6 +417,29 @@ class _ParseAST {
return positionals; return positionals;
} }
parseTemplateBindings() {
var bindings = [];
while (this.index < this.tokens.length) {
var key = this.expectIdentifierOrKeywordOrString();
this.optionalCharacter($COLON);
var name = null;
var expression = null;
if (this.optionalOperator("#")) {
name = this.expectIdentifierOrKeyword();
} else {
var start = this.inputIndex;
var ast = this.parseExpression();
var source = this.input.substring(start, this.inputIndex);
expression = new ASTWithSource(ast, source);
}
ListWrapper.push(bindings, new TemplateBinding(key, name, expression));
if (!this.optionalCharacter($SEMICOLON)) {
this.optionalCharacter($COMMA);
};
}
return bindings;
}
error(message:string, index:int = null) { error(message:string, index:int = null) {
if (isBlank(index)) index = this.index; if (isBlank(index)) index = this.index;

View File

@ -310,6 +310,8 @@ class ProtoRecordCreator {
visitAssignment(ast:Assignment, dest) {this.unsupported();} visitAssignment(ast:Assignment, dest) {this.unsupported();}
visitTemplateBindings(ast, dest) {this.unsupported();}
createRecordsFromAST(ast:AST, memento){ createRecordsFromAST(ast:AST, memento){
ast.visit(this, memento); ast.visit(this, memento);
} }

View File

@ -18,7 +18,7 @@ import {Record} from 'change_detection/record';
export function main() { export function main() {
function ast(exp:string) { function ast(exp:string) {
var parser = new Parser(new Lexer(), new ClosureMap()); var parser = new Parser(new Lexer(), new ClosureMap());
return parser.parseBinding(exp); return parser.parseBinding(exp).ast;
} }
function createChangeDetector(memo:string, exp:string, context = null, formatters = null) { function createChangeDetector(memo:string, exp:string, context = null, formatters = null) {

View File

@ -237,6 +237,11 @@ export function main() {
}).toThrowError("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']"); }).toThrowError("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']");
}); });
it('should tokenize hash as operator', function() {
var tokens:List<Token> = lex("#");
expectOperatorToken(tokens[0], 0, '#');
});
}); });
}); });
} }

View File

@ -1,6 +1,6 @@
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'test_lib/test_lib'; import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'test_lib/test_lib';
import {BaseException, isBlank} from 'facade/lang'; import {BaseException, isBlank, isPresent} from 'facade/lang';
import {MapWrapper} from 'facade/collection'; import {MapWrapper, ListWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser'; import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer'; import {Lexer} from 'change_detection/parser/lexer';
import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast'; import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast';
@ -32,11 +32,15 @@ export function main() {
} }
function parseAction(text) { function parseAction(text) {
return createParser().parseAction(text); return createParser().parseAction(text).ast;
} }
function parseBinding(text) { function parseBinding(text) {
return createParser().parseBinding(text); return createParser().parseBinding(text).ast;
}
function parseTemplateBindings(text) {
return createParser().parseTemplateBindings(text);
} }
function expectEval(text, passedInContext = null) { function expectEval(text, passedInContext = null) {
@ -48,6 +52,15 @@ export function main() {
return expect(() => parseAction(text).eval(td())); return expect(() => parseAction(text).eval(td()));
} }
function evalAsts(asts, passedInContext = null) {
var c = isBlank(passedInContext) ? td() : passedInContext;
var res = [];
for (var i=0; i<asts.length; i++) {
ListWrapper.push(res, asts[i].eval(c));
}
return res;
}
describe("parser", () => { describe("parser", () => {
describe("parseAction", () => { describe("parseAction", () => {
describe("basic expressions", () => { describe("basic expressions", () => {
@ -287,7 +300,7 @@ export function main() {
it('should pass exceptions', () => { it('should pass exceptions', () => {
expect(() => { expect(() => {
createParser().parseAction('a()').eval(td(() => {throw new BaseException("boo to you")})); createParser().parseAction('a()').ast.eval(td(() => {throw new BaseException("boo to you")}));
}).toThrowError('boo to you'); }).toThrowError('boo to you');
}); });
@ -297,6 +310,10 @@ export function main() {
expectEval("1;;").toEqual(1); expectEval("1;;").toEqual(1);
}); });
}); });
it('should store the source in the result', () => {
expect(createParser().parseAction('someExpr').source).toBe('someExpr');
});
}); });
describe("parseBinding", () => { describe("parseBinding", () => {
@ -319,6 +336,11 @@ export function main() {
expect(() => parseBinding('"Foo"|1234')).toThrowError(new RegExp('identifier or keyword')); expect(() => parseBinding('"Foo"|1234')).toThrowError(new RegExp('identifier or keyword'));
expect(() => parseBinding('"Foo"|"uppercase"')).toThrowError(new RegExp('identifier or keyword')); expect(() => parseBinding('"Foo"|"uppercase"')).toThrowError(new RegExp('identifier or keyword'));
}); });
});
it('should store the source in the result', () => {
expect(createParser().parseBinding('someExpr').source).toBe('someExpr');
}); });
it('should throw on chain expressions', () => { it('should throw on chain expressions', () => {
@ -329,6 +351,90 @@ export function main() {
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression")); expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
}); });
}); });
describe('parseTemplateBindings', () => {
function keys(templateBindings) {
return ListWrapper.map(templateBindings, (binding) => binding.key );
}
function names(templateBindings) {
return ListWrapper.map(templateBindings, (binding) => binding.name );
}
function exprSources(templateBindings) {
return ListWrapper.map(templateBindings,
(binding) => isPresent(binding.expression) ? binding.expression.source : null );
}
function exprAsts(templateBindings) {
return ListWrapper.map(templateBindings,
(binding) => isPresent(binding.expression) ? binding.expression.ast : null );
}
it('should parse an empty string', () => {
var bindings = parseTemplateBindings("");
expect(bindings).toEqual([]);
});
it('should only allow identifier, string, or keyword as keys', () => {
var bindings = parseTemplateBindings("a:'b'");
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings("'a':'b'");
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings("\"a\":'b'");
expect(keys(bindings)).toEqual(['a']);
expect( () => {
parseTemplateBindings('(:0');
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
expect( () => {
parseTemplateBindings('1234:0');
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
});
it('should detect expressions as value', () => {
var bindings = parseTemplateBindings("a:b");
expect(exprSources(bindings)).toEqual(['b']);
expect(evalAsts(exprAsts(bindings), td(0, 23))).toEqual([23]);
bindings = parseTemplateBindings("a:1+1");
expect(exprSources(bindings)).toEqual(['1+1']);
expect(evalAsts(exprAsts(bindings))).toEqual([2]);
});
it('should detect names as value', () => {
var bindings = parseTemplateBindings("a:#b");
expect(names(bindings)).toEqual(['b']);
expect(exprSources(bindings)).toEqual([null]);
expect(exprAsts(bindings)).toEqual([null]);
});
it('should allow space and colon as separators', () => {
var bindings = parseTemplateBindings("a:b");
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings("a b");
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
});
it('should allow multiple pairs', () => {
var bindings = parseTemplateBindings("a 1 b 2");
expect(keys(bindings)).toEqual(['a', 'b']);
expect(exprSources(bindings)).toEqual(['1 ', '2']);
});
it('should store the sources in the result', () => {
var bindings = parseTemplateBindings("a 1,b 2");
expect(bindings[0].expression.source).toEqual('1');
expect(bindings[1].expression.source).toEqual('2');
});
});
}); });
} }

View File

@ -6,6 +6,8 @@ import {Decorator} from '../../annotations/decorator';
import {Component} from '../../annotations/component'; import {Component} from '../../annotations/component';
import {Template} from '../../annotations/template'; import {Template} from '../../annotations/template';
import {ASTWithSource} from 'change_detection/parser/ast';
/** /**
* Collects all data that is needed to process an element * Collects all data that is needed to process an element
* in the compile process. Fields are filled * in the compile process. Fields are filled
@ -18,6 +20,7 @@ export class CompileElement {
this._classList = null; this._classList = null;
this.textNodeBindings = null; this.textNodeBindings = null;
this.propertyBindings = null; this.propertyBindings = null;
this.variableBindings = null;
this.decoratorDirectives = null; this.decoratorDirectives = null;
this.templateDirective = null; this.templateDirective = null;
this.componentDirective = null; this.componentDirective = null;
@ -60,20 +63,27 @@ export class CompileElement {
return this._classList; return this._classList;
} }
addTextNodeBinding(indexInParent:int, expression:string) { addTextNodeBinding(indexInParent:int, expression:ASTWithSource) {
if (isBlank(this.textNodeBindings)) { if (isBlank(this.textNodeBindings)) {
this.textNodeBindings = MapWrapper.create(); this.textNodeBindings = MapWrapper.create();
} }
MapWrapper.set(this.textNodeBindings, indexInParent, expression); MapWrapper.set(this.textNodeBindings, indexInParent, expression);
} }
addPropertyBinding(property:string, expression:string) { addPropertyBinding(property:string, expression:ASTWithSource) {
if (isBlank(this.propertyBindings)) { if (isBlank(this.propertyBindings)) {
this.propertyBindings = MapWrapper.create(); this.propertyBindings = MapWrapper.create();
} }
MapWrapper.set(this.propertyBindings, property, expression); MapWrapper.set(this.propertyBindings, property, expression);
} }
addVariableBinding(contextName:string, templateName:string) {
if (isBlank(this.variableBindings)) {
this.variableBindings = MapWrapper.create();
}
MapWrapper.set(this.variableBindings, contextName, templateName);
}
addDirective(directive:AnnotatedType) { addDirective(directive:AnnotatedType) {
var annotation = directive.annotation; var annotation = directive.annotation;
if (annotation instanceof Decorator) { if (annotation instanceof Decorator) {

View File

@ -21,13 +21,13 @@ export function createDefaultSteps(
directives: List<AnnotatedType> directives: List<AnnotatedType>
) { ) {
return [ return [
new PropertyBindingParser(), new ViewSplitter(parser),
new TextInterpolationParser(), new TextInterpolationParser(parser),
new PropertyBindingParser(parser),
new DirectiveParser(directives), new DirectiveParser(directives),
new ViewSplitter(),
new ElementBindingMarker(), new ElementBindingMarker(),
new ProtoViewBuilder(), new ProtoViewBuilder(),
new ProtoElementInjectorBuilder(), new ProtoElementInjectorBuilder(),
new ElementBinderBuilder(parser, closureMap) new ElementBinderBuilder(closureMap)
]; ];
} }

View File

@ -1,5 +1,6 @@
import {isPresent, BaseException} from 'facade/lang'; import {isPresent, BaseException} from 'facade/lang';
import {List, MapWrapper} from 'facade/collection'; import {List, MapWrapper} from 'facade/collection';
import {TemplateElement} from 'facade/dom';
import {SelectorMatcher} from '../selector'; import {SelectorMatcher} from '../selector';
import {CssSelector} from '../selector'; import {CssSelector} from '../selector';
@ -12,7 +13,8 @@ import {CompileControl} from './compile_control';
import {Reflector} from '../reflector'; import {Reflector} from '../reflector';
/** /**
* Parses the directives on a single element. * Parses the directives on a single element. Assumes ViewSplitter has already created
* <template> elements for template directives.
* *
* Fills: * Fills:
* - CompileElement#decoratorDirectives * - CompileElement#decoratorDirectives
@ -22,6 +24,8 @@ import {Reflector} from '../reflector';
* Reads: * Reads:
* - CompileElement#propertyBindings (to find directives contained * - CompileElement#propertyBindings (to find directives contained
* in the property bindings) * in the property bindings)
* - CompileElement#variableBindings (to find directives contained
* in the property bindings)
*/ */
export class DirectiveParser extends CompileStep { export class DirectiveParser extends CompileStep {
constructor(directives:List<AnnotatedType>) { constructor(directives:List<AnnotatedType>) {
@ -47,17 +51,29 @@ export class DirectiveParser extends CompileStep {
MapWrapper.forEach(attrs, (attrValue, attrName) => { MapWrapper.forEach(attrs, (attrValue, attrName) => {
cssSelector.addAttribute(attrName, attrValue); cssSelector.addAttribute(attrName, attrValue);
}); });
// Allow to find directives even though the attribute is bound
if (isPresent(current.propertyBindings)) { if (isPresent(current.propertyBindings)) {
MapWrapper.forEach(current.propertyBindings, (expression, boundProp) => { MapWrapper.forEach(current.propertyBindings, (expression, prop) => {
cssSelector.addAttribute(boundProp, expression); cssSelector.addAttribute(prop, expression.source);
}); });
} }
if (isPresent(current.variableBindings)) {
MapWrapper.forEach(current.variableBindings, (value, name) => {
cssSelector.addAttribute(name, value);
});
}
// 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 = current.element instanceof TemplateElement;
this._selectorMatcher.match(cssSelector, (directive) => { this._selectorMatcher.match(cssSelector, (directive) => {
if (isPresent(current.templateDirective) && (directive.annotation instanceof Template)) { if (directive.annotation instanceof Template) {
if (!isTemplateElement) {
throw new BaseException('Template directives need to be placed on <template> elements or elements with template attribute!');
} else if (isPresent(current.templateDirective)) {
throw new BaseException('Only one template directive per element is allowed!'); throw new BaseException('Only one template directive per element is allowed!');
} }
if (isPresent(current.componentDirective) && (directive.annotation instanceof Component)) { } 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!'); throw new BaseException('Only one component directive per element is allowed!');
} }
current.addDirective(directive); current.addDirective(directive);

View File

@ -43,8 +43,7 @@ import {CompileControl} from './compile_control';
* with the flag `isViewRoot`. * with the flag `isViewRoot`.
*/ */
export class ElementBinderBuilder extends CompileStep { export class ElementBinderBuilder extends CompileStep {
constructor(parser:Parser, closureMap:ClosureMap) { constructor(closureMap:ClosureMap) {
this._parser = parser;
this._closureMap = closureMap; this._closureMap = closureMap;
} }
@ -56,10 +55,10 @@ export class ElementBinderBuilder extends CompileStep {
current.componentDirective, current.templateDirective); current.componentDirective, current.templateDirective);
if (isPresent(current.textNodeBindings)) { if (isPresent(current.textNodeBindings)) {
this._bindTextNodes(protoView, current.textNodeBindings); this._bindTextNodes(protoView, current);
} }
if (isPresent(current.propertyBindings)) { if (isPresent(current.propertyBindings)) {
this._bindElementProperties(protoView, current.propertyBindings); this._bindElementProperties(protoView, current);
} }
this._bindDirectiveProperties(this._collectDirectives(current), current); this._bindDirectiveProperties(this._collectDirectives(current), current);
} else if (isPresent(parent)) { } else if (isPresent(parent)) {
@ -68,36 +67,36 @@ export class ElementBinderBuilder extends CompileStep {
current.inheritedElementBinder = elementBinder; current.inheritedElementBinder = elementBinder;
} }
_bindTextNodes(protoView, textNodeBindings) { _bindTextNodes(protoView, compileElement) {
MapWrapper.forEach(textNodeBindings, (expression, indexInParent) => { MapWrapper.forEach(compileElement.textNodeBindings, (expression, indexInParent) => {
protoView.bindTextNode(indexInParent, this._parser.parseBinding(expression)); protoView.bindTextNode(indexInParent, expression.ast);
}); });
} }
_bindElementProperties(protoView, propertyBindings) { _bindElementProperties(protoView, compileElement) {
MapWrapper.forEach(propertyBindings, (expression, property) => { MapWrapper.forEach(compileElement.propertyBindings, (expression, property) => {
protoView.bindElementProperty(property, this._parser.parseBinding(expression)); protoView.bindElementProperty(property, expression.ast);
}); });
} }
_collectDirectives(pipelineElement) { _collectDirectives(compileElement) {
var directives; var directives;
if (isPresent(pipelineElement.decoratorDirectives)) { if (isPresent(compileElement.decoratorDirectives)) {
directives = ListWrapper.clone(pipelineElement.decoratorDirectives); directives = ListWrapper.clone(compileElement.decoratorDirectives);
} else { } else {
directives = []; directives = [];
} }
if (isPresent(pipelineElement.templateDirective)) { if (isPresent(compileElement.templateDirective)) {
ListWrapper.push(directives, pipelineElement.templateDirective); ListWrapper.push(directives, compileElement.templateDirective);
} }
if (isPresent(pipelineElement.componentDirective)) { if (isPresent(compileElement.componentDirective)) {
ListWrapper.push(directives, pipelineElement.componentDirective); ListWrapper.push(directives, compileElement.componentDirective);
} }
return directives; return directives;
} }
_bindDirectiveProperties(typesWithAnnotations, pipelineElement) { _bindDirectiveProperties(typesWithAnnotations, compileElement) {
var protoView = pipelineElement.inheritedProtoView; var protoView = compileElement.inheritedProtoView;
var directiveIndex = 0; var directiveIndex = 0;
ListWrapper.forEach(typesWithAnnotations, (typeWithAnnotation) => { ListWrapper.forEach(typesWithAnnotations, (typeWithAnnotation) => {
var annotation = typeWithAnnotation.annotation; var annotation = typeWithAnnotation.annotation;
@ -105,8 +104,8 @@ export class ElementBinderBuilder extends CompileStep {
return; return;
} }
StringMapWrapper.forEach(annotation.bind, (dirProp, elProp) => { StringMapWrapper.forEach(annotation.bind, (dirProp, elProp) => {
var expression = isPresent(pipelineElement.propertyBindings) ? var expression = isPresent(compileElement.propertyBindings) ?
MapWrapper.get(pipelineElement.propertyBindings, elProp) : MapWrapper.get(compileElement.propertyBindings, elProp) :
null; null;
if (isBlank(expression)) { if (isBlank(expression)) {
throw new BaseException('No element binding found for property '+elProp throw new BaseException('No element binding found for property '+elProp
@ -114,7 +113,7 @@ export class ElementBinderBuilder extends CompileStep {
} }
protoView.bindDirectiveProperty( protoView.bindDirectiveProperty(
directiveIndex++, directiveIndex++,
this._parser.parseBinding(expression), expression.ast,
dirProp, dirProp,
this._closureMap.setter(dirProp) this._closureMap.setter(dirProp)
); );

View File

@ -18,6 +18,7 @@ const NG_BINDING_CLASS = 'ng-binding';
* Reads: * Reads:
* - CompileElement#textNodeBindings * - CompileElement#textNodeBindings
* - CompileElement#propertyBindings * - CompileElement#propertyBindings
* - CompileElement#variableBindings
* - CompileElement#decoratorDirectives * - CompileElement#decoratorDirectives
* - CompileElement#componentDirective * - CompileElement#componentDirective
* - CompileElement#templateDirective * - CompileElement#templateDirective
@ -27,6 +28,7 @@ export class ElementBindingMarker extends CompileStep {
var hasBindings = var hasBindings =
(isPresent(current.textNodeBindings) && MapWrapper.size(current.textNodeBindings)>0) || (isPresent(current.textNodeBindings) && MapWrapper.size(current.textNodeBindings)>0) ||
(isPresent(current.propertyBindings) && MapWrapper.size(current.propertyBindings)>0) || (isPresent(current.propertyBindings) && MapWrapper.size(current.propertyBindings)>0) ||
(isPresent(current.variableBindings) && MapWrapper.size(current.variableBindings)>0) ||
(isPresent(current.decoratorDirectives) && current.decoratorDirectives.length > 0) || (isPresent(current.decoratorDirectives) && current.decoratorDirectives.length > 0) ||
isPresent(current.templateDirective) || isPresent(current.templateDirective) ||
isPresent(current.componentDirective); isPresent(current.componentDirective);

View File

@ -1,13 +1,18 @@
import {isPresent, isBlank, RegExpWrapper} from 'facade/lang'; import {isPresent, isBlank, RegExpWrapper, BaseException} from 'facade/lang';
import {MapWrapper} from 'facade/collection'; import {MapWrapper} from 'facade/collection';
import {TemplateElement} from 'facade/dom';
import {Parser} from 'change_detection/parser/parser';
import {ExpressionWithSource} from 'change_detection/parser/ast';
import {CompileStep} from './compile_step'; 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 {interpolationToExpression} from './text_interpolation_parser';
// TODO(tbosch): Cannot make this const/final right now because of the transpiler... // TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var BIND_DASH_REGEXP = RegExpWrapper.create('bind-((?:[^-]|-(?!-))+)(?:--(.+))?'); var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let))-(.+))|\\[([^\\]]+)\\]');
var PROP_BIND_REGEXP = RegExpWrapper.create('\\[([^|]+)(?:\\|(.+))?\\]');
/** /**
* Parses the property bindings on a single element. * Parses the property bindings on a single element.
@ -16,15 +21,35 @@ var PROP_BIND_REGEXP = RegExpWrapper.create('\\[([^|]+)(?:\\|(.+))?\\]');
* - CompileElement#propertyBindings * - CompileElement#propertyBindings
*/ */
export class PropertyBindingParser extends CompileStep { export class PropertyBindingParser extends CompileStep {
constructor(parser:Parser) {
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var attrs = current.attrs(); var attrs = current.attrs();
MapWrapper.forEach(attrs, (attrValue, attrName) => { MapWrapper.forEach(attrs, (attrValue, attrName) => {
var parts = RegExpWrapper.firstMatch(BIND_DASH_REGEXP, attrName); var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
if (isBlank(parts)) { if (isPresent(bindParts)) {
parts = RegExpWrapper.firstMatch(PROP_BIND_REGEXP, attrName); if (isPresent(bindParts[1])) {
// match: bind-prop
current.addPropertyBinding(bindParts[3], this._parser.parseBinding(attrValue));
} else if (isPresent(bindParts[2])) {
// match: let-prop
// Note: We assume that the ViewSplitter already did its work, i.e. template directive should
// only be present on <template> elements any more!
if (!(current.element instanceof TemplateElement)) {
throw new BaseException('let-* is only allowed on <template> elements!');
}
current.addVariableBinding(bindParts[3], attrValue);
} else if (isPresent(bindParts[4])) {
// match: [prop]
current.addPropertyBinding(bindParts[4], this._parser.parseBinding(attrValue));
}
} else {
var expression = interpolationToExpression(attrValue);
if (isPresent(expression)) {
current.addPropertyBinding(attrName, this._parser.parseBinding(expression));
} }
if (isPresent(parts)) {
current.addPropertyBinding(parts[1], attrValue);
} }
}); });
} }

View File

@ -1,5 +1,5 @@
import {isPresent, BaseException} from 'facade/lang'; import {isPresent, BaseException} from 'facade/lang';
import {ListWrapper} from 'facade/collection'; import {ListWrapper, MapWrapper} from 'facade/collection';
import {ProtoView} from '../view'; import {ProtoView} from '../view';
import {ProtoWatchGroup} from 'change_detection/watch_group'; import {ProtoWatchGroup} from 'change_detection/watch_group';
@ -27,6 +27,11 @@ export class ProtoViewBuilder extends CompileStep {
throw new BaseException('Only one nested view per element is allowed'); throw new BaseException('Only one nested view per element is allowed');
} }
parent.inheritedElementBinder.nestedProtoView = inheritedProtoView; parent.inheritedElementBinder.nestedProtoView = inheritedProtoView;
if (isPresent(parent.variableBindings)) {
MapWrapper.forEach(parent.variableBindings, (mappedName, varName) => {
inheritedProtoView.bindVariable(varName, mappedName);
});
}
} }
} else if (isPresent(parent)) { } else if (isPresent(parent)) {
inheritedProtoView = parent.inheritedProtoView; inheritedProtoView = parent.inheritedProtoView;

View File

@ -1,6 +1,8 @@
import {RegExpWrapper, StringWrapper, isPresent} from 'facade/lang'; import {RegExpWrapper, StringWrapper, isPresent} from 'facade/lang';
import {Node, DOM} from 'facade/dom'; import {Node, DOM} from 'facade/dom';
import {Parser} from 'change_detection/parser/parser';
import {CompileStep} from './compile_step'; 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';
@ -9,28 +11,12 @@ import {CompileControl} from './compile_control';
var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}'); var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}');
var QUOTE_REGEXP = RegExpWrapper.create("'"); var QUOTE_REGEXP = RegExpWrapper.create("'");
/** export function interpolationToExpression(value:string):string {
* Parses interpolations in direct text child nodes of the current element.
*
* Fills:
* - CompileElement#textNodeBindings
*/
export class TextInterpolationParser extends CompileStep {
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var element = current.element;
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var i=0; i<childNodes.length; i++) {
var node = childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
this._parseTextNode(current, node, i);
}
}
}
_parseTextNode(pipelineElement, node, nodeIndex) {
// TODO: add stringify formatter when we support formatters // TODO: add stringify formatter when we support formatters
var parts = StringWrapper.split(node.nodeValue, INTERPOLATION_REGEXP); var parts = StringWrapper.split(value, INTERPOLATION_REGEXP);
if (parts.length > 1) { if (parts.length <= 1) {
return null;
}
var expression = ''; var expression = '';
for (var i=0; i<parts.length; i++) { for (var i=0; i<parts.length; i++) {
var expressionPart = null; var expressionPart = null;
@ -50,8 +36,36 @@ export class TextInterpolationParser extends CompileStep {
expression += expressionPart; expression += expressionPart;
} }
} }
return expression;
}
/**
* Parses interpolations in direct text child nodes of the current element.
*
* Fills:
* - CompileElement#textNodeBindings
*/
export class TextInterpolationParser extends CompileStep {
constructor(parser:Parser) {
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var element = current.element;
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var i=0; i<childNodes.length; i++) {
var node = childNodes[i];
if (node.nodeType === Node.TEXT_NODE) {
this._parseTextNode(current, node, i);
}
}
}
_parseTextNode(pipelineElement, node, nodeIndex) {
var expression = interpolationToExpression(node.nodeValue);
if (isPresent(expression)) {
DOM.setText(node, ' '); DOM.setText(node, ' ');
pipelineElement.addTextNodeBinding(nodeIndex, expression); pipelineElement.addTextNodeBinding(nodeIndex, this._parser.parseBinding(expression));
} }
} }
} }

View File

@ -1,7 +1,9 @@
import {isBlank, isPresent} from 'facade/lang'; import {isBlank, isPresent} from 'facade/lang';
import {DOM} from 'facade/dom'; import {DOM, TemplateElement} from 'facade/dom';
import {MapWrapper, StringMapWrapper} from 'facade/collection'; import {MapWrapper, StringMapWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser';
import {CompileStep} from './compile_step'; 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';
@ -13,50 +15,39 @@ import {CompileControl} from './compile_control';
* *
* Fills: * Fills:
* - CompileElement#isViewRoot * - CompileElement#isViewRoot
* * - CompileElement#variableBindings
* Updates:
* - CompileElement#templateDirective
* - CompileElement#propertyBindings
*
* Reads:
* - CompileElement#templateDirective
* - CompileElement#propertyBindings * - CompileElement#propertyBindings
*/ */
export class ViewSplitter extends CompileStep { export class ViewSplitter extends CompileStep {
constructor(parser:Parser) {
this._parser = parser;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var element = current.element; var element = current.element;
if (isPresent(current.templateDirective)) { if (isBlank(parent) || (current.element instanceof TemplateElement)) {
var templateElement = DOM.createTemplate('');
var templateBoundProperties = MapWrapper.create();
var nonTemplateBoundProperties = MapWrapper.create();
this._splitElementPropertyBindings(current, templateBoundProperties, nonTemplateBoundProperties);
var newParentElement = new CompileElement(templateElement);
newParentElement.propertyBindings = templateBoundProperties;
newParentElement.templateDirective = current.templateDirective;
control.addParent(newParentElement);
// disconnect child view from their parent view
element.remove();
current.templateDirective = null;
current.propertyBindings = nonTemplateBoundProperties;
current.isViewRoot = true; current.isViewRoot = true;
} else if (isBlank(parent)) {
current.isViewRoot = true;
}
}
_splitElementPropertyBindings(compileElement, templateBoundProperties, nonTemplateBoundProperties) {
var dirBindings = compileElement.templateDirective.annotation.bind;
if (isPresent(dirBindings) && isPresent(compileElement.propertyBindings)) {
MapWrapper.forEach(compileElement.propertyBindings, (expr, elProp) => {
if (isPresent(StringMapWrapper.get(dirBindings, elProp))) {
MapWrapper.set(templateBoundProperties, elProp, expr);
} else { } else {
MapWrapper.set(nonTemplateBoundProperties, elProp, expr); var templateBindings = MapWrapper.get(current.attrs(), 'template');
} if (isPresent(templateBindings)) {
}); current.isViewRoot = true;
var templateElement = DOM.createTemplate('');
var newParentElement = new CompileElement(templateElement);
this._parseTemplateBindings(templateBindings, newParentElement);
control.addParent(newParentElement);
}
}
}
_parseTemplateBindings(templateBindings:string, compileElement:CompileElement) {
var bindings = this._parser.parseTemplateBindings(templateBindings);
for (var i=0; i<bindings.length; i++) {
var binding = bindings[i];
if (isPresent(binding.name)) {
compileElement.addVariableBinding(binding.key, binding.name);
} else {
compileElement.addPropertyBinding(binding.key, binding.expression);
}
} }
} }
} }

View File

@ -78,6 +78,7 @@ export class ProtoView {
protoWatchGroup:ProtoWatchGroup) { protoWatchGroup:ProtoWatchGroup) {
this.element = template; this.element = template;
this.elementBinders = []; this.elementBinders = [];
this.variableBindings = MapWrapper.create();
this.protoWatchGroup = protoWatchGroup; this.protoWatchGroup = protoWatchGroup;
this.textNodesWithBindingCount = 0; this.textNodesWithBindingCount = 0;
this.elementsWithBindingCount = 0; this.elementsWithBindingCount = 0;
@ -124,6 +125,10 @@ export class ProtoView {
return view; return view;
} }
bindVariable(contextName:string, templateName:string) {
MapWrapper.set(this.variableBindings, contextName, templateName);
}
bindElement(protoElementInjector:ProtoElementInjector, bindElement(protoElementInjector:ProtoElementInjector,
componentDirective:AnnotatedType = null, templateDirective:AnnotatedType = null):ElementBinder { componentDirective:AnnotatedType = null, templateDirective:AnnotatedType = null):ElementBinder {
var elBinder = new ElementBinder(protoElementInjector, componentDirective, templateDirective); var elBinder = new ElementBinder(protoElementInjector, componentDirective, templateDirective);

View File

@ -1,6 +1,6 @@
import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib';
import {isPresent} from 'facade/lang'; import {isPresent} from 'facade/lang';
import {ListWrapper, MapWrapper} from 'facade/collection'; import {ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
import {DirectiveParser} from 'core/compiler/pipeline/directive_parser'; import {DirectiveParser} from 'core/compiler/pipeline/directive_parser';
import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline';
import {CompileStep} from 'core/compiler/pipeline/compile_step'; import {CompileStep} from 'core/compiler/pipeline/compile_step';
@ -12,6 +12,9 @@ import {Decorator} from 'core/annotations/decorator';
import {Template} from 'core/annotations/template'; import {Template} from 'core/annotations/template';
import {TemplateConfig} from 'core/annotations/template_config'; import {TemplateConfig} from 'core/annotations/template_config';
import {Reflector} from 'core/compiler/reflector'; import {Reflector} from 'core/compiler/reflector';
import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer';
import {ClosureMap} from 'change_detection/parser/closure_map';
export function main() { export function main() {
describe('DirectiveParser', () => { describe('DirectiveParser', () => {
@ -22,7 +25,9 @@ export function main() {
directives = [SomeDecorator, SomeTemplate, SomeTemplate2, SomeComponent, SomeComponent2]; directives = [SomeDecorator, SomeTemplate, SomeTemplate2, SomeComponent, SomeComponent2];
}); });
function createPipeline(propertyBindings = null) { function createPipeline({propertyBindings, variableBindings}={}) {
var closureMap = new ClosureMap();
var parser = new Parser(new Lexer(), closureMap);
var annotatedDirectives = ListWrapper.create(); var annotatedDirectives = ListWrapper.create();
for (var i=0; i<directives.length; i++) { for (var i=0; i<directives.length; i++) {
ListWrapper.push(annotatedDirectives, reflector.annotatedType(directives[i])); ListWrapper.push(annotatedDirectives, reflector.annotatedType(directives[i]));
@ -30,7 +35,12 @@ export function main() {
return new CompilePipeline([new MockStep((parent, current, control) => { return new CompilePipeline([new MockStep((parent, current, control) => {
if (isPresent(propertyBindings)) { if (isPresent(propertyBindings)) {
current.propertyBindings = propertyBindings; StringMapWrapper.forEach(propertyBindings, (v, k) => {
current.addPropertyBinding(k, parser.parseBinding(v));
});
}
if (isPresent(variableBindings)) {
current.variableBindings = MapWrapper.createFromStringMap(variableBindings);
} }
}), new DirectiveParser(annotatedDirectives)]); }), new DirectiveParser(annotatedDirectives)]);
} }
@ -42,33 +52,26 @@ export function main() {
expect(results[0].templateDirective).toBe(null); expect(results[0].templateDirective).toBe(null);
}); });
it('should detect directives in attributes', () => { describe('component directives', () => {
var results = createPipeline().process(createElement('<div some-decor some-templ some-comp></div>')); it('should detect them in attributes', () => {
expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]); var results = createPipeline().process(createElement('<div some-comp></div>'));
expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate));
expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent)); expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent));
}); });
it('should detect directives in property bindings', () => { it('should detect them in property bindings', () => {
var pipeline = createPipeline(MapWrapper.createFromStringMap({ var pipeline = createPipeline({propertyBindings: {
'some-decor': 'someExpr',
'some-templ': 'someExpr',
'some-comp': 'someExpr' 'some-comp': 'someExpr'
})); }});
var results = pipeline.process(createElement('<div></div>')); var results = pipeline.process(createElement('<div></div>'));
expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]);
expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate));
expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent)); expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent));
}); });
describe('errors', () => { it('should detect them in variable bindings', () => {
var pipeline = createPipeline({variableBindings: {
it('should not allow multiple template directives on the same element', () => { 'some-comp': 'someExpr'
expect( () => { }});
createPipeline().process( var results = pipeline.process(createElement('<div></div>'));
createElement('<div some-templ some-templ2></div>') expect(results[0].componentDirective).toEqual(reflector.annotatedType(SomeComponent));
);
}).toThrowError('Only one template directive per element is allowed!');
}); });
it('should not allow multiple component directives on the same element', () => { it('should not allow multiple component directives on the same element', () => {
@ -78,6 +81,84 @@ export function main() {
); );
}).toThrowError('Only one component directive per element is allowed!'); }).toThrowError('Only one component directive per element is allowed!');
}); });
it('should not allow component directives on <template> elements', () => {
expect( () => {
createPipeline().process(
createElement('<template some-comp></template>')
);
}).toThrowError('Only template directives are allowed on <template> elements!');
});
});
describe('template directives', () => {
it('should detect them in attributes', () => {
var results = createPipeline().process(createElement('<template some-templ></template>'));
expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate));
});
it('should detect them in property bindings', () => {
var pipeline = createPipeline({propertyBindings: {
'some-templ': 'someExpr'
}});
var results = pipeline.process(createElement('<template></template>'));
expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate));
});
it('should detect them in variable bindings', () => {
var pipeline = createPipeline({variableBindings: {
'some-templ': 'someExpr'
}});
var results = pipeline.process(createElement('<template></template>'));
expect(results[0].templateDirective).toEqual(reflector.annotatedType(SomeTemplate));
});
it('should not allow multiple template directives on the same element', () => {
expect( () => {
createPipeline().process(
createElement('<template some-templ some-templ2></template>')
);
}).toThrowError('Only one template directive per element is allowed!');
});
it('should not allow template directives on non <template> elements', () => {
expect( () => {
createPipeline().process(
createElement('<div some-templ></div>')
);
}).toThrowError('Template directives need to be placed on <template> elements or elements with template attribute!');
});
});
describe('decorator directives', () => {
it('should detect them in attributes', () => {
var results = createPipeline().process(createElement('<div some-decor></div>'));
expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]);
});
it('should detect them in property bindings', () => {
var pipeline = createPipeline({propertyBindings: {
'some-decor': 'someExpr'
}});
var results = pipeline.process(createElement('<div></div>'));
expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]);
});
it('should detect them in variable bindings', () => {
var pipeline = createPipeline({variableBindings: {
'some-decor': 'someExpr'
}});
var results = pipeline.process(createElement('<div></div>'));
expect(results[0].decoratorDirectives).toEqual([reflector.annotatedType(SomeDecorator)]);
});
it('should not allow decorator directives on <template> elements', () => {
expect( () => {
createPipeline().process(
createElement('<template some-decor></template>')
);
}).toThrowError('Only template directives are allowed on <template> elements!');
});
}); });
}); });

View File

@ -31,6 +31,7 @@ export function main() {
}={}) { }={}) {
var reflector = new Reflector(); var reflector = new Reflector();
var closureMap = new ClosureMap(); var closureMap = new ClosureMap();
var parser = new Parser(new Lexer(), closureMap);
return new CompilePipeline([ return new CompilePipeline([
new MockStep((parent, current, control) => { new MockStep((parent, current, control) => {
if (isPresent(current.element.getAttribute('viewroot'))) { if (isPresent(current.element.getAttribute('viewroot'))) {
@ -38,22 +39,24 @@ export function main() {
current.inheritedProtoView = new ProtoView(current.element, new ProtoWatchGroup()); current.inheritedProtoView = new ProtoView(current.element, new ProtoWatchGroup());
} else if (isPresent(parent)) { } else if (isPresent(parent)) {
current.inheritedProtoView = parent.inheritedProtoView; current.inheritedProtoView = parent.inheritedProtoView;
} else {
current.inheritedProtoView = null;
} }
var hasBinding = false; var hasBinding = false;
if (isPresent(current.element.getAttribute('text-binding'))) { if (isPresent(current.element.getAttribute('text-binding'))) {
current.textNodeBindings = textNodeBindings; MapWrapper.forEach(textNodeBindings, (v,k) => {
current.addTextNodeBinding(k, parser.parseBinding(v));
});
hasBinding = true; hasBinding = true;
} }
if (isPresent(current.element.getAttribute('prop-binding'))) { if (isPresent(current.element.getAttribute('prop-binding'))) {
current.propertyBindings = propertyBindings; if (isPresent(propertyBindings)) {
MapWrapper.forEach(propertyBindings, (v,k) => {
current.addPropertyBinding(k, parser.parseBinding(v));
});
}
hasBinding = true; hasBinding = true;
} }
if (isPresent(protoElementInjector)) { if (isPresent(protoElementInjector)) {
current.inheritedProtoElementInjector = protoElementInjector; current.inheritedProtoElementInjector = protoElementInjector;
} else {
current.inheritedProtoElementInjector = null;
} }
if (isPresent(current.element.getAttribute('directives'))) { if (isPresent(current.element.getAttribute('directives'))) {
hasBinding = true; hasBinding = true;
@ -65,7 +68,7 @@ export function main() {
current.hasBindings = true; current.hasBindings = true;
DOM.addClass(current.element, 'ng-binding'); DOM.addClass(current.element, 'ng-binding');
} }
}), new ElementBinderBuilder(new Parser(new Lexer(), closureMap), closureMap) }), new ElementBinderBuilder(closureMap)
]); ]);
} }

View File

@ -16,7 +16,7 @@ import {Component} from 'core/annotations/component';
export function main() { export function main() {
describe('ElementBindingMarker', () => { describe('ElementBindingMarker', () => {
function createPipeline({textNodeBindings, propertyBindings, directives}={}) { function createPipeline({textNodeBindings, propertyBindings, variableBindings, directives}={}) {
var reflector = new Reflector(); var reflector = new Reflector();
return new CompilePipeline([ return new CompilePipeline([
new MockStep((parent, current, control) => { new MockStep((parent, current, control) => {
@ -26,6 +26,9 @@ export function main() {
if (isPresent(propertyBindings)) { if (isPresent(propertyBindings)) {
current.propertyBindings = propertyBindings; current.propertyBindings = propertyBindings;
} }
if (isPresent(variableBindings)) {
current.variableBindings = variableBindings;
}
if (isPresent(directives)) { if (isPresent(directives)) {
for (var i=0; i<directives.length; i++) { for (var i=0; i<directives.length; i++) {
current.addDirective(reflector.annotatedType(directives[i])); current.addDirective(reflector.annotatedType(directives[i]));
@ -53,6 +56,12 @@ export function main() {
assertBinding(results[0], true); assertBinding(results[0], true);
}); });
it('should mark elements with variable bindings', () => {
var variableBindings = MapWrapper.createFromStringMap({'a': 'expr'});
var results = createPipeline({variableBindings: variableBindings}).process(createElement('<div></div>'));
assertBinding(results[0], true);
});
it('should mark elements with decorator directives', () => { it('should mark elements with decorator directives', () => {
var results = createPipeline({ var results = createPipeline({
directives: [SomeDecoratorDirective] directives: [SomeDecoratorDirective]

View File

@ -4,20 +4,42 @@ import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline';
import {DOM} from 'facade/dom'; import {DOM} from 'facade/dom';
import {MapWrapper} from 'facade/collection'; import {MapWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser';
import {ClosureMap} from 'change_detection/parser/closure_map';
import {Lexer} from 'change_detection/parser/lexer';
export function main() { export function main() {
describe('PropertyBindingParser', () => { describe('PropertyBindingParser', () => {
function createPipeline() { function createPipeline() {
return new CompilePipeline([new PropertyBindingParser()]); return new CompilePipeline([new PropertyBindingParser(new Parser(new Lexer(), new ClosureMap()))]);
} }
it('should detect [] syntax', () => { it('should detect [] syntax', () => {
var results = createPipeline().process(createElement('<div [a]="b"></div>')); var results = createPipeline().process(createElement('<div [a]="b"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a')).toEqual('b'); expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('b');
}); });
it('should detect bind- syntax', () => { it('should detect bind- syntax', () => {
var results = createPipeline().process(createElement('<div bind-a="b"></div>')); var results = createPipeline().process(createElement('<div bind-a="b"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a')).toEqual('b'); expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('b');
});
it('should detect interpolation syntax', () => {
// Note: we don't test all corner cases of interpolation as we assume shared functionality between text interpolation
// and attribute interpolation.
var results = createPipeline().process(createElement('<div a="{{b}}"></div>'));
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('(b)');
});
it('should detect let- syntax', () => {
var results = createPipeline().process(createElement('<template let-a="b"></template>'));
expect(MapWrapper.get(results[0].variableBindings, 'a')).toEqual('b');
});
it('should not allow let- syntax on non template elements', () => {
expect( () => {
createPipeline().process(createElement('<div let-a="b"></div>'))
}).toThrowError('let-* is only allowed on <template> elements!');
}); });
}); });
} }

View File

@ -7,14 +7,18 @@ import {CompileElement} from 'core/compiler/pipeline/compile_element';
import {CompileStep} from 'core/compiler/pipeline/compile_step' import {CompileStep} from 'core/compiler/pipeline/compile_step'
import {CompileControl} from 'core/compiler/pipeline/compile_control'; import {CompileControl} from 'core/compiler/pipeline/compile_control';
import {DOM} from 'facade/dom'; import {DOM} from 'facade/dom';
import {MapWrapper} from 'facade/collection';
export function main() { export function main() {
describe('ProtoViewBuilder', () => { describe('ProtoViewBuilder', () => {
function createPipeline() { function createPipeline(variableBindings=null) {
return new CompilePipeline([new MockStep((parent, current, control) => { return new CompilePipeline([new MockStep((parent, current, control) => {
if (isPresent(current.element.getAttribute('viewroot'))) { if (isPresent(current.element.getAttribute('viewroot'))) {
current.isViewRoot = true; current.isViewRoot = true;
} }
if (isPresent(current.element.getAttribute('var-binding'))) {
current.variableBindings = MapWrapper.createFromStringMap(variableBindings);
}
current.inheritedElementBinder = new ElementBinder(null, null, null); current.inheritedElementBinder = new ElementBinder(null, null, null);
}), new ProtoViewBuilder()]); }), new ProtoViewBuilder()]);
} }
@ -37,16 +41,29 @@ export function main() {
expect(results[1].inheritedProtoView.element).toBe(viewRootElement); expect(results[1].inheritedProtoView.element).toBe(viewRootElement);
}); });
it('should save ProtoView into elementBinder of parent element', () => { it('should save ProtoView into the elementBinder of parent element', () => {
var el = createElement('<div viewroot><span><a viewroot></a></span></div>'); var el = createElement('<div viewroot><template><a viewroot></a></template></div>');
var results = createPipeline().process(el); var results = createPipeline().process(el);
expect(results[1].inheritedElementBinder.nestedProtoView).toBe(results[2].inheritedProtoView); expect(results[1].inheritedElementBinder.nestedProtoView).toBe(results[2].inheritedProtoView);
}); });
it('should bind variables to the nested ProtoView', () => {
var el = createElement('<div viewroot><template var-binding><a viewroot></a></template></div>');
var results = createPipeline({
'var1': 'map1',
'var2': 'map2'
}).process(el);
var npv = results[1].inheritedElementBinder.nestedProtoView;
expect(npv.variableBindings).toEqual(MapWrapper.createFromStringMap({
'var1': 'map1',
'var2': 'map2'
}));
});
describe('errors', () => { describe('errors', () => {
it('should not allow multiple nested ProtoViews for the same parent element', () => { it('should not allow multiple nested ProtoViews for the same parent element', () => {
var el = createElement('<div viewroot><span><a viewroot></a><a viewroot></a></span></div>'); var el = createElement('<div viewroot><template><a viewroot></a><a viewroot></a></template></div>');
expect( () => { expect( () => {
createPipeline().process(el); createPipeline().process(el);
}).toThrowError('Only one nested view per element is allowed'); }).toThrowError('Only one nested view per element is allowed');

View File

@ -4,41 +4,45 @@ import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline';
import {DOM} from 'facade/dom'; import {DOM} from 'facade/dom';
import {MapWrapper} from 'facade/collection'; import {MapWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser';
import {ClosureMap} from 'change_detection/parser/closure_map';
import {Lexer} from 'change_detection/parser/lexer';
export function main() { export function main() {
describe('TextInterpolationParser', () => { describe('TextInterpolationParser', () => {
function createPipeline() { function createPipeline() {
return new CompilePipeline([new TextInterpolationParser()]); return new CompilePipeline([new TextInterpolationParser(new Parser(new Lexer(), new ClosureMap()))]);
} }
it('should find text interpolation in normal elements', () => { it('should find text interpolation in normal elements', () => {
var results = createPipeline().process(createElement('<div>{{expr1}}<span></span>{{expr2}}</div>')); var results = createPipeline().process(createElement('<div>{{expr1}}<span></span>{{expr2}}</div>'));
var bindings = results[0].textNodeBindings; var bindings = results[0].textNodeBindings;
expect(MapWrapper.get(bindings, 0)).toEqual("(expr1)"); expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)");
expect(MapWrapper.get(bindings, 2)).toEqual("(expr2)"); expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)");
}); });
it('should find text interpolation in template elements', () => { it('should find text interpolation in template elements', () => {
var results = createPipeline().process(createElement('<template>{{expr1}}<span></span>{{expr2}}</template>')); var results = createPipeline().process(createElement('<template>{{expr1}}<span></span>{{expr2}}</template>'));
var bindings = results[0].textNodeBindings; var bindings = results[0].textNodeBindings;
expect(MapWrapper.get(bindings, 0)).toEqual("(expr1)"); expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)");
expect(MapWrapper.get(bindings, 2)).toEqual("(expr2)"); expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)");
}); });
it('should allow multiple expressions', () => { it('should allow multiple expressions', () => {
var results = createPipeline().process(createElement('<div>{{expr1}}{{expr2}}</div>')); var results = createPipeline().process(createElement('<div>{{expr1}}{{expr2}}</div>'));
var bindings = results[0].textNodeBindings; var bindings = results[0].textNodeBindings;
expect(MapWrapper.get(bindings, 0)).toEqual("(expr1)+(expr2)"); expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)+(expr2)");
}); });
it('should allow fixed text before, in between and after expressions', () => { it('should allow fixed text before, in between and after expressions', () => {
var results = createPipeline().process(createElement('<div>a{{expr1}}b{{expr2}}c</div>')); var results = createPipeline().process(createElement('<div>a{{expr1}}b{{expr2}}c</div>'));
var bindings = results[0].textNodeBindings; var bindings = results[0].textNodeBindings;
expect(MapWrapper.get(bindings, 0)).toEqual("'a'+(expr1)+'b'+(expr2)+'c'"); expect(MapWrapper.get(bindings, 0).source).toEqual("'a'+(expr1)+'b'+(expr2)+'c'");
}); });
it('should escape quotes in fixed parts', () => { it('should escape quotes in fixed parts', () => {
var results = createPipeline().process(createElement("<div>'\"a{{expr1}}</div>")); var results = createPipeline().process(createElement("<div>'\"a{{expr1}}</div>"));
expect(MapWrapper.get(results[0].textNodeBindings, 0)).toEqual("'\\'\"a'+(expr1)"); expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\\'\"a'+(expr1)");
}); });
}); });
} }

View File

@ -4,51 +4,35 @@ import {MapWrapper} from 'facade/collection';
import {ViewSplitter} from 'core/compiler/pipeline/view_splitter'; import {ViewSplitter} from 'core/compiler/pipeline/view_splitter';
import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline';
import {CompileElement} from 'core/compiler/pipeline/compile_element';
import {CompileStep} from 'core/compiler/pipeline/compile_step'
import {CompileControl} from 'core/compiler/pipeline/compile_control';
import {DOM, TemplateElement} from 'facade/dom'; import {DOM, TemplateElement} from 'facade/dom';
import {Reflector} from 'core/compiler/reflector';
import {Template} from 'core/annotations/template'; import {Parser} from 'change_detection/parser/parser';
import {Decorator} from 'core/annotations/decorator'; import {ClosureMap} from 'change_detection/parser/closure_map';
import {Component} from 'core/annotations/component'; import {Lexer} from 'change_detection/parser/lexer';
export function main() { export function main() {
describe('ViewSplitter', () => { describe('ViewSplitter', () => {
function createPipeline({textNodeBindings, propertyBindings, directives}={}) { function createPipeline() {
var reflector = new Reflector(); return new CompilePipeline([new ViewSplitter(new Parser(new Lexer(), new ClosureMap()))]);
return new CompilePipeline([
new MockStep((parent, current, control) => {
if (isPresent(current.element.getAttribute('tmpl'))) {
current.addDirective(reflector.annotatedType(SomeTemplateDirective));
if (isPresent(textNodeBindings)) {
current.textNodeBindings = textNodeBindings;
}
if (isPresent(propertyBindings)) {
current.propertyBindings = propertyBindings;
}
if (isPresent(directives)) {
for (var i=0; i<directives.length; i++) {
current.addDirective(reflector.annotatedType(directives[i]));
}
}
}
}), new ViewSplitter()
]);
} }
function commonTests(useTemplateElement) { it('should mark root elements as viewRoot', () => {
var rootElement; var rootElement = createElement('<div></div>');
beforeEach( () => { var results = createPipeline().process(rootElement);
if (useTemplateElement) { expect(results[0].isViewRoot).toBe(true);
rootElement = createElement('<div><span tmpl></span></div>');
} else {
rootElement = createElement('<div><span tmpl></span></div>');
}
}); });
it('should mark <template> elements as viewRoot', () => {
var rootElement = createElement('<div><template></template></div>');
var results = createPipeline().process(rootElement);
expect(results[1].isViewRoot).toBe(true);
});
describe('elements with template attribute', () => {
it('should insert an empty <template> element', () => { it('should insert an empty <template> element', () => {
var rootElement = createElement('<div><div template></div></div>');
var originalChild = rootElement.childNodes[0]; var originalChild = rootElement.childNodes[0];
var results = createPipeline().process(rootElement); var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement); expect(results[0].element).toBe(rootElement);
@ -57,79 +41,29 @@ export function main() {
expect(results[2].element).toBe(originalChild); expect(results[2].element).toBe(originalChild);
}); });
it('should move the template directive to the new element', () => { it('should mark the element as viewRoot', () => {
var rootElement = createElement('<div><div template></div></div>');
var results = createPipeline().process(rootElement); var results = createPipeline().process(rootElement);
expect(results[1].templateDirective.type).toBe(SomeTemplateDirective);
expect(results[2].templateDirective).toBe(null);
});
it('should split the property bindings depending on the bindings on the directive', () => {
var propertyBindings = MapWrapper.createFromStringMap({
'templateBoundProp': 'a',
'nonBoundProp': 'c'
});
var results = createPipeline({propertyBindings: propertyBindings}).process(rootElement);
expect(MapWrapper.get(results[1].propertyBindings, 'templateBoundProp')).toEqual('a');
expect(MapWrapper.get(results[2].propertyBindings, 'nonBoundProp')).toEqual('c');
});
it('should keep the component, decorator directives and text node bindings on the original element', () => {
var textNodeBindings = MapWrapper.create();
MapWrapper.set(textNodeBindings, 0, 'someExpr');
var directives = [SomeDecoratorDirective, SomeComponentDirective];
var results = createPipeline({
textNodeBindings: textNodeBindings,
directives: directives
}).process(rootElement);
expect(results[1].componentDirective).toBe(null);
expect(results[1].decoratorDirectives).toBe(null);
expect(results[1].textNodeBindings).toBe(null);
expect(results[2].componentDirective.type).toEqual(SomeComponentDirective);
expect(results[2].decoratorDirectives[0].type).toEqual(SomeDecoratorDirective);
expect(results[2].textNodeBindings).toEqual(textNodeBindings);
});
it('should set the isViewRoot flag for the root and nested views', () => {
var results = createPipeline().process(rootElement);
expect(results[0].isViewRoot).toBe(true);
expect(results[1].isViewRoot).toBe(false);
expect(results[2].isViewRoot).toBe(true); expect(results[2].isViewRoot).toBe(true);
}); });
}
describe('template directive on normal element', () => { it('should add property bindings from the template attribute', () => {
commonTests(false); var rootElement = createElement('<div><div template="prop:expr"></div></div>');
var results = createPipeline().process(rootElement);
expect(MapWrapper.get(results[1].propertyBindings, 'prop').source).toEqual('expr');
});
it('should add variable mappings from the template attribute', () => {
var rootElement = createElement('<div><div template="varName #mapName"></div></div>');
var results = createPipeline().process(rootElement);
expect(results[1].variableBindings).toEqual(MapWrapper.createFromStringMap({'varName': 'mapName'}));
}); });
describe('template directive on <template> element', () => {
commonTests(true);
}); });
}); });
} }
class MockStep extends CompileStep {
constructor(process) {
this.processClosure = process;
}
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
this.processClosure(parent, current, control);
}
}
@Template({
bind: {
'templateBoundProp': 'dirProp'
}
})
class SomeTemplateDirective {}
@Component()
class SomeComponentDirective {}
@Decorator()
class SomeDecoratorDirective {}
function createElement(html) { function createElement(html) {
return DOM.createTemplate(html).content.firstChild; return DOM.createTemplate(html).content.firstChild;
} }

View File

@ -46,7 +46,7 @@ export function main() {
it('should collect property bindings on the root element if it has the ng-binding class', () => { it('should collect property bindings on the root element if it has the ng-binding class', () => {
var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup()); var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('prop', parser.parseBinding('a')); pv.bindElementProperty('prop', parser.parseBinding('a').ast);
var view = pv.instantiate(null, null, null); var view = pv.instantiate(null, null, null);
expect(view.bindElements.length).toEqual(1); expect(view.bindElements.length).toEqual(1);
@ -57,7 +57,7 @@ export function main() {
var pv = new ProtoView(templateAwareCreateElement('<div><span></span><span class="ng-binding"></span></div>'), var pv = new ProtoView(templateAwareCreateElement('<div><span></span><span class="ng-binding"></span></div>'),
new ProtoWatchGroup()); new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('a', parser.parseBinding('b')); pv.bindElementProperty('a', parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null); var view = pv.instantiate(null, null, null);
expect(view.bindElements.length).toEqual(1); expect(view.bindElements.length).toEqual(1);
@ -71,8 +71,8 @@ export function main() {
it('should collect text nodes under the root element', () => { it('should collect text nodes under the root element', () => {
var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup()); var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('a')); pv.bindTextNode(0, parser.parseBinding('a').ast);
pv.bindTextNode(2, parser.parseBinding('b')); pv.bindTextNode(2, parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null); var view = pv.instantiate(null, null, null);
expect(view.textNodes.length).toEqual(2); expect(view.textNodes.length).toEqual(2);
@ -84,7 +84,7 @@ export function main() {
var pv = new ProtoView(templateAwareCreateElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'), var pv = new ProtoView(templateAwareCreateElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
new ProtoWatchGroup()); new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('b')); pv.bindTextNode(0, parser.parseBinding('b').ast);
var view = pv.instantiate(null, null, null); var view = pv.instantiate(null, null, null);
expect(view.textNodes.length).toEqual(1); expect(view.textNodes.length).toEqual(1);
@ -239,7 +239,7 @@ export function main() {
var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'), var pv = new ProtoView(createElement('<div class="ng-binding">{{}}</div>'),
new ProtoWatchGroup()); new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindTextNode(0, parser.parseBinding('foo')); pv.bindTextNode(0, parser.parseBinding('foo').ast);
createView(pv); createView(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';
@ -251,7 +251,7 @@ export function main() {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoWatchGroup()); new ProtoWatchGroup());
pv.bindElement(null); pv.bindElement(null);
pv.bindElementProperty('id', parser.parseBinding('foo')); pv.bindElementProperty('id', parser.parseBinding('foo').ast);
createView(pv); createView(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';
@ -263,7 +263,7 @@ export function main() {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoWatchGroup()); new ProtoWatchGroup());
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
pv.bindDirectiveProperty( 0, parser.parseBinding('foo'), 'prop', closureMap.setter('prop')); pv.bindDirectiveProperty( 0, parser.parseBinding('foo').ast, 'prop', closureMap.setter('prop'));
createView(pv); createView(pv);
ctx.foo = 'buz'; ctx.foo = 'buz';