From 4845583dcfe3c0506454e0c8d0a21201a232e207 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 12 Aug 2015 16:26:21 -0700 Subject: [PATCH] refactor(change_detector): made change detection responsible for processing events Closes #3666 --- .../abstract_change_detector.ts | 4 + .../src/change_detection/binding_record.ts | 40 +- .../src/change_detection/change_detection.ts | 2 +- .../change_detection_jit_generator.ts | 44 +- .../src/change_detection/codegen_facade.dart | 4 + .../src/change_detection/codegen_facade.ts | 4 + .../change_detection/codegen_logic_util.ts | 49 +- .../src/change_detection/codegen_name_util.ts | 39 +- .../dynamic_change_detector.ts | 136 +++-- .../src/change_detection/event_binding.ts | 7 + .../src/change_detection/interfaces.ts | 6 +- .../jit_proto_change_detector.ts | 14 +- .../src/change_detection/parser/ast.ts | 298 ++--------- .../src/change_detection/parser/parser.ts | 73 +-- .../change_detection/proto_change_detector.ts | 85 ++- .../src/change_detection/proto_record.ts | 9 +- .../src/core/compiler/element_binder.ts | 4 - .../src/core/compiler/proto_view_factory.ts | 71 ++- modules/angular2/src/core/compiler/view.ts | 58 +-- .../src/render/dom/view/proto_view_builder.ts | 10 +- .../change_detector_codegen.dart | 50 +- .../change_detection/change_detection_spec.ts | 2 +- .../change_detector_config.ts | 55 +- .../change_detection/change_detector_spec.ts | 64 +++ .../test/change_detection/coalesce_spec.ts | 2 +- .../change_detection/parser/parser_spec.ts | 493 +++++------------- .../test/change_detection/parser/unparser.ts | 31 +- .../change_detection/parser/unparser_spec.ts | 132 ----- .../change_detection/proto_record_spec.ts | 2 +- .../core/compiler/element_injector_spec.ts | 4 +- .../core/compiler/proto_view_factory_spec.ts | 49 +- .../angular2/test/forms/integration_spec.ts | 1 + .../expected/bar.ng_deps.dart | 5 + .../change_detection_benchmark.ts | 4 +- 34 files changed, 837 insertions(+), 1014 deletions(-) create mode 100644 modules/angular2/src/change_detection/event_binding.ts delete mode 100644 modules/angular2/test/change_detection/parser/unparser_spec.ts diff --git a/modules/angular2/src/change_detection/abstract_change_detector.ts b/modules/angular2/src/change_detection/abstract_change_detector.ts index 77e27586d0..4e7ae6d502 100644 --- a/modules/angular2/src/change_detection/abstract_change_detector.ts +++ b/modules/angular2/src/change_detection/abstract_change_detector.ts @@ -66,6 +66,10 @@ export class AbstractChangeDetector implements ChangeDetector { remove(): void { this.parent.removeChild(this); } + handleEvent(eventName: string, elIndex: number, locals: Locals): boolean { + throw new BaseException("Not implemented"); + } + detectChanges(): void { this.runDetectChanges(false); } checkNoChanges(): void { throw new BaseException("Not implemented"); } diff --git a/modules/angular2/src/change_detection/binding_record.ts b/modules/angular2/src/change_detection/binding_record.ts index 6665d45a23..6b38039342 100644 --- a/modules/angular2/src/change_detection/binding_record.ts +++ b/modules/angular2/src/change_detection/binding_record.ts @@ -10,11 +10,13 @@ const ELEMENT_ATTRIBUTE = "elementAttribute"; const ELEMENT_CLASS = "elementClass"; const ELEMENT_STYLE = "elementStyle"; const TEXT_NODE = "textNode"; +const EVENT = "event"; +const HOST_EVENT = "hostEvent"; export class BindingRecord { constructor(public mode: string, public implicitReceiver: any, public ast: AST, public elementIndex: number, public propertyName: string, public propertyUnit: string, - public setter: SetterFn, public lifecycleEvent: string, + public eventName: string, public setter: SetterFn, public lifecycleEvent: string, public directiveRecord: DirectiveRecord) {} callOnChange(): boolean { @@ -41,73 +43,83 @@ export class BindingRecord { static createForDirective(ast: AST, propertyName: string, setter: SetterFn, directiveRecord: DirectiveRecord): BindingRecord { - return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, null, setter, null, + return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, null, null, setter, null, directiveRecord); } static createDirectiveOnCheck(directiveRecord: DirectiveRecord): BindingRecord { - return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onCheck", + return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, null, "onCheck", directiveRecord); } static createDirectiveOnInit(directiveRecord: DirectiveRecord): BindingRecord { - return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onInit", + return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, null, "onInit", directiveRecord); } static createDirectiveOnChange(directiveRecord: DirectiveRecord): BindingRecord { - return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, "onChange", + return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, null, null, "onChange", directiveRecord); } static createForElementProperty(ast: AST, elementIndex: number, propertyName: string): BindingRecord { return new BindingRecord(ELEMENT_PROPERTY, 0, ast, elementIndex, propertyName, null, null, null, - null); + null, null); } static createForElementAttribute(ast: AST, elementIndex: number, attributeName: string): BindingRecord { return new BindingRecord(ELEMENT_ATTRIBUTE, 0, ast, elementIndex, attributeName, null, null, - null, null); + null, null, null); } static createForElementClass(ast: AST, elementIndex: number, className: string): BindingRecord { - return new BindingRecord(ELEMENT_CLASS, 0, ast, elementIndex, className, null, null, null, + return new BindingRecord(ELEMENT_CLASS, 0, ast, elementIndex, className, null, null, null, null, null); } static createForElementStyle(ast: AST, elementIndex: number, styleName: string, unit: string): BindingRecord { - return new BindingRecord(ELEMENT_STYLE, 0, ast, elementIndex, styleName, unit, null, null, + return new BindingRecord(ELEMENT_STYLE, 0, ast, elementIndex, styleName, unit, null, null, null, null); } static createForHostProperty(directiveIndex: DirectiveIndex, ast: AST, propertyName: string): BindingRecord { return new BindingRecord(ELEMENT_PROPERTY, directiveIndex, ast, directiveIndex.elementIndex, - propertyName, null, null, null, null); + propertyName, null, null, null, null, null); } static createForHostAttribute(directiveIndex: DirectiveIndex, ast: AST, attributeName: string): BindingRecord { return new BindingRecord(ELEMENT_ATTRIBUTE, directiveIndex, ast, directiveIndex.elementIndex, - attributeName, null, null, null, null); + attributeName, null, null, null, null, null); } static createForHostClass(directiveIndex: DirectiveIndex, ast: AST, className: string): BindingRecord { return new BindingRecord(ELEMENT_CLASS, directiveIndex, ast, directiveIndex.elementIndex, - className, null, null, null, null); + className, null, null, null, null, null); } static createForHostStyle(directiveIndex: DirectiveIndex, ast: AST, styleName: string, unit: string): BindingRecord { return new BindingRecord(ELEMENT_STYLE, directiveIndex, ast, directiveIndex.elementIndex, - styleName, unit, null, null, null); + styleName, unit, null, null, null, null); } static createForTextNode(ast: AST, elementIndex: number): BindingRecord { - return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null, null, null); + return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null, null, null, null); + } + + static createForEvent(ast: AST, eventName: string, elementIndex: number): BindingRecord { + return new BindingRecord(EVENT, 0, ast, elementIndex, null, null, eventName, null, null, null); + } + + static createForHostEvent(ast: AST, eventName: string, + directiveIndex: DirectiveIndex): BindingRecord { + return new BindingRecord(EVENT, directiveIndex, ast, directiveIndex.elementIndex, null, null, + eventName, null, null, null); } } diff --git a/modules/angular2/src/change_detection/change_detection.ts b/modules/angular2/src/change_detection/change_detection.ts index 01345873fe..e189844ba0 100644 --- a/modules/angular2/src/change_detection/change_detection.ts +++ b/modules/angular2/src/change_detection/change_detection.ts @@ -14,7 +14,7 @@ export { ASTWithSource, AST, AstTransformer, - AccessMember, + PropertyRead, LiteralArray, ImplicitReceiver } from './parser/ast'; diff --git a/modules/angular2/src/change_detection/change_detection_jit_generator.ts b/modules/angular2/src/change_detection/change_detection_jit_generator.ts index d2ce2b8d1e..ebe76dd9c7 100644 --- a/modules/angular2/src/change_detection/change_detection_jit_generator.ts +++ b/modules/angular2/src/change_detection/change_detection_jit_generator.ts @@ -8,6 +8,7 @@ import {DirectiveIndex, DirectiveRecord} from './directive_record'; import {ProtoRecord, RecordType} from './proto_record'; import {CodegenNameUtil, sanitizeName} from './codegen_name_util'; import {CodegenLogicUtil} from './codegen_logic_util'; +import {EventBinding} from './event_binding'; /** @@ -30,9 +31,10 @@ export class ChangeDetectorJITGenerator { _typeName: string; constructor(public id: string, private changeDetectionStrategy: string, - public records: List, public directiveRecords: List, - private generateCheckNoChanges: boolean) { - this._names = new CodegenNameUtil(this.records, this.directiveRecords, UTIL); + public records: List, public eventBindings: EventBinding[], + public directiveRecords: List, private generateCheckNoChanges: boolean) { + this._names = + new CodegenNameUtil(this.records, this.eventBindings, this.directiveRecords, UTIL); this._logic = new CodegenLogicUtil(this._names, UTIL); this._typeName = sanitizeName(`ChangeDetector_${this.id}`); } @@ -48,6 +50,13 @@ export class ChangeDetectorJITGenerator { ${this._typeName}.prototype = Object.create(${ABSTRACT_CHANGE_DETECTOR}.prototype); + ${this._typeName}.prototype.handleEvent = function(eventName, elIndex, locals) { + var ${this._names.getPreventDefaultAccesor()} = false; + ${this._names.genInitEventLocals()} + ${this._genHandleEvent()} + return ${this._names.getPreventDefaultAccesor()}; + } + ${this._typeName}.prototype.detectChangesInRecordsInternal = function(throwOnChange) { ${this._names.genInitLocals()} var ${IS_CHANGED_LOCAL} = false; @@ -75,6 +84,33 @@ export class ChangeDetectorJITGenerator { AbstractChangeDetector, ChangeDetectionUtil, this.records, this.directiveRecords); } + _genHandleEvent(): string { + return this.eventBindings.map(eb => this._genEventBinding(eb)).join("\n"); + } + + _genEventBinding(eb: EventBinding): string { + var recs = eb.records.map(r => this._genEventBindingEval(eb, r)).join("\n"); + return ` + if (eventName === "${eb.eventName}" && elIndex === ${eb.elIndex}) { + ${recs} + }`; + } + + _genEventBindingEval(eb: EventBinding, r: ProtoRecord): string { + if (r.lastInBinding) { + var evalRecord = this._logic.genEventBindingEvalValue(eb, r); + var prevDefault = this._genUpdatePreventDefault(eb, r); + return `${evalRecord}\n${prevDefault}`; + } else { + return this._logic.genEventBindingEvalValue(eb, r); + } + } + + _genUpdatePreventDefault(eb: EventBinding, r: ProtoRecord): string { + var local = this._names.getEventLocalName(eb, r.selfIndex); + return `if (${local} === false) { ${this._names.getPreventDefaultAccesor()} = true};`; + } + _maybeGenDehydrateDirectives(): string { var destroyPipesCode = this._names.genPipeOnDestroy(); if (destroyPipesCode) { @@ -204,7 +240,7 @@ export class ChangeDetectorJITGenerator { var oldValue = this._names.getFieldName(r.selfIndex); var newValue = this._names.getLocalName(r.selfIndex); var read = ` - ${this._logic.genUpdateCurrentValue(r)} + ${this._logic.genPropertyBindingEvalValue(r)} `; var check = ` diff --git a/modules/angular2/src/change_detection/codegen_facade.dart b/modules/angular2/src/change_detection/codegen_facade.dart index 0c3e9275b9..347c32fab1 100644 --- a/modules/angular2/src/change_detection/codegen_facade.dart +++ b/modules/angular2/src/change_detection/codegen_facade.dart @@ -13,3 +13,7 @@ String codify(funcOrValue) => JSON.encode(funcOrValue).replaceAll(r'$', r'\$'); String combineGeneratedStrings(List vals) { return '"${vals.map((v) => '\${$v}').join('')}"'; } + +String rawString(String str) { + return "r'$str'"; +} \ No newline at end of file diff --git a/modules/angular2/src/change_detection/codegen_facade.ts b/modules/angular2/src/change_detection/codegen_facade.ts index 8064f7616e..5f50350efe 100644 --- a/modules/angular2/src/change_detection/codegen_facade.ts +++ b/modules/angular2/src/change_detection/codegen_facade.ts @@ -7,6 +7,10 @@ export function codify(obj: any): string { return JSON.stringify(obj); } +export function rawString(str: string): string { + return `'${str}'`; +} + /** * Combine the strings of generated code into a single interpolated string. * Each element of `vals` is expected to be a string literal or a codegen'd diff --git a/modules/angular2/src/change_detection/codegen_logic_util.ts b/modules/angular2/src/change_detection/codegen_logic_util.ts index c2d4431d4c..57f7b0dd16 100644 --- a/modules/angular2/src/change_detection/codegen_logic_util.ts +++ b/modules/angular2/src/change_detection/codegen_logic_util.ts @@ -1,7 +1,7 @@ import {ListWrapper} from 'angular2/src/facade/collection'; import {BaseException, Json} from 'angular2/src/facade/lang'; import {CodegenNameUtil} from './codegen_name_util'; -import {codify, combineGeneratedStrings} from './codegen_facade'; +import {codify, combineGeneratedStrings, rawString} from './codegen_facade'; import {ProtoRecord, RecordType} from './proto_record'; /** @@ -12,14 +12,28 @@ export class CodegenLogicUtil { /** * Generates a statement which updates the local variable representing `protoRec` with the current - * value of the record. + * value of the record. Used by property bindings. */ - genUpdateCurrentValue(protoRec: ProtoRecord): string { + genPropertyBindingEvalValue(protoRec: ProtoRecord): string { + return this.genEvalValue(protoRec, idx => this._names.getLocalName(idx), + this._names.getLocalsAccessorName()); + } + + /** + * Generates a statement which updates the local variable representing `protoRec` with the current + * value of the record. Used by event bindings. + */ + genEventBindingEvalValue(eventRecord: any, protoRec: ProtoRecord): string { + return this.genEvalValue(protoRec, idx => this._names.getEventLocalName(eventRecord, idx), + "locals"); + } + + private genEvalValue(protoRec: ProtoRecord, getLocalName: Function, + localsAccessor: string): string { var context = (protoRec.contextIndex == -1) ? this._names.getDirectiveName(protoRec.directiveIndex) : - this._names.getLocalName(protoRec.contextIndex); - var argString = - ListWrapper.map(protoRec.args, (arg) => this._names.getLocalName(arg)).join(", "); + getLocalName(protoRec.contextIndex); + var argString = ListWrapper.map(protoRec.args, (arg) => getLocalName(arg)).join(", "); var rhs: string; switch (protoRec.mode) { @@ -31,7 +45,7 @@ export class CodegenLogicUtil { rhs = codify(protoRec.funcOrValue); break; - case RecordType.PROPERTY: + case RecordType.PROPERTY_READ: rhs = `${context}.${protoRec.name}`; break; @@ -39,8 +53,12 @@ export class CodegenLogicUtil { rhs = `${this._utilName}.isValueBlank(${context}) ? null : ${context}.${protoRec.name}`; break; + case RecordType.PROPERTY_WRITE: + rhs = `${context}.${protoRec.name} = ${getLocalName(protoRec.args[0])}`; + break; + case RecordType.LOCAL: - rhs = `${this._names.getLocalsAccessorName()}.get('${protoRec.name}')`; + rhs = `${localsAccessor}.get(${rawString(protoRec.name)})`; break; case RecordType.INVOKE_METHOD: @@ -68,16 +86,25 @@ export class CodegenLogicUtil { rhs = this._genInterpolation(protoRec); break; - case RecordType.KEYED_ACCESS: - rhs = `${context}[${this._names.getLocalName(protoRec.args[0])}]`; + case RecordType.KEYED_READ: + rhs = `${context}[${getLocalName(protoRec.args[0])}]`; + break; + + case RecordType.KEYED_WRITE: + rhs = `${context}[${getLocalName(protoRec.args[0])}] = ${getLocalName(protoRec.args[1])}`; + break; + + case RecordType.CHAIN: + rhs = 'null'; break; default: throw new BaseException(`Unknown operation ${protoRec.mode}`); } - return `${this._names.getLocalName(protoRec.selfIndex)} = ${rhs};`; + return `${getLocalName(protoRec.selfIndex)} = ${rhs};`; } + _genInterpolation(protoRec: ProtoRecord): string { var iVals = []; for (var i = 0; i < protoRec.args.length; ++i) { diff --git a/modules/angular2/src/change_detection/codegen_name_util.ts b/modules/angular2/src/change_detection/codegen_name_util.ts index aba7ad6573..c3f9bc4a8d 100644 --- a/modules/angular2/src/change_detection/codegen_name_util.ts +++ b/modules/angular2/src/change_detection/codegen_name_util.ts @@ -1,9 +1,10 @@ import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang'; -import {List, ListWrapper} from 'angular2/src/facade/collection'; +import {List, ListWrapper, MapWrapper, Map} from 'angular2/src/facade/collection'; import {DirectiveIndex} from './directive_record'; import {ProtoRecord} from './proto_record'; +import {EventBinding} from './event_binding'; // The names of these fields must be kept in sync with abstract_change_detector.ts or change // detection will fail. @@ -41,14 +42,25 @@ export class CodegenNameUtil { * See [sanitizeName] for details. */ _sanitizedNames: List; + _sanitizedEventNames: Map>; - constructor(private records: List, private directiveRecords: List, - private utilName: string) { + constructor(private records: List, private eventBindings: EventBinding[], + private directiveRecords: List, private utilName: string) { this._sanitizedNames = ListWrapper.createFixedSize(this.records.length + 1); this._sanitizedNames[CONTEXT_INDEX] = _CONTEXT_ACCESSOR; for (var i = 0, iLen = this.records.length; i < iLen; ++i) { this._sanitizedNames[i + 1] = sanitizeName(`${this.records[i].name}${i}`); } + + this._sanitizedEventNames = new Map(); + for (var ebIndex = 0; ebIndex < eventBindings.length; ++ebIndex) { + var eb = eventBindings[ebIndex]; + var names = [_CONTEXT_ACCESSOR]; + for (var i = 0, iLen = eb.records.length; i < iLen; ++i) { + names.push(sanitizeName(`${eb.records[i].name}${i}_${ebIndex}`)); + } + this._sanitizedEventNames.set(eb, names); + } } _addFieldPrefix(name: string): string { return `${_FIELD_PREFIX}${name}`; } @@ -73,6 +85,10 @@ export class CodegenNameUtil { getLocalName(idx: int): string { return `l_${this._sanitizedNames[idx]}`; } + getEventLocalName(eb: EventBinding, idx: int): string { + return `l_${MapWrapper.get(this._sanitizedEventNames, eb)[idx]}`; + } + getChangeName(idx: int): string { return `c_${this._sanitizedNames[idx]}`; } /** @@ -100,6 +116,23 @@ export class CodegenNameUtil { return `var ${ListWrapper.join(declarations, ',')};${assignmentsCode}`; } + /** + * Generate a statement initializing local variables for event handlers. + */ + genInitEventLocals(): string { + var res = [`${this.getLocalName(CONTEXT_INDEX)} = ${this.getFieldName(CONTEXT_INDEX)}`]; + MapWrapper.forEach(this._sanitizedEventNames, (names, eb) => { + for (var i = 0; i < names.length; ++i) { + if (i !== CONTEXT_INDEX) { + res.push(this.getEventLocalName(eb, i)); + } + } + }); + return res.length > 1 ? `var ${res.join(',')};` : ''; + } + + getPreventDefaultAccesor(): string { return "preventDefault"; } + getFieldCount(): int { return this._sanitizedNames.length; } getFieldName(idx: int): string { return this._addFieldPrefix(this._sanitizedNames[idx]); } diff --git a/modules/angular2/src/change_detection/dynamic_change_detector.ts b/modules/angular2/src/change_detection/dynamic_change_detector.ts index 597bfd1d12..8c77f261b0 100644 --- a/modules/angular2/src/change_detection/dynamic_change_detector.ts +++ b/modules/angular2/src/change_detection/dynamic_change_detector.ts @@ -2,7 +2,9 @@ import {isPresent, isBlank, BaseException, FunctionWrapper} from 'angular2/src/f import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {AbstractChangeDetector} from './abstract_change_detector'; +import {EventBinding} from './event_binding'; import {BindingRecord} from './binding_record'; +import {Locals} from './parser/locals'; import {ChangeDetectionUtil, SimpleChange} from './change_detection_util'; @@ -16,7 +18,8 @@ export class DynamicChangeDetector extends AbstractChangeDetector { directives: any = null; constructor(id: string, changeDetectionStrategy: string, dispatcher: any, - protos: List, directiveRecords: List) { + protos: List, public eventBindings: EventBinding[], + directiveRecords: List) { super(id, dispatcher, protos, directiveRecords, ChangeDetectionUtil.changeDetectionMode(changeDetectionStrategy)); var len = protos.length + 1; @@ -28,6 +31,41 @@ export class DynamicChangeDetector extends AbstractChangeDetector { this.dehydrateDirectives(false); } + handleEvent(eventName: string, elIndex: number, locals: Locals): boolean { + var preventDefault = false; + + this._matchingEventBindings(eventName, elIndex) + .forEach(rec => { + var res = this._processEventBinding(rec, locals); + if (res === false) { + preventDefault = true; + } + }); + return preventDefault; + } + + _processEventBinding(eb: EventBinding, locals: Locals): any { + var values = ListWrapper.createFixedSize(eb.records.length); + values[0] = this.values[0]; + + for (var i = 0; i < eb.records.length; ++i) { + var proto = eb.records[i]; + var res = this._calculateCurrValue(proto, values, locals); + if (proto.lastInBinding) { + return res; + } else { + this._writeSelf(proto, res, values); + } + } + + throw new BaseException("Cannot be reached"); + } + + _matchingEventBindings(eventName: string, elIndex: number): EventBinding[] { + return ListWrapper.filter(this.eventBindings, + eb => eb.eventName == eventName && eb.elIndex === elIndex); + } + hydrateDirectives(directives: any): void { this.values[0] = this.context; this.directives = directives; @@ -79,7 +117,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector { } } else { - var change = this._check(proto, throwOnChange); + var change = this._check(proto, throwOnChange, this.values, this.locals); if (isPresent(change)) { this._updateDirectiveOrElement(change, bindingRecord); isChanged = true; @@ -137,33 +175,33 @@ export class DynamicChangeDetector extends AbstractChangeDetector { _getDetectorFor(directiveIndex) { return this.directives.getDetectorFor(directiveIndex); } - _check(proto: ProtoRecord, throwOnChange: boolean): SimpleChange { + _check(proto: ProtoRecord, throwOnChange: boolean, values: any[], locals: Locals): SimpleChange { if (proto.isPipeRecord()) { - return this._pipeCheck(proto, throwOnChange); + return this._pipeCheck(proto, throwOnChange, values); } else { - return this._referenceCheck(proto, throwOnChange); + return this._referenceCheck(proto, throwOnChange, values, locals); } } - _referenceCheck(proto: ProtoRecord, throwOnChange: boolean) { + _referenceCheck(proto: ProtoRecord, throwOnChange: boolean, values: any[], locals: Locals) { if (this._pureFuncAndArgsDidNotChange(proto)) { this._setChanged(proto, false); return null; } - var currValue = this._calculateCurrValue(proto); + var currValue = this._calculateCurrValue(proto, values, locals); if (proto.shouldBeChecked()) { - var prevValue = this._readSelf(proto); + var prevValue = this._readSelf(proto, values); if (!isSame(prevValue, currValue)) { if (proto.lastInBinding) { var change = ChangeDetectionUtil.simpleChange(prevValue, currValue); if (throwOnChange) this.throwOnChangeError(prevValue, currValue); - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return change; } else { - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return null; } @@ -173,70 +211,88 @@ export class DynamicChangeDetector extends AbstractChangeDetector { } } else { - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return null; } } - _calculateCurrValue(proto: ProtoRecord) { + _calculateCurrValue(proto: ProtoRecord, values: any[], locals: Locals) { switch (proto.mode) { case RecordType.SELF: - return this._readContext(proto); + return this._readContext(proto, values); case RecordType.CONST: return proto.funcOrValue; - case RecordType.PROPERTY: - var context = this._readContext(proto); + case RecordType.PROPERTY_READ: + var context = this._readContext(proto, values); return proto.funcOrValue(context); case RecordType.SAFE_PROPERTY: - var context = this._readContext(proto); + var context = this._readContext(proto, values); return isBlank(context) ? null : proto.funcOrValue(context); + case RecordType.PROPERTY_WRITE: + var context = this._readContext(proto, values); + var value = this._readArgs(proto, values)[0]; + proto.funcOrValue(context, value); + return value; + + case RecordType.KEYED_WRITE: + var context = this._readContext(proto, values); + var key = this._readArgs(proto, values)[0]; + var value = this._readArgs(proto, values)[1]; + context[key] = value; + return value; + case RecordType.LOCAL: - return this.locals.get(proto.name); + return locals.get(proto.name); case RecordType.INVOKE_METHOD: - var context = this._readContext(proto); - var args = this._readArgs(proto); + var context = this._readContext(proto, values); + var args = this._readArgs(proto, values); return proto.funcOrValue(context, args); case RecordType.SAFE_INVOKE_METHOD: - var context = this._readContext(proto); + var context = this._readContext(proto, values); if (isBlank(context)) { return null; } - var args = this._readArgs(proto); + var args = this._readArgs(proto, values); return proto.funcOrValue(context, args); - case RecordType.KEYED_ACCESS: - var arg = this._readArgs(proto)[0]; - return this._readContext(proto)[arg]; + case RecordType.KEYED_READ: + var arg = this._readArgs(proto, values)[0]; + return this._readContext(proto, values)[arg]; + + case RecordType.CHAIN: + var args = this._readArgs(proto, values); + return args[args.length - 1]; case RecordType.INVOKE_CLOSURE: - return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto)); + return FunctionWrapper.apply(this._readContext(proto, values), + this._readArgs(proto, values)); case RecordType.INTERPOLATE: case RecordType.PRIMITIVE_OP: case RecordType.COLLECTION_LITERAL: - return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto)); + return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto, values)); default: throw new BaseException(`Unknown operation ${proto.mode}`); } } - _pipeCheck(proto: ProtoRecord, throwOnChange: boolean) { - var context = this._readContext(proto); - var args = this._readArgs(proto); + _pipeCheck(proto: ProtoRecord, throwOnChange: boolean, values: any[]) { + var context = this._readContext(proto, values); + var args = this._readArgs(proto, values); var pipe = this._pipeFor(proto, context); var currValue = pipe.transform(context, args); if (proto.shouldBeChecked()) { - var prevValue = this._readSelf(proto); + var prevValue = this._readSelf(proto, values); if (!isSame(prevValue, currValue)) { currValue = ChangeDetectionUtil.unwrapValue(currValue); @@ -244,13 +300,13 @@ export class DynamicChangeDetector extends AbstractChangeDetector { var change = ChangeDetectionUtil.simpleChange(prevValue, currValue); if (throwOnChange) this.throwOnChangeError(prevValue, currValue); - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return change; } else { - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return null; } @@ -259,7 +315,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector { return null; } } else { - this._writeSelf(proto, currValue); + this._writeSelf(proto, currValue, values); this._setChanged(proto, true); return null; } @@ -274,19 +330,19 @@ export class DynamicChangeDetector extends AbstractChangeDetector { return pipe; } - _readContext(proto: ProtoRecord) { + _readContext(proto: ProtoRecord, values: any[]) { if (proto.contextIndex == -1) { return this._getDirectiveFor(proto.directiveIndex); } else { - return this.values[proto.contextIndex]; + return values[proto.contextIndex]; } - return this.values[proto.contextIndex]; + return values[proto.contextIndex]; } - _readSelf(proto: ProtoRecord) { return this.values[proto.selfIndex]; } + _readSelf(proto: ProtoRecord, values: any[]) { return values[proto.selfIndex]; } - _writeSelf(proto: ProtoRecord, value) { this.values[proto.selfIndex] = value; } + _writeSelf(proto: ProtoRecord, value, values: any[]) { values[proto.selfIndex] = value; } _readPipe(proto: ProtoRecord) { return this.localPipes[proto.selfIndex]; } @@ -310,11 +366,11 @@ export class DynamicChangeDetector extends AbstractChangeDetector { return false; } - _readArgs(proto: ProtoRecord) { + _readArgs(proto: ProtoRecord, values: any[]) { var res = ListWrapper.createFixedSize(proto.args.length); var args = proto.args; for (var i = 0; i < args.length; ++i) { - res[i] = this.values[args[i]]; + res[i] = values[args[i]]; } return res; } diff --git a/modules/angular2/src/change_detection/event_binding.ts b/modules/angular2/src/change_detection/event_binding.ts new file mode 100644 index 0000000000..52682ced2d --- /dev/null +++ b/modules/angular2/src/change_detection/event_binding.ts @@ -0,0 +1,7 @@ +import {DirectiveIndex} from './directive_record'; +import {ProtoRecord} from './proto_record'; + +export class EventBinding { + constructor(public eventName: string, public elIndex: number, public dirIndex: DirectiveIndex, + public records: ProtoRecord[]) {} +} \ No newline at end of file diff --git a/modules/angular2/src/change_detection/interfaces.ts b/modules/angular2/src/change_detection/interfaces.ts index 5068d4609b..b5d45ebf3e 100644 --- a/modules/angular2/src/change_detection/interfaces.ts +++ b/modules/angular2/src/change_detection/interfaces.ts @@ -62,6 +62,7 @@ export interface ChangeDetector { dehydrate(): void; markPathToRootAsCheckOnce(): void; + handleEvent(eventName: string, elIndex: number, locals: Locals); detectChanges(): void; checkNoChanges(): void; } @@ -70,7 +71,6 @@ export interface ProtoChangeDetector { instantiate(dispatcher: ChangeDispatcher) export class ChangeDetectorDefinition { constructor(public id: string, public strategy: string, public variableNames: List, - public bindingRecords: List, - public directiveRecords: List, - public generateCheckNoChanges: boolean) {} + public bindingRecords: BindingRecord[], public eventRecords: BindingRecord[], + public directiveRecords: DirectiveRecord[], public generateCheckNoChanges: boolean) {} } diff --git a/modules/angular2/src/change_detection/jit_proto_change_detector.ts b/modules/angular2/src/change_detection/jit_proto_change_detector.ts index 88b93bc4a2..df009b5cc4 100644 --- a/modules/angular2/src/change_detection/jit_proto_change_detector.ts +++ b/modules/angular2/src/change_detection/jit_proto_change_detector.ts @@ -4,7 +4,7 @@ import {ProtoChangeDetector, ChangeDetector, ChangeDetectorDefinition} from './i import {ChangeDetectorJITGenerator} from './change_detection_jit_generator'; import {coalesce} from './coalesce'; -import {ProtoRecordBuilder} from './proto_change_detector'; +import {createPropertyRecords, createEventRecords} from './proto_change_detector'; export class JitProtoChangeDetector implements ProtoChangeDetector { _factory: Function; @@ -18,13 +18,11 @@ export class JitProtoChangeDetector implements ProtoChangeDetector { instantiate(dispatcher: any): ChangeDetector { return this._factory(dispatcher); } _createFactory(definition: ChangeDetectorDefinition) { - var recordBuilder = new ProtoRecordBuilder(); - ListWrapper.forEach(definition.bindingRecords, - (b) => { recordBuilder.add(b, definition.variableNames); }); - var records = coalesce(recordBuilder.records); - return new ChangeDetectorJITGenerator(definition.id, definition.strategy, records, - this.definition.directiveRecords, - this.definition.generateCheckNoChanges) + var propertyBindingRecords = createPropertyRecords(definition); + var eventBindingRecords = createEventRecords(definition); + return new ChangeDetectorJITGenerator( + definition.id, definition.strategy, propertyBindingRecords, eventBindingRecords, + this.definition.directiveRecords, this.definition.generateCheckNoChanges) .generate(); } } diff --git a/modules/angular2/src/change_detection/parser/ast.ts b/modules/angular2/src/change_detection/parser/ast.ts index 0cfee7cdd3..001b9c7081 100644 --- a/modules/angular2/src/change_detection/parser/ast.ts +++ b/modules/angular2/src/change_detection/parser/ast.ts @@ -1,30 +1,18 @@ import {isBlank, isPresent, FunctionWrapper, BaseException} from "angular2/src/facade/lang"; import {List, Map, ListWrapper, StringMapWrapper} from "angular2/src/facade/collection"; -import {Locals} from "./locals"; export class AST { - eval(context: any, locals: Locals): any { throw new BaseException("Not supported"); } - - get isAssignable(): boolean { return false; } - - assign(context: any, locals: Locals, value: any) { throw new BaseException("Not supported"); } - visit(visitor: AstVisitor): any { return null; } - toString(): string { return "AST"; } } export class EmptyExpr extends AST { - eval(context: any, locals: Locals): any { return null; } - visit(visitor: AstVisitor) { // do nothing } } export class ImplicitReceiver extends AST { - eval(context: any, locals: Locals): any { return context; } - visit(visitor: AstVisitor): any { return visitor.visitImplicitReceiver(this); } } @@ -33,112 +21,45 @@ export class ImplicitReceiver extends AST { */ export class Chain extends AST { constructor(public expressions: List) { super(); } - - eval(context: any, locals: Locals): any { - var result; - for (var i = 0; i < this.expressions.length; i++) { - var last = this.expressions[i].eval(context, locals); - if (isPresent(last)) result = last; - } - return result; - } - visit(visitor: AstVisitor): any { return visitor.visitChain(this); } } export class Conditional extends AST { constructor(public condition: AST, public trueExp: AST, public falseExp: AST) { super(); } - - eval(context: any, locals: Locals): any { - if (this.condition.eval(context, locals)) { - return this.trueExp.eval(context, locals); - } else { - return this.falseExp.eval(context, locals); - } - } - visit(visitor: AstVisitor): any { return visitor.visitConditional(this); } } export class If extends AST { constructor(public condition: AST, public trueExp: AST, public falseExp?: AST) { super(); } - - eval(context: any, locals: Locals) { - if (this.condition.eval(context, locals)) { - this.trueExp.eval(context, locals); - } else if (isPresent(this.falseExp)) { - this.falseExp.eval(context, locals); - } - } - visit(visitor: AstVisitor): any { return visitor.visitIf(this); } } -export class AccessMember extends AST { - constructor(public receiver: AST, public name: string, public getter: Function, - public setter: Function) { - super(); - } - - eval(context: any, locals: Locals): any { - if (this.receiver instanceof ImplicitReceiver && isPresent(locals) && - locals.contains(this.name)) { - return locals.get(this.name); - } else { - var evaluatedReceiver = this.receiver.eval(context, locals); - return this.getter(evaluatedReceiver); - } - } - - get isAssignable(): boolean { return true; } - - assign(context: any, locals: Locals, value: any): any { - var evaluatedContext = this.receiver.eval(context, locals); - - if (this.receiver instanceof ImplicitReceiver && isPresent(locals) && - locals.contains(this.name)) { - throw new BaseException(`Cannot reassign a variable binding ${this.name}`); - } else { - return this.setter(evaluatedContext, value); - } - } - - visit(visitor: AstVisitor): any { return visitor.visitAccessMember(this); } +export class PropertyRead extends AST { + constructor(public receiver: AST, public name: string, public getter: Function) { super(); } + visit(visitor: AstVisitor): any { return visitor.visitPropertyRead(this); } } -export class SafeAccessMember extends AST { - constructor(public receiver: AST, public name: string, public getter: Function, - public setter: Function) { +export class PropertyWrite extends AST { + constructor(public receiver: AST, public name: string, public setter: Function, + public value: AST) { super(); } - - eval(context: any, locals: Locals): any { - var evaluatedReceiver = this.receiver.eval(context, locals); - return isBlank(evaluatedReceiver) ? null : this.getter(evaluatedReceiver); - } - - visit(visitor: AstVisitor): any { return visitor.visitSafeAccessMember(this); } + visit(visitor: AstVisitor): any { return visitor.visitPropertyWrite(this); } } -export class KeyedAccess extends AST { +export class SafePropertyRead extends AST { + constructor(public receiver: AST, public name: string, public getter: Function) { super(); } + visit(visitor: AstVisitor): any { return visitor.visitSafePropertyRead(this); } +} + +export class KeyedRead extends AST { constructor(public obj: AST, public key: AST) { super(); } + visit(visitor: AstVisitor): any { return visitor.visitKeyedRead(this); } +} - eval(context: any, locals: Locals): any { - var obj: any = this.obj.eval(context, locals); - var key: any = this.key.eval(context, locals); - return obj[key]; - } - - get isAssignable(): boolean { return true; } - - assign(context: any, locals: Locals, value: any): any { - var obj: any = this.obj.eval(context, locals); - var key: any = this.key.eval(context, locals); - obj[key] = value; - return value; - } - - visit(visitor: AstVisitor): any { return visitor.visitKeyedAccess(this); } +export class KeyedWrite extends AST { + constructor(public obj: AST, public key: AST, public value: AST) { super(); } + visit(visitor: AstVisitor): any { return visitor.visitKeyedWrite(this); } } export class BindingPipe extends AST { @@ -149,133 +70,39 @@ export class BindingPipe extends AST { export class LiteralPrimitive extends AST { constructor(public value) { super(); } - - eval(context: any, locals: Locals): any { return this.value; } - visit(visitor: AstVisitor): any { return visitor.visitLiteralPrimitive(this); } } export class LiteralArray extends AST { constructor(public expressions: List) { super(); } - - eval(context: any, locals: Locals): any { - return ListWrapper.map(this.expressions, (e) => e.eval(context, locals)); - } - visit(visitor: AstVisitor): any { return visitor.visitLiteralArray(this); } } export class LiteralMap extends AST { constructor(public keys: List, public values: List) { super(); } - - eval(context: any, locals: Locals): any { - var res = StringMapWrapper.create(); - for (var i = 0; i < this.keys.length; ++i) { - StringMapWrapper.set(res, this.keys[i], this.values[i].eval(context, locals)); - } - return res; - } - visit(visitor: AstVisitor): any { return visitor.visitLiteralMap(this); } } export class Interpolation extends AST { constructor(public strings: List, public expressions: List) { super(); } - - eval(context: any, locals: Locals): any { - throw new BaseException("evaluating an Interpolation is not supported"); - } - visit(visitor: AstVisitor) { visitor.visitInterpolation(this); } } export class Binary extends AST { constructor(public operation: string, public left: AST, public right: AST) { super(); } - - eval(context: any, locals: Locals): any { - var left: any = this.left.eval(context, locals); - switch (this.operation) { - case '&&': - return left && this.right.eval(context, locals); - case '||': - return left || this.right.eval(context, locals); - } - var right: any = this.right.eval(context, locals); - - switch (this.operation) { - case '+': - return left + right; - case '-': - return left - right; - case '*': - return left * right; - case '/': - return left / right; - case '%': - return left % right; - case '==': - return left == right; - case '!=': - return left != right; - case '===': - return left === right; - case '!==': - return left !== right; - case '<': - return left < right; - case '>': - return left > right; - case '<=': - return left <= right; - case '>=': - return left >= right; - case '^': - return left ^ right; - case '&': - return left & right; - } - throw 'Internal error [$operation] not handled'; - } - visit(visitor: AstVisitor): any { return visitor.visitBinary(this); } } export class PrefixNot extends AST { constructor(public expression: AST) { super(); } - - eval(context: any, locals: Locals): any { return !this.expression.eval(context, locals); } - visit(visitor: AstVisitor): any { return visitor.visitPrefixNot(this); } } -export class Assignment extends AST { - constructor(public target: AST, public value: any) { super(); } - - eval(context: any, locals: Locals): any { - return this.target.assign(context, locals, this.value.eval(context, locals)); - } - - visit(visitor: AstVisitor): any { return visitor.visitAssignment(this); } -} - export class MethodCall extends AST { constructor(public receiver: AST, public name: string, public fn: Function, public args: List) { super(); } - - eval(context: any, locals: Locals): any { - var evaluatedArgs = evalList(context, locals, this.args); - if (this.receiver instanceof ImplicitReceiver && isPresent(locals) && - locals.contains(this.name)) { - var fn = locals.get(this.name); - return FunctionWrapper.apply(fn, evaluatedArgs); - } else { - var evaluatedReceiver = this.receiver.eval(context, locals); - return this.fn(evaluatedReceiver, evaluatedArgs); - } - } - visit(visitor: AstVisitor): any { return visitor.visitMethodCall(this); } } @@ -284,44 +111,17 @@ export class SafeMethodCall extends AST { public args: List) { super(); } - - eval(context: any, locals: Locals): any { - var evaluatedReceiver = this.receiver.eval(context, locals); - if (isBlank(evaluatedReceiver)) return null; - var evaluatedArgs = evalList(context, locals, this.args); - return this.fn(evaluatedReceiver, evaluatedArgs); - } - visit(visitor: AstVisitor): any { return visitor.visitSafeMethodCall(this); } } export class FunctionCall extends AST { constructor(public target: AST, public args: List) { super(); } - - eval(context: any, locals: Locals): any { - var obj: any = this.target.eval(context, locals); - if (!(obj instanceof Function)) { - throw new BaseException(`${obj} is not a function`); - } - return FunctionWrapper.apply(obj, evalList(context, locals, this.args)); - } - visit(visitor: AstVisitor): any { return visitor.visitFunctionCall(this); } } export class ASTWithSource extends AST { constructor(public ast: AST, public source: string, public location: string) { super(); } - - eval(context: any, locals: Locals): any { return this.ast.eval(context, locals); } - - get isAssignable(): boolean { return this.ast.isAssignable; } - - assign(context: any, locals: Locals, value: any): any { - return this.ast.assign(context, locals, value); - } - visit(visitor: AstVisitor): any { return this.ast.visit(visitor); } - toString(): string { return `${this.source} in ${this.location}`; } } @@ -331,8 +131,8 @@ export class TemplateBinding { } export interface AstVisitor { - visitAccessMember(ast: AccessMember): any; - visitAssignment(ast: Assignment): any; + visitPropertyRead(ast: PropertyRead): any; + visitPropertyWrite(ast: PropertyWrite): any; visitBinary(ast: Binary): any; visitChain(ast: Chain): any; visitConditional(ast: Conditional): any; @@ -341,13 +141,14 @@ export interface AstVisitor { visitFunctionCall(ast: FunctionCall): any; visitImplicitReceiver(ast: ImplicitReceiver): any; visitInterpolation(ast: Interpolation): any; - visitKeyedAccess(ast: KeyedAccess): any; + visitKeyedRead(ast: KeyedRead): any; + visitKeyedWrite(ast: KeyedWrite): any; visitLiteralArray(ast: LiteralArray): any; visitLiteralMap(ast: LiteralMap): any; visitLiteralPrimitive(ast: LiteralPrimitive): any; visitMethodCall(ast: MethodCall): any; visitPrefixNot(ast: PrefixNot): any; - visitSafeAccessMember(ast: SafeAccessMember): any; + visitSafePropertyRead(ast: SafePropertyRead): any; visitSafeMethodCall(ast: SafeMethodCall): any; } @@ -362,12 +163,16 @@ export class AstTransformer implements AstVisitor { return new LiteralPrimitive(ast.value); } - visitAccessMember(ast: AccessMember): AccessMember { - return new AccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter); + visitPropertyRead(ast: PropertyRead): PropertyRead { + return new PropertyRead(ast.receiver.visit(this), ast.name, ast.getter); } - visitSafeAccessMember(ast: SafeAccessMember): SafeAccessMember { - return new SafeAccessMember(ast.receiver.visit(this), ast.name, ast.getter, ast.setter); + visitPropertyWrite(ast: PropertyWrite): PropertyWrite { + return new PropertyWrite(ast.receiver.visit(this), ast.name, ast.setter, ast.value); + } + + visitSafePropertyRead(ast: SafePropertyRead): SafePropertyRead { + return new SafePropertyRead(ast.receiver.visit(this), ast.name, ast.getter); } visitMethodCall(ast: MethodCall): MethodCall { @@ -405,8 +210,12 @@ export class AstTransformer implements AstVisitor { return new BindingPipe(ast.exp.visit(this), ast.name, this.visitAll(ast.args)); } - visitKeyedAccess(ast: KeyedAccess): KeyedAccess { - return new KeyedAccess(ast.obj.visit(this), ast.key.visit(this)); + visitKeyedRead(ast: KeyedRead): KeyedRead { + return new KeyedRead(ast.obj.visit(this), ast.key.visit(this)); + } + + visitKeyedWrite(ast: KeyedWrite): KeyedWrite { + return new KeyedWrite(ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this)); } visitAll(asts: List): List { @@ -419,39 +228,8 @@ export class AstTransformer implements AstVisitor { visitChain(ast: Chain): Chain { return new Chain(this.visitAll(ast.expressions)); } - visitAssignment(ast: Assignment): Assignment { - return new Assignment(ast.target.visit(this), ast.value.visit(this)); - } - visitIf(ast: If): If { let falseExp = isPresent(ast.falseExp) ? ast.falseExp.visit(this) : null; return new If(ast.condition.visit(this), ast.trueExp.visit(this), falseExp); } -} - -var _evalListCache = [ - [], - [0], - [0, 0], - [0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -]; - -function evalList(context, locals: Locals, exps: List): any[] { - var length = exps.length; - if (length > 10) { - throw new BaseException("Cannot have more than 10 argument"); - } - - var result = _evalListCache[length]; - for (var i = 0; i < length; i++) { - result[i] = exps[i].eval(context, locals); - } - return result; -} +} \ No newline at end of file diff --git a/modules/angular2/src/change_detection/parser/parser.ts b/modules/angular2/src/change_detection/parser/parser.ts index c9c38d8fc2..edc89251fb 100644 --- a/modules/angular2/src/change_detection/parser/parser.ts +++ b/modules/angular2/src/change_detection/parser/parser.ts @@ -21,17 +21,18 @@ import { AST, EmptyExpr, ImplicitReceiver, - AccessMember, - SafeAccessMember, + PropertyRead, + PropertyWrite, + SafePropertyRead, LiteralPrimitive, Binary, PrefixNot, Conditional, If, BindingPipe, - Assignment, Chain, - KeyedAccess, + KeyedRead, + KeyedWrite, LiteralArray, LiteralMap, Interpolation, @@ -245,27 +246,7 @@ export class _ParseAST { return result; } - parseExpression(): AST { - var start = this.inputIndex; - var result = this.parseConditional(); - - while (this.next.isOperator('=')) { - if (!result.isAssignable) { - var end = this.inputIndex; - var expression = this.input.substring(start, end); - this.error(`Expression ${expression} is not assignable`); - } - - if (!this.parseAction) { - this.error("Binding expression cannot contain assignments"); - } - - this.expectOperator('='); - result = new Assignment(result, this.parseConditional()); - } - - return result; - } + parseExpression(): AST { return this.parseConditional(); } parseConditional(): AST { var start = this.inputIndex; @@ -393,7 +374,12 @@ export class _ParseAST { } else if (this.optionalCharacter($LBRACKET)) { var key = this.parsePipe(); this.expectCharacter($RBRACKET); - result = new KeyedAccess(result, key); + if (this.optionalOperator("=")) { + var value = this.parseConditional(); + result = new KeyedWrite(result, key, value); + } else { + result = new KeyedRead(result, key); + } } else if (this.optionalCharacter($LPAREN)) { var args = this.parseCallArguments(); @@ -506,9 +492,28 @@ export class _ParseAST { } else { let getter = this.reflector.getter(id); let setter = this.reflector.setter(id); - return isSafe ? new SafeAccessMember(receiver, id, getter, setter) : - new AccessMember(receiver, id, getter, setter); + + if (isSafe) { + if (this.optionalOperator("=")) { + this.error("The '?.' operator cannot be used in the assignment"); + } else { + return new SafePropertyRead(receiver, id, getter); + } + } else { + if (this.optionalOperator("=")) { + if (!this.parseAction) { + this.error("Bindings cannot contain assignments"); + } + + let value = this.parseConditional(); + return new PropertyWrite(receiver, id, setter, value); + } else { + return new PropertyRead(receiver, id, getter); + } + } } + + return null; } parseCallArguments(): BindingPipe[] { @@ -629,9 +634,11 @@ class SimpleExpressionChecker implements AstVisitor { visitLiteralPrimitive(ast: LiteralPrimitive) {} - visitAccessMember(ast: AccessMember) {} + visitPropertyRead(ast: PropertyRead) {} - visitSafeAccessMember(ast: SafeAccessMember) { this.simple = false; } + visitPropertyWrite(ast: PropertyWrite) { this.simple = false; } + + visitSafePropertyRead(ast: SafePropertyRead) { this.simple = false; } visitMethodCall(ast: MethodCall) { this.simple = false; } @@ -651,7 +658,9 @@ class SimpleExpressionChecker implements AstVisitor { visitPipe(ast: BindingPipe) { this.simple = false; } - visitKeyedAccess(ast: KeyedAccess) { this.simple = false; } + visitKeyedRead(ast: KeyedRead) { this.simple = false; } + + visitKeyedWrite(ast: KeyedWrite) { this.simple = false; } visitAll(asts: List): List { var res = ListWrapper.createFixedSize(asts.length); @@ -663,7 +672,5 @@ class SimpleExpressionChecker implements AstVisitor { visitChain(ast: Chain) { this.simple = false; } - visitAssignment(ast: Assignment) { this.simple = false; } - visitIf(ast: If) { this.simple = false; } } diff --git a/modules/angular2/src/change_detection/proto_change_detector.ts b/modules/angular2/src/change_detection/proto_change_detector.ts index 6176cadd73..28e8c246c3 100644 --- a/modules/angular2/src/change_detection/proto_change_detector.ts +++ b/modules/angular2/src/change_detection/proto_change_detector.ts @@ -2,8 +2,9 @@ import {BaseException, Type, isBlank, isPresent, isString} from 'angular2/src/fa import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import { - AccessMember, - Assignment, + PropertyRead, + PropertyWrite, + KeyedWrite, AST, ASTWithSource, AstVisitor, @@ -15,13 +16,13 @@ import { FunctionCall, ImplicitReceiver, Interpolation, - KeyedAccess, + KeyedRead, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, - SafeAccessMember, + SafePropertyRead, SafeMethodCall } from './parser/ast'; @@ -30,29 +31,42 @@ import {ChangeDetectionUtil} from './change_detection_util'; import {DynamicChangeDetector} from './dynamic_change_detector'; import {BindingRecord} from './binding_record'; import {DirectiveRecord, DirectiveIndex} from './directive_record'; +import {EventBinding} from './event_binding'; import {coalesce} from './coalesce'; - import {ProtoRecord, RecordType} from './proto_record'; export class DynamicProtoChangeDetector implements ProtoChangeDetector { - _records: List; + _propertyBindingRecords: ProtoRecord[]; + _eventBindingRecords: EventBinding[]; constructor(private definition: ChangeDetectorDefinition) { - this._records = this._createRecords(definition); + this._propertyBindingRecords = createPropertyRecords(definition); + this._eventBindingRecords = createEventRecords(definition); } instantiate(dispatcher: any): ChangeDetector { return new DynamicChangeDetector(this.definition.id, this.definition.strategy, dispatcher, - this._records, this.definition.directiveRecords); + this._propertyBindingRecords, this._eventBindingRecords, + this.definition.directiveRecords); } +} - _createRecords(definition: ChangeDetectorDefinition) { - var recordBuilder = new ProtoRecordBuilder(); - ListWrapper.forEach(definition.bindingRecords, - (b) => { recordBuilder.add(b, definition.variableNames); }); - return coalesce(recordBuilder.records); - } +export function createPropertyRecords(definition: ChangeDetectorDefinition): ProtoRecord[] { + var recordBuilder = new ProtoRecordBuilder(); + ListWrapper.forEach(definition.bindingRecords, + (b) => { recordBuilder.add(b, definition.variableNames); }); + return coalesce(recordBuilder.records); +} + +export function createEventRecords(definition: ChangeDetectorDefinition): EventBinding[] { + // TODO: vsavkin: remove $event when the compiler handles render-side variables properly + var varNames = ListWrapper.concat(['$event'], definition.variableNames); + return definition.eventRecords.map(er => { + var records = _ConvertAstIntoProtoRecords.create(er, varNames); + var dirIndex = er.implicitReceiver instanceof DirectiveIndex ? er.implicitReceiver : null; + return new EventBinding(er.eventName, er.elementIndex, dirIndex, records); + }); } export class ProtoRecordBuilder { @@ -105,6 +119,13 @@ class _ConvertAstIntoProtoRecords implements AstVisitor { b.ast.visit(c); } + static create(b: BindingRecord, variableNames: List): ProtoRecord[] { + var rec = []; + _ConvertAstIntoProtoRecords.append(rec, b, variableNames); + rec[rec.length - 1].lastInBinding = true; + return rec; + } + visitImplicitReceiver(ast: ImplicitReceiver): any { return this._bindingRecord.implicitReceiver; } visitInterpolation(ast: Interpolation): number { @@ -117,17 +138,36 @@ class _ConvertAstIntoProtoRecords implements AstVisitor { return this._addRecord(RecordType.CONST, "literal", ast.value, [], null, 0); } - visitAccessMember(ast: AccessMember): number { + visitPropertyRead(ast: PropertyRead): number { var receiver = ast.receiver.visit(this); if (isPresent(this._variableNames) && ListWrapper.contains(this._variableNames, ast.name) && ast.receiver instanceof ImplicitReceiver) { return this._addRecord(RecordType.LOCAL, ast.name, ast.name, [], null, receiver); } else { - return this._addRecord(RecordType.PROPERTY, ast.name, ast.getter, [], null, receiver); + return this._addRecord(RecordType.PROPERTY_READ, ast.name, ast.getter, [], null, receiver); } } - visitSafeAccessMember(ast: SafeAccessMember): number { + visitPropertyWrite(ast: PropertyWrite): number { + if (isPresent(this._variableNames) && ListWrapper.contains(this._variableNames, ast.name) && + ast.receiver instanceof ImplicitReceiver) { + throw new BaseException(`Cannot reassign a variable binding ${ast.name}`); + } else { + var receiver = ast.receiver.visit(this); + var value = ast.value.visit(this); + return this._addRecord(RecordType.PROPERTY_WRITE, ast.name, ast.setter, [value], null, + receiver); + } + } + + visitKeyedWrite(ast: KeyedWrite): number { + var obj = ast.obj.visit(this); + var key = ast.key.visit(this); + var value = ast.value.visit(this); + return this._addRecord(RecordType.KEYED_WRITE, null, null, [key, value], null, obj); + } + + visitSafePropertyRead(ast: SafePropertyRead): number { var receiver = ast.receiver.visit(this); return this._addRecord(RecordType.SAFE_PROPERTY, ast.name, ast.getter, [], null, receiver); } @@ -195,16 +235,17 @@ class _ConvertAstIntoProtoRecords implements AstVisitor { return this._addRecord(RecordType.PIPE, ast.name, ast.name, args, null, value); } - visitKeyedAccess(ast: KeyedAccess): number { + visitKeyedRead(ast: KeyedRead): number { var obj = ast.obj.visit(this); var key = ast.key.visit(this); - return this._addRecord(RecordType.KEYED_ACCESS, "keyedAccess", ChangeDetectionUtil.keyedAccess, + return this._addRecord(RecordType.KEYED_READ, "keyedAccess", ChangeDetectionUtil.keyedAccess, [key], null, obj); } - visitAssignment(ast: Assignment) { throw new BaseException('Not supported'); } - - visitChain(ast: Chain) { throw new BaseException('Not supported'); } + visitChain(ast: Chain): number { + var args = ast.expressions.map(e => e.visit(this)); + return this._addRecord(RecordType.CHAIN, "chain", null, args, null, 0); + } visitIf(ast: If) { throw new BaseException('Not supported'); } diff --git a/modules/angular2/src/change_detection/proto_record.ts b/modules/angular2/src/change_detection/proto_record.ts index 25c1359837..4f53cb3852 100644 --- a/modules/angular2/src/change_detection/proto_record.ts +++ b/modules/angular2/src/change_detection/proto_record.ts @@ -6,17 +6,20 @@ export enum RecordType { SELF, CONST, PRIMITIVE_OP, - PROPERTY, + PROPERTY_READ, + PROPERTY_WRITE, LOCAL, INVOKE_METHOD, INVOKE_CLOSURE, - KEYED_ACCESS, + KEYED_READ, + KEYED_WRITE, PIPE, INTERPOLATE, SAFE_PROPERTY, COLLECTION_LITERAL, SAFE_INVOKE_METHOD, - DIRECTIVE_LIFECYCLE + DIRECTIVE_LIFECYCLE, + CHAIN } export class ProtoRecord { diff --git a/modules/angular2/src/core/compiler/element_binder.ts b/modules/angular2/src/core/compiler/element_binder.ts index beeee6369b..7a2676c008 100644 --- a/modules/angular2/src/core/compiler/element_binder.ts +++ b/modules/angular2/src/core/compiler/element_binder.ts @@ -1,15 +1,11 @@ -import {AST} from 'angular2/src/change_detection/change_detection'; import {isBlank, isPresent, BaseException} from 'angular2/src/facade/lang'; import * as eiModule from './element_injector'; import {DirectiveBinding} from './element_injector'; -import {List, StringMap} from 'angular2/src/facade/collection'; import * as viewModule from './view'; export class ElementBinder { // updated later, so we are able to resolve cycles nestedProtoView: viewModule.AppProtoView = null; - // updated later when events are bound - hostListeners: StringMap> = null; constructor(public index: int, public parent: ElementBinder, public distanceToParent: int, public protoElementInjector: eiModule.ProtoElementInjector, diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index a510865afe..0ae1a71be1 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -23,12 +23,50 @@ import {AppProtoView} from './view'; import {ElementBinder} from './element_binder'; import {ProtoElementInjector, DirectiveBinding} from './element_injector'; -class BindingRecordsCreator { +export class BindingRecordsCreator { _directiveRecordsMap: Map = new Map(); - getBindingRecords(textBindings: List, - elementBinders: List, - allDirectiveMetadatas: List): List { + getEventBindingRecords(elementBinders: List, + allDirectiveMetadatas: renderApi.DirectiveMetadata[]): BindingRecord[] { + var res = []; + for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; + boundElementIndex++) { + var renderElementBinder = elementBinders[boundElementIndex]; + + this._createTemplateEventRecords(res, renderElementBinder, boundElementIndex); + this._createHostEventRecords(res, renderElementBinder, allDirectiveMetadatas, + boundElementIndex); + } + return res; + } + + private _createTemplateEventRecords(res: BindingRecord[], + renderElementBinder: renderApi.ElementBinder, + boundElementIndex: number): void { + renderElementBinder.eventBindings.forEach(eb => { + res.push(BindingRecord.createForEvent(eb.source, eb.fullName, boundElementIndex)); + }); + } + + private _createHostEventRecords(res: BindingRecord[], + renderElementBinder: renderApi.ElementBinder, + allDirectiveMetadatas: renderApi.DirectiveMetadata[], + boundElementIndex: number): void { + for (var i = 0; i < renderElementBinder.directives.length; ++i) { + var dir = renderElementBinder.directives[i]; + var directiveMetadata = allDirectiveMetadatas[dir.directiveIndex]; + var dirRecord = this._getDirectiveRecord(boundElementIndex, i, directiveMetadata); + dir.eventBindings.forEach(heb => { + res.push( + BindingRecord.createForHostEvent(heb.source, heb.fullName, dirRecord.directiveIndex)); + }); + } + } + + getPropertyBindingRecords(textBindings: List, + elementBinders: List, + allDirectiveMetadatas: + List): List { var bindings = []; this._createTextNodeRecords(bindings, textBindings); @@ -232,8 +270,10 @@ function _getChangeDetectorDefinitions( return ListWrapper.map(nestedPvsWithIndex, (pvWithIndex) => { var elementBinders = pvWithIndex.renderProtoView.elementBinders; var bindingRecordsCreator = new BindingRecordsCreator(); - var bindingRecords = bindingRecordsCreator.getBindingRecords( + var propBindingRecords = bindingRecordsCreator.getPropertyBindingRecords( pvWithIndex.renderProtoView.textBindings, elementBinders, allRenderDirectiveMetadata); + var eventBindingRecords = + bindingRecordsCreator.getEventBindingRecords(elementBinders, allRenderDirectiveMetadata); var directiveRecords = bindingRecordsCreator.getDirectiveRecords(elementBinders, allRenderDirectiveMetadata); var strategyName = DEFAULT; @@ -248,8 +288,8 @@ function _getChangeDetectorDefinitions( } var id = `${hostComponentMetadata.id}_${typeString}_${pvWithIndex.index}`; var variableNames = nestedPvVariableNames[pvWithIndex.index]; - return new ChangeDetectorDefinition(id, strategyName, variableNames, bindingRecords, - directiveRecords, assertionsEnabled()); + return new ChangeDetectorDefinition(id, strategyName, variableNames, propBindingRecords, + eventBindingRecords, directiveRecords, assertionsEnabled()); }); } @@ -266,8 +306,6 @@ function _createAppProtoView( protoChangeDetector, variableBindings, createVariableLocations(elementBinders), renderProtoView.textBindings.length, protoPipes); _createElementBinders(protoView, elementBinders, allDirectives); - _bindDirectiveEvents(protoView, elementBinders); - return protoView; } @@ -393,8 +431,6 @@ function _createElementBinder(protoView: AppProtoView, boundElementIndex, render } var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent, protoElementInjector, componentDirectiveBinding); - protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1); - // variables // The view's locals needs to have a full set of variable names at construction time // in order to prevent new variables from being set later in the lifecycle. Since we don't want // to actually create variable bindings for the $implicit bindings, add to the @@ -450,19 +486,6 @@ function _directiveExportAs(directive): string { } } -function _bindDirectiveEvents(protoView, elementBinders: List) { - for (var boundElementIndex = 0; boundElementIndex < elementBinders.length; ++boundElementIndex) { - var dirs = elementBinders[boundElementIndex].directives; - for (var i = 0; i < dirs.length; i++) { - var directiveBinder = dirs[i]; - - // directive events - protoView.bindEvent(directiveBinder.eventBindings, boundElementIndex, i); - } - } -} - - class RenderProtoViewWithIndex { constructor(public renderProtoView: renderApi.ProtoViewDto, public index: number, public parentIndex: number, public boundElementIndex: number) {} diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index b988029b08..dbcb8777ec 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -262,29 +262,12 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher { // returns false if preventDefault must be applied to the DOM event dispatchEvent(boundElementIndex: number, eventName: string, locals: Map): boolean { try { - // Most of the time the event will be fired only when the view is in the live document. - // However, in a rare circumstance the view might get dehydrated, in between the event - // queuing up and firing. - var allowDefaultBehavior = true; if (this.hydrated()) { - var elBinder = this.proto.elementBinders[boundElementIndex - this.elementOffset]; - if (isBlank(elBinder.hostListeners)) return allowDefaultBehavior; - var eventMap = elBinder.hostListeners[eventName]; - if (isBlank(eventMap)) return allowDefaultBehavior; - MapWrapper.forEach(eventMap, (expr, directiveIndex) => { - var context; - if (directiveIndex === -1) { - context = this.context; - } else { - context = this.elementInjectors[boundElementIndex].getDirectiveAtIndex(directiveIndex); - } - var result = expr.eval(context, new Locals(this.locals, locals)); - if (isPresent(result)) { - allowDefaultBehavior = allowDefaultBehavior && result == true; - } - }); + return !this.changeDetector.handleEvent(eventName, boundElementIndex - this.elementOffset, + new Locals(this.locals, locals)); + } else { + return true; } - return allowDefaultBehavior; } catch (e) { var c = this.getDebugContext(boundElementIndex - this.elementOffset, null); var context = isPresent(c) ? new _Context(c.element, c.componentElement, c.context, c.locals, @@ -354,37 +337,4 @@ export class AppProtoView { this.elementBinders.push(elBinder); return elBinder; } - - /** - * Adds an event binding for the last created ElementBinder via bindElement. - * - * If the directive index is a positive integer, the event is evaluated in the context of - * the given directive. - * - * If the directive index is -1, the event is evaluated in the context of the enclosing view. - * - * @param {string} eventName - * @param {AST} expression - * @param {int} directiveIndex The directive index in the binder or -1 when the event is not bound - * to a directive - */ - bindEvent(eventBindings: List, boundElementIndex: number, - directiveIndex: int = -1): void { - var elBinder = this.elementBinders[boundElementIndex]; - var events = elBinder.hostListeners; - if (isBlank(events)) { - events = StringMapWrapper.create(); - elBinder.hostListeners = events; - } - for (var i = 0; i < eventBindings.length; i++) { - var eventBinding = eventBindings[i]; - var eventName = eventBinding.fullName; - var event = StringMapWrapper.get(events, eventName); - if (isBlank(event)) { - event = new Map(); - StringMapWrapper.set(events, eventName, event); - } - event.set(directiveIndex, eventBinding.source); - } - } } diff --git a/modules/angular2/src/render/dom/view/proto_view_builder.ts b/modules/angular2/src/render/dom/view/proto_view_builder.ts index cdc0659f59..697b2f4bd1 100644 --- a/modules/angular2/src/render/dom/view/proto_view_builder.ts +++ b/modules/angular2/src/render/dom/view/proto_view_builder.ts @@ -13,7 +13,7 @@ import { ASTWithSource, AST, AstTransformer, - AccessMember, + PropertyRead, LiteralArray, ImplicitReceiver } from 'angular2/src/change_detection/change_detection'; @@ -278,11 +278,11 @@ export class EventBuilder extends AstTransformer { return result; } - visitAccessMember(ast: AccessMember): AccessMember { + visitPropertyRead(ast: PropertyRead): PropertyRead { var isEventAccess = false; var current: AST = ast; - while (!isEventAccess && (current instanceof AccessMember)) { - var am = current; + while (!isEventAccess && (current instanceof PropertyRead)) { + var am = current; if (am.name == '$event') { isEventAccess = true; } @@ -292,7 +292,7 @@ export class EventBuilder extends AstTransformer { if (isEventAccess) { this.locals.push(ast); var index = this.locals.length - 1; - return new AccessMember(this._implicitReceiver, `${index}`, (arr) => arr[index], null); + return new PropertyRead(this._implicitReceiver, `${index}`, (arr) => arr[index]); } else { return ast; } diff --git a/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart b/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart index 60a4fba90f..482ea08fad 100644 --- a/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart +++ b/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart @@ -1,7 +1,6 @@ library angular2.transform.template_compiler.change_detector_codegen; import 'package:angular2/src/change_detection/change_detection_util.dart'; -import 'package:angular2/src/change_detection/coalesce.dart'; import 'package:angular2/src/change_detection/codegen_facade.dart'; import 'package:angular2/src/change_detection/codegen_logic_util.dart'; import 'package:angular2/src/change_detection/codegen_name_util.dart'; @@ -9,6 +8,7 @@ import 'package:angular2/src/change_detection/directive_record.dart'; import 'package:angular2/src/change_detection/interfaces.dart'; import 'package:angular2/src/change_detection/proto_change_detector.dart'; import 'package:angular2/src/change_detection/proto_record.dart'; +import 'package:angular2/src/change_detection/event_binding.dart'; import 'package:angular2/src/facade/lang.dart' show BaseException; /// Responsible for generating change detector classes for Angular 2. @@ -74,8 +74,9 @@ class _CodegenState { /// detail and should not be visible to users. final String _changeDetectorTypeName; final String _changeDetectionMode; - final List _records; final List _directiveRecords; + final List _records; + final List _eventBindings; final CodegenLogicUtil _logic; final CodegenNameUtil _names; final bool _generateCheckNoChanges; @@ -86,6 +87,7 @@ class _CodegenState { this._changeDetectorTypeName, String changeDetectionStrategy, this._records, + this._eventBindings, this._directiveRecords, this._logic, this._names, @@ -95,10 +97,9 @@ class _CodegenState { factory _CodegenState(String typeName, String changeDetectorTypeName, ChangeDetectorDefinition def) { - var recBuilder = new ProtoRecordBuilder(); - def.bindingRecords.forEach((rec) => recBuilder.add(rec, def.variableNames)); - var protoRecords = coalesce(recBuilder.records); - var names = new CodegenNameUtil(protoRecords, def.directiveRecords, _UTIL); + var protoRecords = createPropertyRecords(def); + var eventBindings = createEventRecords(def); + var names = new CodegenNameUtil(protoRecords, eventBindings, def.directiveRecords, _UTIL); var logic = new CodegenLogicUtil(names, _UTIL); return new _CodegenState._( def.id, @@ -106,6 +107,7 @@ class _CodegenState { changeDetectorTypeName, def.strategy, protoRecords, + eventBindings, def.directiveRecords, logic, names, @@ -123,6 +125,13 @@ class _CodegenState { dehydrateDirectives(false); } + bool handleEvent(eventName, elIndex, locals) { + var ${_names.getPreventDefaultAccesor()} = false; + ${_names.genInitEventLocals()} + ${_genHandleEvent()} + return ${this._names.getPreventDefaultAccesor()}; + } + void detectChangesInRecordsInternal(throwOnChange) { ${_names.genInitLocals()} var $_IS_CHANGED_LOCAL = false; @@ -152,6 +161,33 @@ class _CodegenState { '''); } + String _genHandleEvent() { + return _eventBindings.map((eb) => _genEventBinding(eb)).join("\n"); + } + + String _genEventBinding(EventBinding eb) { + var recs = eb.records.map((r) => _genEventBindingEval(eb, r)).join("\n"); + return ''' + if (eventName == "${eb.eventName}" && elIndex == ${eb.elIndex}) { + ${recs} + }'''; + } + + String _genEventBindingEval(EventBinding eb, ProtoRecord r){ + if (r.lastInBinding) { + var evalRecord = _logic.genEventBindingEvalValue(eb, r); + var prevDefault = _genUpdatePreventDefault(eb, r); + return "${evalRecord}\n${prevDefault}"; + } else { + return _logic.genEventBindingEvalValue(eb, r); + } + } + + String _genUpdatePreventDefault(EventBinding eb, ProtoRecord r) { + var local = this._names.getEventLocalName(eb, r.selfIndex); + return """if (${local} == false) { ${_names.getPreventDefaultAccesor()} = true; }"""; + } + void _writeInitToBuf(StringBuffer buf) { buf.write(''' $_GEN_PREFIX.preGeneratedProtoDetectors['$_changeDetectorDefId'] = @@ -295,7 +331,7 @@ class _CodegenState { var oldValue = _names.getFieldName(r.selfIndex); var newValue = _names.getLocalName(r.selfIndex); var read = ''' - ${_logic.genUpdateCurrentValue(r)} + ${_logic.genPropertyBindingEvalValue(r)} '''; var check = ''' diff --git a/modules/angular2/test/change_detection/change_detection_spec.ts b/modules/angular2/test/change_detection/change_detection_spec.ts index 5db3ff5fc5..d08803aae6 100644 --- a/modules/angular2/test/change_detection/change_detection_spec.ts +++ b/modules/angular2/test/change_detection/change_detection_spec.ts @@ -23,7 +23,7 @@ export function main() { beforeEach(() => { proto = new SpyProtoChangeDetector(); - def = new ChangeDetectorDefinition('id', null, [], [], [], true); + def = new ChangeDetectorDefinition('id', null, [], [], [], [], true); }); it("should return a proto change detector when one is available", () => { diff --git a/modules/angular2/test/change_detection/change_detector_config.ts b/modules/angular2/test/change_detection/change_detector_config.ts index aeea49b83e..dc08b94e02 100644 --- a/modules/angular2/test/change_detection/change_detector_config.ts +++ b/modules/angular2/test/change_detection/change_detector_config.ts @@ -31,6 +31,23 @@ function _createBindingRecords(expression: string): List { return [BindingRecord.createForElementProperty(ast, 0, PROP_NAME)]; } +function _createEventRecords(expression: string): List { + var eq = expression.indexOf("="); + var eventName = expression.substring(1, eq - 1); + var exp = expression.substring(eq + 2, expression.length - 1); + var ast = _getParser().parseAction(exp, 'location'); + return [BindingRecord.createForEvent(ast, eventName, 0)]; +} + +function _createHostEventRecords(expression: string): List { + var parts = expression.split("="); + var eventName = parts[0].substring(1, parts[0].length - 1); + var exp = parts[1].substring(1, parts[1].length - 1); + + var ast = _getParser().parseAction(exp, 'location'); + return [BindingRecord.createForHostEvent(ast, eventName, new DirectiveIndex(0, 0))]; +} + function _convertLocalsToVariableBindings(locals: Locals): List { var variableBindings = []; var loc = locals; @@ -53,24 +70,38 @@ export function getDefinition(id: string): TestDefinition { let cdDef = val.createChangeDetectorDefinition(); cdDef.id = id; testDef = new TestDefinition(id, cdDef, val.locals); + } else if (StringMapWrapper.contains(_ExpressionWithMode.availableDefinitions, id)) { let val = StringMapWrapper.get(_ExpressionWithMode.availableDefinitions, id); let cdDef = val.createChangeDetectorDefinition(); cdDef.id = id; testDef = new TestDefinition(id, cdDef, null); + } else if (StringMapWrapper.contains(_DirectiveUpdating.availableDefinitions, id)) { let val = StringMapWrapper.get(_DirectiveUpdating.availableDefinitions, id); let cdDef = val.createChangeDetectorDefinition(); cdDef.id = id; testDef = new TestDefinition(id, cdDef, null); + } else if (ListWrapper.indexOf(_availableDefinitions, id) >= 0) { var strategy = null; var variableBindings = []; - var bindingRecords = _createBindingRecords(id); + var eventRecords = _createBindingRecords(id); var directiveRecords = []; - let cdDef = new ChangeDetectorDefinition(id, strategy, variableBindings, bindingRecords, + let cdDef = new ChangeDetectorDefinition(id, strategy, variableBindings, eventRecords, [], directiveRecords, true); testDef = new TestDefinition(id, cdDef, null); + + } else if (ListWrapper.indexOf(_availableEventDefinitions, id) >= 0) { + var eventRecords = _createEventRecords(id); + let cdDef = new ChangeDetectorDefinition(id, null, [], [], eventRecords, [], true); + testDef = new TestDefinition(id, cdDef, null); + + } else if (ListWrapper.indexOf(_availableHostEventDefinitions, id) >= 0) { + var eventRecords = _createHostEventRecords(id); + let cdDef = new ChangeDetectorDefinition(id, null, [], [], eventRecords, + [_DirectiveUpdating.basicRecords[0]], true); + testDef = new TestDefinition(id, cdDef, null); } if (isBlank(testDef)) { throw `No ChangeDetectorDefinition for ${id} available. Please modify this file if necessary.`; @@ -95,6 +126,8 @@ export function getAllDefinitions(): List { ListWrapper.concat(allDefs, StringMapWrapper.keys(_ExpressionWithMode.availableDefinitions)); allDefs = ListWrapper.concat(allDefs, StringMapWrapper.keys(_DirectiveUpdating.availableDefinitions)); + allDefs = ListWrapper.concat(allDefs, _availableEventDefinitions); + allDefs = ListWrapper.concat(allDefs, _availableHostEventDefinitions); return ListWrapper.map(allDefs, (id) => getDefinition(id)); } @@ -107,7 +140,7 @@ class _ExpressionWithLocals { var bindingRecords = _createBindingRecords(this._expression); var directiveRecords = []; return new ChangeDetectorDefinition('(empty id)', strategy, variableBindings, bindingRecords, - directiveRecords, true); + [], directiveRecords, true); } /** @@ -151,7 +184,7 @@ class _ExpressionWithMode { directiveRecords = []; } return new ChangeDetectorDefinition('(empty id)', this._strategy, variableBindings, - bindingRecords, directiveRecords, true); + bindingRecords, [], directiveRecords, true); } /** @@ -174,7 +207,7 @@ class _DirectiveUpdating { var variableBindings = []; return new ChangeDetectorDefinition('(empty id)', strategy, variableBindings, - this._bindingRecords, this._directiveRecords, true); + this._bindingRecords, [], this._directiveRecords, true); } static updateA(expression: string, dirRecord): BindingRecord { @@ -317,3 +350,15 @@ var _availableDefinitions = [ 'passThrough([12])', 'invalidFn(1)' ]; + +var _availableEventDefinitions = [ + '(event)="onEvent(\$event)"', + '(event)="b=a=\$event"', + '(event)="a[0]=\$event"', + // '(event)="\$event=1"', + '(event)="a=a+1; a=a+1;"', + '(event)="false"', + '(event)="true"' +]; + +var _availableHostEventDefinitions = ['(host-event)="onEvent(\$event)"']; \ No newline at end of file diff --git a/modules/angular2/test/change_detection/change_detector_spec.ts b/modules/angular2/test/change_detection/change_detector_spec.ts index 7e2fa901fe..c67c6079a3 100644 --- a/modules/angular2/test/change_detection/change_detector_spec.ts +++ b/modules/angular2/test/change_detection/change_detector_spec.ts @@ -839,6 +839,67 @@ export function main() { expect(val.dispatcher.log).toEqual(['propName=Megatron']); }); + + describe('handleEvent', () => { + var locals; + var d: TestDirective; + + beforeEach(() => { + locals = new Locals(null, MapWrapper.createFromStringMap({"$event": "EVENT"})); + d = new TestDirective(); + }); + + it('should execute events', () => { + var val = _createChangeDetector('(event)="onEvent($event)"', d, null); + val.changeDetector.handleEvent("event", 0, locals); + expect(d.event).toEqual("EVENT"); + }); + + it('should execute host events', () => { + var val = _createWithoutHydrate('(host-event)="onEvent($event)"'); + val.changeDetector.hydrate(_DEFAULT_CONTEXT, null, new FakeDirectives([d], []), null); + val.changeDetector.handleEvent("host-event", 0, locals); + expect(d.event).toEqual("EVENT"); + }); + + it('should support field assignments', () => { + var val = _createChangeDetector('(event)="b=a=$event"', d, null); + val.changeDetector.handleEvent("event", 0, locals); + expect(d.a).toEqual("EVENT"); + expect(d.b).toEqual("EVENT"); + }); + + it('should support keyed assignments', () => { + d.a = ["OLD"]; + var val = _createChangeDetector('(event)="a[0]=$event"', d, null); + val.changeDetector.handleEvent("event", 0, locals); + expect(d.a).toEqual(["EVENT"]); + }); + + it('should support chains', () => { + d.a = 0; + var val = _createChangeDetector('(event)="a=a+1; a=a+1;"', d, null); + val.changeDetector.handleEvent("event", 0, locals); + expect(d.a).toEqual(2); + }); + + // TODO: enable after chaning dart infrastructure for generating tests + // it('should throw when trying to assign to a local', () => { + // expect(() => { + // _createChangeDetector('(event)="$event=1"', d, null) + // }).toThrowError(new RegExp("Cannot reassign a variable binding")); + // }); + + it('should return the prevent default value', () => { + var val = _createChangeDetector('(event)="false"', d, null); + var res = val.changeDetector.handleEvent("event", 0, locals); + expect(res).toBe(true); + + val = _createChangeDetector('(event)="true"', d, null); + res = val.changeDetector.handleEvent("event", 0, locals); + expect(res).toBe(false); + }); + }); }); }); } @@ -892,6 +953,7 @@ class TestDirective { onChangesDoneSpy; onCheckCalled; onInitCalled; + event; constructor(onChangesDoneSpy = null) { this.onChangesDoneCalled = false; @@ -903,6 +965,8 @@ class TestDirective { this.changes = null; } + onEvent(event) { this.event = event; } + onCheck() { this.onCheckCalled = true; } onInit() { this.onInitCalled = true; } diff --git a/modules/angular2/test/change_detection/coalesce_spec.ts b/modules/angular2/test/change_detection/coalesce_spec.ts index 297ee59390..e147a66f67 100644 --- a/modules/angular2/test/change_detection/coalesce_spec.ts +++ b/modules/angular2/test/change_detection/coalesce_spec.ts @@ -15,7 +15,7 @@ export function main() { argumentToPureFunction?: boolean } = {}) { if (isBlank(lastInBinding)) lastInBinding = false; - if (isBlank(mode)) mode = RecordType.PROPERTY; + if (isBlank(mode)) mode = RecordType.PROPERTY_READ; if (isBlank(name)) name = "name"; if (isBlank(directiveIndex)) directiveIndex = null; if (isBlank(argumentToPureFunction)) argumentToPureFunction = false; diff --git a/modules/angular2/test/change_detection/parser/parser_spec.ts b/modules/angular2/test/change_detection/parser/parser_spec.ts index c8cb3de5f4..4c21cde143 100644 --- a/modules/angular2/test/change_detection/parser/parser_spec.ts +++ b/modules/angular2/test/change_detection/parser/parser_spec.ts @@ -5,9 +5,7 @@ import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {Parser} from 'angular2/src/change_detection/parser/parser'; import {Unparser} from './unparser'; import {Lexer} from 'angular2/src/change_detection/parser/lexer'; -import {Locals} from 'angular2/src/change_detection/parser/locals'; import {BindingPipe, LiteralPrimitive, AST} from 'angular2/src/change_detection/parser/ast'; -import {IS_DART} from '../../platform'; class TestData { constructor(public a?: any, public b?: any, public fnReturnValue?: any) {} @@ -18,10 +16,6 @@ class TestData { } export function main() { - function td(a: any = 0, b: any = 0, fnReturnValue: any = "constant") { - return new TestData(a, b, fnReturnValue); - } - function createParser() { return new Parser(new Lexer(), reflector); } function parseAction(text, location = null): any { @@ -46,364 +40,164 @@ export function main() { function unparse(ast: AST): string { return new Unparser().unparse(ast); } - function emptyLocals() { return new Locals(null, new Map()); } - - function evalAction(text, passedInContext = null, passedInLocals = null) { - var c = isBlank(passedInContext) ? td() : passedInContext; - var l = isBlank(passedInLocals) ? emptyLocals() : passedInLocals; - return parseAction(text).eval(c, l); + function checkBinding(exp: string, expected?: string) { + var ast = parseBinding(exp); + if (isBlank(expected)) expected = exp; + expect(unparse(ast)).toEqual(expected); } - function expectEval(text, passedInContext = null, passedInLocals = null) { - return expect(evalAction(text, passedInContext, passedInLocals)); + function checkAction(exp: string, expected?: string) { + var ast = parseAction(exp); + if (isBlank(expected)) expected = exp; + expect(unparse(ast)).toEqual(expected); } - function expectEvalError(text, passedInContext = null, passedInLocals = null) { - var c = isBlank(passedInContext) ? td() : passedInContext; - var l = isBlank(passedInLocals) ? emptyLocals() : passedInLocals; - return expect(() => parseAction(text).eval(c, l)); - } + function expectActionError(text) { return expect(() => parseAction(text)); } - function evalAsts(asts, passedInContext = null) { - var c = isBlank(passedInContext) ? td() : passedInContext; - var res = []; - for (var i = 0; i < asts.length; i++) { - res.push(asts[i].eval(c, emptyLocals())); - } - return res; - } + function expectBindingError(text) { return expect(() => parseBinding(text)); } describe("parser", () => { describe("parseAction", () => { - describe("basic expressions", () => { - it('should parse numerical expressions', () => { expectEval("1").toEqual(1); }); + it('should parse numbers', () => { checkAction("1"); }); - it('should parse strings', () => { - expectEval("'1'").toEqual('1'); - expectEval('"1"').toEqual('1'); - }); - - it('should parse null', () => { expectEval("null").toBe(null); }); - - it('should parse unary - expressions', () => { - expectEval("-1").toEqual(-1); - expectEval("+1").toEqual(1); - }); - - it('should parse unary ! expressions', () => { - expectEval("!true").toEqual(!true); - expectEval("!!true").toEqual(!!true); - expectEval("!!!true").toEqual(!!!true); - }); - - it('should parse multiplicative expressions', - () => { expectEval("3*4/2%5").toEqual(3 * 4 / 2 % 5); }); - - it('should parse additive expressions', () => { expectEval("3+6-2").toEqual(3 + 6 - 2); }); - - it('should parse relational expressions', () => { - expectEval("2<3").toEqual(2 < 3); - expectEval("2>3").toEqual(2 > 3); - expectEval("2<=2").toEqual(2 <= 2); - expectEval("2>=2").toEqual(2 >= 2); - }); - - it('should parse equality expressions', () => { - expectEval("2==3").toEqual(2 == 3); - expectEval("2=='2'").toEqual(2 == '2'); - expectEval("2=='3'").toEqual(2 == '3'); - expectEval("2!=3").toEqual(2 != 3); - expectEval("2!='3'").toEqual(2 != '3'); - expectEval("2!='2'").toEqual(2 != '2'); - expectEval("2!=!false").toEqual(2 != !false); - }); - - it('should parse strict equality expressions', () => { - expectEval("2===3").toEqual(2 === 3); - expectEval("2==='3'").toEqual(2 === '3'); - expectEval("2==='2'").toEqual(2 === '2'); - expectEval("2!==3").toEqual(2 !== 3); - expectEval("2!=='3'").toEqual(2 !== '3'); - expectEval("2!=='2'").toEqual(2 !== '2'); - expectEval("false===!true").toEqual(false === !true); - expectEval("false!==!!true").toEqual(false !== !!true); - }); - - it('should parse logicalAND expressions', () => { - expectEval("true&&true").toEqual(true && true); - expectEval("true&&false").toEqual(true && false); - }); - - it('should parse logicalOR expressions', () => { - expectEval("false||true").toEqual(false || true); - expectEval("false||false").toEqual(false || false); - }); - - it('should short-circuit AND operator', - () => { expectEval('false && a()', td(() => {throw "BOOM"})).toBe(false); }); - - it('should short-circuit OR operator', - () => { expectEval('true || a()', td(() => {throw "BOOM"})).toBe(true); }); - - it('should evaluate grouped expressions', - () => { expectEval("(1+2)*3").toEqual((1 + 2) * 3); }); - - it('should parse an empty string', () => { expectEval('').toBeNull(); }); + it('should parse strings', () => { + checkAction("'1'", '"1"'); + checkAction('"1"'); }); + it('should parse null', () => { checkAction("null"); }); + + it('should parse unary - expressions', () => { + checkAction("-1", "0 - 1"); + checkAction("+1", "1"); + }); + + it('should parse unary ! expressions', () => { + checkAction("!true"); + checkAction("!!true"); + checkAction("!!!true"); + }); + + it('should parse multiplicative expressions', + () => { checkAction("3*4/2%5", "3 * 4 / 2 % 5"); }); + + it('should parse additive expressions', () => { checkAction("3 + 6 - 2"); }); + + it('should parse relational expressions', () => { + checkAction("2 < 3"); + checkAction("2 > 3"); + checkAction("2 <= 2"); + checkAction("2 >= 2"); + }); + + it('should parse equality expressions', () => { + checkAction("2 == 3"); + checkAction("2 != 3"); + }); + + it('should parse strict equality expressions', () => { + checkAction("2 === 3"); + checkAction("2 !== 3"); + }); + + it('should parse expressions', () => { + checkAction("true && true"); + checkAction("true || false"); + }); + + it('should parse grouped expressions', () => { checkAction("(1 + 2) * 3", "1 + 2 * 3"); }); + + it('should parse an empty string', () => { checkAction(''); }); + describe("literals", () => { - it('should evaluate array', () => { - expectEval("[1][0]").toEqual(1); - expectEval("[[1]][0][0]").toEqual(1); - expectEval("[]").toEqual([]); - expectEval("[].length").toEqual(0); - expectEval("[1, 2].length").toEqual(2); + it('should parse array', () => { + checkAction("[1][0]"); + checkAction("[[1]][0][0]"); + checkAction("[]"); + checkAction("[].length"); + checkAction("[1, 2].length"); }); - it('should evaluate map', () => { - expectEval("{}").toEqual({}); - expectEval("{a:'b'}['a']").toEqual('b'); - expectEval("{'a':'b'}['a']").toEqual('b'); - expectEval("{\"a\":'b'}['a']").toEqual('b'); - expectEval("{\"a\":'b'}['a']").toEqual("b"); - expectEval("{}['a']").not.toBeDefined(); - expectEval("{\"a\":'b'}['invalid']").not.toBeDefined(); + it('should parse map', () => { + checkAction("{}"); + checkAction("{a: 1}[2]"); + checkAction("{}[\"a\"]"); }); it('should only allow identifier, string, or keyword as map key', () => { - expectEvalError('{(:0}') + expectActionError('{(:0}') .toThrowError(new RegExp('expected identifier, keyword, or string')); - expectEvalError('{1234:0}') + expectActionError('{1234:0}') .toThrowError(new RegExp('expected identifier, keyword, or string')); }); }); describe("member access", () => { it("should parse field access", () => { - expectEval("a", td(999)).toEqual(999); - expectEval("a.a", td(td(999))).toEqual(999); + checkAction("a"); + checkAction("a.a"); }); - it('should throw when accessing a field on null', - () => { expectEvalError("a.a.a").toThrowError(); }); - it('should only allow identifier or keyword as member names', () => { - expectEvalError('x.(').toThrowError(new RegExp('identifier or keyword')); - expectEvalError('x. 1234').toThrowError(new RegExp('identifier or keyword')); - expectEvalError('x."foo"').toThrowError(new RegExp('identifier or keyword')); + expectActionError('x.(').toThrowError(new RegExp('identifier or keyword')); + expectActionError('x. 1234').toThrowError(new RegExp('identifier or keyword')); + expectActionError('x."foo"').toThrowError(new RegExp('identifier or keyword')); }); - it("should read a field from Locals", () => { - var locals = new Locals(null, MapWrapper.createFromPairs([["key", "value"]])); - expectEval("key", null, locals).toEqual("value"); - }); - - it("should handle nested Locals", () => { - var nested = new Locals(null, MapWrapper.createFromPairs([["key", "value"]])); - var locals = new Locals(nested, new Map()); - expectEval("key", null, locals).toEqual("value"); - }); - - it("should fall back to a regular field read when Locals " + - "does not have the requested field", - () => { - var locals = new Locals(null, new Map()); - expectEval("a", td(999), locals).toEqual(999); - }); - }); - - describe('safe navigation operator', () => { - it('should parse field access', () => { - expectEval('a?.a', td(td(999))).toEqual(999); - expectEval('a.a?.a', td(td(td(999)))).toEqual(999); - }); - - it('should return null when accessing a field on null', - () => { expect(() => { expectEval('null?.a', td()).toEqual(null); }).not.toThrow(); }); - - it('should have the same priority as .', () => { - expect(() => { expectEval('null?.a.a', td()).toEqual(null); }).toThrowError(); - }); - - if (!IS_DART) { - it('should return null when accessing a field on undefined', () => { - expect(() => { expectEval('_undefined?.a', td()).toEqual(null); }).not.toThrow(); - }); - } - - it('should evaluate method calls', - () => { expectEval('a?.add(1,2)', td(td())).toEqual(3); }); - - it('should return null when accessing a method on null', () => { - expect(() => { expectEval('null?.add(1, 2)', td()).toEqual(null); }).not.toThrow(); + it('should parse safe field access', () => { + checkAction('a?.a'); + checkAction('a.a?.a'); }); }); describe("method calls", () => { - it("should evaluate method calls", () => { - expectEval("fn()", td(0, 0, "constant")).toEqual("constant"); - expectEval("add(1,2)").toEqual(3); - expectEval("a.add(1,2)", td(td())).toEqual(3); - expectEval("fn().add(1,2)", td(0, 0, td())).toEqual(3); + it("should parse method calls", () => { + checkAction("fn()"); + checkAction("add(1, 2)"); + checkAction("a.add(1, 2)"); + checkAction("fn().add(1, 2)"); }); - - it('should throw when more than 10 arguments', () => { - expectEvalError("fn(1,2,3,4,5,6,7,8,9,10,11)").toThrowError(new RegExp('more than')); - }); - - it('should throw when no method', () => { expectEvalError("blah()").toThrowError(); }); - - it('should evaluate a method from Locals', () => { - var locals = new Locals(null, MapWrapper.createFromPairs([['fn', () => 'child']])); - expectEval("fn()", td(0, 0, 'parent'), locals).toEqual('child'); - }); - - it('should fall back to the parent context when Locals does not ' + - 'have the requested method', - () => { - var locals = new Locals(null, new Map()); - expectEval("fn()", td(0, 0, 'parent'), locals).toEqual('parent'); - }); }); - describe("functional calls", () => { - it("should evaluate function calls", - () => { expectEval("fn()(1,2)", td(0, 0, (a, b) => a + b)).toEqual(3); }); - - it('should throw on non-function function calls', - () => { expectEvalError("4()").toThrowError(new RegExp('4 is not a function')); }); - - it('should parse functions for object indices', - () => { expectEval('a[b()]()', td([() => 6], () => 0)).toEqual(6); }); - }); + describe("functional calls", + () => { it("should parse function calls", () => { checkAction("fn()(1, 2)"); }); }); describe("conditional", () => { it('should parse ternary/conditional expressions', () => { - expectEval("7==3+4?10:20").toEqual(10); - expectEval("false?10:20").toEqual(20); + checkAction("7 == 3 + 4 ? 10 : 20"); + checkAction("false ? 10 : 20"); }); it('should throw on incorrect ternary operator syntax', () => { - expectEvalError("true?1").toThrowError(new RegExp( + expectActionError("true?1").toThrowError(new RegExp( 'Parser Error: Conditional expression true\\?1 requires all 3 expressions')); }); }); describe("if", () => { it('should parse if statements', () => { - - var fixtures = [ - ['if (true) a = 0', 0, null], - ['if (false) a = 0', null, null], - ['if (a == null) b = 0', null, 0], - ['if (true) { a = 0; b = 0 }', 0, 0], - ['if (true) { a = 0; b = 0 } else { a = 1; b = 1; }', 0, 0], - ['if (false) { a = 0; b = 0 } else { a = 1; b = 1; }', 1, 1], - ['if (false) { } else { a = 1; b = 1; }', 1, 1], - ]; - - fixtures.forEach(fix => { - var testData = td(null, null); - evalAction(fix[0], testData); - expect(testData.a).toEqual(fix[1]); - expect(testData.b).toEqual(fix[2]); - }); + checkAction("if (true) a = 0"); + checkAction("if (true) {a = 0;}", "if (true) a = 0"); }); }); describe("assignment", () => { it("should support field assignments", () => { - var context = td(); - expectEval("a=12", context).toEqual(12); - expect(context.a).toEqual(12); + checkAction("a = 12"); + checkAction("a.a.a = 123"); + checkAction("a = 123; b = 234;"); }); - it("should support nested field assignments", () => { - var context = td(td(td())); - expectEval("a.a.a=123;", context).toEqual(123); - expect(context.a.a.a).toEqual(123); + it("should throw on safe field assignments", () => { + expectActionError("a?.a = 123") + .toThrowError(new RegExp('cannot be used in the assignment')); }); - it("should support multiple assignments", () => { - var context = td(); - expectEval("a=123; b=234", context).toEqual(234); - expect(context.a).toEqual(123); - expect(context.b).toEqual(234); - }); - - it("should support array updates", () => { - var context = td([100]); - expectEval('a[0] = 200', context).toEqual(200); - expect(context.a[0]).toEqual(200); - }); - - it("should support map updates", () => { - var context = td({"key": 100}); - expectEval('a["key"] = 200', context).toEqual(200); - expect(context.a["key"]).toEqual(200); - }); - - it("should support array/map updates", () => { - var context = td([{"key": 100}]); - expectEval('a[0]["key"] = 200', context).toEqual(200); - expect(context.a[0]["key"]).toEqual(200); - }); - - it('should allow assignment after array dereference', () => { - var context = td([td()]); - expectEval('a[0].a = 200', context).toEqual(200); - expect(context.a[0].a).toEqual(200); - }); - - it('should throw on bad assignment', () => { - expectEvalError("5=4").toThrowError(new RegExp("Expression 5 is not assignable")); - }); - - it('should reassign when no variable binding with the given name', () => { - var context = td(); - var locals = new Locals(null, new Map()); - expectEval('a = 200', context, locals).toEqual(200); - expect(context.a).toEqual(200); - }); - - it('should throw when reassigning a variable binding', () => { - var locals = new Locals(null, MapWrapper.createFromPairs([["key", "value"]])); - expectEvalError('key = 200', null, locals) - .toThrowError(new RegExp("Cannot reassign a variable binding")); - }); - }); - - describe("general error handling", () => { - it("should throw on an unexpected token", () => { - expectEvalError("[1,2] trac").toThrowError(new RegExp('Unexpected token \'trac\'')); - }); - - it('should throw a reasonable error for unconsumed tokens', () => { - expectEvalError(")") - .toThrowError(new RegExp("Unexpected token \\) at column 1 in \\[\\)\\]")); - }); - - it('should throw on missing expected token', () => { - expectEvalError("a(b").toThrowError( - new RegExp("Missing expected \\) at the end of the expression \\[a\\(b\\]")); - }); + it("should support array updates", () => { checkAction("a[0] = 200"); }); }); it("should error when using pipes", - () => { expectEvalError('x|blah').toThrowError(new RegExp('Cannot have a pipe')); }); - - it('should pass exceptions', () => { - expect(() => { - parseAction('a()').eval(td(() => {throw new BaseException("boo to you")}), emptyLocals()); - }).toThrowError('boo to you'); - }); - - describe("multiple statements", () => { - it("should return the last non-blank value", () => { - expectEval("a=1;b=3;a+b").toEqual(4); - expectEval("1;;").toEqual(1); - }); - }); + () => { expectActionError('x|blah').toThrowError(new RegExp('Cannot have a pipe')); }); it('should store the source in the result', () => { expect(parseAction('someExpr').source).toBe('someExpr'); }); @@ -412,51 +206,40 @@ export function main() { () => { expect(parseAction('someExpr', 'location').location).toBe('location'); }); }); + describe("general error handling", () => { + it("should throw on an unexpected token", () => { + expectActionError("[1,2] trac").toThrowError(new RegExp('Unexpected token \'trac\'')); + }); + + it('should throw a reasonable error for unconsumed tokens', () => { + expectActionError(")") + .toThrowError(new RegExp("Unexpected token \\) at column 1 in \\[\\)\\]")); + }); + + it('should throw on missing expected token', () => { + expectActionError("a(b").toThrowError( + new RegExp("Missing expected \\) at the end of the expression \\[a\\(b\\]")); + }); + }); + describe("parseBinding", () => { describe("pipes", () => { it("should parse pipes", () => { - var originalExp = '"Foo" | uppercase'; - var ast = parseBinding(originalExp).ast; - expect(ast).toBeAnInstanceOf(BindingPipe); - expect(new Unparser().unparse(ast)).toEqual(`(${originalExp})`); - }); - - it("should parse pipes in the middle of a binding", () => { - var ast = parseBinding('(user | a | b).name').ast; - expect(new Unparser().unparse(ast)).toEqual('((user | a) | b).name'); - }); - - it("should parse pipes with args", () => { - var ast = parseBinding("(1|a:2)|b:3").ast; - expect(new Unparser().unparse(ast)).toEqual('((1 | a:2) | b:3)'); + checkBinding('a(b | c)', 'a((b | c))'); + checkBinding('a.b(c.d(e) | f)', 'a.b((c.d(e) | f))'); + checkBinding('[1, 2, 3] | a', '([1, 2, 3] | a)'); + checkBinding('{a: 1} | b', '({a: 1} | b)'); + checkBinding('a[b] | c', '(a[b] | c)'); + checkBinding('a?.b | c', '(a?.b | c)'); + checkBinding('true | a', '(true | a)'); + checkBinding('a | b:c | d', '(a | b:(c | d))'); + checkBinding('(a | b:c) | d', '((a | b:c) | d)'); }); it('should only allow identifier or keyword as formatter names', () => { - expect(() => parseBinding('"Foo"|(')).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')); - }); - - it('should parse pipes', () => { - let unparser = new Unparser(); - let exps = [ - ['a(b | c)', 'a((b | c))'], - ['a.b(c.d(e) | f)', 'a.b((c.d(e) | f))'], - ['[1, 2, 3] | a', '([1, 2, 3] | a)'], - ['{a: 1} | b', '({a: 1} | b)'], - ['a[b] | c', '(a[b] | c)'], - ['a?.b | c', '(a?.b | c)'], - ['true | a', '(true | a)'], - ['a | b:c | d', '(a | b:(c | d))'], - ['(a | b:c) | d', '((a | b:c) | d)'] - ]; - - ListWrapper.forEach(exps, e => { - var ast = parseBinding(e[0]).ast; - expect(unparser.unparse(ast)).toEqual(e[1]); - }); + expectBindingError('"Foo"|(').toThrowError(new RegExp('identifier or keyword')); + expectBindingError('"Foo"|1234').toThrowError(new RegExp('identifier or keyword')); + expectBindingError('"Foo"|"uppercase"').toThrowError(new RegExp('identifier or keyword')); }); }); @@ -471,7 +254,7 @@ export function main() { }); it('should throw on assignment', () => { - expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression")); + expect(() => parseBinding("a=2")).toThrowError(new RegExp("contain assignments")); }); }); @@ -497,21 +280,10 @@ export function main() { null); } - function exprAsts(templateBindings) { - return ListWrapper.map(templateBindings, (binding) => isPresent(binding.expression) ? - binding.expression : - null); - } + it('should parse an empty string', () => { expect(parseTemplateBindings('')).toEqual([]); }); - it('should parse an empty string', () => { - var bindings = parseTemplateBindings(''); - expect(bindings).toEqual([]); - }); - - it('should parse a string without a value', () => { - var bindings = parseTemplateBindings('a'); - expect(keys(bindings)).toEqual(['a']); - }); + it('should parse a string without a value', + () => { expect(keys(parseTemplateBindings('a'))).toEqual(['a']); }); it('should only allow identifier, string, or keyword including dashes as keys', () => { var bindings = parseTemplateBindings("a:'b'"); @@ -536,11 +308,9 @@ export function main() { 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', () => { @@ -657,8 +427,7 @@ export function main() { describe('wrapLiteralPrimitive', () => { it('should wrap a literal primitive', () => { - expect(createParser().wrapLiteralPrimitive("foo", null).eval(null, emptyLocals())) - .toEqual("foo"); + expect(unparse(createParser().wrapLiteralPrimitive("foo", null))).toEqual('"foo"'); }); }); }); diff --git a/modules/angular2/test/change_detection/parser/unparser.ts b/modules/angular2/test/change_detection/parser/unparser.ts index 172ebd63c2..1037c75292 100644 --- a/modules/angular2/test/change_detection/parser/unparser.ts +++ b/modules/angular2/test/change_detection/parser/unparser.ts @@ -1,8 +1,8 @@ import { AST, AstVisitor, - AccessMember, - Assignment, + PropertyRead, + PropertyWrite, Binary, Chain, Conditional, @@ -12,13 +12,14 @@ import { FunctionCall, ImplicitReceiver, Interpolation, - KeyedAccess, + KeyedRead, + KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, - SafeAccessMember, + SafePropertyRead, SafeMethodCall } from 'angular2/src/change_detection/parser/ast'; @@ -35,15 +36,15 @@ export class Unparser implements AstVisitor { return this._expression; } - visitAccessMember(ast: AccessMember) { + visitPropertyRead(ast: PropertyRead) { this._visit(ast.receiver); - this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}` : `.${ast.name}`; } - visitAssignment(ast: Assignment) { - this._visit(ast.target); - this._expression += ' = '; + visitPropertyWrite(ast: PropertyWrite) { + this._visit(ast.receiver); + this._expression += + ast.receiver instanceof ImplicitReceiver ? `${ast.name} = ` : `.${ast.name} = `; this._visit(ast.value); } @@ -116,13 +117,21 @@ export class Unparser implements AstVisitor { } } - visitKeyedAccess(ast: KeyedAccess) { + visitKeyedRead(ast: KeyedRead) { this._visit(ast.obj); this._expression += '['; this._visit(ast.key); this._expression += ']'; } + visitKeyedWrite(ast: KeyedWrite) { + this._visit(ast.obj); + this._expression += '['; + this._visit(ast.key); + this._expression += '] = '; + this._visit(ast.value); + } + visitLiteralArray(ast: LiteralArray) { this._expression += '['; var isFirst = true; @@ -173,7 +182,7 @@ export class Unparser implements AstVisitor { this._visit(ast.expression); } - visitSafeAccessMember(ast: SafeAccessMember) { + visitSafePropertyRead(ast: SafePropertyRead) { this._visit(ast.receiver); this._expression += `?.${ast.name}`; } diff --git a/modules/angular2/test/change_detection/parser/unparser_spec.ts b/modules/angular2/test/change_detection/parser/unparser_spec.ts deleted file mode 100644 index d10970ff07..0000000000 --- a/modules/angular2/test/change_detection/parser/unparser_spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'angular2/test_lib'; - -import { - AST, - ASTWithSource, - AccessMember, - Assignment, - Binary, - Chain, - Conditional, - EmptyExpr, - If, - BindingPipe, - ImplicitReceiver, - Interpolation, - KeyedAccess, - LiteralArray, - LiteralMap, - LiteralPrimitive, - MethodCall, - PrefixNot, - SafeAccessMember, - SafeMethodCall -} from 'angular2/src/change_detection/parser/ast'; - -import {Parser} from 'angular2/src/change_detection/parser/parser'; -import {Lexer} from 'angular2/src/change_detection/parser/lexer'; -import {Unparser} from './unparser'; - -import {reflector} from 'angular2/src/reflection/reflection'; - -import {isPresent, Type} from 'angular2/src/facade/lang'; - -export function main() { - let parser: Parser = new Parser(new Lexer(), reflector); - let unparser: Unparser = new Unparser(); - - function parseAction(text, location = null): ASTWithSource { - return parser.parseAction(text, location); - } - - function parseBinding(text, location = null): ASTWithSource { - return parser.parseBinding(text, location); - } - - function check(expression: string, type: Type): void { - var ast = parseAction(expression).ast; - if (isPresent(type)) { - expect(ast).toBeAnInstanceOf(type); - } - expect(unparser.unparse(ast)).toEqual(expression); - } - - describe('Unparser', () => { - it('should support AccessMember', () => { - check('a', AccessMember); - check('a.b', AccessMember); - }); - - it('should support Assignment', () => { check('a = b', Assignment); }); - - it('should support Binary', () => { check('a && b', Binary); }); - - it('should support Chain', () => { check('a; b;', Chain); }); - - it('should support Conditional', () => { check('a ? b : c', Conditional); }); - - it('should support Pipe', () => { - var originalExp = '(a | b)'; - var ast = parseBinding(originalExp).ast; - expect(ast).toBeAnInstanceOf(BindingPipe); - expect(unparser.unparse(ast)).toEqual(originalExp); - }); - - it('should support KeyedAccess', () => { check('a[b]', KeyedAccess); }); - - it('should support LiteralArray', () => { check('[a, b]', LiteralArray); }); - - it('should support LiteralMap', () => { check('{a: b, c: d}', LiteralMap); }); - - it('should support LiteralPrimitive', () => { - check('true', LiteralPrimitive); - check('"a"', LiteralPrimitive); - check('1.234', LiteralPrimitive); - }); - - it('should support MethodCall', () => { - check('a(b, c)', MethodCall); - check('a.b(c, d)', MethodCall); - }); - - it('should support PrefixNot', () => { check('!a', PrefixNot); }); - - it('should support SafeAccessMember', () => { check('a?.b', SafeAccessMember); }); - - it('should support SafeMethodCall', () => { check('a?.b(c, d)', SafeMethodCall); }); - - it('should support if statements', () => { - var ifs = [ - 'if (true) a()', - 'if (true) a() else b()', - 'if (a()) { b = 1; c = 2; }', - 'if (a()) b = 1 else { c = 2; d = e(); }' - ]; - - ifs.forEach(ifStmt => check(ifStmt, If)); - }); - - it('should support complex expression', () => { - var originalExp = 'a + 3 * fn([(c + d | e).f], {a: 3})[g].h && i'; - var ast = parseBinding(originalExp).ast; - expect(unparser.unparse(ast)).toEqual(originalExp); - }); - - it('should support Interpolation', () => { - var ast = parser.parseInterpolation('a {{ b }}', null).ast; - expect(ast).toBeAnInstanceOf(Interpolation); - expect(unparser.unparse(ast)).toEqual('a {{ b }}'); - - ast = parser.parseInterpolation('a {{ b }} c', null).ast; - expect(ast).toBeAnInstanceOf(Interpolation); - expect(unparser.unparse(ast)).toEqual('a {{ b }} c'); - }); - - it('should support EmptyExpr', () => { - var ast = parser.parseAction('if (true) { }', null).ast; - expect(ast).toBeAnInstanceOf(If); - expect((ast).trueExp).toBeAnInstanceOf(EmptyExpr); - expect(unparser.unparse(ast)).toEqual('if (true) { }'); - }); - }); -} diff --git a/modules/angular2/test/change_detection/proto_record_spec.ts b/modules/angular2/test/change_detection/proto_record_spec.ts index 3968b8e688..c7071c09fc 100644 --- a/modules/angular2/test/change_detection/proto_record_spec.ts +++ b/modules/angular2/test/change_detection/proto_record_spec.ts @@ -14,7 +14,7 @@ export function main() { referencedBySelf?: boolean } = {}) { if (isBlank(lastInBinding)) lastInBinding = false; - if (isBlank(mode)) mode = RecordType.PROPERTY; + if (isBlank(mode)) mode = RecordType.PROPERTY_READ; if (isBlank(name)) name = "name"; if (isBlank(directiveIndex)) directiveIndex = null; if (isBlank(argumentToPureFunction)) argumentToPureFunction = false; diff --git a/modules/angular2/test/core/compiler/element_injector_spec.ts b/modules/angular2/test/core/compiler/element_injector_spec.ts index 4aede997fc..b5e425f3d4 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.ts +++ b/modules/angular2/test/core/compiler/element_injector_spec.ts @@ -937,7 +937,7 @@ export function main() { }); it("should inject ChangeDetectorRef of the component's view into the component", () => { - var cd = new DynamicChangeDetector(null, null, null, [], []); + var cd = new DynamicChangeDetector(null, null, null, [], [], []); var view = new DummyView(); var childView = new DummyView(); childView.changeDetector = cd; @@ -950,7 +950,7 @@ export function main() { }); it("should inject ChangeDetectorRef of the containing component into directives", () => { - var cd = new DynamicChangeDetector(null, null, null, [], []); + var cd = new DynamicChangeDetector(null, null, null, [], [], []); var view = new DummyView(); view.changeDetector =cd; var binding = DirectiveBinding.createFromType(DirectiveNeedsChangeDetectorRef, new dirAnn.Directive()); diff --git a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts index 8839366038..14e9ac3d93 100644 --- a/modules/angular2/test/core/compiler/proto_view_factory_spec.ts +++ b/modules/angular2/test/core/compiler/proto_view_factory_spec.ts @@ -18,9 +18,12 @@ import {MapWrapper} from 'angular2/src/facade/collection'; import { ChangeDetection, - ChangeDetectorDefinition + ChangeDetectorDefinition, + BindingRecord, + DirectiveIndex } from 'angular2/src/change_detection/change_detection'; import { + BindingRecordsCreator, ProtoViewFactory, getChangeDetectorDefinitions, createDirectiveVariableBindings, @@ -162,6 +165,50 @@ export function main() { ])).toEqual(MapWrapper.createFromStringMap({'a': 0, 'b': 1})); }); }); + + describe('BindingRecordsCreator', () => { + var creator: BindingRecordsCreator; + + beforeEach(() => { creator = new BindingRecordsCreator(); }); + + describe('getEventBindingRecords', () => { + it("should return template event records", () => { + var rec = creator.getEventBindingRecords( + [ + new renderApi.ElementBinder( + {eventBindings: [new renderApi.EventBinding("a", null)], directives: []}), + new renderApi.ElementBinder( + {eventBindings: [new renderApi.EventBinding("b", null)], directives: []}) + ], + []); + + expect(rec).toEqual([ + BindingRecord.createForEvent(null, "a", 0), + BindingRecord.createForEvent(null, "b", 1) + ]); + }); + + it('should return host event records', () => { + var rec = creator.getEventBindingRecords( + [ + new renderApi.ElementBinder({ + eventBindings: [], + directives: [ + new renderApi.DirectiveBinder({ + directiveIndex: 0, + eventBindings: [new renderApi.EventBinding("a", null)] + }) + ] + }) + ], + [renderApi.DirectiveMetadata.create({id: 'some-id'})]); + + expect(rec.length).toEqual(1); + expect(rec[0].eventName).toEqual("a"); + expect(rec[0].implicitReceiver).toBeAnInstanceOf(DirectiveIndex); + }); + }); + }); }); } diff --git a/modules/angular2/test/forms/integration_spec.ts b/modules/angular2/test/forms/integration_spec.ts index f31815378b..15b254a7fb 100644 --- a/modules/angular2/test/forms/integration_spec.ts +++ b/modules/angular2/test/forms/integration_spec.ts @@ -79,6 +79,7 @@ export function main() { tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((root) => { rootTC = root; }); tick(); + rootTC.componentInstance.form = new ControlGroup({}); rootTC.componentInstance.name = 'old'; diff --git a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart index 1e63908547..9211864be2 100644 --- a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart @@ -36,6 +36,11 @@ class _MyComponent_ChangeDetector0 dehydrateDirectives(false); } + bool handleEvent(eventName, elIndex, locals) { + var preventDefault = false; + return preventDefault; + } + void detectChangesInRecordsInternal(throwOnChange) { var l_context = this.context, l_myNum0, c_myNum0, l_interpolate1; c_myNum0 = false; diff --git a/modules/benchmarks/src/change_detection/change_detection_benchmark.ts b/modules/benchmarks/src/change_detection/change_detection_benchmark.ts index f3472712f4..f0b55db733 100644 --- a/modules/benchmarks/src/change_detection/change_detection_benchmark.ts +++ b/modules/benchmarks/src/change_detection/change_detection_benchmark.ts @@ -250,7 +250,7 @@ function setUpChangeDetection(changeDetection: ChangeDetection, iterations, obje var parser = new Parser(new Lexer()); var parentProto = changeDetection.createProtoChangeDetector( - new ChangeDetectorDefinition('parent', null, [], [], [], false)); + new ChangeDetectorDefinition('parent', null, [], [], [], [], false)); var parentCd = parentProto.instantiate(dispatcher); var directiveRecord = new DirectiveRecord({directiveIndex: new DirectiveIndex(0, 0)}); @@ -278,7 +278,7 @@ function setUpChangeDetection(changeDetection: ChangeDetection, iterations, obje ]; var proto = changeDetection.createProtoChangeDetector( - new ChangeDetectorDefinition("proto", null, [], bindings, [directiveRecord], false)); + new ChangeDetectorDefinition("proto", null, [], bindings, [], [directiveRecord], false)); var targetObj = new Obj(); parentCd.hydrate(object, null, new FakeDirectives(targetObj), null);