diff --git a/modules/benchmarks/src/change_detection/change_detection_benchmark.js b/modules/benchmarks/src/change_detection/change_detection_benchmark.js index 6b3be5a906..724591e4d8 100644 --- a/modules/benchmarks/src/change_detection/change_detection_benchmark.js +++ b/modules/benchmarks/src/change_detection/change_detection_benchmark.js @@ -7,7 +7,7 @@ import { Lexer, Parser, ChangeDetector, - ProtoRecordRange, + ProtoChangeDetector, ChangeDispatcher, } from 'change_detection/change_detection'; @@ -102,8 +102,8 @@ function setUpChangeDetection(iterations) { var dispatcher = new DummyDispatcher(); var parser = new Parser(new Lexer()); - var parentProto = new ProtoRecordRange(); - var parentRange = parentProto.instantiate(dispatcher, MapWrapper.create()); + var parentProto = new ProtoChangeDetector(); + var parentCD = parentProto.instantiate(dispatcher, MapWrapper.create()); var astWithSource = [ parser.parseBinding('field0', null), @@ -119,12 +119,12 @@ function setUpChangeDetection(iterations) { ]; function proto(i) { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(astWithSource[i % 10].ast, "memo", i, false); - return prr; + var pcd = new ProtoChangeDetector(); + pcd.addAst(astWithSource[i % 10].ast, "memo", i, false); + return pcd; } - var prr = [ + var pcd = [ proto(0), proto(1), proto(2), @@ -142,13 +142,13 @@ function setUpChangeDetection(iterations) { var index = i % 10; obj.setField(index, i); - var rr = prr[index].instantiate(dispatcher, null); + var rr = pcd[index].instantiate(dispatcher, null); rr.setContext(obj); - parentRange.addRange(rr); + parentCD.addChild(rr); } - return new ChangeDetector(parentRange); + return parentCD; } export function main () { diff --git a/modules/benchmarks/src/tree/tree_benchmark.js b/modules/benchmarks/src/tree/tree_benchmark.js index ae0497dadd..888aa3529d 100644 --- a/modules/benchmarks/src/tree/tree_benchmark.js +++ b/modules/benchmarks/src/tree/tree_benchmark.js @@ -125,7 +125,7 @@ export function main() { setupReflector(); var app; - var changeDetector; + var lifeCycle; var baselineRootTreeComponent; var count = 0; @@ -133,7 +133,7 @@ export function main() { // TODO: We need an initial value as otherwise the getter for data.value will fail // --> this should be already caught in change detection! app.initData = new TreeNode('', null, null); - changeDetector.detectChanges(); + lifeCycle.tick(); } function profile(create, destroy, name) { @@ -171,14 +171,15 @@ export function main() { ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', '-']; app.initData = buildTree(maxDepth, values, 0); - changeDetector.detectChanges(); + lifeCycle.tick(); } function noop() {} function initNg2() { bootstrap(AppComponent).then((injector) => { - changeDetector = injector.get(ChangeDetector); + lifeCycle = injector.get(LifeCycle); + app = injector.get(AppComponent); bindAction('#ng2DestroyDom', ng2DestroyDom); bindAction('#ng2CreateDom', ng2CreateDom); diff --git a/modules/change_detection/src/change_detection.js b/modules/change_detection/src/change_detection.js index 6fac7d72de..32fac81672 100644 --- a/modules/change_detection/src/change_detection.js +++ b/modules/change_detection/src/change_detection.js @@ -1,7 +1,9 @@ -export {ChangeDetectionError, ChangeDetector} from './change_detector'; -export {AST, ASTWithSource} from './parser/ast'; +export {AST} from './parser/ast'; export {Lexer} from './parser/lexer'; export {Parser} from './parser/parser'; -export {ProtoRecordRange, RecordRange, ChangeDispatcher} from './record_range'; -export {ProtoRecord, Record} from './record'; export {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; + +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 diff --git a/modules/change_detection/src/change_detector.js b/modules/change_detection/src/change_detector.js deleted file mode 100644 index 91fa16e9b3..0000000000 --- a/modules/change_detection/src/change_detector.js +++ /dev/null @@ -1,87 +0,0 @@ -import {ProtoRecordRange, RecordRange} from './record_range'; -import {ProtoRecord, Record} from './record'; -import {int, isPresent, isBlank} from 'facade/lang'; -import {ListWrapper, List} from 'facade/collection'; - -export * from './record'; -export * from './record_range' - -class ExpressionChangedAfterItHasBeenChecked extends Error { - message:string; - - constructor(record:Record) { - this.message = `Expression '${record.expressionAsString()}' has changed after it was checked. ` + - `Previous value: '${record.previousValue}'. Current value: '${record.currentValue}'`; - } - - toString():string { - return this.message; - } -} - -export class ChangeDetector { - _rootRecordRange:RecordRange; - _enforceNoNewChanges:boolean; - - constructor(recordRange:RecordRange, enforceNoNewChanges:boolean = false) { - this._rootRecordRange = recordRange; - this._enforceNoNewChanges = enforceNoNewChanges; - } - - detectChanges():int { - var count = this._detectChanges(false); - if (this._enforceNoNewChanges) { - this._detectChanges(true) - } - return count; - } - - _detectChanges(throwOnChange:boolean):int { - var count = 0; - var updatedRecords = null; - var record = this._rootRecordRange.findFirstEnabledRecord(); - var currentRange, currentGroup; - - while (isPresent(record)) { - if (record.check()) { - count++; - if (record.terminatesExpression()) { - if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(record); - currentRange = record.recordRange; - currentGroup = record.groupMemento(); - updatedRecords = this._addRecord(updatedRecords, record); - } - } - - if (isPresent(updatedRecords)) { - var nextEnabled = record.nextEnabled; - if (isBlank(nextEnabled) || // we have reached the last enabled record - currentRange !== nextEnabled.recordRange || // the next record is in a different range - currentGroup !== nextEnabled.groupMemento()) { // the next record is in a different group - currentRange.dispatcher.onRecordChange(currentGroup, updatedRecords); - updatedRecords = null; - } - } - - record = record.findNextEnabled(); - } - - return count; - } - - _addRecord(updatedRecords:List, record:Record) { - if (isBlank(updatedRecords)) { - updatedRecords = _singleElementList; - updatedRecords[0] = record; - - } else if (updatedRecords === _singleElementList) { - updatedRecords = [_singleElementList[0], record]; - - } else { - ListWrapper.push(updatedRecords, record); - } - return updatedRecords; - } -} - -var _singleElementList = [null]; diff --git a/modules/change_detection/src/dynamic_change_detector.js b/modules/change_detection/src/dynamic_change_detector.js new file mode 100644 index 0000000000..fa76569571 --- /dev/null +++ b/modules/change_detection/src/dynamic_change_detector.js @@ -0,0 +1,259 @@ +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 {ArrayChanges} from './array_changes'; +import {KeyValueChanges} from './keyvalue_changes'; + +import { + ProtoRecord, + RECORD_TYPE_SELF, + RECORD_TYPE_PROPERTY, + RECORD_TYPE_INVOKE_METHOD, + RECORD_TYPE_CONST, + RECORD_TYPE_INVOKE_CLOSURE, + RECORD_TYPE_INVOKE_PURE_FUNCTION, + RECORD_TYPE_INVOKE_FORMATTER, + RECORD_TYPE_STRUCTURAL_CHECK, + ProtoChangeDetector + } from './proto_change_detector'; + +import {ChangeDetector, ChangeRecord, ChangeDispatcher} from './interfaces'; +import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions'; + +class SimpleChange { + previousValue:any; + currentValue:any; + + constructor(previousValue:any, currentValue:any) { + this.previousValue = previousValue; + this.currentValue = currentValue; + } +} + +export class DynamicChangeDetector extends ChangeDetector { + dispatcher:any; + formatters:Map; + children:List; + values:List; + protos:List; + parent:ChangeDetector; + + constructor(dispatcher:any, formatters:Map, protoRecords:List) { + this.dispatcher = dispatcher; + this.formatters = formatters; + this.values = ListWrapper.createFixedSize(protoRecords.length + 1); + 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) { + var protos:List = this.protos; + + var updatedRecords = null; + var currentGroup = null; + + for (var i = 0; i < protos.length; ++i) { + 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); + 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; + } + } + } + } + + _check(proto:ProtoRecord) { + try { + if (proto.mode == RECORD_TYPE_STRUCTURAL_CHECK) { + return this._structuralCheck(proto); + } else { + return this._referenceCheck(proto); + } + } catch (e) { + throw new ChangeDetectionError(proto, e); + } + } + + _referenceCheck(proto) { + var prevValue = this._readSelf(proto); + var currValue = this._calculateCurrValue(proto); + + if (! isSame(prevValue, currValue)) { + this._writeSelf(proto, currValue); + return new SimpleChange(prevValue, currValue); + + } else { + return null; + } + } + + _calculateCurrValue(proto) { + switch (proto.mode) { + case RECORD_TYPE_SELF: + throw new BaseException("Cannot evaluate self"); + + case RECORD_TYPE_CONST: + return proto.funcOrValue; + + 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 propertyGetter:Function = proto.funcOrValue; + return propertyGetter(context); + + case RECORD_TYPE_INVOKE_METHOD: + var methodInvoker:Function = proto.funcOrValue; + return methodInvoker(this._readContext(proto), this._readArgs(proto)); + + case RECORD_TYPE_INVOKE_CLOSURE: + return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto)); + + case RECORD_TYPE_INVOKE_PURE_FUNCTION: + return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto)); + + case RECORD_TYPE_INVOKE_FORMATTER: + var formatter = MapWrapper.get(this.formatters, proto.funcOrValue); + return FunctionWrapper.apply(formatter, this._readArgs(proto)); + + default: + throw new BaseException(`Unknown operation ${proto.mode}`); + } + } + + _structuralCheck(proto) { + var self = this._readSelf(proto); + var context = this._readContext(proto); + + if (isBlank(self)) { + if (ArrayChanges.supports(context)) { + self = new ArrayChanges(); + } else if (KeyValueChanges.supports(context)) { + self = new KeyValueChanges(); + } + } + + 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})`); + } + + } + + _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); + if (isBlank(updatedRecords)) { + updatedRecords = _singleElementList; + updatedRecords[0] = record; + + } else if (updatedRecords === _singleElementList) { + updatedRecords = [_singleElementList[0], record]; + + } else { + ListWrapper.push(updatedRecords, record); + } + return updatedRecords; + } + + _detectChangesInChildren(throwOnChange:boolean) { + var children = this.children; + for(var i = 0; i < children.length; ++i) { + children[i]._detectChanges(throwOnChange); + } + } + + _readContext(proto) { + return this.values[proto.contextIndex]; + } + + _readSelf(proto) { + return this.values[proto.record_type_selfIndex]; + } + + _writeSelf(proto, value) { + this.values[proto.record_type_selfIndex] = value; + } + + _readArgs(proto) { + var res = ListWrapper.createFixedSize(proto.args.length); + var args = proto.args; + for (var i = 0; i < args.length; ++i) { + res[i] = this.values[args[i]]; + } + return res; + } +} + +var _singleElementList = [null]; + +function isSame(a, b) { + if (a === b) return true; + if (a instanceof String && b instanceof String && a == b) return true; + if ((a !== a) && (b !== b)) return true; + return false; +} \ No newline at end of file diff --git a/modules/change_detection/src/exceptions.js b/modules/change_detection/src/exceptions.js new file mode 100644 index 0000000000..3da6598c7b --- /dev/null +++ b/modules/change_detection/src/exceptions.js @@ -0,0 +1,30 @@ +import {ProtoRecord} from './proto_change_detector'; + +export class ExpressionChangedAfterItHasBeenChecked extends Error { + message:string; + + constructor(proto:ProtoRecord, change:any) { + this.message = `Expression '${proto.expressionAsString}' has changed after it was checked. ` + + `Previous value: '${change.previousValue}'. Current value: '${change.currentValue}'`; + } + + toString():string { + return this.message; + } +} + +export class ChangeDetectionError extends Error { + message:string; + originalException:any; + location:string; + + constructor(proto:ProtoRecord, originalException:any) { + this.originalException = originalException; + this.location = proto.expressionAsString; + this.message = `${this.originalException} in [${this.location}]`; + } + + toString():string { + return this.message; + } +} \ No newline at end of file diff --git a/modules/change_detection/src/interfaces.js b/modules/change_detection/src/interfaces.js new file mode 100644 index 0000000000..8e2cf5a3c8 --- /dev/null +++ b/modules/change_detection/src/interfaces.js @@ -0,0 +1,36 @@ +import {List} from 'facade/collection'; + +export class ChangeRecord { + bindingMemento:any; + change:any; + + constructor(bindingMemento, change) { + this.bindingMemento = bindingMemento; + this.change = change; + } + + //REMOVE IT + get currentValue() { + return this.change.currentValue; + } + + get previousValue() { + return this.change.previousValue; + } +} + +export class ChangeDispatcher { + onRecordChange(groupMemento, records:List) {} +} + +export class ChangeDetector { + parent:ChangeDetector; + + addChild(cd:ChangeDetector) {} + removeChild(cd:ChangeDetector) {} + remove() {} + setContext(context:any) {} + + detectChanges() {} + checkNoChanges() {} +} diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index bae81ea26d..2c77895941 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -15,7 +15,7 @@ export class AST { throw new BaseException("Not supported"); } - visit(visitor, args) { + visit(visitor) { } toString():string { @@ -28,12 +28,12 @@ export class EmptyExpr extends AST { return null; } - visit(visitor, args) { + visit(visitor) { //do nothing } } -export class Collection extends AST { +export class Structural extends AST { value:AST; constructor(value:AST) { this.value = value; @@ -43,8 +43,8 @@ export class Collection extends AST { return value.eval(context); } - visit(visitor, args) { - visitor.visitCollection(this, args); + visit(visitor) { + return visitor.visitStructural(this); } } @@ -53,8 +53,8 @@ export class ImplicitReceiver extends AST { return context; } - visit(visitor, args) { - visitor.visitImplicitReceiver(this, args); + visit(visitor) { + return visitor.visitImplicitReceiver(this); } } @@ -76,8 +76,8 @@ export class Chain extends AST { return result; } - visit(visitor, args) { - visitor.visitChain(this, args); + visit(visitor) { + return visitor.visitChain(this); } } @@ -99,8 +99,8 @@ export class Conditional extends AST { } } - visit(visitor, args) { - visitor.visitConditional(this, args); + visit(visitor) { + return visitor.visitConditional(this); } } @@ -146,8 +146,8 @@ export class AccessMember extends AST { return this.setter(evaluatedContext, value); } - visit(visitor, args) { - visitor.visitAccessMember(this, args); + visit(visitor) { + return visitor.visitAccessMember(this); } } @@ -176,8 +176,8 @@ export class KeyedAccess extends AST { return value; } - visit(visitor, args) { - visitor.visitKeyedAccess(this, args); + visit(visitor) { + return visitor.visitKeyedAccess(this); } } @@ -193,8 +193,8 @@ export class Formatter extends AST { this.allArgs = ListWrapper.concat([exp], args); } - visit(visitor, args) { - visitor.visitFormatter(this, args); + visit(visitor) { + return visitor.visitFormatter(this); } } @@ -208,8 +208,8 @@ export class LiteralPrimitive extends AST { return this.value; } - visit(visitor, args) { - visitor.visitLiteralPrimitive(this, args); + visit(visitor) { + return visitor.visitLiteralPrimitive(this); } } @@ -223,8 +223,8 @@ export class LiteralArray extends AST { return ListWrapper.map(this.expressions, (e) => e.eval(context)); } - visit(visitor, args) { - visitor.visitLiteralArray(this, args); + visit(visitor) { + return visitor.visitLiteralArray(this); } } @@ -244,8 +244,8 @@ export class LiteralMap extends AST { return res; } - visit(visitor, args) { - visitor.visitLiteralMap(this, args); + visit(visitor) { + return visitor.visitLiteralMap(this); } } @@ -285,8 +285,8 @@ export class Binary extends AST { throw 'Internal error [$operation] not handled'; } - visit(visitor, args) { - visitor.visitBinary(this, args); + visit(visitor) { + return visitor.visitBinary(this); } } @@ -300,8 +300,8 @@ export class PrefixNot extends AST { return !this.expression.eval(context); } - visit(visitor, args) { - visitor.visitPrefixNot(this, args); + visit(visitor) { + return visitor.visitPrefixNot(this); } } @@ -317,8 +317,8 @@ export class Assignment extends AST { return this.target.assign(context, this.value.eval(context)); } - visit(visitor, args) { - visitor.visitAssignment(this, args); + visit(visitor) { + return visitor.visitAssignment(this); } } @@ -349,8 +349,8 @@ export class MethodCall extends AST { return this.fn(evaluatedContext, evaluatedArgs); } - visit(visitor, args) { - visitor.visitMethodCall(this, args); + visit(visitor) { + return visitor.visitMethodCall(this); } } @@ -370,8 +370,8 @@ export class FunctionCall extends AST { return FunctionWrapper.apply(obj, evalList(context, this.args)); } - visit(visitor, args) { - visitor.visitFunctionCall(this, args); + visit(visitor) { + return visitor.visitFunctionCall(this); } } @@ -397,8 +397,8 @@ export class ASTWithSource extends AST { return this.ast.assign(context, value); } - visit(visitor, args) { - return this.ast.visit(visitor, args); + visit(visitor) { + return this.ast.visit(visitor); } toString():string { @@ -420,21 +420,21 @@ export class TemplateBinding { //INTERFACE export class AstVisitor { - visitAccessMember(ast:AccessMember, args) {} - visitAssignment(ast:Assignment, args) {} - visitBinary(ast:Binary, args) {} - visitChain(ast:Chain, args){} - visitCollection(ast:Collection, args) {} - visitConditional(ast:Conditional, args) {} - visitFormatter(ast:Formatter, args) {} - visitFunctionCall(ast:FunctionCall, args) {} - visitImplicitReceiver(ast:ImplicitReceiver, args) {} - visitKeyedAccess(ast:KeyedAccess, args) {} - visitLiteralArray(ast:LiteralArray, args) {} - visitLiteralMap(ast:LiteralMap, args) {} - visitLiteralPrimitive(ast:LiteralPrimitive, args) {} - visitMethodCall(ast:MethodCall, args) {} - visitPrefixNot(ast:PrefixNot, args) {} + visitAccessMember(ast:AccessMember) {} + visitAssignment(ast:Assignment) {} + visitBinary(ast:Binary) {} + visitChain(ast:Chain){} + visitStructural(ast:Structural) {} + visitConditional(ast:Conditional) {} + visitFormatter(ast:Formatter) {} + visitFunctionCall(ast:FunctionCall) {} + visitImplicitReceiver(ast:ImplicitReceiver) {} + visitKeyedAccess(ast:KeyedAccess) {} + visitLiteralArray(ast:LiteralArray) {} + visitLiteralMap(ast:LiteralMap) {} + visitLiteralPrimitive(ast:LiteralPrimitive) {} + visitMethodCall(ast:MethodCall) {} + visitPrefixNot(ast:PrefixNot) {} } var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; diff --git a/modules/change_detection/src/proto_change_detector.js b/modules/change_detection/src/proto_change_detector.js new file mode 100644 index 0000000000..b33ce72a51 --- /dev/null +++ b/modules/change_detection/src/proto_change_detector.js @@ -0,0 +1,277 @@ +import {isPresent, isBlank, BaseException} from 'facade/lang'; +import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; + +import { + AccessMember, + Assignment, + AST, + ASTWithSource, + AstVisitor, + Binary, + Chain, + Structural, + Conditional, + Formatter, + FunctionCall, + ImplicitReceiver, + KeyedAccess, + LiteralArray, + LiteralMap, + LiteralPrimitive, + MethodCall, + PrefixNot + } from './parser/ast'; + +import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; +import {ChangeDispatcher, ChangeDetector} from './interfaces'; +import {DynamicChangeDetector} from './dynamic_change_detector'; + +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 class ProtoRecord { + mode:number; + name:string; + funcOrValue:any; + args:List; + contextIndex:number; + record_type_selfIndex:number; + bindingMemento:any; + groupMemento:any; + terminal:boolean; + expressionAsString:string; + + constructor(mode:number, + name:string, + funcOrValue, + args:List, + contextIndex:number, + record_type_selfIndex:number, + bindingMemento:any, + groupMemento:any, + terminal:boolean, + expressionAsString:string) { + + this.mode = mode; + this.name = name; + this.funcOrValue = funcOrValue; + this.args = args; + this.contextIndex = contextIndex; + this.record_type_selfIndex = record_type_selfIndex; + this.bindingMemento = bindingMemento; + this.groupMemento = groupMemento; + this.terminal = terminal; + this.expressionAsString = expressionAsString; + } +} + +export class ProtoChangeDetector { + records:List; + + constructor() { + this.records = []; + } + + 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); + } + } + + instantiate(dispatcher:any, formatters:Map) { + return new DynamicChangeDetector(dispatcher, formatters, this.records); + } +} + +class ProtoOperationsCreator { + protoRecords:List; + bindingMemento:any; + groupMemento:any; + contextIndex:number; + expressionAsString:string; + + constructor(bindingMemento:any, groupMemento:any, contextIndex:number, expressionAsString:string) { + this.protoRecords = []; + this.bindingMemento = bindingMemento; + this.groupMemento = groupMemento; + this.contextIndex = contextIndex; + this.expressionAsString = expressionAsString; + } + + visitImplicitReceiver(ast:ImplicitReceiver) { + return 0; + } + + visitLiteralPrimitive(ast:LiteralPrimitive) { + return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0); + } + + visitAccessMember(ast:AccessMember) { + var receiver = ast.receiver.visit(this); + return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], receiver); + } + + visitFormatter(ast:Formatter) { + return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), 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); + } + + 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); + } + + visitLiteralArray(ast:LiteralArray) { + return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Array()", _arrayFn(ast.expressions.length), + this._visitAll(ast.expressions), 0); + } + + visitLiteralMap(ast:LiteralMap) { + return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Map()", _mapFn(ast.keys, ast.values.length), + this._visitAll(ast.values), 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); + } + + visitPrefixNot(ast:PrefixNot) { + var exp = ast.expression.visit(this) + return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "-", _operation_negate, [exp], 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); + } + + visitStructural(ast:Structural) { + var value = ast.value.visit(this); + return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "record_type_structural_check", 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); + } + + _visitAll(asts:List) { + var res = ListWrapper.createFixedSize(asts.length); + for (var i = 0; i < asts.length; ++i) { + res[i] = asts[i].visit(this); + } + return res; + } + + _addRecord(type, name, funcOrValue, args, context) { + var record_type_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; + } +} + +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; + } + + 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]); + default: throw new BaseException(`Does not support literal maps with more than 9 elements`); + } +} + +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; + 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]]; +} \ No newline at end of file diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js deleted file mode 100644 index c0352a7cb3..0000000000 --- a/modules/change_detection/src/record.js +++ /dev/null @@ -1,505 +0,0 @@ -import {ProtoRecordRange, RecordRange} from './record_range'; -import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang'; -import {List, Map, ListWrapper, MapWrapper} from 'facade/collection'; -import {ArrayChanges} from './array_changes'; -import {KeyValueChanges} from './keyvalue_changes'; - -var _fresh = new Object(); - -const RECORD_TYPE_MASK = 0x000f; -export const RECORD_TYPE_CONST = 0x0000; -export const RECORD_TYPE_INVOKE_CLOSURE = 0x0001; -export const RECORD_TYPE_INVOKE_FORMATTER = 0x0002; -export const RECORD_TYPE_INVOKE_METHOD = 0x0003; -export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004; -const RECORD_TYPE_ARRAY = 0x0005; -const RECORD_TYPE_KEY_VALUE = 0x0006; -const RECORD_TYPE_MARKER = 0x0007; -export const RECORD_TYPE_PROPERTY = 0x0008; -const RECORD_TYPE_NULL= 0x0009; - -const RECORD_FLAG_DISABLED = 0x0100; -export const RECORD_FLAG_IMPLICIT_RECEIVER = 0x0200; -export const RECORD_FLAG_COLLECTION = 0x0400; - -/** - * For now we are dropping expression coalescence. We can always add it later, but - * real world numbers show that it does not provide significant benefits. - */ -export class ProtoRecord { - recordRange:ProtoRecordRange; - _mode:int; - context:any; - funcOrValue:any; - arity:int; - name:string; - dest:any; - groupMemento:any; - expressionAsString:string; - - next:ProtoRecord; - - recordInConstruction:Record; - - constructor(recordRange:ProtoRecordRange, - mode:int, - funcOrValue, - arity:int, - name:string, - dest, - groupMemento, - expressionAsString:string) { - - this.recordRange = recordRange; - this._mode = mode; - this.funcOrValue = funcOrValue; - this.arity = arity; - this.name = name; - this.dest = dest; - this.groupMemento = groupMemento; - this.expressionAsString = expressionAsString; - - this.next = null; - // The concrete Record instantiated from this ProtoRecord - this.recordInConstruction = null; - } - - setIsImplicitReceiver() { - this._mode |= RECORD_FLAG_IMPLICIT_RECEIVER; - } -} - - -/** - * Represents a Record for keeping track of changes. A change is a difference between previous - * and current value. - * - * By default changes are detected using dirty checking, but a notifier can be present which can - * notify the records of changes by means other than dirty checking. For example Object.observe - * or events on DOM elements. - * - * DESIGN NOTES: - * - No inheritance allowed so that code is monomorphic for performance. - * - Atomic watch operations - * - Defaults to dirty checking - * - Keep this object as lean as possible. (Lean in number of fields) - */ -export class Record { - recordRange:RecordRange; - protoRecord:ProtoRecord; - next:Record; - prev:Record; - - /// This reference can change. - nextEnabled:Record; - - /// This reference can change. - prevEnabled:Record; - - previousValue; - currentValue; - - _mode:int; - context; - funcOrValue; - args:List; - - // Opaque data which will be the target of notification. - // If the object is instance of Record, then it it is directly processed - // Otherwise it is the context used by ChangeDispatcher. - dest; - - constructor(recordRange:RecordRange, protoRecord:ProtoRecord, formatters:Map) { - this.recordRange = recordRange; - this.protoRecord = protoRecord; - - this.next = null; - this.prev = null; - this.nextEnabled = null; - this.prevEnabled = null; - this.dest = null; - - this.previousValue = null; - - this.context = null; - this.funcOrValue = null; - this.args = null; - - if (isBlank(protoRecord)) { - this._mode = RECORD_TYPE_MARKER | RECORD_FLAG_DISABLED; - return; - } - - this._mode = protoRecord._mode; - - // Return early for collections, further init delayed until updateContext() - if (this.isCollection()) return; - - this.currentValue = _fresh; - - var type = this.getType(); - - if (type === RECORD_TYPE_CONST) { - this.funcOrValue = protoRecord.funcOrValue; - - } else if (type === RECORD_TYPE_INVOKE_PURE_FUNCTION) { - this.funcOrValue = protoRecord.funcOrValue; - this.args = ListWrapper.createFixedSize(protoRecord.arity); - - } else if (type === RECORD_TYPE_INVOKE_FORMATTER) { - this.funcOrValue = MapWrapper.get(formatters, protoRecord.funcOrValue); - this.args = ListWrapper.createFixedSize(protoRecord.arity); - - } else if (type === RECORD_TYPE_INVOKE_METHOD) { - this.funcOrValue = protoRecord.funcOrValue; - this.args = ListWrapper.createFixedSize(protoRecord.arity); - - } else if (type === RECORD_TYPE_INVOKE_CLOSURE) { - this.args = ListWrapper.createFixedSize(protoRecord.arity); - - } else if (type === RECORD_TYPE_PROPERTY) { - this.funcOrValue = protoRecord.funcOrValue; - } - } - - // getters & setters perform much worse on some browsers - // see http://jsperf.com/vicb-getter-vs-function - getType():int { - return this._mode & RECORD_TYPE_MASK; - } - - setType(value:int) { - this._mode = (this._mode & ~RECORD_TYPE_MASK) | value; - } - - isDisabled():boolean { - return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED; - } - - isEnabled():boolean { - return !this.isDisabled(); - } - - _setDisabled(value:boolean) { - if (value) { - this._mode |= RECORD_FLAG_DISABLED; - } else { - this._mode &= ~RECORD_FLAG_DISABLED; - } - } - - enable() { - if (this.isEnabled()) return; - - var prevEnabled = this.findPrevEnabled(); - var nextEnabled = this.findNextEnabled(); - - this.prevEnabled = prevEnabled; - this.nextEnabled = nextEnabled; - - if (isPresent(prevEnabled)) prevEnabled.nextEnabled = this; - if (isPresent(nextEnabled)) nextEnabled.prevEnabled = this; - - this._setDisabled(false); - } - - disable() { - var prevEnabled = this.prevEnabled; - var nextEnabled = this.nextEnabled; - - if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled; - if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled; - - this._setDisabled(true); - } - - isImplicitReceiver():boolean { - return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER; - } - - isCollection():boolean { - return (this._mode & RECORD_FLAG_COLLECTION) === RECORD_FLAG_COLLECTION; - } - - static createMarker(rr:RecordRange):Record { - return new Record(rr, null, null); - } - - check():boolean { - if (this.isCollection()) { - return this._checkCollection(); - } else { - return this._checkSingleRecord(); - } - } - - _checkSingleRecord():boolean { - this.previousValue = this.currentValue; - this.currentValue = this._calculateNewValue(); - if (isSame(this.previousValue, this.currentValue)) return false; - this._updateDestination(); - return true; - } - - _updateDestination() { - if (this.dest instanceof Record) { - if (isPresent(this.protoRecord.dest.position)) { - this.dest.updateArg(this.currentValue, this.protoRecord.dest.position); - } else { - this.dest.updateContext(this.currentValue); - } - } - } - - // return whether the content has changed - _checkCollection():boolean { - switch(this.getType()) { - case RECORD_TYPE_KEY_VALUE: - var kvChangeDetector:KeyValueChanges = this.currentValue; - return kvChangeDetector.check(this.context); - - case RECORD_TYPE_ARRAY: - var arrayChangeDetector:ArrayChanges = this.currentValue; - return arrayChangeDetector.check(this.context); - - case RECORD_TYPE_NULL: - // no need to check the content again unless the context changes - this.disable(); - this.currentValue = null; - return true; - - default: - throw new BaseException(`Unsupported record type (${this.getType()})`); - } - } - - _calculateNewValue() { - try { - return this.__calculateNewValue(); - } catch (e) { - throw new ChangeDetectionError(this, e); - } - } - - __calculateNewValue() { - switch (this.getType()) { - case RECORD_TYPE_PROPERTY: - var propertyGetter:Function = this.funcOrValue; - return propertyGetter(this.context); - - case RECORD_TYPE_INVOKE_METHOD: - var methodInvoker:Function = this.funcOrValue; - return methodInvoker(this.context, this.args); - - case RECORD_TYPE_INVOKE_CLOSURE: - return FunctionWrapper.apply(this.context, this.args); - - case RECORD_TYPE_INVOKE_PURE_FUNCTION: - case RECORD_TYPE_INVOKE_FORMATTER: - this.disable(); - return FunctionWrapper.apply(this.funcOrValue, this.args); - - case RECORD_TYPE_CONST: - this.disable(); - return this.funcOrValue; - - default: - throw new BaseException(`Unsupported record type (${this.getType()})`); - } - } - - updateArg(value, position:int) { - this.args[position] = value; - this.enable(); - } - - updateContext(value) { - this.context = value; - this.enable(); - - if (this.isCollection()) { - if (ArrayChanges.supports(value)) { - if (this.getType() != RECORD_TYPE_ARRAY) { - this.setType(RECORD_TYPE_ARRAY); - this.currentValue = new ArrayChanges(); - } - return; - } - - if (KeyValueChanges.supports(value)) { - if (this.getType() != RECORD_TYPE_KEY_VALUE) { - this.setType(RECORD_TYPE_KEY_VALUE); - this.currentValue = new KeyValueChanges(); - } - return; - } - - if (isBlank(value)) { - this.setType(RECORD_TYPE_NULL); - } else { - throw new BaseException("Collection records must be array like, map like or null"); - } - } - } - - terminatesExpression():boolean { - return !(this.dest instanceof Record); - } - - isMarkerRecord():boolean { - return this.getType() == RECORD_TYPE_MARKER; - } - - expressionMemento() { - return this.protoRecord.dest; - } - - expressionAsString() { - return this.protoRecord.expressionAsString; - } - - groupMemento() { - return isPresent(this.protoRecord) ? this.protoRecord.groupMemento : null; - } - - - /** - * Returns the next enabled record. This search is not limited to the current range. - * - * [H ER1 T] [H ER2 T] _nextEnable(ER1) will return ER2 - * - * The function skips disabled ranges. - */ - findNextEnabled() { - if (this.isEnabled()) return this.nextEnabled; - - var record = this.next; - while (isPresent(record) && record.isDisabled()) { - if (record.isMarkerRecord() && record.recordRange.disabled) { - record = record.recordRange.tailRecord.next; - } else { - record = record.next; - } - } - return record; - } - - /** - * Returns the prev enabled record. This search is not limited to the current range. - * - * [H ER1 T] [H ER2 T] _nextEnable(ER2) will return ER1 - * - * The function skips disabled ranges. - */ - findPrevEnabled() { - if (this.isEnabled()) return this.prevEnabled; - - var record = this.prev; - while (isPresent(record) && record.isDisabled()) { - if (record.isMarkerRecord() && record.recordRange.disabled) { - record = record.recordRange.headRecord.prev; - } else { - record = record.prev; - } - } - return record; - } - - inspect() { - return _inspect(this); - } - - inspectRange() { - return this.recordRange.inspect(); - } -} - -function _inspect(record:Record) { - function mode() { - switch (record.getType()) { - case RECORD_TYPE_PROPERTY: - return "property"; - case RECORD_TYPE_INVOKE_METHOD: - return "invoke_method"; - case RECORD_TYPE_INVOKE_CLOSURE: - return "invoke_closure"; - case RECORD_TYPE_INVOKE_PURE_FUNCTION: - return "pure_function"; - case RECORD_TYPE_INVOKE_FORMATTER: - return "invoke_formatter"; - case RECORD_TYPE_CONST: - return "const"; - case RECORD_TYPE_KEY_VALUE: - return "key_value"; - case RECORD_TYPE_ARRAY: - return "array"; - case RECORD_TYPE_NULL: - return "null"; - case RECORD_TYPE_MARKER: - return "marker"; - default: - return "unexpected type!"; - } - } - - function disabled() { - return record.isDisabled() ? "disabled" : "enabled"; - } - - function description() { - var name = isPresent(record.protoRecord) ? record.protoRecord.name : ""; - var exp = isPresent(record.protoRecord) ? record.protoRecord.expressionAsString : ""; - var currValue = record.currentValue; - var context = record.context; - - return `${mode()}, ${name}, ${disabled()} ` + - ` Current: ${currValue}, Context: ${context} in [${exp}]`; - } - - if (isBlank(record)) return null; - if (!(record instanceof Record)) return record; - - return new _RecordInspect(description(), record); -} - -class _RecordInspect { - description:string; - record:Record; - - constructor(description:string,record:Record) { - this.description = description; - this.record = record; - } - - get next() { - return _inspect(this.record.next); - } - get nextEnabled() { - return _inspect(this.record.nextEnabled); - } - get dest() { - return _inspect(this.record.dest); - } -} - -function isSame(a, b) { - if (a === b) return true; - if ((a !== a) && (b !== b)) return true; - return false; -} - -export class ChangeDetectionError extends Error { - message:string; - originalException:any; - location:string; - - constructor(record:Record, originalException:any) { - this.originalException = originalException; - this.location = record.protoRecord.expressionAsString; - this.message = `${this.originalException} in [${this.location}]`; - } - - toString():string { - return this.message; - } -} diff --git a/modules/change_detection/src/record_range.js b/modules/change_detection/src/record_range.js deleted file mode 100644 index 14574ba7be..0000000000 --- a/modules/change_detection/src/record_range.js +++ /dev/null @@ -1,558 +0,0 @@ -import { - ProtoRecord, - Record, - RECORD_FLAG_COLLECTION, - RECORD_FLAG_IMPLICIT_RECEIVER, - RECORD_TYPE_CONST, - RECORD_TYPE_INVOKE_CLOSURE, - RECORD_TYPE_INVOKE_FORMATTER, - RECORD_TYPE_INVOKE_METHOD, - RECORD_TYPE_INVOKE_PURE_FUNCTION, - RECORD_TYPE_PROPERTY -} from './record'; - -import {FIELD, IMPLEMENTS, isBlank, isPresent, int, autoConvertAdd, BaseException, - NumberWrapper} from 'facade/lang'; -import {List, Map, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; -import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; -import { - AccessMember, - Assignment, - AST, - AstVisitor, - Binary, - Chain, - Collection, - Conditional, - Formatter, - FunctionCall, - ImplicitReceiver, - KeyedAccess, - LiteralArray, - LiteralMap, - LiteralPrimitive, - MethodCall, - PrefixNot -} from './parser/ast'; - -export class ProtoRecordRange { - recordCreator: ProtoRecordCreator; - constructor() { - this.recordCreator = null; - } - - /** - * Parses [ast] into [ProtoRecord]s and adds them to [ProtoRecordRange]. - * - * @param astWithSource The expression to watch - * @param expressionMemento an opaque object which will be passed to ChangeDispatcher on - * detecting a change. - * @param groupMemento - * @param content Whether to watch collection content (true) or reference (false, default) - */ - addRecordsFromAST(ast:AST, - expressionMemento, - groupMemento, - content:boolean = false) - { - if (this.recordCreator === null) { - this.recordCreator = new ProtoRecordCreator(this); - } - - if (content) { - ast = new Collection(ast); - } - this.recordCreator.createRecordsFromAST(ast, expressionMemento, groupMemento); - } - - // TODO(rado): the type annotation should be dispatcher:ChangeDispatcher. - // but @Implements is not ready yet. - instantiate(dispatcher, formatters:Map):RecordRange { - var recordRange:RecordRange = new RecordRange(this, dispatcher); - if (this.recordCreator !== null) { - this._createRecords(recordRange, formatters); - this._setDestination(); - } - return recordRange; - } - - _createRecords(recordRange:RecordRange, formatters:Map) { - for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) { - var record = new Record(recordRange, proto, formatters); - proto.recordInConstruction = record; - recordRange.addRecord(record); - } - } - - _setDestination() { - for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) { - if (proto.dest instanceof Destination) { - proto.recordInConstruction.dest = proto.dest.record.recordInConstruction; - } else { - proto.recordInConstruction.dest = proto.dest; - } - proto.recordInConstruction = null; - } - } -} - -export class RecordRange { - protoRecordRange:ProtoRecordRange; - dispatcher:any; //ChangeDispatcher - headRecord:Record; - tailRecord:Record; - disabled:boolean; - // TODO(rado): the type annotation should be dispatcher:ChangeDispatcher. - // but @Implements is not ready yet. - constructor(protoRecordRange:ProtoRecordRange, dispatcher) { - this.protoRecordRange = protoRecordRange; - this.dispatcher = dispatcher; - - this.disabled = false; - - this.headRecord = Record.createMarker(this); - this.tailRecord = Record.createMarker(this); - - _link(this.headRecord, this.tailRecord); - } - - /// addRecord assumes that the record is newly created, so it is enabled. - addRecord(record:Record) { - var lastRecord = this.tailRecord.prev; - - _link(lastRecord, record); - if (!lastRecord.isDisabled()) { - _linkEnabled(lastRecord, record); - } - _link(record, this.tailRecord); - } - - addRange(child:RecordRange) { - var lastRecord = this.tailRecord.prev; - var prevEnabledRecord = this.tailRecord.findPrevEnabled(); - var nextEnabledRerord = this.tailRecord.findNextEnabled(); - - var firstEnabledChildRecord = child.findFirstEnabledRecord(); - var lastEnabledChildRecord = child.findLastEnabledRecord(); - - _link(lastRecord, child.headRecord); - _link(child.tailRecord, this.tailRecord); - - if (isPresent(prevEnabledRecord) && isPresent(firstEnabledChildRecord)) { - _linkEnabled(prevEnabledRecord, firstEnabledChildRecord); - } - - if (isPresent(nextEnabledRerord) && isPresent(lastEnabledChildRecord)) { - _linkEnabled(lastEnabledChildRecord, nextEnabledRerord); - } - } - - remove() { - var firstEnabledChildRecord = this.findFirstEnabledRecord(); - var next = this.tailRecord.next; - var prev = this.headRecord.prev; - - _link(prev, next); - - if (isPresent(firstEnabledChildRecord)) { - var lastEnabledChildRecord = this.findLastEnabledRecord(); - var nextEnabled = lastEnabledChildRecord.nextEnabled; - var prevEnabled = firstEnabledChildRecord.prevEnabled; - if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled; - if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled; - } - } - - disable() { - var firstEnabledChildRecord = this.findFirstEnabledRecord(); - if (isPresent(firstEnabledChildRecord)) { - // There could be a last enabled record only if first enabled exists - var lastEnabledChildRecord = this.findLastEnabledRecord(); - var nextEnabled = lastEnabledChildRecord.nextEnabled; - var prevEnabled = firstEnabledChildRecord.prevEnabled; - if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled; - if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled; - } - - this.disabled = true; - } - - enable() { - var prevEnabledRecord = this.headRecord.findPrevEnabled(); - var nextEnabledRecord = this.tailRecord.findNextEnabled(); - - var firstEnabledthisRecord = this.findFirstEnabledRecord(); - var lastEnabledthisRecord = this.findLastEnabledRecord(); - - if (isPresent(firstEnabledthisRecord) && isPresent(prevEnabledRecord)){ - _linkEnabled(prevEnabledRecord, firstEnabledthisRecord); - } - - if (isPresent(lastEnabledthisRecord) && isPresent(nextEnabledRecord)){ - _linkEnabled(lastEnabledthisRecord, nextEnabledRecord); - } - - this.disabled = false; - } - - /** - * Returns the first enabled record in the current range. - * - * [H ER1 ER2 R3 T] returns ER1 - * [H R1 ER2 R3 T] returns ER2 - * - * If no enabled records, returns null. - * - * [H R1 R2 R3 T] returns null - * - * The function skips disabled sub ranges. - */ - findFirstEnabledRecord() { - var record = this.headRecord.next; - while (record !== this.tailRecord && record.isDisabled()) { - if (record.isMarkerRecord() && record.recordRange.disabled) { - record = record.recordRange.tailRecord.next; - } else { - record = record.next; - } - } - return record === this.tailRecord ? null : record; - } - - /** - * Returns the last enabled record in the current range. - * - * [H ER1 ER2 R3 T] returns ER2 - * [H R1 ER2 R3 T] returns ER2 - * - * If no enabled records, returns null. - * - * [H R1 R2 R3 T] returns null - * - * The function skips disabled sub ranges. - */ - findLastEnabledRecord() { - var record = this.tailRecord.prev; - while (record !== this.headRecord && record.isDisabled()) { - if (record.isMarkerRecord() && record.recordRange.disabled) { - record = record.recordRange.headRecord.prev; - } else { - record = record.prev; - } - } - return record === this.headRecord ? null : record; - } - - /** - * Sets the context (the object) on which the change detection expressions will - * dereference themselves on. Since the RecordRange can be reused the context - * can be re-set many times during the lifetime of the RecordRange. - * - * @param context the new context for change detection for the current RecordRange - */ - setContext(context) { - for (var record:Record = this.headRecord; - record != null; - record = record.next) { - - if (record.isImplicitReceiver()) { - this._setContextForRecord(context, record); - } - } - } - - _setContextForRecord(context, record:Record) { - var proto = record.protoRecord; - - while (context instanceof ContextWithVariableBindings) { - if (context.hasBinding(proto.name)) { - this._setVarBindingGetter(context, record, proto); - return; - } - context = context.parent; - } - - this._setRegularGetter(context, record, proto); - } - - _setVarBindingGetter(context, record:Record, proto:ProtoRecord) { - record.funcOrValue = _mapGetter(proto.name); - record.updateContext(context.varBindings); - } - - _setRegularGetter(context, record:Record, proto:ProtoRecord) { - record.funcOrValue = proto.funcOrValue; - record.updateContext(context); - } - - inspect() { - return _inspect(this); - } -} - -function _inspect(recordRange:RecordRange) { - var res = []; - for(var r = recordRange.headRecord.next; r != recordRange.tailRecord; r = r.next){ - ListWrapper.push(res, r.inspect().description); - } - return res; -} - -function _link(a:Record, b:Record) { - a.next = b; - b.prev = a; -} - -function _linkEnabled(a:Record, b:Record) { - a.nextEnabled = b; - b.prevEnabled = a; -} - -export class ChangeDispatcher { - onRecordChange(groupMemento, records:List) {} -} - -//todo: vsavkin: Create Array and Context destinations? -class Destination { - record:ProtoRecord; - position:int; - constructor(record:ProtoRecord, position:int) { - this.record = record; - this.position = position; - } -} - - -@IMPLEMENTS(AstVisitor) -class ProtoRecordCreator { - protoRecordRange:ProtoRecordRange; - headRecord:ProtoRecord; - tailRecord:ProtoRecord; - groupMemento:any; - expressionAsString:string; - - constructor(protoRecordRange) { - this.protoRecordRange = protoRecordRange; - this.headRecord = null; - this.tailRecord = null; - this.expressionAsString = null; - } - - visitImplicitReceiver(ast:ImplicitReceiver, args) { - throw new BaseException('Should never visit an implicit receiver'); - } - - visitLiteralPrimitive(ast:LiteralPrimitive, dest) { - this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, null, dest)); - } - - visitBinary(ast:Binary, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, - _operationToFunction(ast.operation), 2, ast.operation, dest); - ast.left.visit(this, new Destination(record, 0)); - ast.right.visit(this, new Destination(record, 1)); - this.add(record); - } - - visitPrefixNot(ast:PrefixNot, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, "-", dest); - ast.expression.visit(this, new Destination(record, 0)); - this.add(record); - } - - visitAccessMember(ast:AccessMember, dest) { - var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, ast.name, dest); - if (ast.receiver instanceof ImplicitReceiver) { - record.setIsImplicitReceiver(); - } else { - ast.receiver.visit(this, new Destination(record, null)); - } - this.add(record); - } - - visitFormatter(ast:Formatter, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, ast.name, dest); - for (var i = 0; i < ast.allArgs.length; ++i) { - ast.allArgs[i].visit(this, new Destination(record, i)); - } - this.add(record); - } - - visitMethodCall(ast:MethodCall, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, ast.name, dest); - for (var i = 0; i < ast.args.length; ++i) { - ast.args[i].visit(this, new Destination(record, i)); - } - if (ast.receiver instanceof ImplicitReceiver) { - record.setIsImplicitReceiver(); - } else { - ast.receiver.visit(this, new Destination(record, null)); - } - this.add(record); - } - - visitFunctionCall(ast:FunctionCall, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, null, dest); - ast.target.visit(this, new Destination(record, null)); - for (var i = 0; i < ast.args.length; ++i) { - ast.args[i].visit(this, new Destination(record, i)); - } - this.add(record); - } - - visitCollection(ast: Collection, dest) { - var record = this.construct(RECORD_FLAG_COLLECTION, null, null, null, dest); - ast.value.visit(this, new Destination(record, null)); - this.add(record); - } - - visitConditional(ast:Conditional, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, "?:", dest); - ast.condition.visit(this, new Destination(record, 0)); - ast.trueExp.visit(this, new Destination(record, 1)); - ast.falseExp.visit(this, new Destination(record, 2)); - this.add(record); - } - - visitKeyedAccess(ast:KeyedAccess, dest) { - var record = this.construct(RECORD_TYPE_INVOKE_METHOD, _keyedAccess, 1, "[]", dest); - ast.obj.visit(this, new Destination(record, null)); - ast.key.visit(this, new Destination(record, 0)); - this.add(record); - } - - visitLiteralArray(ast:LiteralArray, dest) { - var length = ast.expressions.length; - var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, "Array()", dest); - for (var i = 0; i < length; ++i) { - ast.expressions[i].visit(this, new Destination(record, i)); - } - this.add(record); - } - - visitLiteralMap(ast:LiteralMap, dest) { - var length = ast.values.length; - var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, "Map()", dest); - for (var i = 0; i < length; ++i) { - ast.values[i].visit(this, new Destination(record, i)); - } - this.add(record); - } - - visitChain(ast:Chain, dest){this._unsupported();} - - visitAssignment(ast:Assignment, dest) {this._unsupported();} - - visitTemplateBindings(ast, dest) {this._unsupported();} - - createRecordsFromAST(ast:AST, expressionMemento:any, groupMemento:any){ - this.groupMemento = groupMemento; - this.expressionAsString = ast.toString(); - ast.visit(this, expressionMemento); - } - - construct(recordType, funcOrValue, arity, name, dest) { - return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, - name, dest, this.groupMemento, this.expressionAsString); - } - - add(protoRecord:ProtoRecord) { - if (this.headRecord === null) { - this.headRecord = this.tailRecord = protoRecord; - } else { - this.tailRecord.next = protoRecord; - this.tailRecord = protoRecord; - } - } - - _unsupported() { - throw new BaseException("Unsupported"); - } -} - - -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; - 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 _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; - } - - 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]); - default: throw new BaseException(`Does not support literal maps with more than 9 elements`); - } -} - -//TODO: cache the getters -function _mapGetter(key) { - return function(map) { - return MapWrapper.get(map, key); - } -} - -function _keyedAccess(obj, args) { - return obj[args[0]]; -} diff --git a/modules/change_detection/test/change_detection_spec.js b/modules/change_detection/test/change_detection_spec.js new file mode 100644 index 0000000000..d224544cb1 --- /dev/null +++ b/modules/change_detection/test/change_detection_spec.js @@ -0,0 +1,477 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; + +import {isPresent, isBlank, isJsObject, BaseException, FunctionWrapper} from 'facade/lang'; +import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; + +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; +import {reflector} from 'reflection/reflection'; +import {arrayChangesAsString, kvChangesAsString} from './util'; + +import {ProtoChangeDetector, ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError, + ContextWithVariableBindings} + from 'change_detection/change_detection'; + + +export function main() { + function ast(exp:string, location:string = 'location') { + var parser = new Parser(new Lexer()); + return parser.parseBinding(exp, location); + } + + function createChangeDetector(memo:string, exp:string, context = null, formatters = null, + structural = false) { + var pcd = new ProtoChangeDetector(); + pcd.addAst(ast(exp), memo, memo, structural); + + 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 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); + + 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(); + + //cd.detectChanges(); + //expect(dispatcher.log).toEqual([]); + + 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 (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]'] + })]); + }); + }); + } + }); + + 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']); + }); + }); + }); +} + +class TestRecord { + a; + b; + c; +} + +class Person { + name:string; + age:number; + address:Address; + constructor(name:string, address:Address = null) { + this.name = name; + this.address = address; + } + + sayHi(m) { + return `Hi, ${m}`; + } + + toString():string { + var address = this.address == null ? '' : ' address=' + this.address.toString(); + + return 'name=' + this.name + address; + } +} + +class Address { + city:string; + constructor(city:string) { + this.city = city; + } + + toString():string { + return this.city; + } +} + +class TestData { + a; + + constructor(a) { + this.a = a; + } +} + +class TestDispatcher extends ChangeDispatcher { + log:List; + loggedValues:List; + onChange:Function; + + constructor() { + this.log = null; + this.loggedValues = null; + this.onChange = (_, __) => {}; + this.clear(); + } + + clear() { + this.log = ListWrapper.create(); + this.loggedValues = ListWrapper.create(); + } + + logValue(value) { + ListWrapper.push(this.loggedValues, value); + } + + onRecordChange(group, updates:List) { + var value = updates[0].change.currentValue; + var memento = updates[0].bindingMemento; + ListWrapper.push(this.log, memento + '=' + this._asString(value)); + + var values = ListWrapper.map(updates, (r) => r.change.currentValue); + ListWrapper.push(this.loggedValues, values); + + this.onChange(group, updates); + } + + + _asString(value) { + return (isBlank(value) ? 'null' : value.toString()); + } +} \ No newline at end of file diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js deleted file mode 100644 index 533d804885..0000000000 --- a/modules/change_detection/test/change_detector_spec.js +++ /dev/null @@ -1,555 +0,0 @@ -import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib'; - -import {isPresent, isBlank, isJsObject, BaseException} from 'facade/lang'; -import {List, ListWrapper, MapWrapper} from 'facade/collection'; - -import {Parser} from 'change_detection/parser/parser'; -import {Lexer} from 'change_detection/parser/lexer'; -import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings'; -import {arrayChangesAsString, kvChangesAsString} from './util'; - -import { - ChangeDetector, - ProtoRecordRange, - RecordRange, - ChangeDispatcher, - ProtoRecord, - ChangeDetectionError -} from 'change_detection/change_detector'; - -import {Record} from 'change_detection/record'; - -export function main() { - function ast(exp:string, location:string = 'location') { - var parser = new Parser(new Lexer()); - return parser.parseBinding(exp, location); - } - - function createChangeDetector(memo:string, exp:string, context = null, formatters = null, - content = false) { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast(exp), memo, memo, content); - - var dispatcher = new TestDispatcher(); - var rr = prr.instantiate(dispatcher, formatters); - rr.setContext(context); - - var cd = new ChangeDetector(rr); - - 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', () => { - describe('ChangeDetection', () => { - function createRange(dispatcher, ast, group) { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast, "memo", group); - return prr.instantiate(dispatcher, null); - } - - function detectChangesInRange(recordRange) { - var cd = new ChangeDetector(recordRange); - cd.detectChanges(); - } - - 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 support chained properties', () => { - 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 literals", () => { - expect(executeWatch('const', '10')).toEqual(['const=10']); - expect(executeWatch('const', '"str"')).toEqual(['const=str']); - }); - - 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']); - }); - }); - - describe("formatters", () => { - 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 rerun formatters only when arguments change", () => { - var counter = 0; - var formatters = MapWrapper.createFromPairs([ - ['formatter', (_) => {counter += 1; return 'value'}] - ]); - - var person = new Person('Jim'); - - var c = createChangeDetector('formatter', 'name | formatter', person, formatters); - var cd = c['changeDetector']; - - cd.detectChanges(); - expect(counter).toEqual(1); - - cd.detectChanges(); - expect(counter).toEqual(1); - - person.name = 'bob'; - cd.detectChanges(); - expect(counter).toEqual(2); - }); - }); - - - 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("collections", () => { - it("should support null values", () => { - var context = new TestData(null); - var c = createChangeDetector('a', 'a', context, null, true); - var cd = c["changeDetector"]; - var dsp = c["dispatcher"]; - - cd.detectChanges(); - expect(dsp.log).toEqual(['a=null']); - dsp.clear(); - - cd.detectChanges(); - expect(dsp.log).toEqual([]); - - context.a = [0]; - cd.detectChanges(); - - expect(dsp.log).toEqual(["a=" + - arrayChangesAsString({ - collection: ['0[null->0]'], - additions: ['0[null->0]'] - }) - ]); - dsp.clear(); - - context.a = null; - cd.detectChanges(); - expect(dsp.log).toEqual(['a=null']); - }); - - it("should throw if not collection / null", () => { - var context = new TestData("not collection / null"); - var c = createChangeDetector('a', 'a', context, null, true); - expect(() => c["changeDetector"].detectChanges()) - .toThrowError(new RegExp("Collection records must be array like, map like or 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 (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]'] - })]); - }); - }); - } - }); - - describe("adding new ranges", () => { - var dispatcher; - - beforeEach(() => { - dispatcher = new TestDispatcher(); - }); - - /** - * Tests that we can add a new range after the current - * record has been disabled. The new range must be processed - * during the same change detection run. - */ - it("should work when disabling the last enabled record", () => { - var rr = createRange(dispatcher, ast("1"), 1); - - dispatcher.onChange = (group, _) => { - if (group === 1) { // to prevent infinite loop - var rangeToAppend = createRange(dispatcher, ast("2"), 2); - rr.addRange(rangeToAppend); - } - }; - - detectChangesInRange(rr); - - expect(dispatcher.loggedValues).toEqual([[1], [2]]); - }); - }); - - describe("group changes", () => { - it("should notify the dispatcher when a group of records changes", () => { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast("1 + 2"), "memo", 1); - prr.addRecordsFromAST(ast("10 + 20"), "memo", 1); - prr.addRecordsFromAST(ast("100 + 200"), "memo2", 2); - - var dispatcher = new TestDispatcher(); - var rr = prr.instantiate(dispatcher, null); - - detectChangesInRange(rr); - - expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]); - }); - - it("should update every instance of a group individually", () => { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast("1 + 2"), "memo", "memo"); - - var dispatcher = new TestDispatcher(); - var rr = new RecordRange(null, dispatcher); - rr.addRange(prr.instantiate(dispatcher, null)); - rr.addRange(prr.instantiate(dispatcher, null)); - - detectChangesInRange(rr); - - expect(dispatcher.loggedValues).toEqual([[3], [3]]); - }); - - it("should notify the dispatcher before switching to the next group", () => { - var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast("a()"), "a", 1); - prr.addRecordsFromAST(ast("b()"), "b", 2); - prr.addRecordsFromAST(ast("c()"), "c", 2); - - var dispatcher = new TestDispatcher(); - var rr = prr.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'}; - rr.setContext(tr); - - detectChangesInRange(rr); - - 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 prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast("a"), "a", 1); - prr.addRecordsFromAST(ast("b()"), "b", 2); - - var tr = new TestRecord(); - tr.a = "a"; - tr.b = () => {tr.a = "newA";}; - - var dispatcher = new TestDispatcher(); - var rr = prr.instantiate(dispatcher, null); - rr.setContext(tr); - - expect(() => { - var cd = new ChangeDetector(rr, true); - cd.detectChanges(); - }).toThrowError(new RegExp("Expression 'a in location' has changed after it was checked")); - }); - }); - - describe("error handling", () => { - it("should wrap exceptions into ChangeDetectionError", () => { - try { - var rr = createRange(new TestDispatcher(), ast("invalidProp", "someComponent"), 1); - detectChangesInRange(rr); - - throw new BaseException("fail"); - } catch (e) { - expect(e).toBeAnInstanceOf(ChangeDetectionError); - expect(e.location).toEqual("invalidProp in someComponent"); - } - }); - }); - }); - }); -} - -class TestRecord { - a; - b; - c; -} - -class Person { - name:string; - address:Address; - constructor(name:string, address:Address = null) { - this.name = name; - this.address = address; - } - - sayHi(m) { - return `Hi, ${m}`; - } - - toString():string { - var address = this.address == null ? '' : ' address=' + this.address.toString(); - - return 'name=' + this.name + address; - } -} - -class Address { - city:string; - constructor(city:string) { - this.city = city; - } - - toString():string { - return this.city; - } -} - -class TestData { - a; - constructor(a) { - this.a = a; - } -} - -class TestDispatcher extends ChangeDispatcher { - log:List; - loggedValues:List; - onChange:Function; - - constructor() { - this.log = null; - this.loggedValues = null; - this.onChange = (_, __) => {}; - this.clear(); - } - - clear() { - this.log = ListWrapper.create(); - this.loggedValues = ListWrapper.create(); - } - - logValue(value) { - ListWrapper.push(this.loggedValues, value); - } - - onRecordChange(group, records:List) { - var value = records[0].currentValue; - var dest = records[0].protoRecord.dest; - ListWrapper.push(this.log, dest + '=' + this._asString(value)); - - var values = ListWrapper.map(records, (r) => r.currentValue); - ListWrapper.push(this.loggedValues, values); - - this.onChange(group, records); - } - - _asString(value) { - return (isBlank(value) ? 'null' : value.toString()); - } -} diff --git a/modules/change_detection/test/record_range_spec.js b/modules/change_detection/test/record_range_spec.js deleted file mode 100644 index 1045cce500..0000000000 --- a/modules/change_detection/test/record_range_spec.js +++ /dev/null @@ -1,364 +0,0 @@ -import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib'; - -import {Parser} from 'change_detection/parser/parser'; -import {Lexer} from 'change_detection/parser/lexer'; - -import {List, ListWrapper, MapWrapper} from 'facade/collection'; -import {isPresent} from 'facade/lang'; - -import { - ChangeDetector, - ProtoRecordRange, - RecordRange, - ProtoRecord, - RECORD_TYPE_CONST - } from 'change_detection/change_detector'; - -import {Record} from 'change_detection/record'; - -export function main() { - var lookupName = (names, item) => - ListWrapper.last( - ListWrapper.find(names, (pair) => pair[0] === item)); - - function enabledRecordsInReverseOrder(rr:RecordRange, names:List) { - var reversed = []; - var record = rr.findLastEnabledRecord(); - while (isPresent(record)) { - ListWrapper.push(reversed, lookupName(names, record)); - record = record.prevEnabled; - } - return reversed; - } - - function enabledRecords(rr:RecordRange, names:List) { - var res = []; - var record = rr.findFirstEnabledRecord(); - while (isPresent(record)) { - ListWrapper.push(res, lookupName(names, record)); - record = record.nextEnabled; - } - - // check that all links are set properly in both directions - var reversed = enabledRecordsInReverseOrder(rr, names); - expect(res).toEqual(ListWrapper.reversed(reversed)); - - return res; - } - - function createRecord(rr) { - return new Record(rr, new ProtoRecord(null, 0, null, null, null, null, null, null), null); - } - - describe('record range', () => { - it('should add records', () => { - var rr = new RecordRange(null, null); - var record1 = createRecord(rr); - var record2 = createRecord(rr); - - rr.addRecord(record1); - rr.addRecord(record2); - - expect(enabledRecords(rr, [ - [record1, 'record1'], - [record2, 'record2'] - ])).toEqual(['record1', 'record2']); - }); - - describe('adding/removing record ranges', () => { - var parent, child1, child2, child3; - var childRecord1, childRecord2, childRecord3; - var recordNames; - - beforeEach(() => { - parent = new RecordRange(null, null); - - child1 = new RecordRange(null, null); - childRecord1 = createRecord(child1); - child1.addRecord(childRecord1); - - child2 = new RecordRange(null, null); - childRecord2 = createRecord(child2); - child2.addRecord(childRecord2); - - child3 = new RecordRange(null, null); - childRecord3 = createRecord(child3); - child3.addRecord(childRecord3); - - recordNames = [ - [childRecord1, 'record1'], - [childRecord2, 'record2'], - [childRecord3, 'record3'] - ]; - }); - - it('should add record ranges', () => { - parent.addRange(child1); - parent.addRange(child2); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']); - }); - - it('should handle adding an empty range', () => { - var emptyRange = new RecordRange(null, null); - parent.addRange(child1); - parent.addRange(child2); - child1.addRange(emptyRange); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']); - }); - - it('should handle enabling/disabling an empty range', () => { - var emptyRange = new RecordRange(null, null); - emptyRange.disable(); - emptyRange.enable(); - - expect(enabledRecords(emptyRange, recordNames)).toEqual([]); - }); - - it('should handle adding a range into an empty range', () => { - var emptyRange = new RecordRange(null, null); - parent.addRange(emptyRange); - parent.addRange(child2); - - emptyRange.addRange(child1); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']); - }); - - it('should add nested record ranges', () => { - parent.addRange(child1); - child1.addRange(child2); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']); - }); - - it('should remove record ranges', () => { - parent.addRange(child1); - parent.addRange(child2); - - child1.remove(); - - expect(enabledRecords(parent, recordNames)).toEqual(['record2']); - - child2.remove(); - - expect(enabledRecords(parent, recordNames)).toEqual([]); - }); - - it('should remove an empty record range', () => { - var emptyRange = new RecordRange(null, null); - parent.addRange(child1); - parent.addRange(emptyRange); - parent.addRange(child2); - - emptyRange.remove(); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']); - }); - - it('should remove a record range surrounded by other ranges', () => { - parent.addRange(child1); - parent.addRange(child2); - parent.addRange(child3); - - child2.remove(); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record3']); - }); - }); - - describe('enabling/disabling records', () => { - var rr; - var record1, record2, record3, record4; - var recordNames; - - beforeEach(() => { - rr = new RecordRange(null, null); - record1 = createRecord(rr); - record2 = createRecord(rr); - record3 = createRecord(rr); - record4 = createRecord(rr); - - recordNames = [ - [record1, 'record1'], - [record2, 'record2'], - [record3, 'record3'], - [record4, 'record4'] - ]; - }); - - it('should disable a single record', () => { - rr.addRecord(record1); - - record1.disable(); - - expect(enabledRecords(rr, recordNames)).toEqual([]); - }); - - it('should enable a single record', () => { - rr.addRecord(record1); - record1.disable(); - - record1.enable(); - - expect(enabledRecords(rr, recordNames)).toEqual(['record1']); - }); - - it('should disable a record', () => { - rr.addRecord(record1); - rr.addRecord(record2); - rr.addRecord(record3); - rr.addRecord(record4); - - record2.disable(); - record3.disable(); - - expect(record2.isDisabled()).toBeTruthy(); - expect(record3.isDisabled()).toBeTruthy(); - - expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record4']); - }); - - it('should enable a record', () => { - rr.addRecord(record1); - rr.addRecord(record2); - rr.addRecord(record3); - rr.addRecord(record4); - record2.disable(); - record3.disable(); - - record2.enable(); - record3.enable(); - - expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record2', 'record3', 'record4']); - }); - - it('should disable a single record in a range', () => { - var rr1 = new RecordRange(null, null); - rr1.addRecord(record1); - - var rr2 = new RecordRange(null, null); - rr2.addRecord(record2); - - var rr3 = new RecordRange(null, null); - rr3.addRecord(record3); - - rr.addRange(rr1); - rr.addRange(rr2); - rr.addRange(rr3); - - record2.disable(); - - expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record3']); - - record2.enable(); - - expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record2', 'record3']); - }); - }); - - describe('enabling/disabling record ranges', () => { - var child1, child2, child3, child4; - var record1, record2, record3, record4; - var recordNames; - - beforeEach(() => { - child1 = new RecordRange(null, null); - record1 = createRecord(child1); - child1.addRecord(record1); - - child2 = new RecordRange(null, null); - record2 = createRecord(child2); - child2.addRecord(record2); - - child3 = new RecordRange(null, null); - record3 = createRecord(child3); - child3.addRecord(record3); - - child4 = new RecordRange(null, null); - record4 = createRecord(child4); - child4.addRecord(record4); - - recordNames = [ - [record1, 'record1'], - [record2, 'record2'], - [record3, 'record3'], - [record4, 'record4'] - ]; - }); - - it('should disable a single record range', () => { - var parent = new RecordRange(null, null); - parent.addRange(child1); - - child1.disable(); - - expect(enabledRecords(parent, recordNames)).toEqual([]); - }); - - it('should enable a single record range', () => { - var parent = new RecordRange(null, null); - parent.addRange(child1); - - child1.disable(); - - child1.enable(); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1']); - }); - - it('should disable a record range', () => { - var parent = new RecordRange(null, null); - parent.addRange(child1); - parent.addRange(child2); - parent.addRange(child3); - parent.addRange(child4); - - child2.disable(); - child3.disable(); - - expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record4']); - }); - - it('should enable a record range', () => { - var parent = new RecordRange(null, null); - parent.addRange(child1); - parent.addRange(child2); - parent.addRange(child3); - parent.addRange(child4); - - child2.disable(); - child2.disable(); - - child2.enable(); - child3.enable(); - - expect(enabledRecords(parent, recordNames)).toEqual([ - 'record1', 'record2', 'record3', 'record4' - ]); - }); - }); - - describe("inspect", () => { - it("should return the description of the record", () => { - var proto = new ProtoRecord(null, RECORD_TYPE_CONST, 1, 0, "name", null, "group", "expression"); - var record = new Record(null, proto, null); - - var i = record.inspect(); - expect(i.description).toContain("const, name, enabled"); - }); - - it("should return the description of the records in the range", () => { - var proto = new ProtoRecord(null, RECORD_TYPE_CONST, 1, 0, "name", null, "group", "expression"); - var record = new Record(null, proto, null); - var range = new RecordRange(null, null); - range.addRecord(record); - - var i = range.inspect();; - expect(i.length).toEqual(1); - expect(i[0]).toContain("const, name, enabled"); - }); - }); - }); -} diff --git a/modules/core/src/application.js b/modules/core/src/application.js index dab33152c2..0bae1c8c24 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, RecordRange} from 'change_detection/change_detection'; +import {Parser, Lexer, ChangeDetector} from 'change_detection/change_detection'; import {TemplateLoader} from './compiler/template_loader'; import {DirectiveMetadataReader} from './compiler/directive_metadata_reader'; import {DirectiveMetadata} from './compiler/directive_metadata'; @@ -21,7 +21,7 @@ var _rootBindings = [ ]; export var appViewToken = new OpaqueToken('AppView'); -export var appRecordRangeToken = new OpaqueToken('AppRecordRange'); +export var appChangeDetectorToken = new OpaqueToken('AppChangeDetector'); export var appElementToken = new OpaqueToken('AppElement'); export var appComponentAnnotatedTypeToken = new OpaqueToken('AppComponentAnnotatedType'); export var appDocumentToken = new OpaqueToken('AppDocument'); @@ -49,24 +49,22 @@ function _injectorBindings(appComponentType) { appComponentAnnotatedType) => { return compiler.compile(appComponentAnnotatedType.type, null).then( (protoView) => { - var appProtoView = ProtoView.createRootProtoView(protoView, + var appProtoView = ProtoView.createRootProtoView(protoView, appElement, appComponentAnnotatedType); // 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]), - bind(appRecordRangeToken).toFactory((rootView) => rootView.recordRange, + bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector, [appViewToken]), - bind(ChangeDetector).toFactory((appRecordRange) => - new ChangeDetector(appRecordRange, assertionsEnabled()), [appRecordRangeToken]), bind(appComponentType).toFactory((rootView) => rootView.elementInjectors[0].getComponent(), [appViewToken]), - bind(LifeCycle).toClass(LifeCycle) + bind(LifeCycle).toFactory((cd) => new LifeCycle(cd, assertionsEnabled()), [appChangeDetectorToken]) ]; } diff --git a/modules/core/src/compiler/pipeline/compile_element.js b/modules/core/src/compiler/pipeline/compile_element.js index 32e3319bfe..bbe8f79ab4 100644 --- a/modules/core/src/compiler/pipeline/compile_element.js +++ b/modules/core/src/compiler/pipeline/compile_element.js @@ -9,7 +9,7 @@ import {ElementBinder} from '../element_binder'; import {ProtoElementInjector} from '../element_injector'; import {ProtoView} from '../view'; -import {ASTWithSource} from 'change_detection/change_detection'; +import {AST} from 'change_detection/change_detection'; /** * Collects all data that is needed to process an element @@ -86,14 +86,14 @@ export class CompileElement { return this._classList; } - addTextNodeBinding(indexInParent:int, expression:ASTWithSource) { + addTextNodeBinding(indexInParent:int, expression:AST) { if (isBlank(this.textNodeBindings)) { this.textNodeBindings = MapWrapper.create(); } MapWrapper.set(this.textNodeBindings, indexInParent, expression); } - addPropertyBinding(property:string, expression:ASTWithSource) { + addPropertyBinding(property:string, expression:AST) { if (isBlank(this.propertyBindings)) { this.propertyBindings = MapWrapper.create(); } @@ -107,7 +107,7 @@ export class CompileElement { MapWrapper.set(this.variableBindings, contextName, templateName); } - addEventBinding(eventName:string, expression:ASTWithSource) { + addEventBinding(eventName:string, expression:AST) { if (isBlank(this.eventBindings)) { this.eventBindings = MapWrapper.create(); } diff --git a/modules/core/src/compiler/pipeline/element_binder_builder.js b/modules/core/src/compiler/pipeline/element_binder_builder.js index 120ac2353f..95967bdaf4 100644 --- a/modules/core/src/compiler/pipeline/element_binder_builder.js +++ b/modules/core/src/compiler/pipeline/element_binder_builder.js @@ -4,7 +4,7 @@ import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'facade/collection import {reflector} from 'reflection/reflection'; -import {Parser, ProtoRecordRange} from 'change_detection/change_detection'; +import {Parser, ProtoChangeDetector} from 'change_detection/change_detection'; import {Component, Directive} from '../../annotations/annotations'; import {DirectiveMetadata} from '../directive_metadata'; @@ -18,7 +18,7 @@ import {CompileControl} from './compile_control'; /** * Creates the ElementBinders and adds watches to the - * ProtoRecordRange. + * ProtoChangeDetector. * * Fills: * - CompileElement#inheritedElementBinder diff --git a/modules/core/src/compiler/pipeline/proto_view_builder.js b/modules/core/src/compiler/pipeline/proto_view_builder.js index 6376dbc267..a557ce7008 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 {ProtoRecordRange} from 'change_detection/change_detection'; +import {ProtoChangeDetector} from 'change_detection/change_detection'; import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; @@ -21,7 +21,7 @@ export class ProtoViewBuilder extends CompileStep { process(parent:CompileElement, current:CompileElement, control:CompileControl) { var inheritedProtoView = null; if (current.isViewRoot) { - inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange()); + inheritedProtoView = new ProtoView(current.element, new 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 b6ea93bd6f..aa1c541b9e 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -1,7 +1,7 @@ import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facade/dom'; import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'facade/collection'; -import {ProtoRecordRange, RecordRange, Record, - ChangeDispatcher, AST, ContextWithVariableBindings} from 'change_detection/change_detection'; +import {AST, ContextWithVariableBindings, ChangeDispatcher, ProtoChangeDetector, ChangeDetector, ChangeRecord} + from 'change_detection/change_detection'; import {ProtoElementInjector, ElementInjector, PreBuiltObjects} from './element_injector'; import {ElementBinder} from './element_binder'; @@ -30,7 +30,7 @@ export class View { elementInjectors:List; bindElements:List; textNodes:List; - recordRange:RecordRange; + changeDetector:ChangeDetector; /// When the view is part of render tree, the DocumentFragment is empty, which is why we need /// to keep track of the nodes. nodes:List; @@ -41,10 +41,10 @@ export class View { context: any; contextWithLocals:ContextWithVariableBindings; - constructor(proto:ProtoView, nodes:List, protoRecordRange:ProtoRecordRange, protoContextLocals:Map) { + constructor(proto:ProtoView, nodes:List, protoChangeDetector:ProtoChangeDetector, protoContextLocals:Map) { this.proto = proto; this.nodes = nodes; - this.recordRange = protoRecordRange.instantiate(this, NO_FORMATTERS); + this.changeDetector = protoChangeDetector.instantiate(this, NO_FORMATTERS); this.elementInjectors = null; this.rootElementInjectors = null; this.textNodes = null; @@ -92,7 +92,7 @@ export class View { // TODO(tbosch): if we have a contextWithLocals we actually only need to // set the contextWithLocals once. Would it be faster to always use a contextWithLocals // even if we don't have locals and not update the recordRange here? - this.recordRange.setContext(this.context); + this.changeDetector.setContext(this.context); } _dehydrateContext() { @@ -195,20 +195,20 @@ export class View { this._dehydrateContext(); } - onRecordChange(groupMemento, records:List) { - this._invokeMementoForRecords(records); + onRecordChange(groupMemento, records:List) { + this._invokeMementos(records); if (groupMemento instanceof DirectivePropertyGroupMemento) { this._notifyDirectiveAboutChanges(groupMemento, records); } } - _invokeMementoForRecords(records:List) { + _invokeMementos(records:List) { for(var i = 0; i < records.length; ++i) { this._invokeMementoFor(records[i]); } } - _notifyDirectiveAboutChanges(groupMemento, records:List) { + _notifyDirectiveAboutChanges(groupMemento, records:List) { var dir = groupMemento.directive(this.elementInjectors); if (dir instanceof OnChange) { dir.onChange(this._collectChanges(records)); @@ -216,8 +216,8 @@ export class View { } // dispatch to element injector or text nodes based on context - _invokeMementoFor(record:Record) { - var memento = record.expressionMemento(); + _invokeMementoFor(record:ChangeRecord) { + var memento = record.bindingMemento; if (memento instanceof DirectivePropertyMemento) { // we know that it is DirectivePropertyMemento var directiveMemento:DirectivePropertyMemento = memento; @@ -234,12 +234,12 @@ export class View { } } - _collectChanges(records:List) { + _collectChanges(records:List) { var changes = StringMapWrapper.create(); for(var i = 0; i < records.length; ++i) { var record = records[i]; var propertyUpdate = new PropertyUpdate(record.currentValue, record.previousValue); - StringMapWrapper.set(changes, record.expressionMemento()._setterName, propertyUpdate); + StringMapWrapper.set(changes, record.bindingMemento._setterName, propertyUpdate); } return changes; } @@ -248,7 +248,7 @@ export class View { export class ProtoView { element:Element; elementBinders:List; - protoRecordRange:ProtoRecordRange; + protoChangeDetector:ProtoChangeDetector; variableBindings: Map; protoContextLocals:Map; textNodesWithBindingCount:int; @@ -258,12 +258,12 @@ export class ProtoView { isTemplateElement:boolean; constructor( template:Element, - protoRecordRange:ProtoRecordRange) { + protoChangeDetector:ProtoChangeDetector) { this.element = template; this.elementBinders = []; this.variableBindings = MapWrapper.create(); this.protoContextLocals = MapWrapper.create(); - this.protoRecordRange = protoRecordRange; + this.protoChangeDetector = protoChangeDetector; this.textNodesWithBindingCount = 0; this.elementsWithBindingCount = 0; this.instantiateInPlace = false; @@ -299,8 +299,8 @@ export class ProtoView { } else { viewNodes = [rootElementClone]; } - var view = new View(this, viewNodes, this.protoRecordRange, this.protoContextLocals); + var view = new View(this, viewNodes, this.protoChangeDetector, this.protoContextLocals); var binders = this.elementBinders; var elementInjectors = ListWrapper.createFixedSize(binders.length); var rootElementInjectors = []; @@ -353,7 +353,7 @@ export class ProtoView { var lightDom = null; if (isPresent(binder.componentDirective)) { var childView = binder.nestedProtoView.instantiate(elementInjector); - view.recordRange.addRange(childView.recordRange); + view.changeDetector.addChild(childView.changeDetector); lightDom = binder.componentDirective.shadowDomStrategy.constructLightDom(view, childView, element); binder.componentDirective.shadowDomStrategy.attachTemplate(element, childView); @@ -434,7 +434,7 @@ export class ProtoView { } ListWrapper.push(elBinder.textNodeIndices, indexInParent); var memento = this.textNodesWithBindingCount++; - this.protoRecordRange.addRecordsFromAST(expression, memento, memento); + this.protoChangeDetector.addAst(expression, memento, memento); } /** @@ -447,7 +447,7 @@ export class ProtoView { this.elementsWithBindingCount++; } var memento = new ElementPropertyMemento(this.elementsWithBindingCount-1, setterName, setter); - this.protoRecordRange.addRecordsFromAST(expression, memento, memento); + this.protoChangeDetector.addAst(expression, memento, memento); } /** @@ -478,7 +478,7 @@ export class ProtoView { setter ); var groupMemento = DirectivePropertyGroupMemento.get(expMemento); - this.protoRecordRange.addRecordsFromAST(expression, expMemento, groupMemento, isContentWatch); + this.protoChangeDetector.addAst(expression, expMemento, groupMemento, isContentWatch); } // Create a rootView as if the compiler encountered , @@ -487,7 +487,7 @@ export class ProtoView { static createRootProtoView(protoView: ProtoView, insertionElement, rootComponentAnnotatedType: DirectiveMetadata): ProtoView { DOM.addClass(insertionElement, 'ng-binding'); - var rootProtoView = new ProtoView(insertionElement, new ProtoRecordRange()); + var rootProtoView = new ProtoView(insertionElement, new ProtoChangeDetector()); rootProtoView.instantiateInPlace = true; var binder = rootProtoView.bindElement( new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true)); @@ -507,7 +507,7 @@ export class ElementPropertyMemento { this._setter = setter; } - invoke(record:Record, bindElements:List) { + invoke(record:ChangeRecord, bindElements:List) { var element:Element = bindElements[this._elementIndex]; this._setter(element, record.currentValue); } @@ -529,7 +529,7 @@ export class DirectivePropertyMemento { this._setter = setter; } - invoke(record:Record, elementInjectors:List) { + invoke(record:ChangeRecord, elementInjectors:List) { var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex]; var directive = elementInjector.getAtIndex(this._directiveIndex); this._setter(directive, record.currentValue); diff --git a/modules/core/src/compiler/viewport.js b/modules/core/src/compiler/viewport.js index ade1c237cf..bef0a64254 100644 --- a/modules/core/src/compiler/viewport.js +++ b/modules/core/src/compiler/viewport.js @@ -84,7 +84,7 @@ export class ViewPort { } else { this._lightDom.redistribute(); } - this.parentView.recordRange.addRange(view.recordRange); + this.parentView.changeDetector.addChild(view.changeDetector); this._linkElementInjectors(view); return view; } @@ -98,7 +98,7 @@ export class ViewPort { } else { this._lightDom.redistribute(); } - removedView.recordRange.remove(); + removedView.changeDetector.remove(); this._unlinkElementInjectors(removedView); return removedView; } diff --git a/modules/core/src/life_cycle/life_cycle.js b/modules/core/src/life_cycle/life_cycle.js index c78761e406..38f0e71fc6 100644 --- a/modules/core/src/life_cycle/life_cycle.js +++ b/modules/core/src/life_cycle/life_cycle.js @@ -5,9 +5,11 @@ import {ListWrapper} from 'facade/collection'; export class LifeCycle { _changeDetector:ChangeDetector; + _enforceNoNewChanges:boolean; - constructor(changeDetector:ChangeDetector) { + constructor(changeDetector:ChangeDetector, enforceNoNewChanges:boolean = false) { this._changeDetector = changeDetector; + this._enforceNoNewChanges = enforceNoNewChanges; } registerWith(zone:VmTurnZone) { @@ -26,5 +28,8 @@ export class LifeCycle { tick() { this._changeDetector.detectChanges(); + if (this._enforceNoNewChanges) { + this._changeDetector.checkNoChanges(); + } } } \ No newline at end of file diff --git a/modules/core/test/compiler/integration_spec.js b/modules/core/test/compiler/integration_spec.js index 244c637079..b33c599917 100644 --- a/modules/core/test/compiler/integration_spec.js +++ b/modules/core/test/compiler/integration_spec.js @@ -29,7 +29,7 @@ export function main() { ctx = new MyComp(); view = pv.instantiate(null); view.hydrate(new Injector([]), null, ctx); - cd = new ChangeDetector(view.recordRange); + cd = view.changeDetector; } it('should consume text node changes', (done) => { 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 ddd74387b7..0be0d4074d 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,7 @@ 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, ProtoRecordRange} from 'change_detection/change_detection'; +import {ChangeDetector, Lexer, Parser, ProtoChangeDetector} from 'change_detection/change_detection'; import {Injector} from 'di/di'; export function main() { @@ -65,7 +65,7 @@ export function main() { } if (isPresent(current.element.getAttribute('viewroot'))) { current.isViewRoot = true; - current.inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange()); + current.inheritedProtoView = new ProtoView(current.element, new ProtoChangeDetector()); } else if (isPresent(parent)) { current.inheritedProtoView = parent.inheritedProtoView; } @@ -77,7 +77,7 @@ export function main() { evalContext = new Context(); view = protoView.instantiate(null); view.hydrate(new Injector([]), null, evalContext); - changeDetector = new ChangeDetector(view.recordRange); + changeDetector = view.changeDetector; } it('should not create an ElementBinder for elements that have no bindings', () => { @@ -204,7 +204,7 @@ export function main() { var results = pipeline.process(el('
')); var pv = results[0].inheritedProtoView; results[0].inheritedElementBinder.nestedProtoView = new ProtoView( - el('
'), new ProtoRecordRange()); + el('
'), new ProtoChangeDetector()); instantiateView(pv); evalContext.prop1 = 'a'; 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 f421ff8a4d..8f92cd9b09 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 @@ -34,7 +34,7 @@ export function main() { compiler.compile(MyComp, el(template)). then(createView). then((view) => { - var lc = new LifeCycle(new ChangeDetector(view.recordRange)); + var lc = new LifeCycle(view.changeDetector, false); assertions(view, lc); }); } diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index ee150c82e4..ed4d0007c1 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -5,7 +5,7 @@ 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, ProtoRecordRange, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser, ProtoChangeDetector, ChangeDetector} from 'change_detection/change_detection'; import {TemplateConfig} from 'core/annotations/template_config'; import {List, MapWrapper} from 'facade/collection'; import {DOM, Element} from 'facade/dom'; @@ -48,10 +48,10 @@ export function main() { someTemplateDirective = new DirectiveMetadataReader().read(SomeTemplate); }); - describe('instatiated from protoView', () => { + describe('instantiated from protoView', () => { var view; beforeEach(() => { - var pv = new ProtoView(el('
'), new ProtoRecordRange()); + var pv = new ProtoView(el('
'), new ProtoChangeDetector()); view = pv.instantiate(null); }); @@ -72,7 +72,7 @@ export function main() { describe('with locals', function() { var view; beforeEach(() => { - var pv = new ProtoView(el('
'), new ProtoRecordRange()); + var pv = new ProtoView(el('
'), new ProtoChangeDetector()); pv.bindVariable('context-foo', 'template-foo'); view = createView(pv); }); @@ -108,7 +108,7 @@ export function main() { } it('should collect the root node in the ProtoView element', () => { - var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoRecordRange()); + var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoChangeDetector()); var view = pv.instantiate(null); view.hydrate(null, null, null); expect(view.nodes.length).toBe(1); @@ -118,7 +118,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 ProtoRecordRange()); + var pv = new ProtoView(templateAwareCreateElement('
'), new ProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('a', null), 'prop', reflector.setter('prop')); @@ -130,7 +130,7 @@ export function main() { it('should collect property bindings on child elements with ng-binding class', () => { var pv = new ProtoView(templateAwareCreateElement('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('b', null), 'a', reflector.setter('a')); @@ -145,7 +145,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 ProtoRecordRange()); + var pv = new ProtoView(templateAwareCreateElement('
{{}}{{}}
'), new ProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('a', null)); pv.bindTextNode(2, parser.parseBinding('b', null)); @@ -159,7 +159,7 @@ export function main() { it('should collect text nodes with bindings on child elements with ng-binding class', () => { var pv = new ProtoView(templateAwareCreateElement('
{{}}
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('b', null)); @@ -175,7 +175,7 @@ export function main() { describe('inplace instantiation', () => { it('should be supported.', () => { var template = el('
'); - var pv = new ProtoView(template, new ProtoRecordRange()); + var pv = new ProtoView(template, new ProtoChangeDetector()); pv.instantiateInPlace = true; var view = pv.instantiate(null); view.hydrate(null, null, null); @@ -184,8 +184,8 @@ export function main() { it('should be off by default.', () => { var template = el('
') - var view = new ProtoView(template, new ProtoRecordRange()) - .instantiate(null); + var view = new ProtoView(template, new ProtoChangeDetector()) + .instantiate(null); view.hydrate(null, null, null); expect(view.nodes[0]).not.toBe(template); }); @@ -201,7 +201,7 @@ export function main() { describe('create ElementInjectors', () => { it('should use the directives of the ProtoElementInjector', () => { - var pv = new ProtoView(el('
'), new ProtoRecordRange()); + var pv = new ProtoView(el('
'), new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); var view = pv.instantiate(null); @@ -212,7 +212,7 @@ export function main() { it('should use the correct parent', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); @@ -226,7 +226,7 @@ export function main() { it('should not pass the host injector when a parent injector exists', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); var testProtoElementInjector = new TestProtoElementInjector(protoParent, 1, [AnotherDirective]); @@ -242,7 +242,7 @@ export function main() { it('should pass the host injector when there is no parent injector', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); var testProtoElementInjector = new TestProtoElementInjector(null, 1, [AnotherDirective]); pv.bindElement(testProtoElementInjector); @@ -259,7 +259,7 @@ export function main() { it('should collect a single root element injector', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]); pv.bindElement(protoParent); pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); @@ -272,7 +272,7 @@ export function main() { it('should collect multiple root element injectors', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective])); @@ -289,7 +289,7 @@ export function main() { var ctx; function createComponentWithSubPV(subProtoView) { - var pv = new ProtoView(el(''), new ProtoRecordRange()); + var pv = new ProtoView(el(''), new ProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true)); binder.componentDirective = someComponentDirective; binder.nestedProtoView = subProtoView; @@ -304,7 +304,7 @@ export function main() { } it('should expose component services to the component', () => { - var subpv = new ProtoView(el(''), new ProtoRecordRange()); + var subpv = new ProtoView(el(''), new ProtoChangeDetector()); var pv = createComponentWithSubPV(subpv); var view = createNestedView(pv); @@ -316,7 +316,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 ProtoRecordRange()); + el('
hello shadow dom
'), new ProtoChangeDetector()); subpv.bindElement( new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); var pv = createComponentWithSubPV(subpv); @@ -339,7 +339,7 @@ export function main() { it('dehydration should dehydrate child component views too', () => { var subpv = new ProtoView( - el('
hello shadow dom
'), new ProtoRecordRange()); + el('
hello shadow dom
'), new ProtoChangeDetector()); subpv.bindElement( new ProtoElementInjector(null, 0, [ServiceDependentDecorator])); var pv = createComponentWithSubPV(subpv); @@ -354,7 +354,7 @@ export function main() { }); it('should create shadow dom', () => { - var subpv = new ProtoView(el('hello shadow dom'), new ProtoRecordRange()); + var subpv = new ProtoView(el('hello shadow dom'), new ProtoChangeDetector()); var pv = createComponentWithSubPV(subpv); var view = createNestedView(pv); @@ -363,9 +363,9 @@ export function main() { }); it('should use the provided shadow DOM strategy', () => { - var subpv = new ProtoView(el('hello shadow dom'), new ProtoRecordRange()); + var subpv = new ProtoView(el('hello shadow dom'), new ProtoChangeDetector()); - var pv = new ProtoView(el(''), new ProtoRecordRange()); + var pv = new ProtoView(el(''), new ProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponentWithEmulatedShadowDom], true)); binder.componentDirective = new DirectiveMetadataReader().read(SomeComponentWithEmulatedShadowDom); binder.nestedProtoView = subpv; @@ -379,8 +379,8 @@ export function main() { describe('with template views', () => { function createViewWithTemplate() { var templateProtoView = new ProtoView( - el('
'), new ProtoRecordRange()); - var pv = new ProtoView(el(''), new ProtoRecordRange()); + el('
'), new ProtoChangeDetector()); + var pv = new ProtoView(el(''), new ProtoChangeDetector()); var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeTemplate])); binder.templateDirective = someTemplateDirective; binder.nestedProtoView = templateProtoView; @@ -424,7 +424,7 @@ export function main() { function createProtoView() { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new TestProtoElementInjector(null, 0, [])); pv.bindEvent('click', parser.parseBinding('callMe(\$event)', null)); return pv; @@ -464,12 +464,12 @@ export function main() { function createViewAndChangeDetector(protoView) { view = createView(protoView); ctx = view.context; - cd = new ChangeDetector(view.recordRange); + cd = view.changeDetector; } it('should consume text node changes', () => { var pv = new ProtoView(el('
{{}}
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(null); pv.bindTextNode(0, parser.parseBinding('foo', null)); createViewAndChangeDetector(pv); @@ -481,7 +481,7 @@ export function main() { it('should consume element binding changes', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(null); pv.bindElementProperty(parser.parseBinding('foo', null), 'id', reflector.setter('id')); createViewAndChangeDetector(pv); @@ -493,7 +493,7 @@ export function main() { it('should consume directive watch expression change', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); pv.bindDirectiveProperty(0, parser.parseBinding('foo', null), 'prop', reflector.setter('prop'), false); createViewAndChangeDetector(pv); @@ -505,7 +505,7 @@ export function main() { it('should notify a directive about changes after all its properties have been set', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false); @@ -522,7 +522,7 @@ export function main() { it('should provide a map of updated properties', () => { var pv = new ProtoView(el('
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false); @@ -547,7 +547,7 @@ export function main() { var element, pv; beforeEach(() => { element = DOM.createElement('div'); - pv = new ProtoView(el('
hi
'), new ProtoRecordRange()); + pv = new ProtoView(el('
hi
'), new ProtoChangeDetector()); }); it('should create the root component when instantiated', () => { diff --git a/modules/core/test/compiler/viewport_spec.js b/modules/core/test/compiler/viewport_spec.js index 634e4c100d..61c9ddbc65 100644 --- a/modules/core/test/compiler/viewport_spec.js +++ b/modules/core/test/compiler/viewport_spec.js @@ -5,10 +5,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 {ProtoRecordRange, Lexer, Parser} from 'change_detection/change_detection'; +import {ProtoChangeDetector, Lexer, Parser} from 'change_detection/change_detection'; function createView(nodes) { - var view = new View(null, nodes, new ProtoRecordRange(), MapWrapper.create()); + var view = new View(null, nodes, new ProtoChangeDetector(), MapWrapper.create()); view.init([], [], [], [], [], [], []); return view; } @@ -22,7 +22,7 @@ export function main() { dom = el(`
`); var insertionElement = dom.childNodes[1]; parentView = createView([dom.childNodes[0]]); - protoView = new ProtoView(el('
hi
'), new ProtoRecordRange()); + protoView = new ProtoView(el('
hi
'), new ProtoChangeDetector()); elementInjector = new ElementInjector(null, null, null); viewPort = new ViewPort(parentView, insertionElement, protoView, elementInjector); customViewWithOneNode = createView([el('
single
')]); @@ -117,25 +117,26 @@ export function main() { viewPort.hydrate(new Injector([]), null); var pv = new ProtoView(el('
{{}}
'), - new ProtoRecordRange()); + new ProtoChangeDetector()); pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective])); pv.bindTextNode(0, parser.parseBinding('foo', null)); fancyView = pv.instantiate(null); }); - it('hydrating should update rootElementInjectors and parent RR', () => { + it('hydrating should update rootElementInjectors and parent change detector', () => { viewPort.insert(fancyView); ListWrapper.forEach(fancyView.rootElementInjectors, (inj) => expect(inj.parent).toBe(elementInjector)); - expect(parentView.recordRange.findFirstEnabledRecord()).not.toBe(null); + + expect(parentView.changeDetector.children.length).toBe(1); }); - it('dehydrating should update rootElementInjectors and parent RR', () => { + it('dehydrating should update rootElementInjectors and parent change detector', () => { viewPort.insert(fancyView); viewPort.remove(); ListWrapper.forEach(fancyView.rootElementInjectors, (inj) => expect(inj.parent).toBe(null)); - expect(parentView.recordRange.findFirstEnabledRecord()).toBe(null); + expect(parentView.changeDetector.children.length).toBe(0); expect(viewPort.length).toBe(0); }); }); diff --git a/modules/directives/test/ng_non_bindable_spec.js b/modules/directives/test/ng_non_bindable_spec.js index abff3720d6..e09db2d5a9 100644 --- a/modules/directives/test/ng_non_bindable_spec.js +++ b/modules/directives/test/ng_non_bindable_spec.js @@ -20,7 +20,7 @@ export function main() { component = new TestComponent(); view = pv.instantiate(null); view.hydrate(new Injector([]), null, component); - cd = new ChangeDetector(view.recordRange); + cd = view.changeDetector; } function compileWithTemplate(template) { diff --git a/modules/directives/test/ng_repeat_spec.js b/modules/directives/test/ng_repeat_spec.js index ff3bf89b1a..ba90c9906a 100644 --- a/modules/directives/test/ng_repeat_spec.js +++ b/modules/directives/test/ng_repeat_spec.js @@ -27,7 +27,7 @@ export function main() { component = new TestComponent(); view = pv.instantiate(null); view.hydrate(new Injector([]), null, component); - cd = new ChangeDetector(view.recordRange); + cd = view.changeDetector; } function compileWithTemplate(template) { diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 6d6e5db625..4ec9eecbad 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -101,6 +101,9 @@ class ListWrapper { list.remove(items[i]); } } + static remove(List list, item) { + list.remove(item); + } static void clear(List l) { l.clear(); } static String join(List l, String s) => l.join(s); static bool isEmpty(list) => list.isEmpty; diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 5071a55df9..4f37a5c1ee 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -149,6 +149,10 @@ export class ListWrapper { list.splice(index, 1); } } + static remove(list, item) { + var index = list.indexOf(item); + list.splice(index, 1); + } static clear(list) { list.splice(0, list.length); }