feat(change_detection): add support for binary operations and literals
This commit is contained in:
parent
b8e3617a1d
commit
79a9430f2c
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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]];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue