feat(change_detection): add support for binary operations and literals

This commit is contained in:
vsavkin 2014-11-10 16:11:29 -08:00
parent b8e3617a1d
commit 79a9430f2c
6 changed files with 371 additions and 239 deletions

View File

@ -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++;
}

View File

@ -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]];

View File

@ -1,27 +1,43 @@
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;

View File

@ -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) {
}
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;
tail.checkPrev = prevRecord;
prevRecord.checkNext = tail;
}
watchGroup.tailRecord = tail;
}
return watchGroup;
_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) {
@ -159,3 +197,40 @@ class ProtoRecordCreator {
}
}
}
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;}

View File

@ -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);
function createChangeDetector(memo:string, exp:string, context = null) {
var pwg = new ProtoWatchGroup();
pwg.watch(ast('name'), 'name');
pwg.watch(ast('age'), 'age');
pwg.watch(ast(exp), memo, false);
var dispatcher = new LoggingDispatcher();
var wg = pwg.instantiate(dispatcher);
wg.setContext(person);
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"];
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;
}
}

View File

@ -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('<div class="ng-binding"></div>'), 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('<div class="ng-binding"><span class="ng-binding"></span></div>'),
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('<div class="ng-binding"><span class="ng-binding"></span></div>'),
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');
});
});