feat: change template micro-syntax to new syntax

Old syntax:
- ng-repeat: #item in items;
- ng-repeat: #item; in: items;
- <template let-ng-repeat=“item” [in]=items>

New syntax:
- ng-repeat: var item in items;
- ng-repeat: var item; in items
- <template ng-repeat var-item [in]=items>


Notice that the var is now a standalone binding 
rather then an argument to ng-repeat. This will 
make the var bindings consistent with the rest of 
the system.

Closes #482
This commit is contained in:
Misko Hevery 2015-01-27 22:34:25 -08:00
parent b1e76c550e
commit 9db13be4c7
14 changed files with 88 additions and 30 deletions

View File

@ -425,10 +425,12 @@ export class ASTWithSource extends AST {
export class TemplateBinding {
key:string;
keyIsVar:boolean;
name:string;
expression:ASTWithSource;
constructor(key:string, name:string, expression:ASTWithSource) {
constructor(key:string, keyIsVar:boolean, name:string, expression:ASTWithSource) {
this.key = key;
this.keyIsVar = keyIsVar;
// only either name or expression will be filled.
this.name = name;
this.expression = expression;

View File

@ -62,6 +62,10 @@ export class Token {
return (this.type == TOKEN_TYPE_KEYWORD);
}
isKeywordVar():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "var");
}
isKeywordNull():boolean {
return (this.type == TOKEN_TYPE_KEYWORD && this._strValue == "null");
}
@ -469,6 +473,7 @@ var OPERATORS = SetWrapper.createFromList([
var KEYWORDS = SetWrapper.createFromList([
'var',
'null',
'undefined',
'true',

View File

@ -123,6 +123,19 @@ class _ParseAST {
}
}
optionalKeywordVar():boolean {
if (this.peekKeywordVar()) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordVar():boolean {
return this.next.isKeywordVar() || this.next.isOperator('#');
}
expectCharacter(code:int) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${StringWrapper.fromCharCode(code)}`);
@ -469,21 +482,26 @@ class _ParseAST {
parseTemplateBindings() {
var bindings = [];
while (this.index < this.tokens.length) {
var keyIsVar:boolean = this.optionalKeywordVar();
var key = this.expectTemplateBindingKey();
this.optionalCharacter($COLON);
var name = null;
var expression = null;
if (this.next !== EOF) {
if (this.optionalOperator("#")) {
name = this.expectIdentifierOrKeyword();
} else {
if (keyIsVar) {
if (this.optionalOperator("=")) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (!this.peekKeywordVar()) {
var start = this.inputIndex;
var ast = this.parseExpression();
var source = this.input.substring(start, this.inputIndex);
expression = new ASTWithSource(ast, source, this.location);
}
}
ListWrapper.push(bindings, new TemplateBinding(key, name, expression));
ListWrapper.push(bindings, new TemplateBinding(key, keyIsVar, name, expression));
if (!this.optionalCharacter($SEMICOLON)) {
this.optionalCharacter($COMMA);
};

View File

@ -408,6 +408,16 @@ export function main() {
return ListWrapper.map(templateBindings, (binding) => binding.key );
}
function keyValues(templateBindings) {
return ListWrapper.map(templateBindings, (binding) => {
if (binding.keyIsVar) {
return '#' + binding.key + (isBlank(binding.name) ? '' : '=' + binding.name);
} else {
return binding.key + (isBlank(binding.expression) ? '' : `=${binding.expression}`)
}
});
}
function names(templateBindings) {
return ListWrapper.map(templateBindings, (binding) => binding.name );
}
@ -466,9 +476,7 @@ export function main() {
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]);
expect(keyValues(bindings)).toEqual(['a', '#b']);
});
it('should allow space and colon as separators', () => {
@ -497,6 +505,26 @@ export function main() {
var bindings = parseTemplateBindings("a 1,b 2", 'location');
expect(bindings[0].expression.location).toEqual('location');
});
it('should support var/# notation', () => {
var bindings = parseTemplateBindings("var i");
expect(keyValues(bindings)).toEqual(['#i']);
bindings = parseTemplateBindings("#i");
expect(keyValues(bindings)).toEqual(['#i']);
bindings = parseTemplateBindings("var i-a = k-a");
expect(keyValues(bindings)).toEqual(['#i-a=k-a']);
bindings = parseTemplateBindings("keyword var item; var i = k");
expect(keyValues(bindings)).toEqual(['keyword', '#item=\$implicit', '#i=k']);
bindings = parseTemplateBindings("keyword: #item; #i = k");
expect(keyValues(bindings)).toEqual(['keyword', '#item=\$implicit', '#i=k']);
bindings = parseTemplateBindings("directive: var item in expr; var a = b", 'location');
expect(keyValues(bindings)).toEqual(['directive', '#item=\$implicit', 'in=expr in location', '#a=b']);
});
});
describe('parseInterpolation', () => {

View File

@ -23,6 +23,10 @@ export class CompileElement {
textNodeBindings:Map;
propertyBindings:Map;
eventBindings:Map;
/// Store directive name to template name mapping.
/// Directive name is what the directive exports the variable as
/// Template name is how it is reffered to it in template
variableBindings:Map;
decoratorDirectives:List<DirectiveMetadata>;
templateDirective:DirectiveMetadata;
@ -102,11 +106,11 @@ export class CompileElement {
MapWrapper.set(this.propertyBindings, property, expression);
}
addVariableBinding(contextName:string, templateName:string) {
addVariableBinding(directiveName:string, templateName:string) {
if (isBlank(this.variableBindings)) {
this.variableBindings = MapWrapper.create();
}
MapWrapper.set(this.variableBindings, contextName, templateName);
MapWrapper.set(this.variableBindings, templateName, directiveName);
}
addEventBinding(eventName:string, expression:AST) {

View File

@ -97,8 +97,8 @@ export class ElementBinderBuilder extends CompileStep {
MapWrapper.get(compileElement.propertyBindings, elProp) :
null;
if (isBlank(expression)) {
throw new BaseException('No element binding found for property '+elProp
+' which is required by directive '+stringify(directive.type));
throw new BaseException("No element binding found for property '" + elProp
+ "' which is required by directive '" + stringify(directive.type) + "'");
}
var len = dirProp.length;
var dirBindingName = dirProp;

View File

@ -9,7 +9,7 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)');
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(var)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)');
/**
* Parses the property bindings on a single element.
@ -40,7 +40,7 @@ export class PropertyBindingParser 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!
if (!(current.element instanceof TemplateElement)) {
throw new BaseException('let-* is only allowed on <template> elements!');
throw new BaseException('var-* is only allowed on <template> elements!');
}
current.addVariableBinding(bindParts[4], attrValue);
} else if (isPresent(bindParts[3])) {

View File

@ -80,7 +80,7 @@ export class ViewSplitter extends CompileStep {
var bindings = this._parser.parseTemplateBindings(templateBindings, this._compilationUnit);
for (var i=0; i<bindings.length; i++) {
var binding = bindings[i];
if (isPresent(binding.name)) {
if (binding.keyIsVar) {
compileElement.addVariableBinding(binding.key, binding.name);
} else if (isPresent(binding.expression)) {
compileElement.addPropertyBinding(binding.key, binding.expression);

View File

@ -97,7 +97,7 @@ export function main() {
});
it('should support template directives via `<template>` elements.', (done) => {
compiler.compile(MyComp, el('<div><template let-some-tmpl="greeting"><copy-me>{{greeting}}</copy-me></template></div>')).then((pv) => {
compiler.compile(MyComp, el('<div><template some-tmplate var-greeting="some-tmpl"><copy-me>{{greeting}}</copy-me></template></div>')).then((pv) => {
createView(pv);
cd.detectChanges();
@ -112,7 +112,7 @@ export function main() {
});
it('should support template directives via `template` attribute.', (done) => {
compiler.compile(MyComp, el('<div><copy-me template="some-tmpl #greeting">{{greeting}}</copy-me></div>')).then((pv) => {
compiler.compile(MyComp, el('<div><copy-me template="some-tmplate: var greeting=some-tmpl">{{greeting}}</copy-me></div>')).then((pv) => {
createView(pv);
cd.detectChanges();
@ -170,7 +170,7 @@ class ChildComp {
}
@Template({
selector: '[some-tmpl]'
selector: '[some-tmplate]'
})
class SomeTemplate {
constructor(viewPort: ViewPort) {

View File

@ -249,7 +249,7 @@ export function main() {
var pipeline = createPipeline({propertyBindings: MapWrapper.create(), directives: [SomeDecoratorDirectiveWithBinding]});
expect( () => {
pipeline.process(el('<div viewroot prop-binding directives>'));
}).toThrowError('No element binding found for property boundprop1 which is required by directive SomeDecoratorDirectiveWithBinding');
}).toThrowError("No element binding found for property 'boundprop1' which is required by directive 'SomeDecoratorDirectiveWithBinding'");
});
});

View File

@ -29,15 +29,15 @@ export function main() {
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('{{b}}');
});
it('should detect let- syntax', () => {
var results = createPipeline().process(el('<template let-a="b"></template>'));
expect(MapWrapper.get(results[0].variableBindings, 'a')).toEqual('b');
it('should detect var- syntax', () => {
var results = createPipeline().process(el('<template var-a="b"></template>'));
expect(MapWrapper.get(results[0].variableBindings, 'b')).toEqual('a');
});
it('should not allow let- syntax on non template elements', () => {
it('should not allow var- syntax on non template elements', () => {
expect( () => {
createPipeline().process(el('<div let-a="b"></div>'))
}).toThrowError('let-* is only allowed on <template> elements!');
createPipeline().process(el('<div var-a="b"></div>'))
}).toThrowError('var-* is only allowed on <template> elements!');
});
it('should detect () syntax', () => {

View File

@ -78,9 +78,9 @@ export function main() {
});
it('should add variable mappings from the template attribute', () => {
var rootElement = el('<div><div template="varName #mapName"></div></div>');
var rootElement = el('<div><div template="var varName=mapName"></div></div>');
var results = createPipeline().process(rootElement);
expect(results[1].variableBindings).toEqual(MapWrapper.createFromStringMap({'varName': 'mapName'}));
expect(results[1].variableBindings).toEqual(MapWrapper.createFromStringMap({'mapName': 'varName'}));
});
it('should add entries without value as attribute to the element', () => {
@ -101,4 +101,4 @@ export function main() {
});
});
}
}

View File

@ -49,7 +49,7 @@ export class NgRepeat extends OnChange {
}
perViewChange(view, record) {
view.setLocal('ng-repeat', record.item);
view.setLocal('\$implicit', record.item);
view.setLocal('index', record.currentIndex);
}

View File

@ -193,7 +193,8 @@ export function main() {
it('should display indices correctly', (done) => {
var INDEX_TEMPLATE = '<div><copy-me template="ng-repeat #item in items index #i">{{i.toString()}}</copy-me></div>';
var INDEX_TEMPLATE =
'<div><copy-me template="ng-repeat: var item in items; var i=index">{{i.toString()}}</copy-me></div>';
compileWithTemplate(INDEX_TEMPLATE).then((pv) => {
createView(pv);
component.items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];