From 850cf0fef483ed6eaf5dc4731a650b95f4e3753c Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 21 Jan 2015 12:05:52 -0800 Subject: [PATCH] feat(change_detection): implement a change detector generator --- .../change_detection_benchmark.js | 5 +- .../src/compiler/compiler_benchmark.js | 4 +- modules/benchmarks/src/tree/tree_benchmark.js | 13 +- .../src/abstract_change_detector.js | 46 ++ .../change_detection/src/change_detection.js | 25 +- .../src/change_detection_jit_generator.dart | 10 + .../src/change_detection_jit_generator.es6 | 362 ++++++++ .../src/change_detection_util.js | 138 ++++ .../src/dynamic_change_detector.js | 146 +--- .../src/proto_change_detector.js | 311 ++++--- .../test/change_detection_spec.js | 780 +++++++++--------- modules/core/src/application.js | 23 +- modules/core/src/compiler/compiler.js | 9 +- .../src/compiler/pipeline/default_steps.js | 10 +- .../compiler/pipeline/proto_view_builder.js | 10 +- modules/core/src/compiler/view.js | 7 +- modules/core/test/compiler/compiler_spec.js | 4 +- .../core/test/compiler/integration_spec.js | 5 +- .../pipeline/element_binder_builder_spec.js | 7 +- .../pipeline/proto_view_builder_spec.js | 3 +- .../shadow_dom_emulation_integration_spec.js | 5 +- modules/core/test/compiler/view_spec.js | 73 +- modules/core/test/compiler/viewport_spec.js | 10 +- modules/directives/test/ng_if_spec.js | 5 +- .../directives/test/ng_non_bindable_spec.js | 5 +- modules/directives/test/ng_repeat_spec.js | 5 +- modules/directives/test/ng_switch_spec.js | 5 +- .../examples/src/hello_world/index_static.js | 6 +- modules/facade/src/collection.dart | 4 +- modules/facade/src/lang.dart | 1 + modules/facade/src/lang.es6 | 4 + 31 files changed, 1347 insertions(+), 694 deletions(-) create mode 100644 modules/change_detection/src/abstract_change_detector.js create mode 100644 modules/change_detection/src/change_detection_jit_generator.dart create mode 100644 modules/change_detection/src/change_detection_jit_generator.es6 create mode 100644 modules/change_detection/src/change_detection_util.js diff --git a/modules/benchmarks/src/change_detection/change_detection_benchmark.js b/modules/benchmarks/src/change_detection/change_detection_benchmark.js index 724591e4d8..cb5bbe1f4b 100644 --- a/modules/benchmarks/src/change_detection/change_detection_benchmark.js +++ b/modules/benchmarks/src/change_detection/change_detection_benchmark.js @@ -8,6 +8,7 @@ import { Parser, ChangeDetector, ProtoChangeDetector, + DynamicProtoChangeDetector, ChangeDispatcher, } from 'change_detection/change_detection'; @@ -102,7 +103,7 @@ function setUpChangeDetection(iterations) { var dispatcher = new DummyDispatcher(); var parser = new Parser(new Lexer()); - var parentProto = new ProtoChangeDetector(); + var parentProto = new DynamicProtoChangeDetector(); var parentCD = parentProto.instantiate(dispatcher, MapWrapper.create()); var astWithSource = [ @@ -119,7 +120,7 @@ function setUpChangeDetection(iterations) { ]; function proto(i) { - var pcd = new ProtoChangeDetector(); + var pcd = new DynamicProtoChangeDetector(); pcd.addAst(astWithSource[i % 10].ast, "memo", i, false); return pcd; } diff --git a/modules/benchmarks/src/compiler/compiler_benchmark.js b/modules/benchmarks/src/compiler/compiler_benchmark.js index 5772cacf4d..b71c37fdf9 100644 --- a/modules/benchmarks/src/compiler/compiler_benchmark.js +++ b/modules/benchmarks/src/compiler/compiler_benchmark.js @@ -3,7 +3,7 @@ import {isBlank, Type} from 'facade/lang'; import {MapWrapper} from 'facade/collection'; import {DirectiveMetadata} from 'core/compiler/directive_metadata'; -import {Parser, Lexer, ProtoRecordRange} from 'change_detection/change_detection'; +import {Parser, Lexer, ProtoRecordRange, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; @@ -79,7 +79,7 @@ export function main() { setupReflector(); var reader = new DirectiveMetadataReader(); var cache = new CompilerCache(); - var compiler = new Compiler(null, reader, new Parser(new Lexer()), cache); + var compiler = new Compiler(dynamicChangeDetection, null, reader, new Parser(new Lexer()), cache); var annotatedComponent = reader.read(BenchmarkComponent); var templateNoBindings = loadTemplate('templateNoBindings', count); diff --git a/modules/benchmarks/src/tree/tree_benchmark.js b/modules/benchmarks/src/tree/tree_benchmark.js index 0aae941fba..f9b4116689 100644 --- a/modules/benchmarks/src/tree/tree_benchmark.js +++ b/modules/benchmarks/src/tree/tree_benchmark.js @@ -1,4 +1,5 @@ -import {Parser, Lexer, ChangeDetector} from 'change_detection/change_detection'; +import {Parser, Lexer, ChangeDetector, ChangeDetection, jitChangeDetection} + from 'change_detection/change_detection'; import {bootstrap, Component, Template, TemplateConfig, ViewPort, Compiler} from 'angular/angular'; @@ -38,11 +39,7 @@ function setupReflector() { }, template: new TemplateConfig({ directives: [TreeComponent, NgIf], - inline: ` - {{data.value}} - - - ` + inline: `{{data.value}}` }) })] }); @@ -59,8 +56,8 @@ function setupReflector() { }); reflector.registerType(Compiler, { - 'factory': (templateLoader, reader, parser, compilerCache) => new Compiler(templateLoader, reader, parser, compilerCache), - 'parameters': [[TemplateLoader], [DirectiveMetadataReader], [Parser], [CompilerCache]], + 'factory': (cd, templateLoader, reader, parser, compilerCache) => new Compiler(cd, templateLoader, reader, parser, compilerCache), + 'parameters': [[ChangeDetection], [TemplateLoader], [DirectiveMetadataReader], [Parser], [CompilerCache]], 'annotations': [] }); diff --git a/modules/change_detection/src/abstract_change_detector.js b/modules/change_detection/src/abstract_change_detector.js new file mode 100644 index 0000000000..2a2b4829c4 --- /dev/null +++ b/modules/change_detection/src/abstract_change_detector.js @@ -0,0 +1,46 @@ +import {List, ListWrapper} from 'facade/collection'; +import {ChangeDetector} from './interfaces'; + +export class AbstractChangeDetector extends ChangeDetector { + children:List; + parent:ChangeDetector; + + constructor() { + this.children = []; + } + + addChild(cd:ChangeDetector) { + ListWrapper.push(this.children, cd); + cd.parent = this; + } + + removeChild(cd:ChangeDetector) { + ListWrapper.remove(this.children, cd); + } + + remove() { + this.parent.removeChild(this); + } + + detectChanges() { + this._detectChanges(false); + } + + checkNoChanges() { + this._detectChanges(true); + } + + _detectChanges(throwOnChange:boolean) { + this.detectChangesInRecords(throwOnChange); + this._detectChangesInChildren(throwOnChange); + } + + detectChangesInRecords(throwOnChange:boolean){} + + _detectChangesInChildren(throwOnChange:boolean) { + var children = this.children; + for(var i = 0; i < children.length; ++i) { + children[i]._detectChanges(throwOnChange); + } + } +} diff --git a/modules/change_detection/src/change_detection.js b/modules/change_detection/src/change_detection.js index 32fac81672..1e53b26351 100644 --- a/modules/change_detection/src/change_detection.js +++ b/modules/change_detection/src/change_detection.js @@ -5,5 +5,26 @@ export {ContextWithVariableBindings} from './parser/context_with_variable_bindin export {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions'; export {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces'; -export {ProtoChangeDetector} from './proto_change_detector'; -export {DynamicChangeDetector} from './dynamic_change_detector'; \ No newline at end of file +export {ProtoChangeDetector, DynamicProtoChangeDetector, JitProtoChangeDetector} from './proto_change_detector'; +export {DynamicChangeDetector} from './dynamic_change_detector'; + +import {ProtoChangeDetector, DynamicProtoChangeDetector, JitProtoChangeDetector} from './proto_change_detector'; + +export class ChangeDetection { + createProtoChangeDetector(name:string){} +} + +export class DynamicChangeDetection extends ChangeDetection { + createProtoChangeDetector(name:string):ProtoChangeDetector{ + return new DynamicProtoChangeDetector(); + } +} + +export class JitChangeDetection extends ChangeDetection { + createProtoChangeDetector(name:string):ProtoChangeDetector{ + return new JitProtoChangeDetector(); + } +} + +export var dynamicChangeDetection = new DynamicChangeDetection(); +export var jitChangeDetection = new JitChangeDetection(); \ No newline at end of file diff --git a/modules/change_detection/src/change_detection_jit_generator.dart b/modules/change_detection/src/change_detection_jit_generator.dart new file mode 100644 index 0000000000..32905f20c9 --- /dev/null +++ b/modules/change_detection/src/change_detection_jit_generator.dart @@ -0,0 +1,10 @@ +library change_detectoin.change_detection_jit_generator; + +class ChangeDetectorJITGenerator { + ChangeDetectorJITGenerator(typeName, records) { + } + + generate() { + throw "Not supported in Dart"; + } +} \ No newline at end of file diff --git a/modules/change_detection/src/change_detection_jit_generator.es6 b/modules/change_detection/src/change_detection_jit_generator.es6 new file mode 100644 index 0000000000..074dbac96a --- /dev/null +++ b/modules/change_detection/src/change_detection_jit_generator.es6 @@ -0,0 +1,362 @@ +import {isPresent, isBlank, BaseException, Type} from 'facade/lang'; +import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; + +import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; +import {AbstractChangeDetector} from './abstract_change_detector'; +import {ChangeDetectionUtil} from './change_detection_util'; + +import { + ProtoRecord, + RECORD_TYPE_SELF, + RECORD_TYPE_PROPERTY, + RECORD_TYPE_INVOKE_METHOD, + RECORD_TYPE_CONST, + RECORD_TYPE_INVOKE_CLOSURE, + RECORD_TYPE_PRIMITIVE_OP, + RECORD_TYPE_KEYED_ACCESS, + RECORD_TYPE_INVOKE_FORMATTER, + RECORD_TYPE_STRUCTURAL_CHECK, + RECORD_TYPE_INTERPOLATE, + ProtoChangeDetector + } from './proto_change_detector'; + +/** + * The code generator takes a list of proto records and creates a function/class + * that "emulates" what the developer would write by hand to implement the same + * kind of behaviour. + * + * For example: An expression `address.city` will result in the following class: + * + * var ChangeDetector0 = function ChangeDetector0(dispatcher, formatters, protos) { + * AbstractChangeDetector.call(this); + * this.dispatcher = dispatcher; + * this.formatters = formatters; + * this.protos = protos; + * + * this.context = null; + * this.address0 = null; + * this.city1 = null; + * } + * ChangeDetector0.prototype = Object.create(AbstractChangeDetector.prototype); + * + * ChangeDetector0.prototype.detectChangesInRecords = function(throwOnChange) { + * var address0; + * var city1; + * var change; + * var changes = []; + * var temp; + * var context = this.context; + * + * temp = ChangeDetectionUtil.findContext("address", context); + * if (temp instanceof ContextWithVariableBindings) { + * address0 = temp.get('address'); + * } else { + * address0 = temp.address; + * } + * + * if (address0 !== this.address0) { + * this.address0 = address0; + * } + * + * city1 = address0.city; + * if (city1 !== this.city1) { + * changes.push(ChangeDetectionUtil.simpleChangeRecord(this.protos[1].bindingMemento, this.city1, city1)); + * this.city1 = city1; + * } + * + * if (changes.length > 0) { + * if(throwOnChange) ChangeDetectionUtil.throwOnChange(this.protos[1], changes[0]); + * this.dispatcher.onRecordChange('address.city', changes); + * changes = []; + * } + * } + * + * + * ChangeDetector0.prototype.setContext = function(context) { + * this.context = context; + * } + * + * return ChangeDetector0; + * + * + * The only thing the generated class depends on is the super class AbstractChangeDetector. + * + * The implementation comprises two parts: + * * ChangeDetectorJITGenerator has the logic of how everything fits together. + * * template functions (e.g., constructorTemplate) define what code is generated. +*/ + +var ABSTRACT_CHANGE_DETECTOR = "AbstractChangeDetector"; +var UTIL = "ChangeDetectionUtil"; +var DISPATCHER_ACCESSOR = "this.dispatcher"; +var FORMATTERS_ACCESSOR = "this.formatters"; +var PROTOS_ACCESSOR = "this.protos"; +var CHANGE_LOCAL = "change"; +var CHANGES_LOCAL = "changes"; +var TEMP_LOCAL = "temp"; + +function typeTemplate(type:string, cons:string, detectChanges:string, setContext:string):string { + return ` +${cons} +${detectChanges} +${setContext}; + +return function(dispatcher, formatters) { + return new ${type}(dispatcher, formatters, protos); +} +`; +} + +function constructorTemplate(type:string, fieldsDefinitions:string):string { + return ` +var ${type} = function ${type}(dispatcher, formatters, protos) { +${ABSTRACT_CHANGE_DETECTOR}.call(this); +${DISPATCHER_ACCESSOR} = dispatcher; +${FORMATTERS_ACCESSOR} = formatters; +${PROTOS_ACCESSOR} = protos; +${fieldsDefinitions} +} + +${type}.prototype = Object.create(${ABSTRACT_CHANGE_DETECTOR}.prototype); +`; +} + +function setContextTemplate(type:string):string { + return ` +${type}.prototype.setContext = function(context) { + this.context = context; +} +`; +} + +function detectChangesTemplate(type:string, body:string):string { + return ` +${type}.prototype.detectChangesInRecords = function(throwOnChange) { + ${body} +} +`; +} + + +function bodyTemplate(localDefinitions:string, records:string):string { + return ` +${localDefinitions} +var ${TEMP_LOCAL}; +var ${CHANGE_LOCAL}; +var ${CHANGES_LOCAL} = []; + +context = this.context; +${records} +`; +} + +function notifyTemplate(index:number):string{ + return ` +if (${CHANGES_LOCAL}.length > 0) { + if(throwOnChange) ${UTIL}.throwOnChange(${PROTOS_ACCESSOR}[${index}], ${CHANGES_LOCAL}[0]); + ${DISPATCHER_ACCESSOR}.onRecordChange(${PROTOS_ACCESSOR}[${index}].groupMemento, ${CHANGES_LOCAL}); + ${CHANGES_LOCAL} = []; +} +`; +} + + +function structuralCheckTemplate(selfIndex:number, field:string, context:string, notify:string):string{ + return ` +${CHANGE_LOCAL} = ${UTIL}.structuralCheck(${field}, ${context}); +if (${CHANGE_LOCAL}) { + ${CHANGES_LOCAL}.push(${UTIL}.changeRecord(${PROTOS_ACCESSOR}[${selfIndex}].bindingMemento, ${CHANGE_LOCAL})); + ${field} = ${CHANGE_LOCAL}.currentValue; +} +${notify} +`; +} + +function referenceCheckTemplate(assignment, newValue, oldValue, addRecord, notify) { + return ` +${assignment} +if (${newValue} !== ${oldValue} || (${newValue} !== ${newValue}) && (${oldValue} !== ${oldValue})) { + ${addRecord} + ${oldValue} = ${newValue}; +} +${notify} +`; +} + +function assignmentTemplate(field:string, value:string) { + return `${field} = ${value};`; +} + +function propertyReadTemplate(name:string, context:string, newValue:string) { + return ` +${TEMP_LOCAL} = ${UTIL}.findContext("${name}", ${context}); +if (${TEMP_LOCAL} instanceof ContextWithVariableBindings) { + ${newValue} = ${TEMP_LOCAL}.get('${name}'); +} else { + ${newValue} = ${TEMP_LOCAL}.${name}; +} +`; +} + +function localDefinitionsTemplate(names:List):string { + return names.map((n) => `var ${n};`).join("\n"); +} + +function fieldDefinitionsTemplate(names:List):string { + return names.map((n) => `${n} = ${UTIL}.unitialized();`).join("\n"); +} + +function addSimpleChangeRecordTemplate(protoIndex:number, oldValue:string, newValue:string) { + return `${CHANGES_LOCAL}.push(${UTIL}.simpleChangeRecord(${PROTOS_ACCESSOR}[${protoIndex}].bindingMemento, ${oldValue}, ${newValue}));`; +} + + +export class ChangeDetectorJITGenerator { + typeName:string; + records:List; + localNames:List; + fieldNames:List; + + constructor(typeName:string, records:List) { + this.typeName = typeName; + this.records = records; + + this.localNames = this.getLocalNames(records); + this.fieldNames = this.getFieldNames(this.localNames); + } + + getLocalNames(records:List):List { + var index = 0; + var names = records.map((r) => { + var sanitizedName = r.name.replace(new RegExp("\\W", "g"), ''); + return `${sanitizedName}${index++}` + }); + return ["context"].concat(names); + } + + getFieldNames(localNames:List):List { + return localNames.map((n) => `this.${n}`); + } + + + generate():Function { + var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genSetContext()); + return new Function('AbstractChangeDetector', 'ChangeDetectionUtil', 'ContextWithVariableBindings', 'protos', text)(AbstractChangeDetector, ChangeDetectionUtil, ContextWithVariableBindings, this.records); + } + + genConstructor():string { + return constructorTemplate(this.typeName, fieldDefinitionsTemplate(this.fieldNames)); + } + + genSetContext():string { + return setContextTemplate(this.typeName); + } + + genDetectChanges():string { + var body = this.genBody(); + return detectChangesTemplate(this.typeName, body); + } + + genBody():string { + var rec = this.records.map((r) => this.genRecord(r)).join("\n"); + return bodyTemplate(this.genLocalDefinitions(), rec); + } + + genLocalDefinitions():string { + return localDefinitionsTemplate(this.localNames); + } + + genRecord(r:ProtoRecord):string { + if (r.mode == RECORD_TYPE_STRUCTURAL_CHECK) { + return this.getStructuralCheck(r); + } else { + return this.genReferenceCheck(r); + } + } + + getStructuralCheck(r:ProtoRecord):string { + var field = this.fieldNames[r.selfIndex]; + var context = this.localNames[r.contextIndex]; + return structuralCheckTemplate(r.selfIndex - 1, field, context, this.genNotify(r)); + } + + genReferenceCheck(r:ProtoRecord):string { + var newValue = this.localNames[r.selfIndex]; + var oldValue = this.fieldNames[r.selfIndex]; + var assignment = this.genUpdateCurrentValue(r); + var addRecord = addSimpleChangeRecordTemplate(r.selfIndex - 1, oldValue, newValue); + var notify = this.genNotify(r); + return referenceCheckTemplate(assignment, newValue, oldValue, r.lastInBinding ? addRecord : '', notify); + } + + genUpdateCurrentValue(r:ProtoRecord):string { + var context = this.localNames[r.contextIndex]; + var newValue = this.localNames[r.selfIndex]; + var args = this.genArgs(r); + + switch (r.mode) { + case RECORD_TYPE_SELF: + throw new BaseException("Cannot evaluate self"); + + case RECORD_TYPE_CONST: + return `${newValue} = ${this.genLiteral(r.funcOrValue)}`; + + case RECORD_TYPE_PROPERTY: + if (r.contextIndex == 0) { // only the first property read can be a local + return propertyReadTemplate(r.name, context, newValue); + } else { + return assignmentTemplate(newValue, `${context}.${r.name}`); + } + + case RECORD_TYPE_INVOKE_METHOD: + return assignmentTemplate(newValue, `${context}.${r.name}(${args})`); + + case RECORD_TYPE_INVOKE_CLOSURE: + return assignmentTemplate(newValue, `${context}(${args})`); + + case RECORD_TYPE_PRIMITIVE_OP: + return assignmentTemplate(newValue, `${UTIL}.${r.name}(${args})`); + + case RECORD_TYPE_INTERPOLATE: + return assignmentTemplate(newValue, this.genInterpolation(r)); + + case RECORD_TYPE_KEYED_ACCESS: + var key = this.localNames[r.args[0]]; + return assignmentTemplate(newValue, `${context}[${key}]`); + + case RECORD_TYPE_INVOKE_FORMATTER: + return assignmentTemplate(newValue, `${FORMATTERS_ACCESSOR}.get("${r.name}")(${args})`); + + default: + throw new BaseException(`Unknown operation ${r.mode}`); + } + } + + genInterpolation(r:ProtoRecord):string{ + var res = ""; + for (var i = 0; i < r.args.length; ++i) { + res += this.genLiteral(r.fixedArgs[i]); + res += " + "; + res += this.localNames[r.args[i]]; + res += " + "; + } + res += this.genLiteral(r.fixedArgs[r.args.length]); + return res; + } + + genLiteral(value):string { + return JSON.stringify(value); + } + + genNotify(r):string{ + return r.lastInGroup ? notifyTemplate(r.selfIndex - 1) : ''; + } + + genArgs(r:ProtoRecord):string { + return r.args.map((arg) => this.localNames[arg]).join(", "); + } +} + + + + diff --git a/modules/change_detection/src/change_detection_util.js b/modules/change_detection/src/change_detection_util.js new file mode 100644 index 0000000000..c9a0ecb96c --- /dev/null +++ b/modules/change_detection/src/change_detection_util.js @@ -0,0 +1,138 @@ +import {isPresent, isBlank, BaseException, Type} from 'facade/lang'; +import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; +import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; +import {ArrayChanges} from './array_changes'; +import {KeyValueChanges} from './keyvalue_changes'; +import {ProtoRecord} from './proto_change_detector'; +import {ExpressionChangedAfterItHasBeenChecked} from './exceptions'; +import {ChangeRecord} from './interfaces'; + +export var uninitialized = new Object(); + +export class SimpleChange { + previousValue:any; + currentValue:any; + + constructor(previousValue:any, currentValue:any) { + this.previousValue = previousValue; + this.currentValue = currentValue; + } +} + +export class ChangeDetectionUtil { + static unitialized() { + return uninitialized; + } + + static arrayFn0() { return []; } + static arrayFn1(a1) { return [a1]; } + static arrayFn2(a1, a2) { return [a1, a2]; } + static arrayFn3(a1, a2, a3) { return [a1, a2, a3]; } + static arrayFn4(a1, a2, a3, a4) { return [a1, a2, a3, a4]; } + static arrayFn5(a1, a2, a3, a4, a5) { return [a1, a2, a3, a4, a5]; } + static arrayFn6(a1, a2, a3, a4, a5, a6) { return [a1, a2, a3, a4, a5, a6]; } + static arrayFn7(a1, a2, a3, a4, a5, a6, a7) { return [a1, a2, a3, a4, a5, a6, a7]; } + static arrayFn8(a1, a2, a3, a4, a5, a6, a7, a8) { return [a1, a2, a3, a4, a5, a6, a7, a8]; } + static arrayFn9(a1, a2, a3, a4, a5, a6, a7, a8, a9) { return [a1, a2, a3, a4, a5, a6, a7, a8, a9]; } + + static operation_negate(value) {return !value;} + static operation_add(left, right) {return left + right;} + static operation_subtract(left, right) {return left - right;} + static operation_multiply(left, right) {return left * right;} + static operation_divide(left, right) {return left / right;} + static operation_remainder(left, right) {return left % right;} + static operation_equals(left, right) {return left == right;} + static operation_not_equals(left, right) {return left != right;} + static operation_less_then(left, right) {return left < right;} + static operation_greater_then(left, right) {return left > right;} + static operation_less_or_equals_then(left, right) {return left <= right;} + static operation_greater_or_equals_then(left, right) {return left >= right;} + static operation_logical_and(left, right) {return left && right;} + static operation_logical_or(left, right) {return left || right;} + static cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;} + + static mapFn(keys:List) { + function buildMap(values) { + var res = StringMapWrapper.create(); + for(var i = 0; i < keys.length; ++i) { + StringMapWrapper.set(res, keys[i], values[i]); + } + return res; + } + + switch (keys.length) { + case 0: return () => []; + case 1: return (a1) => buildMap([a1]); + case 2: return (a1, a2) => buildMap([a1, a2]); + case 3: return (a1, a2, a3) => buildMap([a1, a2, a3]); + case 4: return (a1, a2, a3, a4) => buildMap([a1, a2, a3, a4]); + case 5: return (a1, a2, a3, a4, a5) => buildMap([a1, a2, a3, a4, a5]); + case 6: return (a1, a2, a3, a4, a5, a6) => buildMap([a1, a2, a3, a4, a5, a6]); + case 7: return (a1, a2, a3, a4, a5, a6, a7) => buildMap([a1, a2, a3, a4, a5, a6, a7]); + case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8]); + case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8, a9]); + default: throw new BaseException(`Does not support literal maps with more than 9 elements`); + } + } + + static keyedAccess(obj, args) { + return obj[args[0]]; + } + + static structuralCheck(self, context) { + if (isBlank(self) || self === uninitialized) { + if (ArrayChanges.supports(context)) { + self = new ArrayChanges(); + } else if (KeyValueChanges.supports(context)) { + self = new KeyValueChanges(); + } + } + + if (isBlank(context) || context === uninitialized) { + return new SimpleChange(null, null); + + } else { + if (ArrayChanges.supports(context)) { + + if (self.check(context)) { + return new SimpleChange(null, self); // TODO: don't wrap and return self instead + } else { + return null; + } + + } else if (KeyValueChanges.supports(context)) { + + if (self.check(context)) { + return new SimpleChange(null, self); // TODO: don't wrap and return self instead + } else { + return null; + } + + } else { + throw new BaseException(`Unsupported type (${context})`); + } + } + } + + static findContext(name:string, c){ + while (c instanceof ContextWithVariableBindings) { + if (c.hasBinding(name)) { + return c; + } + c = c.parent; + } + return c; + } + + static throwOnChange(proto:ProtoRecord, change) { + throw new ExpressionChangedAfterItHasBeenChecked(proto, change); + } + + static changeRecord(memento:any, change:any):ChangeRecord { + return new ChangeRecord(memento, change); + } + + static simpleChangeRecord(memento:any, previousValue:any, currentValue:any):ChangeRecord { + return new ChangeRecord(memento, new SimpleChange(previousValue, currentValue)); + } +} diff --git a/modules/change_detection/src/dynamic_change_detector.js b/modules/change_detection/src/dynamic_change_detector.js index b1988bc6a7..2484e92b1d 100644 --- a/modules/change_detection/src/dynamic_change_detector.js +++ b/modules/change_detection/src/dynamic_change_detector.js @@ -2,6 +2,9 @@ import {isPresent, isBlank, BaseException, FunctionWrapper} from 'facade/lang'; import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; +import {AbstractChangeDetector} from './abstract_change_detector'; +import {ChangeDetectionUtil, SimpleChange, uninitialized} from './change_detection_util'; + import {ArrayChanges} from './array_changes'; import {KeyValueChanges} from './keyvalue_changes'; @@ -12,76 +15,37 @@ import { RECORD_TYPE_INVOKE_METHOD, RECORD_TYPE_CONST, RECORD_TYPE_INVOKE_CLOSURE, - RECORD_TYPE_INVOKE_PURE_FUNCTION, + RECORD_TYPE_PRIMITIVE_OP, + RECORD_TYPE_KEYED_ACCESS, RECORD_TYPE_INVOKE_FORMATTER, RECORD_TYPE_STRUCTURAL_CHECK, + RECORD_TYPE_INTERPOLATE, ProtoChangeDetector } from './proto_change_detector'; -import {ChangeDetector, ChangeRecord, ChangeDispatcher} from './interfaces'; +import {ChangeDetector, ChangeDispatcher} from './interfaces'; import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions'; -var _uninitialized = new Object(); - -class SimpleChange { - previousValue:any; - currentValue:any; - - constructor(previousValue:any, currentValue:any) { - this.previousValue = previousValue; - this.currentValue = currentValue; - } -} - -export class DynamicChangeDetector extends ChangeDetector { +export class DynamicChangeDetector extends AbstractChangeDetector { dispatcher:any; formatters:Map; - children:List; values:List; protos:List; - parent:ChangeDetector; constructor(dispatcher:any, formatters:Map, protoRecords:List) { + super(); this.dispatcher = dispatcher; this.formatters = formatters; this.values = ListWrapper.createFixedSize(protoRecords.length + 1); - ListWrapper.fill(this.values, _uninitialized); + ListWrapper.fill(this.values, uninitialized); this.protos = protoRecords; - - this.children = []; - } - - addChild(cd:ChangeDetector) { - ListWrapper.push(this.children, cd); - cd.parent = this; - } - - removeChild(cd:ChangeDetector) { - ListWrapper.remove(this.children, cd); - } - - remove() { - this.parent.removeChild(this); } setContext(context:any) { this.values[0] = context; } - detectChanges() { - this._detectChanges(false); - } - - checkNoChanges() { - this._detectChanges(true); - } - - _detectChanges(throwOnChange:boolean) { - this._detectChangesInRecords(throwOnChange); - this._detectChangesInChildren(throwOnChange); - } - - _detectChangesInRecords(throwOnChange:boolean) { + detectChangesInRecords(throwOnChange:boolean) { var protos:List = this.protos; var updatedRecords = null; @@ -91,21 +55,16 @@ export class DynamicChangeDetector extends ChangeDetector { var proto:ProtoRecord = protos[i]; var change = this._check(proto); - // only when the terminal record, which ends a binding, changes - // we need to add it to a list of changed records - if (isPresent(change) && proto.terminal) { - if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(proto, change); + if (isPresent(change)) { currentGroup = proto.groupMemento; updatedRecords = this._addRecord(updatedRecords, proto, change); } - if (isPresent(updatedRecords)) { - var lastRecordOfCurrentGroup = protos.length == i + 1 || - currentGroup !== protos[i + 1].groupMemento; - if (lastRecordOfCurrentGroup) { - this.dispatcher.onRecordChange(currentGroup, updatedRecords); - updatedRecords = null; - } + if (proto.lastInGroup && isPresent(updatedRecords)) { + if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, updatedRecords[0]); + + this.dispatcher.onRecordChange(currentGroup, updatedRecords); + updatedRecords = null; } } } @@ -128,7 +87,11 @@ export class DynamicChangeDetector extends ChangeDetector { if (!isSame(prevValue, currValue)) { this._writeSelf(proto, currValue); - return new SimpleChange(prevValue === _uninitialized ? null : prevValue, currValue); + if (proto.lastInBinding) { + return new SimpleChange(prevValue, currValue); + } else { + return null; + } } else { return null; } @@ -144,23 +107,28 @@ export class DynamicChangeDetector extends ChangeDetector { case RECORD_TYPE_PROPERTY: var context = this._readContext(proto); - while (context instanceof ContextWithVariableBindings) { - if (context.hasBinding(proto.name)) { - return context.get(proto.name); - } - context = context.parent; + var c = ChangeDetectionUtil.findContext(proto.name, context); + if (c instanceof ContextWithVariableBindings) { + return c.get(proto.name); + } else { + var propertyGetter:Function = proto.funcOrValue; + return propertyGetter(c); } - var propertyGetter:Function = proto.funcOrValue; - return propertyGetter(context); + break; case RECORD_TYPE_INVOKE_METHOD: var methodInvoker:Function = proto.funcOrValue; return methodInvoker(this._readContext(proto), this._readArgs(proto)); + case RECORD_TYPE_KEYED_ACCESS: + var arg = this._readArgs(proto)[0]; + return this._readContext(proto)[arg]; + case RECORD_TYPE_INVOKE_CLOSURE: return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto)); - case RECORD_TYPE_INVOKE_PURE_FUNCTION: + case RECORD_TYPE_INTERPOLATE: + case RECORD_TYPE_PRIMITIVE_OP: return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto)); case RECORD_TYPE_INVOKE_FORMATTER: @@ -176,39 +144,16 @@ export class DynamicChangeDetector extends ChangeDetector { var self = this._readSelf(proto); var context = this._readContext(proto); - if (isBlank(self) || self === _uninitialized) { - if (ArrayChanges.supports(context)) { - self = new ArrayChanges(); - } else if (KeyValueChanges.supports(context)) { - self = new KeyValueChanges(); - } + var change = ChangeDetectionUtil.structuralCheck(self, context); + if (isPresent(change)) { + this._writeSelf(proto, change.currentValue); } - - if (ArrayChanges.supports(context)) { - if (self.check(context)) { - this._writeSelf(proto, self); - return new SimpleChange(null, self); // TODO: don't wrap and return self instead - } - - } else if (KeyValueChanges.supports(context)) { - if (self.check(context)) { - this._writeSelf(proto, self); - return new SimpleChange(null, self); // TODO: don't wrap and return self instead - } - - } else if (context == null) { - this._writeSelf(proto, null); - return new SimpleChange(null, null); - - } else { - throw new BaseException(`Unsupported type (${context})`); - } - + return change; } _addRecord(updatedRecords:List, proto:ProtoRecord, change):List { // we can use a pool of change records not to create extra garbage - var record = new ChangeRecord(proto.bindingMemento, change); + var record = ChangeDetectionUtil.changeRecord(proto.bindingMemento, change); if (isBlank(updatedRecords)) { updatedRecords = _singleElementList; updatedRecords[0] = record; @@ -222,23 +167,16 @@ export class DynamicChangeDetector extends ChangeDetector { return updatedRecords; } - _detectChangesInChildren(throwOnChange:boolean) { - var children = this.children; - for(var i = 0; i < children.length; ++i) { - children[i]._detectChanges(throwOnChange); - } - } - _readContext(proto:ProtoRecord) { return this.values[proto.contextIndex]; } _readSelf(proto:ProtoRecord) { - return this.values[proto.record_type_selfIndex]; + return this.values[proto.selfIndex]; } _writeSelf(proto:ProtoRecord, value) { - this.values[proto.record_type_selfIndex] = value; + this.values[proto.selfIndex] = value; } _readArgs(proto:ProtoRecord) { diff --git a/modules/change_detection/src/proto_change_detector.js b/modules/change_detection/src/proto_change_detector.js index 7db0cae774..12e5b74cbf 100644 --- a/modules/change_detection/src/proto_change_detector.js +++ b/modules/change_detection/src/proto_change_detector.js @@ -1,4 +1,4 @@ -import {isPresent, isBlank, BaseException} from 'facade/lang'; +import {isPresent, isBlank, BaseException, Type, isString} from 'facade/lang'; import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; import { @@ -24,55 +24,118 @@ import { } from './parser/ast'; import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; -import {ChangeDispatcher, ChangeDetector} from './interfaces'; +import {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces'; +import {ChangeDetectionUtil} from './change_detection_util'; import {DynamicChangeDetector} from './dynamic_change_detector'; +import {ChangeDetectorJITGenerator} from './change_detection_jit_generator'; + +import {ArrayChanges} from './array_changes'; +import {KeyValueChanges} from './keyvalue_changes'; export const RECORD_TYPE_SELF = 0; -export const RECORD_TYPE_PROPERTY = 1; -export const RECORD_TYPE_INVOKE_METHOD = 2; -export const RECORD_TYPE_CONST = 3; -export const RECORD_TYPE_INVOKE_CLOSURE = 4; -export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 5; -export const RECORD_TYPE_INVOKE_FORMATTER = 6; -export const RECORD_TYPE_STRUCTURAL_CHECK = 10; +export const RECORD_TYPE_CONST = 1; +export const RECORD_TYPE_PRIMITIVE_OP = 2; +export const RECORD_TYPE_PROPERTY = 3; +export const RECORD_TYPE_INVOKE_METHOD = 4; +export const RECORD_TYPE_INVOKE_CLOSURE = 5; +export const RECORD_TYPE_KEYED_ACCESS = 6; +export const RECORD_TYPE_INVOKE_FORMATTER = 7; +export const RECORD_TYPE_STRUCTURAL_CHECK = 8; +export const RECORD_TYPE_INTERPOLATE = 9; export class ProtoRecord { mode:number; name:string; funcOrValue:any; args:List; + fixedArgs:List; contextIndex:number; - record_type_selfIndex:number; + selfIndex:number; bindingMemento:any; groupMemento:any; - terminal:boolean; + lastInBinding:boolean; + lastInGroup:boolean; expressionAsString:string; constructor(mode:number, name:string, funcOrValue, args:List, + fixedArgs:List, contextIndex:number, - record_type_selfIndex:number, + selfIndex:number, bindingMemento:any, groupMemento:any, - terminal:boolean, expressionAsString:string) { this.mode = mode; this.name = name; this.funcOrValue = funcOrValue; this.args = args; + this.fixedArgs = fixedArgs; this.contextIndex = contextIndex; - this.record_type_selfIndex = record_type_selfIndex; + this.selfIndex = selfIndex; this.bindingMemento = bindingMemento; this.groupMemento = groupMemento; - this.terminal = terminal; + this.lastInBinding = false; + this.lastInGroup = false; this.expressionAsString = expressionAsString; } } -export class ProtoChangeDetector { +export class ProtoChangeDetector { + addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false){} + instantiate(dispatcher:any, formatters:Map):ChangeDetector{ + return null; + } +} + +export class DynamicProtoChangeDetector extends ProtoChangeDetector { + _recordBuilder:ProtoRecordBuilder; + + constructor() { + this._recordBuilder = new ProtoRecordBuilder(); + } + + addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) { + this._recordBuilder.addAst(ast, bindingMemento, groupMemento, structural); + } + + instantiate(dispatcher:any, formatters:Map) { + var records = this._recordBuilder.records; + return new DynamicChangeDetector(dispatcher, formatters, records); + } +} + +var _jitProtoChangeDetectorClassCounter:number = 0; +export class JitProtoChangeDetector extends ProtoChangeDetector { + _factory:Function; + _recordBuilder:ProtoRecordBuilder; + + constructor() { + this._recordBuilder = new ProtoRecordBuilder(); + } + + addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) { + this._recordBuilder.addAst(ast, bindingMemento, groupMemento, structural); + } + + instantiate(dispatcher:any, formatters:Map) { + this._createFactoryIfNecessary(); + return this._factory(dispatcher, formatters); + } + + _createFactoryIfNecessary() { + if (isBlank(this._factory)) { + var c = _jitProtoChangeDetectorClassCounter++; + var records = this._recordBuilder.records; + var typeName = `ChangeDetector${c}`; + this._factory = new ChangeDetectorJITGenerator(typeName, records).generate(); + } + } +} + +class ProtoRecordBuilder { records:List; constructor() { @@ -82,23 +145,23 @@ export class ProtoChangeDetector { addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) { if (structural) ast = new Structural(ast); - var c = new ProtoOperationsCreator(bindingMemento, groupMemento, - this.records.length, ast.toString()); - ast.visit(c); - - if (! ListWrapper.isEmpty(c.protoRecords)) { - var last = ListWrapper.last(c.protoRecords); - last.terminal = true; - this.records = ListWrapper.concat(this.records, c.protoRecords); + var last = ListWrapper.last(this.records); + if (isPresent(last) && last.groupMemento == groupMemento) { + last.lastInGroup = false; } - } - instantiate(dispatcher:any, formatters:Map) { - return new DynamicChangeDetector(dispatcher, formatters, this.records); + var pr = _ConvertAstIntoProtoRecords.convert(ast, bindingMemento, groupMemento, this.records.length); + if (! ListWrapper.isEmpty(pr)) { + var last = ListWrapper.last(pr); + last.lastInBinding = true; + last.lastInGroup = true; + + this.records = ListWrapper.concat(this.records, pr); + } } } -class ProtoOperationsCreator { +class _ConvertAstIntoProtoRecords { protoRecords:List; bindingMemento:any; groupMemento:any; @@ -113,77 +176,89 @@ class ProtoOperationsCreator { this.expressionAsString = expressionAsString; } + static convert(ast:AST, bindingMemento:any, groupMemento:any, contextIndex:number) { + var c = new _ConvertAstIntoProtoRecords(bindingMemento, groupMemento, contextIndex, ast.toString()); + ast.visit(c); + return c.protoRecords; + } + visitImplicitReceiver(ast:ImplicitReceiver) { return 0; } visitInterpolation(ast:Interpolation) { var args = this._visitAll(ast.expressions); - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Interpolate()", _interpolationFn(ast.strings), args, 0); + return this._addRecord(RECORD_TYPE_INTERPOLATE, "interpolate", _interpolationFn(ast.strings), + args, ast.strings, 0); } visitLiteralPrimitive(ast:LiteralPrimitive) { - return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0); + return this._addRecord(RECORD_TYPE_CONST, "literal", ast.value, [], null, 0); } visitAccessMember(ast:AccessMember) { var receiver = ast.receiver.visit(this); - return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], receiver); + return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], null, receiver); } visitFormatter(ast:Formatter) { - return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), 0); + return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), null, 0); } visitMethodCall(ast:MethodCall) { var receiver = ast.receiver.visit(this); var args = this._visitAll(ast.args); - return this._addRecord(RECORD_TYPE_INVOKE_METHOD, ast.name, ast.fn, args, receiver); + return this._addRecord(RECORD_TYPE_INVOKE_METHOD, ast.name, ast.fn, args, null, receiver); } visitFunctionCall(ast:FunctionCall) { var target = ast.target.visit(this); var args = this._visitAll(ast.args); - return this._addRecord(RECORD_TYPE_INVOKE_CLOSURE, null, null, args, target); + return this._addRecord(RECORD_TYPE_INVOKE_CLOSURE, "closure", null, args, null, target); } visitLiteralArray(ast:LiteralArray) { - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Array()", _arrayFn(ast.expressions.length), - this._visitAll(ast.expressions), 0); + var primitiveName = `arrayFn${ast.expressions.length}`; + return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, primitiveName, _arrayFn(ast.expressions.length), + this._visitAll(ast.expressions), null, 0); } visitLiteralMap(ast:LiteralMap) { - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Map()", _mapFn(ast.keys, ast.values.length), - this._visitAll(ast.values), 0); + return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, _mapPrimitiveName(ast.keys), + ChangeDetectionUtil.mapFn(ast.keys), this._visitAll(ast.values), null, 0); } visitBinary(ast:Binary) { var left = ast.left.visit(this); var right = ast.right.visit(this); - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, ast.operation, _operationToFunction(ast.operation), [left, right], 0); + return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, _operationToPrimitiveName(ast.operation), + _operationToFunction(ast.operation), [left, right], null, 0); } visitPrefixNot(ast:PrefixNot) { var exp = ast.expression.visit(this) - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "-", _operation_negate, [exp], 0); + return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, "operation_negate", + ChangeDetectionUtil.operation_negate, [exp], null, 0); } visitConditional(ast:Conditional) { var c = ast.condition.visit(this); var t = ast.trueExp.visit(this); var f = ast.falseExp.visit(this); - return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "?:", _cond, [c,t,f], 0); + return this._addRecord(RECORD_TYPE_PRIMITIVE_OP, "cond", + ChangeDetectionUtil.cond, [c,t,f], null, 0); } visitStructural(ast:Structural) { var value = ast.value.visit(this); - return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "record_type_structural_check", null, [], value); + return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "structural", null, [], null, value); } visitKeyedAccess(ast:KeyedAccess) { var obj = ast.obj.visit(this); var key = ast.key.visit(this); - return this._addRecord(RECORD_TYPE_INVOKE_METHOD, "[]", _keyedAccess, [key], obj); + return this._addRecord(RECORD_TYPE_KEYED_ACCESS, "keyedAccess", + ChangeDetectionUtil.keyedAccess, [key], null, obj); } _visitAll(asts:List) { @@ -194,111 +269,93 @@ class ProtoOperationsCreator { return res; } - _addRecord(type, name, funcOrValue, args, context) { - var record_type_selfIndex = ++ this.contextIndex; + _addRecord(type, name, funcOrValue, args, fixedArgs, context) { + var selfIndex = ++ this.contextIndex; ListWrapper.push(this.protoRecords, - new ProtoRecord(type, name, funcOrValue, args, context, record_type_selfIndex, - this.bindingMemento, this.groupMemento, false, this.expressionAsString)); - return record_type_selfIndex; + new ProtoRecord(type, name, funcOrValue, args, fixedArgs, context, selfIndex, + this.bindingMemento, this.groupMemento, this.expressionAsString)); + return selfIndex; } } -function _arrayFn(length:int) { - switch (length) { - case 0: return () => []; - case 1: return (a1) => [a1]; - case 2: return (a1, a2) => [a1, a2]; - case 3: return (a1, a2, a3) => [a1, a2, a3]; - case 4: return (a1, a2, a3, a4) => [a1, a2, a3, a4]; - case 5: return (a1, a2, a3, a4, a5) => [a1, a2, a3, a4, a5]; - case 6: return (a1, a2, a3, a4, a5, a6) => [a1, a2, a3, a4, a5, a6]; - case 7: return (a1, a2, a3, a4, a5, a6, a7) => [a1, a2, a3, a4, a5, a6, a7]; - case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => [a1, a2, a3, a4, a5, a6, a7, a8]; - case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => [a1, a2, a3, a4, a5, a6, a7, a8, a9]; - default: throw new BaseException(`Does not support literal arrays with more than 9 elements`); - } -} - -function _mapFn(keys:List, length:int) { - function buildMap(values) { - var res = StringMapWrapper.create(); - for(var i = 0; i < keys.length; ++i) { - StringMapWrapper.set(res, keys[i], values[i]); - } - return res; - } +function _arrayFn(length:number):Function { switch (length) { - case 0: return () => []; - case 1: return (a1) => buildMap([a1]); - case 2: return (a1, a2) => buildMap([a1, a2]); - case 3: return (a1, a2, a3) => buildMap([a1, a2, a3]); - case 4: return (a1, a2, a3, a4) => buildMap([a1, a2, a3, a4]); - case 5: return (a1, a2, a3, a4, a5) => buildMap([a1, a2, a3, a4, a5]); - case 6: return (a1, a2, a3, a4, a5, a6) => buildMap([a1, a2, a3, a4, a5, a6]); - case 7: return (a1, a2, a3, a4, a5, a6, a7) => buildMap([a1, a2, a3, a4, a5, a6, a7]); - case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8]); - case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8, a9]); + case 0: return ChangeDetectionUtil.arrayFn0; + case 1: return ChangeDetectionUtil.arrayFn1; + case 2: return ChangeDetectionUtil.arrayFn2; + case 3: return ChangeDetectionUtil.arrayFn3; + case 4: return ChangeDetectionUtil.arrayFn4; + case 5: return ChangeDetectionUtil.arrayFn5; + case 6: return ChangeDetectionUtil.arrayFn6; + case 7: return ChangeDetectionUtil.arrayFn7; + case 8: return ChangeDetectionUtil.arrayFn8; + case 9: return ChangeDetectionUtil.arrayFn9; default: throw new BaseException(`Does not support literal maps with more than 9 elements`); } } +function _mapPrimitiveName(keys:List) { + var stringifiedKeys = ListWrapper.join( + ListWrapper.map(keys, (k) => isString(k) ? `"${k}"` : `${k}`), + ", "); + return `mapFn([${stringifiedKeys}])`; +} + +function _operationToPrimitiveName(operation:string):string { + switch(operation) { + case '+' : return "operation_add"; + case '-' : return "operation_subtract"; + case '*' : return "operation_multiply"; + case '/' : return "operation_divide"; + case '%' : return "operation_remainder"; + case '==' : return "operation_equals"; + case '!=' : return "operation_not_equals"; + case '<' : return "operation_less_then"; + case '>' : return "operation_greater_then"; + case '<=' : return "operation_less_or_equals_then"; + case '>=' : return "operation_greater_or_equals_then"; + case '&&' : return "operation_logical_and"; + case '||' : return "operation_logical_or"; + default: throw new BaseException(`Unsupported operation ${operation}`); + } +} + function _operationToFunction(operation:string):Function { switch(operation) { - case '+' : return _operation_add; - case '-' : return _operation_subtract; - case '*' : return _operation_multiply; - case '/' : return _operation_divide; - case '%' : return _operation_remainder; - case '==' : return _operation_equals; - case '!=' : return _operation_not_equals; - case '<' : return _operation_less_then; - case '>' : return _operation_greater_then; - case '<=' : return _operation_less_or_equals_then; - case '>=' : return _operation_greater_or_equals_then; - case '&&' : return _operation_logical_and; - case '||' : return _operation_logical_or; + case '+' : return ChangeDetectionUtil.operation_add; + case '-' : return ChangeDetectionUtil.operation_subtract; + case '*' : return ChangeDetectionUtil.operation_multiply; + case '/' : return ChangeDetectionUtil.operation_divide; + case '%' : return ChangeDetectionUtil.operation_remainder; + case '==' : return ChangeDetectionUtil.operation_equals; + case '!=' : return ChangeDetectionUtil.operation_not_equals; + case '<' : return ChangeDetectionUtil.operation_less_then; + case '>' : return ChangeDetectionUtil.operation_greater_then; + case '<=' : return ChangeDetectionUtil.operation_less_or_equals_then; + case '>=' : return ChangeDetectionUtil.operation_greater_or_equals_then; + case '&&' : return ChangeDetectionUtil.operation_logical_and; + case '||' : return ChangeDetectionUtil.operation_logical_or; default: throw new BaseException(`Unsupported operation ${operation}`); } } -function _operation_negate(value) {return !value;} -function _operation_add(left, right) {return left + right;} -function _operation_subtract(left, right) {return left - right;} -function _operation_multiply(left, right) {return left * right;} -function _operation_divide(left, right) {return left / right;} -function _operation_remainder(left, right) {return left % right;} -function _operation_equals(left, right) {return left == right;} -function _operation_not_equals(left, right) {return left != right;} -function _operation_less_then(left, right) {return left < right;} -function _operation_greater_then(left, right) {return left > right;} -function _operation_less_or_equals_then(left, right) {return left <= right;} -function _operation_greater_or_equals_then(left, right) {return left >= right;} -function _operation_logical_and(left, right) {return left && right;} -function _operation_logical_or(left, right) {return left || right;} -function _cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;} - -function _keyedAccess(obj, args) { - return obj[args[0]]; -} - function s(v) { return isPresent(v) ? '' + v : ''; } function _interpolationFn(strings:List) { var length = strings.length; - var i = -1; - var c0 = length > ++i ? strings[i] : null; - var c1 = length > ++i ? strings[i] : null; - var c2 = length > ++i ? strings[i] : null; - var c3 = length > ++i ? strings[i] : null; - var c4 = length > ++i ? strings[i] : null; - var c5 = length > ++i ? strings[i] : null; - var c6 = length > ++i ? strings[i] : null; - var c7 = length > ++i ? strings[i] : null; - var c8 = length > ++i ? strings[i] : null; - var c9 = length > ++i ? strings[i] : null; + var c0 = length > 0 ? strings[0] : null; + var c1 = length > 1 ? strings[1] : null; + var c2 = length > 2 ? strings[2] : null; + var c3 = length > 3 ? strings[3] : null; + var c4 = length > 4 ? strings[4] : null; + var c5 = length > 5 ? strings[5] : null; + var c6 = length > 6 ? strings[6] : null; + var c7 = length > 7 ? strings[7] : null; + var c8 = length > 8 ? strings[8] : null; + var c9 = length > 9 ? strings[9] : null; switch (length - 1) { case 1: return (a1) => c0 + s(a1) + c1; case 2: return (a1, a2) => c0 + s(a1) + c1 + s(a2) + c2; @@ -311,4 +368,4 @@ function _interpolationFn(strings:List) { case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7 + s(a8) + c8 + s(a9) + c9; default: throw new BaseException(`Does not support more than 9 expressions`); } -} +} \ No newline at end of file diff --git a/modules/change_detection/test/change_detection_spec.js b/modules/change_detection/test/change_detection_spec.js index cbfae7f09e..905f665be6 100644 --- a/modules/change_detection/test/change_detection_spec.js +++ b/modules/change_detection/test/change_detection_spec.js @@ -1,4 +1,4 @@ -import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, IS_DARTIUM} from 'test_lib/test_lib'; import {isPresent, isBlank, isJsObject, BaseException, FunctionWrapper} from 'facade/lang'; import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; @@ -8,398 +8,442 @@ import {Lexer} from 'change_detection/parser/lexer'; import {reflector} from 'reflection/reflection'; import {arrayChangesAsString, kvChangesAsString} from './util'; -import {ProtoChangeDetector, ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, - ContextWithVariableBindings} +import {ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, ContextWithVariableBindings} from 'change_detection/change_detection'; +import {JitProtoChangeDetector, DynamicProtoChangeDetector} from 'change_detection/proto_change_detector'; + + export function main() { - function ast(exp:string, location:string = 'location') { - var parser = new Parser(new Lexer()); - return parser.parseBinding(exp, location); - } + describe("change detection", () => { + StringMapWrapper.forEach( + { "dynamic": () => new DynamicProtoChangeDetector(), + "JIT": () => new JitProtoChangeDetector() + }, (createProtoChangeDetector, name) => { - function createChangeDetector(memo:string, exp:string, context = null, formatters = null, - structural = false) { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast(exp), memo, memo, structural); + if (name == "JIT" && IS_DARTIUM) return; - var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, formatters); - cd.setContext(context); - - return {"changeDetector" : cd, "dispatcher" : dispatcher}; - } - - function executeWatch(memo:string, exp:string, context = null, formatters = null, - content = false) { - var res = createChangeDetector(memo, exp, context, formatters, content); - res["changeDetector"].detectChanges(); - return res["dispatcher"].log; - } - - describe('change_detection', () => { - it('should do simple watching', () => { - var person = new Person("misko"); - var c = createChangeDetector('name', 'name', person); - var cd = c["changeDetector"]; - var dispatcher = c["dispatcher"]; - - cd.detectChanges(); - expect(dispatcher.log).toEqual(['name=misko']); - - dispatcher.clear(); - cd.detectChanges(); - expect(dispatcher.log).toEqual([]); - - person.name = "Misko"; - cd.detectChanges(); - expect(dispatcher.log).toEqual(['name=Misko']); - }); - - it('should report all changes on the first run including uninitialized values', () => { - var uninit = new Uninitialized(); - var c = createChangeDetector('value', 'value', uninit); - var cd = c["changeDetector"]; - var dispatcher = c["dispatcher"]; - - cd.detectChanges(); - expect(dispatcher.log).toEqual(['value=null']); - }); - - it("should support literals", () => { - expect(executeWatch('const', '10')).toEqual(['const=10']); - expect(executeWatch('const', '"str"')).toEqual(['const=str']); - }); - - it('simple chained property access', () => { - var address = new Address('Grenoble'); - var person = new Person('Victor', address); - - expect(executeWatch('address.city', 'address.city', person)) - .toEqual(['address.city=Grenoble']); - }); - - it("should support method calls", () => { - var person = new Person('Victor'); - expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']); - }); - - it("should support function calls", () => { - var td = new TestData(() => (a) => a); - expect(executeWatch('value', 'a()(99)', td)).toEqual(['value=99']); - }); - - it("should support chained method calls", () => { - var person = new Person('Victor'); - var td = new TestData(person); - expect(executeWatch('m', 'a.sayHi("Jim")', td)).toEqual(['m=Hi, Jim']); - }); - - it("should support literal array", () => { - var c = createChangeDetector('array', '[1,2]'); - c["changeDetector"].detectChanges(); - expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]); - - c = createChangeDetector('array', '[1,a]', new TestData(2)); - c["changeDetector"].detectChanges(); - expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]); - }); - - it("should support literal maps", () => { - var c = createChangeDetector('map', '{z:1}'); - c["changeDetector"].detectChanges(); - expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1); - - c = createChangeDetector('map', '{z:a}', new TestData(1)); - c["changeDetector"].detectChanges(); - expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1); - }); - - it("should support binary operations", () => { - expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']); - expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']); - - expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']); - expect(executeWatch('exp', '10 / 2')).toEqual([`exp=${5.0}`]); //dart exp=5.0, js exp=5 - expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']); - - expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']); - expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']); - - expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']); - expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']); - - expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']); - expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']); - - expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']); - expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']); - expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']); - - expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']); - expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']); - expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']); - - expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']); - expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']); - - expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']); - expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']); - }); - - it("should support negate", () => { - expect(executeWatch('exp', '!true')).toEqual(['exp=false']); - expect(executeWatch('exp', '!!true')).toEqual(['exp=true']); - }); - - it("should support conditionals", () => { - expect(executeWatch('m', '1 < 2 ? 1 : 2')).toEqual(['m=1']); - expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']); - }); - - describe("keyed access", () => { - it("should support accessing a list item", () => { - expect(executeWatch('array[0]', '["foo", "bar"][0]')).toEqual(['array[0]=foo']); - }); - - it("should support accessing a map item", () => { - expect(executeWatch('map[foo]', '{"foo": "bar"}["foo"]')).toEqual(['map[foo]=bar']); - }); - }); - - it("should support formatters", () => { - var formatters = MapWrapper.createFromPairs([ - ['uppercase', (v) => v.toUpperCase()], - ['wrap', (v, before, after) => `${before}${v}${after}`]]); - expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']); - expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']); - }); - - describe("group changes", () => { - it("should notify the dispatcher when a group of records changes", () => { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast("1 + 2"), "memo", 1); - pcd.addAst(ast("10 + 20"), "memo", 1); - pcd.addAst(ast("100 + 200"), "memo2", 2); - - var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, null); - - cd.detectChanges(); - - expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]); - }); - - it("should update every instance of a group individually", () => { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast("1 + 2"), "memo", "memo"); - - var dispatcher = new TestDispatcher(); - var cd = new DynamicChangeDetector(dispatcher, null, []); - cd.addChild(pcd.instantiate(dispatcher, null)); - cd.addChild(pcd.instantiate(dispatcher, null)); - - cd.detectChanges(); - - expect(dispatcher.loggedValues).toEqual([[3], [3]]); - }); - - it("should notify the dispatcher before switching to the next group", () => { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast("a()"), "a", 1); - pcd.addAst(ast("b()"), "b", 2); - pcd.addAst(ast("c()"), "c", 2); - - var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, null); - - var tr = new TestRecord(); - tr.a = () => {dispatcher.logValue('InvokeA'); return 'a'}; - tr.b = () => {dispatcher.logValue('InvokeB'); return 'b'}; - tr.c = () => {dispatcher.logValue('InvokeC'); return 'c'}; - cd.setContext(tr); - - cd.detectChanges(); - - expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]); - }); - }); - - describe("enforce no new changes", () => { - it("should throw when a record gets changed after it has been checked", () => { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast("a"), "a", 1); - - var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, null); - cd.setContext(new TestData('value')); - - expect(() => { - cd.checkNoChanges(); - }).toThrowError(new RegExp("Expression 'a in location' has changed after it was checked")); - }); - }); - - describe("error handling", () => { - it("should wrap exceptions into ChangeDetectionError", () => { - var pcd = new ProtoChangeDetector(); - pcd.addAst(ast('invalidProp', 'someComponent'), "a", 1); - - var cd = pcd.instantiate(new TestDispatcher(), null); - cd.setContext(null); - - try { - cd.detectChanges(); - throw new BaseException("fail"); - } catch (e) { - expect(e).toBeAnInstanceOf(ChangeDetectionError); - expect(e.location).toEqual("invalidProp in someComponent"); + function ast(exp:string, location:string = 'location') { + var parser = new Parser(new Lexer()); + return parser.parseBinding(exp, location); } - }); - }); - describe("collections", () => { - it("should support null values", () => { - var context = new TestData(null); + function createChangeDetector(memo:string, exp:string, context = null, formatters = null, + structural = false) { + var pcd = createProtoChangeDetector(); + pcd.addAst(ast(exp), memo, memo, structural); - var c = createChangeDetector('a', 'a', context, null, true); - var cd = c["changeDetector"]; - var dispatcher = c["dispatcher"]; + var dispatcher = new TestDispatcher(); + var cd = pcd.instantiate(dispatcher, formatters); + cd.setContext(context); - cd.detectChanges(); - expect(dispatcher.log).toEqual(['a=null']); - dispatcher.clear(); + return {"changeDetector" : cd, "dispatcher" : dispatcher}; + } - //cd.detectChanges(); - //expect(dispatcher.log).toEqual([]); + function executeWatch(memo:string, exp:string, context = null, formatters = null, + content = false) { + var res = createChangeDetector(memo, exp, context, formatters, content); + res["changeDetector"].detectChanges(); + return res["dispatcher"].log; + } - context.a = [0]; - cd.detectChanges(); + describe(`${name} change detection`, () => { + it('should do simple watching', () => { + var person = new Person("misko"); + var c = createChangeDetector('name', 'name', person); + var cd = c["changeDetector"]; + var dispatcher = c["dispatcher"]; - expect(dispatcher.log).toEqual(["a=" + - arrayChangesAsString({ - collection: ['0[null->0]'], - additions: ['0[null->0]'] - }) - ]); - dispatcher.clear(); + cd.detectChanges(); + expect(dispatcher.log).toEqual(['name=misko']); + dispatcher.clear(); - context.a = null; - cd.detectChanges(); - expect(dispatcher.log).toEqual(['a=null']); - }); + person.name = "Misko"; + cd.detectChanges(); + expect(dispatcher.log).toEqual(['name=Misko']); + }); - describe("list", () => { - it("should support list changes", () => { - var context = new TestData([1, 2]); + it('should report all changes on the first run including uninitialized values', () => { + expect(executeWatch('value', 'value', new Uninitialized())).toEqual(['value=null']); + }); - expect(executeWatch("a", "a", context, null, true)) - .toEqual(["a=" + - arrayChangesAsString({ - collection: ['1[null->0]', '2[null->1]'], - additions: ['1[null->0]', '2[null->1]'] - })]); - }); + it('should report all changes on the first run including null values', () => { + var td = new TestData(null); + expect(executeWatch('a', 'a', td)).toEqual(['a=null']); + }); - it("should handle reference changes", () => { - var context = new TestData([1, 2]); - var objs = createChangeDetector("a", "a", context, null, true); - var cd = objs["changeDetector"]; - var dispatcher = objs["dispatcher"]; - cd.detectChanges(); - dispatcher.clear(); + it("should support literals", () => { + expect(executeWatch('const', '10')).toEqual(['const=10']); + expect(executeWatch('const', '"str"')).toEqual(['const=str']); + expect(executeWatch('const', '"a\n\nb"')).toEqual(['const=a\n\nb']); + }); - context.a = [2, 1]; - cd.detectChanges(); - expect(dispatcher.log).toEqual(["a=" + - arrayChangesAsString({ - collection: ['2[1->0]', '1[0->1]'], - previous: ['1[0->1]', '2[1->0]'], - moves: ['2[1->0]', '1[0->1]'] - })]); - }); - }); + it('simple chained property access', () => { + var address = new Address('Grenoble'); + var person = new Person('Victor', address); - describe("map", () => { - it("should support map changes", () => { - var map = MapWrapper.create(); - MapWrapper.set(map, "foo", "bar"); - var context = new TestData(map); - expect(executeWatch("a", "a", context, null, true)) - .toEqual(["a=" + - kvChangesAsString({ - map: ['foo[null->bar]'], - additions: ['foo[null->bar]'] - })]); - }); + expect(executeWatch('address.city', 'address.city', person)) + .toEqual(['address.city=Grenoble']); + }); - it("should handle reference changes", () => { - var map = MapWrapper.create(); - MapWrapper.set(map, "foo", "bar"); - var context = new TestData(map); - var objs = createChangeDetector("a", "a", context, null, true); - var cd = objs["changeDetector"]; - var dispatcher = objs["dispatcher"]; - cd.detectChanges(); - dispatcher.clear(); + it("should support method calls", () => { + var person = new Person('Victor'); + expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']); + }); - context.a = MapWrapper.create(); - MapWrapper.set(context.a, "bar", "foo"); - cd.detectChanges(); - expect(dispatcher.log).toEqual(["a=" + - kvChangesAsString({ - map: ['bar[null->foo]'], - previous: ['foo[bar->null]'], - additions: ['bar[null->foo]'], - removals: ['foo[bar->null]'] - })]); - }); - }); + it("should support function calls", () => { + var td = new TestData(() => (a) => a); + expect(executeWatch('value', 'a()(99)', td)).toEqual(['value=99']); + }); - if (isJsObject({})) { - describe("js objects", () => { - it("should support object changes", () => { - var map = {"foo": "bar"}; - var context = new TestData(map); - expect(executeWatch("a", "a", context, null, true)) - .toEqual(["a=" + - kvChangesAsString({ - map: ['foo[null->bar]'], - additions: ['foo[null->bar]'] - })]); + it("should support chained method calls", () => { + var person = new Person('Victor'); + var td = new TestData(person); + expect(executeWatch('m', 'a.sayHi("Jim")', td)).toEqual(['m=Hi, Jim']); + }); + + it("should support literal array", () => { + var c = createChangeDetector('array', '[1,2]'); + c["changeDetector"].detectChanges(); + expect(c["dispatcher"].loggedValues).toEqual([[[1, 2]]]); + + c = createChangeDetector('array', '[1,a]', new TestData(2)); + c["changeDetector"].detectChanges(); + expect(c["dispatcher"].loggedValues).toEqual([[[1, 2]]]); + }); + + it("should support literal maps", () => { + var c = createChangeDetector('map', '{z:1}'); + c["changeDetector"].detectChanges(); + expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1); + + c = createChangeDetector('map', '{z:a}', new TestData(1)); + c["changeDetector"].detectChanges(); + expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1); + }); + + it("should support binary operations", () => { + expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']); + expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']); + + expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']); + expect(executeWatch('exp', '10 / 2')).toEqual([`exp=${5.0}`]); //dart exp=5.0, js exp=5 + expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']); + + expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']); + expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']); + + expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']); + expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']); + + expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']); + expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']); + + expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']); + expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']); + expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']); + + expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']); + expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']); + expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']); + + expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']); + expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']); + + expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']); + expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']); + }); + + it("should support negate", () => { + expect(executeWatch('exp', '!true')).toEqual(['exp=false']); + expect(executeWatch('exp', '!!true')).toEqual(['exp=true']); + }); + + it("should support conditionals", () => { + expect(executeWatch('m', '1 < 2 ? 1 : 2')).toEqual(['m=1']); + expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']); + }); + + describe("keyed access", () => { + it("should support accessing a list item", () => { + expect(executeWatch('array[0]', '["foo", "bar"][0]')).toEqual(['array[0]=foo']); + }); + + it("should support accessing a map item", () => { + expect(executeWatch('map[foo]', '{"foo": "bar"}["foo"]')).toEqual(['map[foo]=bar']); + }); + }); + + it("should support formatters", () => { + var formatters = MapWrapper.createFromPairs([ + ['uppercase', (v) => v.toUpperCase()], + ['wrap', (v, before, after) => `${before}${v}${after}`]]); + expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']); + expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']); + }); + + it("should support interpolation", () => { + var parser = new Parser(new Lexer()); + var pcd = createProtoChangeDetector(); + var ast = parser.parseInterpolation("B{{a}}A", "location"); + pcd.addAst(ast, "memo", "memo", false); + + var dispatcher = new TestDispatcher(); + var cd = pcd.instantiate(dispatcher, MapWrapper.create()); + cd.setContext(new TestData("value")); + + cd.detectChanges(); + + expect(dispatcher.log).toEqual(["memo=BvalueA"]); + }); + + describe("group changes", () => { + it("should notify the dispatcher when a group of records changes", () => { + var pcd = createProtoChangeDetector(); + pcd.addAst(ast("1 + 2"), "memo", "1"); + pcd.addAst(ast("10 + 20"), "memo", "1"); + pcd.addAst(ast("100 + 200"), "memo2", "2"); + + var dispatcher = new TestDispatcher(); + var cd = pcd.instantiate(dispatcher, null); + + cd.detectChanges(); + + expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]); + }); + + it("should notify the dispatcher before switching to the next group", () => { + var pcd = createProtoChangeDetector(); + pcd.addAst(ast("a()"), "a", "1"); + pcd.addAst(ast("b()"), "b", "2"); + pcd.addAst(ast("c()"), "c", "2"); + + var dispatcher = new TestDispatcher(); + var cd = pcd.instantiate(dispatcher, null); + + var tr = new TestRecord(); + tr.a = () => { + dispatcher.logValue('InvokeA'); + return 'a' + }; + tr.b = () => { + dispatcher.logValue('InvokeB'); + return 'b' + }; + tr.c = () => { + dispatcher.logValue('InvokeC'); + return 'c' + }; + cd.setContext(tr); + + cd.detectChanges(); + + expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]); + }); + }); + + describe("enforce no new changes", () => { + it("should throw when a record gets changed after it has been checked", () => { + var pcd = createProtoChangeDetector(); + pcd.addAst(ast("a"), "a", 1); + + var dispatcher = new TestDispatcher(); + var cd = pcd.instantiate(dispatcher, null); + cd.setContext(new TestData('value')); + + expect(() => { + cd.checkNoChanges(); + }).toThrowError(new RegExp("Expression 'a in location' has changed after it was checked")); + }); + }); + + //TODO vsavkin: implement it + describe("error handling", () => { + xit("should wrap exceptions into ChangeDetectionError", () => { + var pcd = createProtoChangeDetector(); + pcd.addAst(ast('invalidProp', 'someComponent'), "a", 1); + + var cd = pcd.instantiate(new TestDispatcher(), null); + cd.setContext(null); + + try { + cd.detectChanges(); + + throw new BaseException("fail"); + } catch (e) { + expect(e).toBeAnInstanceOf(ChangeDetectionError); + expect(e.location).toEqual("invalidProp in someComponent"); + } + }); + }); + + describe("collections", () => { + it("should support null values", () => { + var context = new TestData(null); + + var c = createChangeDetector('a', 'a', context, null, true); + var cd = c["changeDetector"]; + var dispatcher = c["dispatcher"]; + + cd.detectChanges(); + expect(dispatcher.log).toEqual(['a=null']); + dispatcher.clear(); + + context.a = [0]; + cd.detectChanges(); + + expect(dispatcher.log).toEqual(["a=" + + arrayChangesAsString({ + collection: ['0[null->0]'], + additions: ['0[null->0]'] + }) + ]); + dispatcher.clear(); + + context.a = null; + cd.detectChanges(); + expect(dispatcher.log).toEqual(['a=null']); + }); + + describe("list", () => { + it("should support list changes", () => { + var context = new TestData([1, 2]); + + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + arrayChangesAsString({ + collection: ['1[null->0]', '2[null->1]'], + additions: ['1[null->0]', '2[null->1]'] + })]); + }); + + it("should handle reference changes", () => { + var context = new TestData([1, 2]); + var objs = createChangeDetector("a", "a", context, null, true); + var cd = objs["changeDetector"]; + var dispatcher = objs["dispatcher"]; + cd.detectChanges(); + dispatcher.clear(); + + context.a = [2, 1]; + cd.detectChanges(); + expect(dispatcher.log).toEqual(["a=" + + arrayChangesAsString({ + collection: ['2[1->0]', '1[0->1]'], + previous: ['1[0->1]', '2[1->0]'], + moves: ['2[1->0]', '1[0->1]'] + })]); + }); + }); + + describe("map", () => { + it("should support map changes", () => { + var map = MapWrapper.create(); + MapWrapper.set(map, "foo", "bar"); + var context = new TestData(map); + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + kvChangesAsString({ + map: ['foo[null->bar]'], + additions: ['foo[null->bar]'] + })]); + }); + + it("should handle reference changes", () => { + var map = MapWrapper.create(); + MapWrapper.set(map, "foo", "bar"); + var context = new TestData(map); + var objs = createChangeDetector("a", "a", context, null, true); + var cd = objs["changeDetector"]; + var dispatcher = objs["dispatcher"]; + cd.detectChanges(); + dispatcher.clear(); + + context.a = MapWrapper.create(); + MapWrapper.set(context.a, "bar", "foo"); + cd.detectChanges(); + expect(dispatcher.log).toEqual(["a=" + + kvChangesAsString({ + map: ['bar[null->foo]'], + previous: ['foo[bar->null]'], + additions: ['bar[null->foo]'], + removals: ['foo[bar->null]'] + })]); + }); + }); + + if (!IS_DARTIUM) { + describe("js objects", () => { + it("should support object changes", () => { + var map = {"foo": "bar"}; + var context = new TestData(map); + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + kvChangesAsString({ + map: ['foo[null->bar]'], + additions: ['foo[null->bar]'] + })]); + }); + }); + } + }); + + describe("ContextWithVariableBindings", () => { + it('should read a field from ContextWithVariableBindings', () => { + var locals = new ContextWithVariableBindings(null, + MapWrapper.createFromPairs([["key", "value"]])); + + expect(executeWatch('key', 'key', locals)) + .toEqual(['key=value']); + }); + + it('should handle nested ContextWithVariableBindings', () => { + var nested = new ContextWithVariableBindings(null, + MapWrapper.createFromPairs([["key", "value"]])); + var locals = new ContextWithVariableBindings(nested, MapWrapper.create()); + + expect(executeWatch('key', 'key', locals)) + .toEqual(['key=value']); + }); + + it("should fall back to a regular field read when ContextWithVariableBindings " + + "does not have the requested field", () => { + var locals = new ContextWithVariableBindings(new Person("Jim"), + MapWrapper.createFromPairs([["key", "value"]])); + + expect(executeWatch('name', 'name', locals)) + .toEqual(['name=Jim']); + }); + }); + + describe("handle children", () => { + var parent, child; + + beforeEach(() => { + var protoParent = createProtoChangeDetector(); + parent = protoParent.instantiate(null, null); + + var protoChild = createProtoChangeDetector(); + child = protoChild.instantiate(null, null); + }); + + it("should add children", () => { + parent.addChild(child); + + expect(parent.children.length).toEqual(1); + expect(parent.children[0]).toBe(child); + }); + + it("should remove children", () => { + parent.addChild(child); + parent.removeChild(child); + + expect(parent.children).toEqual([]); + }); }); }); - } - }); - - describe("ContextWithVariableBindings", () => { - it('should read a field from ContextWithVariableBindings', () => { - var locals = new ContextWithVariableBindings(null, - MapWrapper.createFromPairs([["key", "value"]])); - - expect(executeWatch('key', 'key', locals)) - .toEqual(['key=value']); }); - - it('should handle nested ContextWithVariableBindings', () => { - var nested = new ContextWithVariableBindings(null, - MapWrapper.createFromPairs([["key", "value"]])); - var locals = new ContextWithVariableBindings(nested, MapWrapper.create()); - - expect(executeWatch('key', 'key', locals)) - .toEqual(['key=value']); - }); - - it("should fall back to a regular field read when ContextWithVariableBindings " + - "does not have the requested field", () => { - var locals = new ContextWithVariableBindings(new Person("Jim"), - MapWrapper.createFromPairs([["key", "value"]])); - - expect(executeWatch('name', 'name', locals)) - .toEqual(['name=Jim']); - }); - }); }); } diff --git a/modules/core/src/application.js b/modules/core/src/application.js index 0bae1c8c24..b744fba0a8 100644 --- a/modules/core/src/application.js +++ b/modules/core/src/application.js @@ -4,7 +4,7 @@ import {DOM, Element} from 'facade/dom'; import {Compiler, CompilerCache} from './compiler/compiler'; import {ProtoView} from './compiler/view'; import {Reflector, reflector} from 'reflection/reflection'; -import {Parser, Lexer, ChangeDetector} from 'change_detection/change_detection'; +import {Parser, Lexer, ChangeDetection, dynamicChangeDetection, jitChangeDetection} from 'change_detection/change_detection'; import {TemplateLoader} from './compiler/template_loader'; import {DirectiveMetadataReader} from './compiler/directive_metadata_reader'; import {DirectiveMetadata} from './compiler/directive_metadata'; @@ -17,7 +17,14 @@ var _rootInjector: Injector; // Contains everything that is safe to share between applications. var _rootBindings = [ - bind(Reflector).toValue(reflector), Compiler, CompilerCache, TemplateLoader, DirectiveMetadataReader, Parser, Lexer + bind(Reflector).toValue(reflector), + bind(ChangeDetection).toValue(dynamicChangeDetection), + Compiler, + CompilerCache, + TemplateLoader, + DirectiveMetadataReader, + Parser, + Lexer ]; export var appViewToken = new OpaqueToken('AppView'); @@ -45,20 +52,20 @@ function _injectorBindings(appComponentType) { return element; }, [appComponentAnnotatedTypeToken, appDocumentToken]), - bind(appViewToken).toAsyncFactory((compiler, injector, appElement, + bind(appViewToken).toAsyncFactory((changeDetection, compiler, injector, appElement, appComponentAnnotatedType) => { return compiler.compile(appComponentAnnotatedType.type, null).then( (protoView) => { - var appProtoView = ProtoView.createRootProtoView(protoView, - appElement, appComponentAnnotatedType); + var appProtoView = ProtoView.createRootProtoView(protoView, + appElement, appComponentAnnotatedType, changeDetection.createProtoChangeDetector('root')); // The light Dom of the app element is not considered part of // the angular application. Thus the context and lightDomInjector are // empty. - var view = appProtoView.instantiate(null); - view.hydrate(injector, null, new Object()); + var view = appProtoView.instantiate(null); + view.hydrate(injector, null, new Object()); return view; }); - }, [Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]), + }, [ChangeDetection, Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]), bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector, [appViewToken]), diff --git a/modules/core/src/compiler/compiler.js b/modules/core/src/compiler/compiler.js index c78b27abf8..56cb745d01 100644 --- a/modules/core/src/compiler/compiler.js +++ b/modules/core/src/compiler/compiler.js @@ -3,7 +3,7 @@ import {Promise, PromiseWrapper} from 'facade/async'; import {List, ListWrapper, MapWrapper} from 'facade/collection'; import {DOM, Element} from 'facade/dom'; -import {Parser} from 'change_detection/change_detection'; +import {ChangeDetection, Parser} from 'change_detection/change_detection'; import {DirectiveMetadataReader} from './directive_metadata_reader'; import {ProtoView} from './view'; @@ -52,7 +52,10 @@ export class Compiler { _reader: DirectiveMetadataReader; _parser:Parser; _compilerCache:CompilerCache; - constructor(templateLoader:TemplateLoader, reader: DirectiveMetadataReader, parser:Parser, cache:CompilerCache) { + _changeDetection:ChangeDetection; + + constructor(changeDetection:ChangeDetection, templateLoader:TemplateLoader, reader: DirectiveMetadataReader, parser:Parser, cache:CompilerCache) { + this._changeDetection = changeDetection; this._reader = reader; this._parser = parser; this._compilerCache = cache; @@ -60,7 +63,7 @@ export class Compiler { createSteps(component:DirectiveMetadata):List { var dirs = ListWrapper.map(component.componentDirectives, (d) => this._reader.read(d)); - return createDefaultSteps(this._parser, component, dirs); + return createDefaultSteps(this._changeDetection, this._parser, component, dirs); } compile(component:Type, templateRoot:Element = null):Promise { diff --git a/modules/core/src/compiler/pipeline/default_steps.js b/modules/core/src/compiler/pipeline/default_steps.js index 012db56ae6..02b53f95b0 100644 --- a/modules/core/src/compiler/pipeline/default_steps.js +++ b/modules/core/src/compiler/pipeline/default_steps.js @@ -1,4 +1,4 @@ -import {Parser} from 'change_detection/change_detection'; +import {ChangeDetection, Parser} from 'change_detection/change_detection'; import {List} from 'facade/collection'; import {PropertyBindingParser} from './property_binding_parser'; @@ -17,8 +17,12 @@ import {stringify} from 'facade/lang'; * Takes in an HTMLElement and produces the ProtoViews, * ProtoElementInjectors and ElementBinders in the end. */ -export function createDefaultSteps(parser:Parser, compiledComponent: DirectiveMetadata, +export function createDefaultSteps( + changeDetection:ChangeDetection, + parser:Parser, + compiledComponent: DirectiveMetadata, directives: List) { + var compilationUnit = stringify(compiledComponent.type); return [ @@ -27,7 +31,7 @@ export function createDefaultSteps(parser:Parser, compiledComponent: DirectiveMe new DirectiveParser(directives), new TextInterpolationParser(parser, compilationUnit), new ElementBindingMarker(), - new ProtoViewBuilder(), + new ProtoViewBuilder(changeDetection), new ProtoElementInjectorBuilder(), new ElementBinderBuilder() ]; diff --git a/modules/core/src/compiler/pipeline/proto_view_builder.js b/modules/core/src/compiler/pipeline/proto_view_builder.js index a557ce7008..4f5a66cdf4 100644 --- a/modules/core/src/compiler/pipeline/proto_view_builder.js +++ b/modules/core/src/compiler/pipeline/proto_view_builder.js @@ -2,7 +2,7 @@ import {isPresent, BaseException} from 'facade/lang'; import {ListWrapper, MapWrapper} from 'facade/collection'; import {ProtoView} from '../view'; -import {ProtoChangeDetector} from 'change_detection/change_detection'; +import {ChangeDetection} from 'change_detection/change_detection'; import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; @@ -18,10 +18,16 @@ import {CompileControl} from './compile_control'; * - CompileElement#isViewRoot */ export class ProtoViewBuilder extends CompileStep { + changeDetection:ChangeDetection; + constructor(changeDetection:ChangeDetection) { + this.changeDetection = changeDetection; + } + process(parent:CompileElement, current:CompileElement, control:CompileControl) { var inheritedProtoView = null; if (current.isViewRoot) { - inheritedProtoView = new ProtoView(current.element, new ProtoChangeDetector()); + var protoChangeDetector = this.changeDetection.createProtoChangeDetector('dummy'); + inheritedProtoView = new ProtoView(current.element, protoChangeDetector); if (isPresent(parent)) { if (isPresent(parent.inheritedElementBinder.nestedProtoView)) { throw new BaseException('Only one nested view per element is allowed'); diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 44192550fa..8f1c1d7728 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -492,9 +492,12 @@ export class ProtoView { // and the component template is already compiled into protoView. // Used for bootstrapping. static createRootProtoView(protoView: ProtoView, - insertionElement, rootComponentAnnotatedType: DirectiveMetadata): ProtoView { + insertionElement, rootComponentAnnotatedType: DirectiveMetadata, + protoChangeDetector:ProtoChangeDetector + ): ProtoView { + DOM.addClass(insertionElement, 'ng-binding'); - var rootProtoView = new ProtoView(insertionElement, new ProtoChangeDetector()); + var rootProtoView = new ProtoView(insertionElement, protoChangeDetector); rootProtoView.instantiateInPlace = true; var binder = rootProtoView.bindElement( new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); diff --git a/modules/core/test/compiler/compiler_spec.js b/modules/core/test/compiler/compiler_spec.js index b44bdd786a..a5371cda83 100644 --- a/modules/core/test/compiler/compiler_spec.js +++ b/modules/core/test/compiler/compiler_spec.js @@ -12,7 +12,7 @@ import {CompileElement} from 'core/compiler/pipeline/compile_element'; import {CompileStep} from 'core/compiler/pipeline/compile_step' import {CompileControl} from 'core/compiler/pipeline/compile_control'; -import {Lexer, Parser} from 'change_detection/change_detection'; +import {Lexer, Parser, dynamicChangeDetection} from 'change_detection/change_detection'; export function main() { describe('compiler', function() { @@ -134,7 +134,7 @@ class RecursiveComponent {} class TestableCompiler extends Compiler { steps:List; constructor(reader:DirectiveMetadataReader, steps:List) { - super(null, reader, new Parser(new Lexer()), new CompilerCache()); + super(dynamicChangeDetection, null, reader, new Parser(new Lexer()), new CompilerCache()); this.steps = steps; } createSteps(component):List { diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index 65feaf3851..ec023672d1 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -3,7 +3,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_li import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; @@ -20,7 +20,8 @@ export function main() { var compiler; beforeEach( () => { - compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); }); describe('react to record changes', function() { diff --git a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js index d8a2215fb4..7473463b31 100644 --- a/modules/core/test/compiler/pipeline/element_binder_builder_spec.js +++ b/modules/core/test/compiler/pipeline/element_binder_builder_spec.js @@ -16,7 +16,8 @@ import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/ import {ProtoElementInjector} from 'core/compiler/element_injector'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; -import {ChangeDetector, Lexer, Parser, ProtoChangeDetector} from 'change_detection/change_detection'; +import {ChangeDetector, Lexer, Parser, DynamicProtoChangeDetector, + } from 'change_detection/change_detection'; import {Injector} from 'di/di'; export function main() { @@ -66,7 +67,7 @@ export function main() { } if (isPresent(current.element.getAttribute('viewroot'))) { current.isViewRoot = true; - current.inheritedProtoView = new ProtoView(current.element, new ProtoChangeDetector()); + current.inheritedProtoView = new ProtoView(current.element, new DynamicProtoChangeDetector()); } else if (isPresent(parent)) { current.inheritedProtoView = parent.inheritedProtoView; } @@ -205,7 +206,7 @@ export function main() { var results = pipeline.process(el('
')); var pv = results[0].inheritedProtoView; results[0].inheritedElementBinder.nestedProtoView = new ProtoView( - el('
'), new ProtoChangeDetector()); + el('
'), new DynamicProtoChangeDetector()); instantiateView(pv); evalContext.prop1 = 'a'; diff --git a/modules/core/test/compiler/pipeline/proto_view_builder_spec.js b/modules/core/test/compiler/pipeline/proto_view_builder_spec.js index 7b609dca43..d8d4e82c79 100644 --- a/modules/core/test/compiler/pipeline/proto_view_builder_spec.js +++ b/modules/core/test/compiler/pipeline/proto_view_builder_spec.js @@ -1,5 +1,6 @@ import {describe, beforeEach, it, expect, iit, ddescribe, el} from 'test_lib/test_lib'; import {isPresent} from 'facade/lang'; +import {dynamicChangeDetection} from 'change_detection/change_detection'; import {ElementBinder} from 'core/compiler/element_binder'; import {ProtoViewBuilder} from 'core/compiler/pipeline/proto_view_builder'; import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; @@ -20,7 +21,7 @@ export function main() { current.variableBindings = MapWrapper.createFromStringMap(variableBindings); } current.inheritedElementBinder = new ElementBinder(null, null, null); - }), new ProtoViewBuilder()]); + }), new ProtoViewBuilder(dynamicChangeDetection)]); } it('should not create a ProtoView when the isViewRoot flag is not set', () => { diff --git a/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js b/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js index 8f92cd9b09..ae8029e5e0 100644 --- a/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js +++ b/modules/core/test/compiler/shadow_dom/shadow_dom_emulation_integration_spec.js @@ -3,7 +3,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_li import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {LifeCycle} from 'core/life_cycle/life_cycle'; @@ -26,7 +26,8 @@ export function main() { var compiler; beforeEach( () => { - compiler = new Compiler(null, new TestDirectiveMetadataReader(strategy), + compiler = new Compiler(dynamicChangeDetection, null, + new TestDirectiveMetadataReader(strategy), new Parser(new Lexer()), new CompilerCache()); }); diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index e4bb8737c3..1a4e269bc5 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -5,7 +5,8 @@ import {ShadowDomEmulated} from 'core/compiler/shadow_dom'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; import {Component, Decorator, Template} from 'core/annotations/annotations'; import {OnChange} from 'core/core'; -import {Lexer, Parser, ProtoChangeDetector, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, DynamicProtoChangeDetector, + ChangeDetector} from 'change_detection/change_detection'; import {TemplateConfig} from 'core/annotations/template_config'; import {EventEmitter} from 'core/annotations/events'; import {List, MapWrapper} from 'facade/collection'; @@ -52,7 +53,7 @@ export function main() { describe('instantiated from protoView', () => { var view; beforeEach(() => { - var pv = new ProtoView(el('
'), new ProtoChangeDetector()); + var pv = new ProtoView(el('
'), new DynamicProtoChangeDetector()); view = pv.instantiate(null); }); @@ -73,7 +74,7 @@ export function main() { describe('with locals', function() { var view; beforeEach(() => { - var pv = new ProtoView(el('
'), new ProtoChangeDetector()); + var pv = new ProtoView(el('
'), new DynamicProtoChangeDetector()); pv.bindVariable('context-foo', 'template-foo'); view = createView(pv); }); @@ -109,7 +110,7 @@ export function main() { } it('should collect the root node in the ProtoView element', () => { - var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoChangeDetector()); + var pv = new ProtoView(templateAwareCreateElement('
'), new DynamicProtoChangeDetector()); var view = pv.instantiate(null); view.hydrate(null, null, null); expect(view.nodes.length).toBe(1); @@ -119,7 +120,7 @@ export function main() { describe('collect elements with property bindings', () => { it('should collect property bindings on the root element if it has the ng-binding class', () => { - var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoChangeDetector()); + var pv = new ProtoView(templateAwareCreateElement('
'), new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('a', null), 'prop', reflector.setter('prop')); @@ -131,7 +132,7 @@ export function main() { it('should collect property bindings on child elements with ng-binding class', () => { var pv = new ProtoView(templateAwareCreateElement('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('b', null), 'a', reflector.setter('a')); @@ -146,7 +147,7 @@ export function main() { describe('collect text nodes with bindings', () => { it('should collect text nodes under the root element', () => { - var pv = new ProtoView(templateAwareCreateElement('
{{}}{{}}
'), new ProtoChangeDetector()); + var pv = new ProtoView(templateAwareCreateElement('
{{}}{{}}
'), new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('a', null)); pv.bindTextNode(2, parser.parseBinding('b', null)); @@ -160,7 +161,7 @@ export function main() { it('should collect text nodes with bindings on child elements with ng-binding class', () => { var pv = new ProtoView(templateAwareCreateElement('
{{}}
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('b', null)); @@ -176,7 +177,7 @@ export function main() { describe('inplace instantiation', () => { it('should be supported.', () => { var template = el('
'); - var pv = new ProtoView(template, new ProtoChangeDetector()); + var pv = new ProtoView(template, new DynamicProtoChangeDetector()); pv.instantiateInPlace = true; var view = pv.instantiate(null); view.hydrate(null, null, null); @@ -185,7 +186,7 @@ export function main() { it('should be off by default.', () => { var template = el('
') - var view = new ProtoView(template, new ProtoChangeDetector()) + var view = new ProtoView(template, new DynamicProtoChangeDetector()) .instantiate(null); view.hydrate(null, null, null); expect(view.nodes[0]).not.toBe(template); @@ -202,7 +203,7 @@ export function main() { describe('create ElementInjectors', () => { it('should use the directives of the ProtoElementInjector', () => { - var pv = new ProtoView(el('
'), new ProtoChangeDetector()); + var pv = new ProtoView(el('
'), new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); var view = pv.instantiate(null); @@ -213,7 +214,7 @@ export function main() { it('should use the correct parent', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); @@ -227,7 +228,7 @@ export function main() { it('should not pass the host injector when a parent injector exists', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); var testProtoElementInjector = new TestProtoElementInjector(protoParent, 1, [AnotherDirective]); @@ -243,7 +244,7 @@ export function main() { it('should pass the host injector when there is no parent injector', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); var testProtoElementInjector = new TestProtoElementInjector(null, 1, [AnotherDirective]); pv.bindElement(testProtoElementInjector); @@ -260,7 +261,7 @@ export function main() { it('should collect a single root element injector', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); @@ -273,7 +274,7 @@ export function main() { it('should collect multiple root element injectors', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective])); @@ -290,7 +291,7 @@ export function main() { var ctx; function createComponentWithSubPV(subProtoView) { - var pv = new ProtoView(el(''), new ProtoChangeDetector()); + var pv = new ProtoView(el(''), new DynamicProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true)); binder.componentDirective = someComponentDirective; binder.nestedProtoView = subProtoView; @@ -305,7 +306,7 @@ export function main() { } it('should expose component services to the component', () => { - var subpv = new ProtoView(el(''), new ProtoChangeDetector()); + var subpv = new ProtoView(el(''), new DynamicProtoChangeDetector()); var pv = createComponentWithSubPV(subpv); var view = createNestedView(pv); @@ -317,7 +318,7 @@ export function main() { it('should expose component services and component instance to directives in the shadow Dom', () => { var subpv = new ProtoView( - el('
hello shadow dom
'), new ProtoChangeDetector()); + el('
hello shadow dom
'), new DynamicProtoChangeDetector()); subpv.bindElement( new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); var pv = createComponentWithSubPV(subpv); @@ -340,7 +341,7 @@ export function main() { it('dehydration should dehydrate child component views too', () => { var subpv = new ProtoView( - el('
hello shadow dom
'), new ProtoChangeDetector()); + el('
hello shadow dom
'), new DynamicProtoChangeDetector()); subpv.bindElement( new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); var pv = createComponentWithSubPV(subpv); @@ -355,7 +356,7 @@ export function main() { }); it('should create shadow dom', () => { - var subpv = new ProtoView(el('hello shadow dom'), new ProtoChangeDetector()); + var subpv = new ProtoView(el('hello shadow dom'), new DynamicProtoChangeDetector()); var pv = createComponentWithSubPV(subpv); var view = createNestedView(pv); @@ -364,9 +365,9 @@ export function main() { }); it('should use the provided shadow DOM strategy', () => { - var subpv = new ProtoView(el('hello shadow dom'), new ProtoChangeDetector()); + var subpv = new ProtoView(el('hello shadow dom'), new DynamicProtoChangeDetector()); - var pv = new ProtoView(el(''), new ProtoChangeDetector()); + var pv = new ProtoView(el(''), new DynamicProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponentWithEmulatedShadowDom], true)); binder.componentDirective = new DirectiveMetadataReader().read(SomeComponentWithEmulatedShadowDom); binder.nestedProtoView = subpv; @@ -380,8 +381,8 @@ export function main() { describe('with template views', () => { function createViewWithTemplate() { var templateProtoView = new ProtoView( - el('
'), new ProtoChangeDetector()); - var pv = new ProtoView(el(''), new ProtoChangeDetector()); + el('
'), new DynamicProtoChangeDetector()); + var pv = new ProtoView(el(''), new DynamicProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeTemplate])); binder.templateDirective = someTemplateDirective; binder.nestedProtoView = templateProtoView; @@ -425,7 +426,7 @@ export function main() { function createProtoView() { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new TestProtoElementInjector(null, 0, [])); pv.bindEvent('click', parser.parseBinding('callMe(\$event)', null)); return pv; @@ -460,7 +461,7 @@ export function main() { it('should support custom event emitters', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new TestProtoElementInjector(null, 0, [EventEmitterDirective])); pv.bindEvent('click', parser.parseBinding('callMe(\$event)', null)); @@ -491,7 +492,7 @@ export function main() { it('should consume text node changes', () => { var pv = new ProtoView(el('
{{}}
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('foo', null)); createViewAndChangeDetector(pv); @@ -503,7 +504,7 @@ export function main() { it('should consume element binding changes', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('foo', null), 'id', reflector.setter('id')); createViewAndChangeDetector(pv); @@ -515,7 +516,7 @@ export function main() { it('should consume directive watch expression change', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); pv.bindDirectiveProperty(0, parser.parseBinding('foo', null), 'prop', reflector.setter('prop'), false); createViewAndChangeDetector(pv); @@ -527,7 +528,7 @@ export function main() { it('should notify a directive about changes after all its properties have been set', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false); @@ -544,7 +545,7 @@ export function main() { it('should provide a map of updated properties', () => { var pv = new ProtoView(el('
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false); @@ -569,18 +570,20 @@ export function main() { var element, pv; beforeEach(() => { element = DOM.createElement('div'); - pv = new ProtoView(el('
hi
'), new ProtoChangeDetector()); + pv = new ProtoView(el('
hi
'), new DynamicProtoChangeDetector()); }); it('should create the root component when instantiated', () => { - var rootProtoView = ProtoView.createRootProtoView(pv, element, someComponentDirective); + var rootProtoView = ProtoView.createRootProtoView(pv, element, + someComponentDirective, new DynamicProtoChangeDetector()); var view = rootProtoView.instantiate(null); view.hydrate(new Injector([]), null, null); expect(view.rootElementInjectors[0].get(SomeComponent)).not.toBe(null); }); it('should inject the protoView into the shadowDom', () => { - var rootProtoView = ProtoView.createRootProtoView(pv, element, someComponentDirective); + var rootProtoView = ProtoView.createRootProtoView(pv, element, + someComponentDirective, new DynamicProtoChangeDetector()); var view = rootProtoView.instantiate(null); view.hydrate(new Injector([]), null, null); expect(element.shadowRoot.childNodes[0].childNodes[0].nodeValue).toEqual('hi'); diff --git a/modules/core/test/compiler/viewport_spec.js b/modules/core/test/compiler/viewport_spec.js index c2411baf53..0c37dea903 100644 --- a/modules/core/test/compiler/viewport_spec.js +++ b/modules/core/test/compiler/viewport_spec.js @@ -6,10 +6,10 @@ import {DOM} from 'facade/dom'; import {ListWrapper, MapWrapper} from 'facade/collection'; import {Injector} from 'di/di'; import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector'; -import {ProtoChangeDetector, ChangeDetector, Lexer, Parser} from 'change_detection/change_detection'; +import {DynamicProtoChangeDetector, ChangeDetector, Lexer, Parser} from 'change_detection/change_detection'; function createView(nodes) { - var view = new View(null, nodes, new ProtoChangeDetector(), MapWrapper.create()); + var view = new View(null, nodes, new DynamicProtoChangeDetector(), MapWrapper.create()); view.init([], [], [], [], [], [], []); return view; } @@ -68,7 +68,7 @@ export function main() { dom = el(`
`); var insertionElement = dom.childNodes[1]; parentView = createView([dom.childNodes[0]]); - protoView = new ProtoView(el('
hi
'), new ProtoChangeDetector()); + protoView = new ProtoView(el('
hi
'), new DynamicProtoChangeDetector()); elementInjector = new ElementInjector(null, null, null, null); viewPort = new ViewPort(parentView, insertionElement, protoView, elementInjector); customViewWithOneNode = createView([el('
single
')]); @@ -165,7 +165,7 @@ export function main() { expect(textInViewPort()).toEqual('filler one two'); expect(viewPort.length).toBe(2); }); - + it('should keep views hydration state during insert', () => { var hydratedView = new HydrateAwareFakeView(true); var dehydratedView = new HydrateAwareFakeView(false); @@ -212,7 +212,7 @@ export function main() { viewPort.hydrate(new Injector([]), null); var pv = new ProtoView(el('
{{}}
'), - new ProtoChangeDetector()); + new DynamicProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); pv.bindTextNode(0, parser.parseBinding('foo', null)); fancyView = pv.instantiate(null); diff --git a/modules/directives/test/ng_if_spec.js b/modules/directives/test/ng_if_spec.js index af3f2f6132..b24ba2a8c1 100644 --- a/modules/directives/test/ng_if_spec.js +++ b/modules/directives/test/ng_if_spec.js @@ -3,7 +3,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, IS_DARTIUM, el} f import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; @@ -17,7 +17,8 @@ export function main() { describe('ng-if', () => { var view, cd, compiler, component; beforeEach(() => { - compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); }); function createView(pv) { diff --git a/modules/directives/test/ng_non_bindable_spec.js b/modules/directives/test/ng_non_bindable_spec.js index e09db2d5a9..bd796459cd 100644 --- a/modules/directives/test/ng_non_bindable_spec.js +++ b/modules/directives/test/ng_non_bindable_spec.js @@ -1,7 +1,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_lib/test_lib'; import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; import {Decorator, Component} from 'core/annotations/annotations'; @@ -13,7 +13,8 @@ export function main() { describe('ng-non-bindable', () => { var view, cd, compiler, component; beforeEach(() => { - compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, + null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); }); function createView(pv) { diff --git a/modules/directives/test/ng_repeat_spec.js b/modules/directives/test/ng_repeat_spec.js index adb939c4e0..0e9a69509d 100644 --- a/modules/directives/test/ng_repeat_spec.js +++ b/modules/directives/test/ng_repeat_spec.js @@ -3,7 +3,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_li import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetector, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {OnChange} from 'core/compiler/interfaces'; @@ -20,7 +20,8 @@ export function main() { describe('ng-repeat', () => { var view, cd, compiler, component; beforeEach(() => { - compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); }); function createView(pv) { diff --git a/modules/directives/test/ng_switch_spec.js b/modules/directives/test/ng_switch_spec.js index f7c1fdbe71..40875cf8d1 100644 --- a/modules/directives/test/ng_switch_spec.js +++ b/modules/directives/test/ng_switch_spec.js @@ -1,7 +1,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_lib/test_lib'; import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser} from 'change_detection/change_detection'; +import {Lexer, Parser, dynamicChangeDetection} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; import {Component} from 'core/annotations/annotations'; @@ -12,7 +12,8 @@ export function main() { describe('ng-switch', () => { var view, cd, compiler, component; beforeEach(() => { - compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + compiler = new Compiler(dynamicChangeDetection, null, new DirectiveMetadataReader(), + new Parser(new Lexer()), new CompilerCache()); }); function createView(pv) { diff --git a/modules/examples/src/hello_world/index_static.js b/modules/examples/src/hello_world/index_static.js index 03d05ca166..361ad09f58 100644 --- a/modules/examples/src/hello_world/index_static.js +++ b/modules/examples/src/hello_world/index_static.js @@ -1,7 +1,7 @@ import * as app from './index_common'; import {Component, Decorator, TemplateConfig, NgElement} from 'angular/angular'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ChangeDetection, ChangeDetector} from 'change_detection/change_detection'; import {LifeCycle} from 'core/life_cycle/life_cycle'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; @@ -37,8 +37,8 @@ function setup() { }); reflector.registerType(Compiler, { - "factory": (templateLoader, reader, parser, compilerCache) => new Compiler(templateLoader, reader, parser, compilerCache), - "parameters": [[TemplateLoader], [DirectiveMetadataReader], [Parser], [CompilerCache]], + "factory": (changeDetection, templateLoader, reader, parser, compilerCache) => new Compiler(changeDetection, templateLoader, reader, parser, compilerCache), + "parameters": [[ChangeDetection], [TemplateLoader], [DirectiveMetadataReader], [Parser], [CompilerCache]], "annotations": [] }); diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 23080448ed..e3da87e3d7 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -89,8 +89,8 @@ class ListWrapper { static reduce(List list, Function fn, init) { return list.fold(init, fn); } - static first(List list) => list.first; - static last(List list) => list.last; + static first(List list) => list.isEmpty ? null : list.first; + static last(List list) => list.isEmpty ? null : list.last; static List reversed(List list) => list.reversed.toList(); static void push(List l, e) { l.add(e); } static List concat(List a, List b) {a.addAll(b); return a;} diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index f754f1a687..8e524eb05a 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -27,6 +27,7 @@ class IMPLEMENTS { bool isPresent(obj) => obj != null; bool isBlank(obj) => obj == null; +bool isString(obj) => obj is String; String stringify(obj) => obj.toString(); diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index 9a6c5f3d4f..9928333943 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -27,6 +27,10 @@ export function isBlank(obj):boolean { return obj === undefined || obj === null; } +export function isString(obj):boolean { + return typeof obj === "string"; +} + export function stringify(token):string { if (typeof token === 'string') { return token;