diff --git a/modules/change_detection/src/change_detector.js b/modules/change_detection/src/change_detector.js index a81b707f5c..014e468843 100644 --- a/modules/change_detection/src/change_detector.js +++ b/modules/change_detection/src/change_detector.js @@ -16,7 +16,7 @@ export class ChangeDetector { var count:int = 0; for (record = this._rootWatchGroup.headRecord; record != null; - record = record.checkNext) { + record = record.next) { if (record.check()) { count++; } diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index e342381af4..c2a7cbbb99 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) { + visit(visitor, args) { } } @@ -24,8 +24,8 @@ export class ImplicitReceiver extends AST { return context; } - visit(visitor) { - visitor.visitImplicitReceiver(this); + visit(visitor, args) { + visitor.visitImplicitReceiver(this, args); } } @@ -47,8 +47,8 @@ export class Chain extends AST { return result; } - visit(visitor) { - visitor.visitChain(this); + visit(visitor, args) { + visitor.visitChain(this, args); } } @@ -70,8 +70,8 @@ export class Conditional extends AST { } } - visit(visitor) { - visitor.visitConditional(this); + visit(visitor, args) { + visitor.visitConditional(this, args); } } @@ -99,8 +99,8 @@ export class AccessMember extends AST { return this.setter(this.receiver.eval(context), value); } - visit(visitor) { - visitor.visitAccessMember(this); + visit(visitor, args) { + visitor.visitAccessMember(this, args); } } @@ -143,8 +143,8 @@ export class KeyedAccess extends AST { return value; } - visit(visitor) { - visitor.visitKeyedAccess(this); + visit(visitor, args) { + visitor.visitKeyedAccess(this, args); } } @@ -159,8 +159,8 @@ export class Formatter extends AST { this.allArgs = ListWrapper.concat([exp], args); } - visit(visitor) { - visitor.visitFormatter(this); + visit(visitor, args) { + visitor.visitFormatter(this, args); } } @@ -174,8 +174,8 @@ export class LiteralPrimitive extends AST { return this.value; } - visit(visitor) { - visitor.visitLiteralPrimitive(this); + visit(visitor, args) { + visitor.visitLiteralPrimitive(this, args); } } @@ -189,8 +189,8 @@ export class LiteralArray extends AST { return ListWrapper.map(this.expressions, (e) => e.eval(context)); } - visit(visitor) { - visitor.visitLiteralArray(this); + visit(visitor, args) { + visitor.visitLiteralArray(this, args); } } @@ -210,8 +210,8 @@ export class LiteralMap extends AST { return res; } - visit(visitor) { - visitor.visitLiteralMap(this); + visit(visitor, args) { + visitor.visitLiteralMap(this, args); } } @@ -256,8 +256,8 @@ export class Binary extends AST { throw 'Internal error [$operation] not handled'; } - visit(visitor) { - visitor.visitBinary(this); + visit(visitor, args) { + visitor.visitBinary(this, args); } } @@ -272,8 +272,8 @@ export class PrefixNot extends AST { return !this.expression.eval(context); } - visit(visitor) { - visitor.visitPrefixNot(this); + visit(visitor, args) { + visitor.visitPrefixNot(this, args); } } @@ -289,8 +289,8 @@ export class Assignment extends AST { return this.target.assign(context, this.value.eval(context)); } - visit(visitor) { - visitor.visitAssignment(this); + visit(visitor, args) { + visitor.visitAssignment(this, args); } } @@ -309,8 +309,8 @@ export class MethodCall extends AST { return this.fn(obj, evalList(context, this.args)); } - visit(visitor) { - visitor.visitMethodCall(this); + visit(visitor, args) { + visitor.visitMethodCall(this, args); } } @@ -332,27 +332,27 @@ export class FunctionCall extends AST { return FunctionWrapper.apply(obj, evalList(context, this.args)); } - visit(visitor) { - visitor.visitFunctionCall(this); + visit(visitor, args) { + visitor.visitFunctionCall(this, args); } } //INTERFACE export class AstVisitor { - visitChain(ast:Chain){} - visitImplicitReceiver(ast:ImplicitReceiver) {} - visitConditional(ast:Conditional) {} - visitAccessMember(ast:AccessMember) {} - visitKeyedAccess(ast:KeyedAccess) {} - visitBinary(ast:Binary) {} - visitPrefixNot(ast:PrefixNot) {} - visitLiteralPrimitive(ast:LiteralPrimitive) {} - visitFormatter(ast:Formatter) {} - visitAssignment(ast:Assignment) {} - visitLiteralArray(ast:LiteralArray) {} - visitLiteralMap(ast:LiteralMap) {} - visitMethodCall(ast:MethodCall) {} - visitFunctionCall(ast:FunctionCall) {} + visitChain(ast:Chain, args){} + visitImplicitReceiver(ast:ImplicitReceiver, args) {} + visitConditional(ast:Conditional, args) {} + visitAccessMember(ast:AccessMember, args) {} + visitKeyedAccess(ast:KeyedAccess, args) {} + visitBinary(ast:Binary, args) {} + visitPrefixNot(ast:PrefixNot, args) {} + visitLiteralPrimitive(ast:LiteralPrimitive, args) {} + visitFormatter(ast:Formatter, args) {} + visitAssignment(ast:Assignment, args) {} + visitLiteralArray(ast:LiteralArray, args) {} + visitLiteralMap(ast:LiteralMap, args) {} + visitMethodCall(ast:MethodCall, args) {} + visitFunctionCall(ast:FunctionCall, args) {} } var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index 28c827c501..6fc5b5fc0b 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -1,28 +1,44 @@ import {ProtoWatchGroup, WatchGroup} from './watch_group'; -import {FIELD} from 'facade/lang'; +import {FIELD, isPresent, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang'; +import {ListWrapper} from 'facade/collection'; import {ClosureMap} from 'change_detection/parser/closure_map'; +var _fresh = new Object(); + +export const PROTO_RECORD_CONST = 'const'; +export const PROTO_RECORD_FUNC = 'func'; +export const PROTO_RECORD_PROPERTY = 'property'; + /** * 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 { @FIELD('final watchGroup:wg.ProtoWatchGroup') - @FIELD('final fieldName:String') - /// order list of all records. Including head/tail markers + @FIELD('final context:Object') + @FIELD('final arity:int') + @FIELD('final dest') + @FIELD('next:ProtoRecord') @FIELD('prev:ProtoRecord') - // Opaque data which will be the target of notification. - // If the object is instance of Record, than it it is directly processed - // Otherwise it is the context used by WatchGroupDispatcher. - @FIELD('memento') - constructor(watchGroup:ProtoWatchGroup, fieldName:string, dispatchMemento) { + @FIELD('recordInConstruction:Record') + constructor(watchGroup:ProtoWatchGroup, + recordType:string, + funcOrValue, + arity:int, + dest) { + this.watchGroup = watchGroup; - this.fieldName = fieldName; - this.dispatchMemento = dispatchMemento; + this.recordType = recordType; + this.funcOrValue = funcOrValue; + this.arity = arity; + this.dest = dest; + this.next = null; this.prev = null; - } + + this.recordInConstruction = null; + } } @@ -43,101 +59,114 @@ export class ProtoRecord { export class Record { @FIELD('final watchGroup:WatchGroup') @FIELD('final protoRecord:ProtoRecord') - /// order list of all records. Including head/tail markers @FIELD('next:Record') @FIELD('prev:Record') - /// next record to dirty check - @FIELD('checkNext:Record') - @FIELD('checkPrev:Record') - // next notifier - @FIELD('notifierNext:Record') + @FIELD('dest:Record') + + @FIELD('previousValue') + @FIELD('currentValue') @FIELD('mode:int') @FIELD('context') - @FIELD('getter') - @FIELD('previousValue') - @FIELD('currentValue') - constructor(watchGroup/*:wg.WatchGroup*/, protoRecord:ProtoRecord) { - this.protoRecord = protoRecord; + @FIELD('funcOrValue') + @FIELD('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 WatchGroupDispatcher. + @FIELD('dest') + + constructor(watchGroup:WatchGroup, protoRecord:ProtoRecord) { this.watchGroup = watchGroup; + this.protoRecord = protoRecord; + this.next = null; this.prev = null; - this.checkNext = null; - this.checkPrev = null; - this.notifierNext = null; + this.dest = null; - this.mode = MODE_STATE_MARKER; - this.context = null; - this.getter = null; - this.arguments = null; this.previousValue = null; - // `this` means that the record is fresh - this.currentValue = this; + this.currentValue = _fresh; + + this.mode = null; + this.context = null; + this.funcOrValue = null; + this.args = null; + + if (protoRecord.recordType === PROTO_RECORD_CONST) { + this.mode = MODE_STATE_CONST; + this.funcOrValue = protoRecord.funcOrValue; + + } else if (protoRecord.recordType === PROTO_RECORD_FUNC) { + this.mode = MODE_STATE_INVOKE_FUNCTION; + this.funcOrValue = protoRecord.funcOrValue; + this.args = ListWrapper.createFixedSize(protoRecord.arity); + + } else if (protoRecord.recordType === PROTO_RECORD_PROPERTY) { + this.mode = MODE_STATE_PROPERTY; + this.funcOrValue = protoRecord.funcOrValue; + } } check():boolean { - var mode = this.mode; - var state = mode & MODE_MASK_STATE; - var notify = mode & MODE_MASK_NOTIFY; - var newValue; - switch (state) { - case MODE_STATE_MARKER: - return false; - case MODE_STATE_PROPERTY: - newValue = this.getter(this.context); - break; - case MODE_STATE_INVOKE_CLOSURE: - newValue = this.context(this.arguments); - break; - case MODE_STATE_INVOKE_METHOD: - newValue = this.getter(this.context, this.arguments); - break; - case MODE_STATE_MAP: - throw 'not implemented'; - case MODE_STATE_LIST: - throw 'not implemented'; - default: - throw 'not implemented'; - } + this.previousValue = this.currentValue; + this.currentValue = this._calculateNewValue(); + if (isSame(this.previousValue, this.currentValue)) return false; - var previousValue = this.currentValue; - if (previousValue === this) { - // When the record is checked for the first time we should always notify - this.currentValue = newValue; - this.previousValue = previousValue = null; - } else { - this.currentValue = newValue; - this.previousValue = previousValue; - - if (isSame(previousValue, newValue)) return false; - - // In Dart, we can have `str1 !== str2` but `str1 == str2` - if (previousValue instanceof String && - newValue instanceof String && - previousValue == newValue) { - return false - } - } - - // todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req) - if (this.protoRecord.dispatchMemento === null) { - this.next.setContext(this.currentValue); - } else { - // notify through dispatcher - this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dispatchMemento); - } + this._updateDestination(); return true; } - setContext(context) { - this.mode = MODE_STATE_PROPERTY; - this.context = context; - var closureMap = new ClosureMap(); - this.getter = closureMap.getter(this.protoRecord.fieldName); + _updateDestination() { + // todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req) + 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); + } + } else { + this.watchGroup.dispatcher.onRecordChange(this, this.protoRecord.dest); + } } + _calculateNewValue() { + var state = this.mode; + switch (state) { + case MODE_STATE_PROPERTY: + return this.funcOrValue(this.context); + + case MODE_STATE_INVOKE_FUNCTION: + return FunctionWrapper.apply(this.funcOrValue, this.args); + + case MODE_STATE_CONST: + return this.funcOrValue; + + case MODE_STATE_MARKER: + throw new BaseException('MODE_STATE_MARKER not implemented'); + + case MODE_STATE_INVOKE_METHOD: + throw new BaseException('MODE_STATE_INVOKE_METHOD not implemented'); + + case MODE_STATE_MAP: + throw new BaseException('MODE_STATE_MAP not implemented'); + + case MODE_STATE_LIST: + throw new BaseException('MODE_STATE_LIST not implemented'); + + default: + throw new BaseException('DEFAULT not implemented'); + } + } + + updateArg(value, position:int) { + this.args[position] = value; + } + + updateContext(value) { + this.context = value; + } } // The mode is divided into two parts. Which notification mechanism @@ -154,7 +183,7 @@ const MODE_STATE_MARKER = 0x0000; /// _context[_protoRecord.propname] => _getter(_context) const MODE_STATE_PROPERTY = 0x0001; /// _context(_arguments) -const MODE_STATE_INVOKE_CLOSURE = 0x0002; +const MODE_STATE_INVOKE_FUNCTION = 0x0002; /// _getter(_context, _arguments) const MODE_STATE_INVOKE_METHOD = 0x0003; @@ -163,7 +192,11 @@ const MODE_STATE_MAP = 0x0004; /// _context is Array/List/Iterable => _previousValue = ListChangeRecord const MODE_STATE_LIST = 0x0005; +/// _context is number/string +const MODE_STATE_CONST = 0x0006; + function isSame(a, b) { + if (a instanceof String && b instanceof String) return a == b; if (a === b) return true; if ((a !== a) && (b !== b)) return true; return false; diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index d327b72a69..ffad34e384 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -1,6 +1,7 @@ -import {ProtoRecord, Record} from './record'; -import {FIELD, IMPLEMENTS, isBlank, isPresent} from 'facade/lang'; -import {AST, AccessMember, ImplicitReceiver, AstVisitor, Binary, LiteralPrimitive} from './parser/ast'; +import {ProtoRecord, Record, PROTO_RECORD_CONST, PROTO_RECORD_FUNC, PROTO_RECORD_PROPERTY} from './record'; +import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException} from 'facade/lang'; +import {ListWrapper} from 'facade/collection'; +import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive, Binary} from './parser/ast'; export class ProtoWatchGroup { @FIELD('headRecord:ProtoRecord') @@ -43,28 +44,40 @@ export class ProtoWatchGroup { // but @Implements is not ready yet. instantiate(dispatcher):WatchGroup { var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); - var tail:Record = null; - var proto:ProtoRecord = null; - var prevRecord:Record = null; - if (this.headRecord !== null) { - watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord); + this._createRecords(watchGroup); + this._setDestination(); - for (proto = this.headRecord.next; proto != null; proto = proto.next) { - prevRecord = tail; - tail = new Record(watchGroup, proto); - tail.prev = prevRecord; - prevRecord.next = tail; - tail.checkPrev = prevRecord; - prevRecord.checkNext = tail; - } - - watchGroup.tailRecord = tail; } - return watchGroup; } + _createRecords(watchGroup:WatchGroup) { + var tail, prevRecord; + watchGroup.headRecord = tail = new Record(watchGroup, this.headRecord); + this.headRecord.recordInConstruction = watchGroup.headRecord; + + for (var proto = this.headRecord.next; proto != null; proto = proto.next) { + prevRecord = tail; + + tail = new Record(watchGroup, proto); + proto.recordInConstruction = tail; + + tail.prev = prevRecord; + prevRecord.next = tail; + } + + watchGroup.tailRecord = tail; + } + + _setDestination() { + for (var proto = this.headRecord; proto != null; proto = proto.next) { + if (proto.dest instanceof Destination) { + proto.recordInConstruction.dest = proto.dest.record.recordInConstruction; + } + proto.recordInConstruction = null; + } + } } export class WatchGroup { @@ -101,7 +114,8 @@ export class WatchGroup { for (var record:Record = this.headRecord; record != null; record = record.next) { - record.setContext(context); + + record.updateContext(context); } } } @@ -111,6 +125,15 @@ export class WatchGroupDispatcher { onRecordChange(record:Record, context) {} } +//todo: vsavkin: Create Array and Context destinations? +class Destination { + constructor(record:ProtoRecord, position:int) { + this.record = record; + this.position = position; + } +} + + @IMPLEMENTS(AstVisitor) class ProtoRecordCreator { @FIELD('final protoWatchGroup:ProtoWatchGroup') @@ -122,7 +145,7 @@ class ProtoRecordCreator { this.tailRecord = null; } - visitImplicitReceiver(ast:ImplicitReceiver) { + visitImplicitReceiver(ast:ImplicitReceiver, args) { //do nothing } @@ -137,16 +160,31 @@ class ProtoRecordCreator { ast.right.visit(this); } - visitAccessMember(ast:AccessMember) { - ast.receiver.visit(this); - this.add(new ProtoRecord(this.protoWatchGroup, ast.name, null)); + visitLiteralPrimitive(ast:LiteralPrimitive, dest) { + this.add(this.construct(PROTO_RECORD_CONST, ast.value, 0, dest)); + } + + visitBinary(ast:Binary, dest) { + var record = this.construct(PROTO_RECORD_FUNC, _operationToFunction(ast.operation), 2, dest); + + ast.left.visit(this, new Destination(record, 0)); + ast.right.visit(this, new Destination(record, 1)); + + this.add(record); + } + + visitAccessMember(ast:AccessMember, dest) { + var record = this.construct(PROTO_RECORD_PROPERTY, ast.getter, 0, dest); + ast.receiver.visit(this, new Destination(record, null)); + this.add(record); } createRecordsFromAST(ast:AST, memento){ - ast.visit(this); - if (isPresent(this.tailRecord)) { - this.tailRecord.dispatchMemento = memento; - } + ast.visit(this, memento); + } + + construct(recordType, funcOrValue, arity, dest) { + return new ProtoRecord(this.protoWatchGroup, recordType, funcOrValue, arity, dest); } add(protoRecord:ProtoRecord) { @@ -158,4 +196,41 @@ class ProtoRecordCreator { this.tailRecord = protoRecord; } } -} \ No newline at end of file +} + + +function _operationToFunction(operation:string):Function { + switch(operation) { + case '!' : return _operation_negate; + 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;} + diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index aa422d2d87..690e5e15c8 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -1,7 +1,8 @@ -import {describe, it, xit, expect} from 'test_lib/test_lib'; +import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib'; import {List, ListWrapper} from 'facade/collection'; -import {ImplicitReceiver, AccessMember} from 'change_detection/parser/ast'; +import {Parser} from 'change_detection/parser/parser'; +import {Lexer} from 'change_detection/parser/lexer'; import {ClosureMap} from 'change_detection/parser/closure_map'; import { @@ -16,77 +17,107 @@ import {Record} from 'change_detection/record'; export function main() { function ast(exp:string) { - var parts = exp.split("."); - var cm = new ClosureMap(); - return ListWrapper.reduce(parts, function (ast, fieldName) { - return new AccessMember(ast, fieldName, cm.getter(fieldName), cm.setter(fieldName)); - }, new ImplicitReceiver()); + var parser = new Parser(new Lexer(), new ClosureMap()); + return parser.parseBinding(exp); } - describe('change_detection', function() { - describe('ChangeDetection', function() { - it('should do simple watching', function() { - var person = new Person('misko', 38); - var pwg = new ProtoWatchGroup(); - pwg.watch(ast('name'), 'name'); - pwg.watch(ast('age'), 'age'); - var dispatcher = new LoggingDispatcher(); - var wg = pwg.instantiate(dispatcher); - wg.setContext(person); + function createChangeDetector(memo:string, exp:string, context = null) { + var pwg = new ProtoWatchGroup(); + pwg.watch(ast(exp), memo, false); + + var dispatcher = new LoggingDispatcher(); + var wg = pwg.instantiate(dispatcher); + wg.setContext(context); + + var cd = new ChangeDetector(wg); + + return {"changeDetector" : cd, "dispatcher" : dispatcher}; + } + + function executeWatch(memo:string, exp:string, context = null) { + var res = createChangeDetector(memo, exp, context); + res["changeDetector"].detectChanges(); + return res["dispatcher"].log; + } + + describe('change_detection', () => { + describe('ChangeDetection', () => { + it('should do simple watching', () => { + var person = new Person("misko"); + + var c = createChangeDetector('name', 'name', person); + var cd = c["changeDetector"]; + var dispatcher = c["dispatcher"]; - var cd = new ChangeDetector(wg); cd.detectChanges(); - expect(dispatcher.log).toEqual(['name=misko', 'age=38']); + expect(dispatcher.log).toEqual(['name=misko']); dispatcher.clear(); cd.detectChanges(); expect(dispatcher.log).toEqual([]); - person.age = 1; person.name = "Misko"; cd.detectChanges(); - expect(dispatcher.log).toEqual(['name=Misko', 'age=1']); + expect(dispatcher.log).toEqual(['name=Misko']); }); - it('should watch chained properties', function() { + it('should watch chained properties', () => { var address = new Address('Grenoble'); - var person = new Person('Victor', 36, address); - var pwg = new ProtoWatchGroup(); - pwg.watch(ast('address.city'), 'address.city', false); - var dispatcher = new LoggingDispatcher(); - var wg = pwg.instantiate(dispatcher); - wg.setContext(person); + var person = new Person('Victor', address); - var cd = new ChangeDetector(wg); - cd.detectChanges(); - expect(dispatcher.log).toEqual(['address.city=Grenoble']); - - dispatcher.clear(); - cd.detectChanges(); - expect(dispatcher.log).toEqual([]); - - address.city = 'Mountain View'; - cd.detectChanges(); - expect(dispatcher.log).toEqual(['address.city=Mountain View']); + expect(executeWatch('address.city', 'address.city', person)) + .toEqual(['address.city=Grenoble']); }); + it("should watch literals", () => { + expect(executeWatch('const', '10')).toEqual(['const=10']); + }); + + it("should watch 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']); + }); }); }); } class Person { - constructor(name:string, age:number, address:Address = null) { + constructor(name:string, address:Address = null) { this.name = name; - this.age = age; this.address = address; } toString():string { var address = this.address == null ? '' : ' address=' + this.address.toString(); - return 'name=' + this.name + - ' age=' + this.age.toString() + - address; + return 'name=' + this.name + address; } } diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index ea8dff3cf3..1ad1da2214 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -1,5 +1,6 @@ -import {describe, xit, it, expect, beforeEach, ddescribe, iit} from 'test_lib/test_lib'; +import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib'; import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view'; +import {Record, ProtoRecord} from 'change_detection/record'; import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector'; import {ProtoWatchGroup} from 'change_detection/watch_group'; import {ChangeDetector} from 'change_detection/change_detector'; @@ -94,42 +95,31 @@ export function main() { createCollectDomNodesTestCases(true); }); - describe('create ElementInjectors', () => { - it('should use the directives of the ProtoElementInjector', () => { - var pv = new ProtoView(createElement('
'), new ProtoWatchGroup()); - pv.bindElement(new ProtoElementInjector(null, 1, [Directive])); - - var view = pv.instantiate(null, null); - expect(view.elementInjectors.length).toBe(1); - expect(view.elementInjectors[0].get(Directive) instanceof Directive).toBe(true); + describe('react to watch group changes', function() { + var view; + beforeEach(() => { + var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); + var pv = new ProtoView(template, templateElementBinders(), + new ProtoWatchGroup(), false); + view = pv.instantiate(null, null); }); - it('should use the correct parent', () => { - var pv = new ProtoView(createElement('
'), - new ProtoWatchGroup()); - var protoParent = new ProtoElementInjector(null, 0, [Directive]); - pv.bindElement(protoParent); - pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); - - var view = pv.instantiate(null, null); - expect(view.elementInjectors.length).toBe(2); - expect(view.elementInjectors[0].get(Directive) instanceof Directive).toBe(true); - expect(view.elementInjectors[1].parent).toBe(view.elementInjectors[0]); + it('should consume text node changes', () => { + var record = new Record(null, null); + record.currentValue = 'Hello World!'; + view.onRecordChange(record , 0); + expect(view.textNodes[0].nodeValue).toEqual('Hello World!'); }); }); - describe('collect root element injectors', () => { - - it('should collect a single root element injector', () => { - var pv = new ProtoView(createElement('
'), - new ProtoWatchGroup()); - var protoParent = new ProtoElementInjector(null, 0, [Directive]); - pv.bindElement(protoParent); - pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective])); - - var view = pv.instantiate(null, null); - expect(view.rootElementInjectors.length).toBe(1); - expect(view.rootElementInjectors[0].get(Directive) instanceof Directive).toBe(true); + it('should consume element binding changes', () => { + var elementWithBinding = view.bindElements[0]; + expect(elementWithBinding.id).toEqual(''); + var record = new Record(null, null); + var memento = new ElementPropertyMemento(0, 'id'); + record.currentValue = 'foo'; + view.onRecordChange(record, memento); + expect(elementWithBinding.id).toEqual('foo'); }); it('should collect multiple root element injectors', () => { @@ -138,10 +128,13 @@ export function main() { pv.bindElement(new ProtoElementInjector(null, 1, [Directive])); pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective])); - var view = pv.instantiate(null, null); - expect(view.rootElementInjectors.length).toBe(2); - expect(view.rootElementInjectors[0].get(Directive) instanceof Directive).toBe(true); - expect(view.rootElementInjectors[1].get(AnotherDirective) instanceof AnotherDirective).toBe(true); + expect(elInj.get(Directive).prop).toEqual('foo'); + var record = new Record(null, null); + var memento = new DirectivePropertyMemento(1, 0, 'prop', + (o, v) => o.prop = v); + record.currentValue = 'bar'; + view.onRecordChange(record, memento); + expect(elInj.get(Directive).prop).toEqual('bar'); }); });