feat(compiler): support `on-` and `[]`

This commit is contained in:
Tobias Bosch 2014-11-19 14:54:07 -08:00
parent c6846f1163
commit fc5b7edca4
9 changed files with 97 additions and 12 deletions

View File

@ -1,5 +1,6 @@
import {ProtoElementInjector} from './element_injector';
import {FIELD} from 'facade/lang';
import {MapWrapper} from 'facade/collection';
import {AnnotatedType} from './annotated_type';
// Comment out as dartanalyzer does not look into @FIELD
// import {List} from 'facade/collection';
@ -11,12 +12,15 @@ export class ElementBinder {
@FIELD('final templateDirective:AnnotatedType')
@FIELD('final textNodeIndices:List<int>')
@FIELD('hasElementPropertyBindings:bool')
constructor(protoElementInjector: ProtoElementInjector, componentDirective:AnnotatedType, templateDirective:AnnotatedType) {
constructor(
protoElementInjector: ProtoElementInjector, componentDirective:AnnotatedType, templateDirective:AnnotatedType) {
this.protoElementInjector = protoElementInjector;
this.componentDirective = componentDirective;
this.templateDirective = templateDirective;
// updated later when events are bound
this.events = null;
// updated later when text nodes are bound
this.textNodeIndices = [];
this.textNodeIndices = null;
// updated later when element properties are bound
this.hasElementPropertyBindings = false;
// updated later, so we are able to resolve cycles

View File

@ -20,6 +20,7 @@ export class CompileElement {
this._classList = null;
this.textNodeBindings = null;
this.propertyBindings = null;
this.eventBindings = null;
this.variableBindings = null;
this.decoratorDirectives = null;
this.templateDirective = null;
@ -84,6 +85,13 @@ export class CompileElement {
MapWrapper.set(this.variableBindings, contextName, templateName);
}
addEventBinding(eventName:string, expression:ASTWithSource) {
if (isBlank(this.eventBindings)) {
this.eventBindings = MapWrapper.create();
}
MapWrapper.set(this.eventBindings, eventName, expression);
}
addDirective(directive:AnnotatedType) {
var annotation = directive.annotation;
if (annotation instanceof Decorator) {

View File

@ -33,6 +33,7 @@ import {CompileControl} from './compile_control';
* - CompileElement#inheritedProtoElementInjector
* - CompileElement#textNodeBindings
* - CompileElement#propertyBindings
* - CompileElement#eventBindings
* - CompileElement#decoratorDirectives
* - CompileElement#componentDirective
* - CompileElement#templateDirective
@ -60,6 +61,9 @@ export class ElementBinderBuilder extends CompileStep {
if (isPresent(current.propertyBindings)) {
this._bindElementProperties(protoView, current);
}
if (isPresent(current.eventBindings)) {
this._bindEvents(protoView, current);
}
this._bindDirectiveProperties(this._collectDirectives(current), current);
} else if (isPresent(parent)) {
elementBinder = parent.inheritedElementBinder;
@ -79,6 +83,12 @@ export class ElementBinderBuilder extends CompileStep {
});
}
_bindEvents(protoView, compileElement) {
MapWrapper.forEach(compileElement.eventBindings, (expression, eventName) => {
protoView.bindEvent(eventName, expression.ast);
});
}
_collectDirectives(compileElement) {
var directives;
if (isPresent(compileElement.decoratorDirectives)) {

View File

@ -19,6 +19,7 @@ const NG_BINDING_CLASS = 'ng-binding';
* - CompileElement#textNodeBindings
* - CompileElement#propertyBindings
* - CompileElement#variableBindings
* - CompileElement#eventBindings
* - CompileElement#decoratorDirectives
* - CompileElement#componentDirective
* - CompileElement#templateDirective
@ -29,6 +30,7 @@ export class ElementBindingMarker extends CompileStep {
(isPresent(current.textNodeBindings) && MapWrapper.size(current.textNodeBindings)>0) ||
(isPresent(current.propertyBindings) && MapWrapper.size(current.propertyBindings)>0) ||
(isPresent(current.variableBindings) && MapWrapper.size(current.variableBindings)>0) ||
(isPresent(current.eventBindings) && MapWrapper.size(current.eventBindings)>0) ||
(isPresent(current.decoratorDirectives) && current.decoratorDirectives.length > 0) ||
isPresent(current.templateDirective) ||
isPresent(current.componentDirective);

View File

@ -12,13 +12,15 @@ import {CompileControl} from './compile_control';
import {interpolationToExpression} from './text_interpolation_parser';
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let))-(.+))|\\[([^\\]]+)\\]');
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\]]+)\\)');
/**
* Parses the property bindings on a single element.
*
* Fills:
* - CompileElement#propertyBindings
* - CompileElement#eventBindings
* - CompileElement#variableBindings
*/
export class PropertyBindingParser extends CompileStep {
constructor(parser:Parser) {
@ -32,7 +34,7 @@ export class PropertyBindingParser extends CompileStep {
if (isPresent(bindParts)) {
if (isPresent(bindParts[1])) {
// match: bind-prop
current.addPropertyBinding(bindParts[3], this._parser.parseBinding(attrValue));
current.addPropertyBinding(bindParts[4], 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
@ -40,10 +42,16 @@ export class PropertyBindingParser extends CompileStep {
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])) {
current.addVariableBinding(bindParts[4], attrValue);
} else if (isPresent(bindParts[3])) {
// match: on-prop
current.addEventBinding(bindParts[4], this._parser.parseAction(attrValue));
} else if (isPresent(bindParts[5])) {
// match: [prop]
current.addPropertyBinding(bindParts[4], this._parser.parseBinding(attrValue));
current.addPropertyBinding(bindParts[5], this._parser.parseBinding(attrValue));
} else if (isPresent(bindParts[6])) {
// match: (prop)
current.addEventBinding(bindParts[6], this._parser.parseBinding(attrValue));
}
} else {
var expression = interpolationToExpression(attrValue);

View File

@ -141,6 +141,9 @@ export class ProtoView {
*/
bindTextNode(indexInParent:int, expression:AST) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (isBlank(elBinder.textNodeIndices)) {
elBinder.textNodeIndices = ListWrapper.create();
}
ListWrapper.push(elBinder.textNodeIndices, indexInParent);
this.protoWatchGroup.watch(expression, this.textNodesWithBindingCount++);
}
@ -162,6 +165,17 @@ export class ProtoView {
);
}
/**
* Adds an event binding for the last created ElementBinder via bindElement
*/
bindEvent(eventName:string, expression:AST) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (isBlank(elBinder.events)) {
elBinder.events = MapWrapper.create();
}
MapWrapper.set(elBinder.events, eventName, expression);
}
/**
* Adds a directive property binding for the last created ElementBinder via bindElement
*/
@ -228,9 +242,11 @@ export class ProtoView {
}
static _collectTextNodes(allTextNodes, element, indices) {
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var i = 0; i < indices.length; ++i) {
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
if (isPresent(indices)) {
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var i = 0; i < indices.length; ++i) {
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
}
}
}

View File

@ -27,7 +27,7 @@ export function main() {
describe('ElementBinderBuilder', () => {
var evalContext, view, changeDetector;
function createPipeline({textNodeBindings, propertyBindings, directives, protoElementInjector
function createPipeline({textNodeBindings, propertyBindings, eventBindings, directives, protoElementInjector
}={}) {
var reflector = new Reflector();
var closureMap = new ClosureMap();
@ -55,6 +55,12 @@ export function main() {
}
hasBinding = true;
}
if (isPresent(current.element.getAttribute('event-binding'))) {
MapWrapper.forEach(eventBindings, (v,k) => {
current.addEventBinding(k, parser.parseAction(v));
});
hasBinding = true;
}
if (isPresent(protoElementInjector)) {
current.inheritedProtoElementInjector = protoElementInjector;
}
@ -172,6 +178,18 @@ export function main() {
expect(DOM.getProperty(view.nodes[0], 'elprop2')).toEqual('b');
});
it('should bind events', () => {
var eventBindings = MapWrapper.createFromStringMap({
'event1': '1+1'
});
var pipeline = createPipeline({eventBindings: eventBindings});
var results = pipeline.process(createElement('<div viewroot event-binding></div>'));
var pv = results[0].inheritedProtoView;
var ast = MapWrapper.get(pv.elementBinders[0].events, 'event1');
expect(ast.eval(null)).toBe(2);
});
it('should bind directive properties', () => {
var propertyBindings = MapWrapper.createFromStringMap({
'boundprop1': 'prop1',

View File

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

View File

@ -41,6 +41,16 @@ export function main() {
createPipeline().process(createElement('<div let-a="b"></div>'))
}).toThrowError('let-* is only allowed on <template> elements!');
});
it('should detect () syntax', () => {
var results = createPipeline().process(createElement('<div (click)="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
});
it('should detect on- syntax', () => {
var results = createPipeline().process(createElement('<div on-click="b()"></div>'));
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
});
});
}