feat(Change Detector): Add support for collection content watch
This commit is contained in:
parent
2d2f44949d
commit
bf71b94bde
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
isListLikeIterable,
|
||||
iterateListLike,
|
||||
ListWrapper,
|
||||
MapWrapper
|
||||
} from 'facade/collection';
|
||||
|
@ -12,7 +14,7 @@ import {
|
|||
looseIdentical,
|
||||
} from 'facade/lang';
|
||||
|
||||
export class CollectionChanges {
|
||||
export class ArrayChanges {
|
||||
_collection;
|
||||
_length:int;
|
||||
_linkedRecords:_DuplicateMap;
|
||||
|
@ -26,6 +28,7 @@ export class CollectionChanges {
|
|||
_movesTail:CollectionChangeRecord<V> ;
|
||||
_removalsHead:CollectionChangeRecord<V>;
|
||||
_removalsTail:CollectionChangeRecord<V>;
|
||||
|
||||
constructor() {
|
||||
this._collection = null;
|
||||
this._length = null;
|
||||
|
@ -45,6 +48,10 @@ export class CollectionChanges {
|
|||
this._removalsTail = null;
|
||||
}
|
||||
|
||||
static supports(obj):boolean {
|
||||
return isListLikeIterable(obj);
|
||||
}
|
||||
|
||||
get collection() {
|
||||
return this._collection;
|
||||
}
|
||||
|
@ -112,8 +119,19 @@ export class CollectionChanges {
|
|||
record = record._next;
|
||||
}
|
||||
} else {
|
||||
// todo(vicb) implement iterators
|
||||
throw "NYI";
|
||||
index = 0;
|
||||
iterateListLike(collection, (item) => {
|
||||
if (record === null || !looseIdentical(record.item, item)) {
|
||||
record = this._mismatch(record, item, index);
|
||||
mayBeDirty = true;
|
||||
} else if (mayBeDirty) {
|
||||
// TODO(misko): can we limit this to duplicates only?
|
||||
record = this._verifyReinsertion(record, item, index);
|
||||
}
|
||||
record = record._next;
|
||||
index++
|
||||
});
|
||||
this._length = index;
|
||||
}
|
||||
|
||||
this._truncate(record);
|
|
@ -1,19 +1,19 @@
|
|||
import {ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
|
||||
|
||||
import {stringify, looseIdentical} from 'facade/lang';
|
||||
import {stringify, looseIdentical, isJsObject} from 'facade/lang';
|
||||
|
||||
export class MapChanges {
|
||||
export class KeyValueChanges {
|
||||
_records:Map;
|
||||
_map:Map;
|
||||
_map:any;
|
||||
|
||||
_mapHead:MapChangeRecord;
|
||||
_previousMapHead:MapChangeRecord;
|
||||
_changesHead:MapChangeRecord;
|
||||
_changesTail:MapChangeRecord;
|
||||
_additionsHead:MapChangeRecord;
|
||||
_additionsTail:MapChangeRecord;
|
||||
_removalsHead:MapChangeRecord;
|
||||
_removalsTail:MapChangeRecord;
|
||||
_mapHead:KVChangeRecord;
|
||||
_previousMapHead:KVChangeRecord;
|
||||
_changesHead:KVChangeRecord;
|
||||
_changesTail:KVChangeRecord;
|
||||
_additionsHead:KVChangeRecord;
|
||||
_additionsTail:KVChangeRecord;
|
||||
_removalsHead:KVChangeRecord;
|
||||
_removalsTail:KVChangeRecord;
|
||||
|
||||
constructor() {
|
||||
this._records = MapWrapper.create();
|
||||
|
@ -28,6 +28,10 @@ export class MapChanges {
|
|||
this._removalsTail = null;
|
||||
}
|
||||
|
||||
static supports(obj):boolean {
|
||||
return obj instanceof Map || isJsObject(obj);
|
||||
}
|
||||
|
||||
get isDirty():boolean {
|
||||
return this._additionsHead !== null ||
|
||||
this._changesHead !== null ||
|
||||
|
@ -35,35 +39,35 @@ export class MapChanges {
|
|||
}
|
||||
|
||||
forEachItem(fn:Function) {
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
for (record = this._mapHead; record !== null; record = record._next) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachPreviousItem(fn:Function) {
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachChangedItem(fn:Function) {
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachAddedItem(fn:Function){
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachRemovedItem(fn:Function){
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||
fn(record);
|
||||
}
|
||||
|
@ -73,12 +77,12 @@ export class MapChanges {
|
|||
this._reset();
|
||||
this._map = map;
|
||||
var records = this._records;
|
||||
var oldSeqRecord:MapChangeRecord = this._mapHead;
|
||||
var lastOldSeqRecord:MapChangeRecord = null;
|
||||
var lastNewSeqRecord:MapChangeRecord = null;
|
||||
var oldSeqRecord:KVChangeRecord = this._mapHead;
|
||||
var lastOldSeqRecord:KVChangeRecord = null;
|
||||
var lastNewSeqRecord:KVChangeRecord = null;
|
||||
var seqChanged:boolean = false;
|
||||
|
||||
MapWrapper.forEach(map, (value, key) => {
|
||||
this._forEach(map, (value, key) => {
|
||||
var newSeqRecord;
|
||||
if (oldSeqRecord !== null && key === oldSeqRecord.key) {
|
||||
newSeqRecord = oldSeqRecord;
|
||||
|
@ -97,7 +101,7 @@ export class MapChanges {
|
|||
if (MapWrapper.contains(records, key)) {
|
||||
newSeqRecord = MapWrapper.get(records, key);
|
||||
} else {
|
||||
newSeqRecord = new MapChangeRecord(key);
|
||||
newSeqRecord = new KVChangeRecord(key);
|
||||
MapWrapper.set(records, key, newSeqRecord);
|
||||
newSeqRecord._currentValue = value;
|
||||
this._addToAdditions(newSeqRecord);
|
||||
|
@ -124,7 +128,7 @@ export class MapChanges {
|
|||
|
||||
_reset() {
|
||||
if (this.isDirty) {
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
// Record the state of the mapping
|
||||
for (record = this._previousMapHead = this._mapHead;
|
||||
record !== null;
|
||||
|
@ -171,7 +175,7 @@ export class MapChanges {
|
|||
}
|
||||
}
|
||||
|
||||
_truncate(lastRecord:MapChangeRecord, record:MapChangeRecord) {
|
||||
_truncate(lastRecord:KVChangeRecord, record:KVChangeRecord) {
|
||||
while (record !== null) {
|
||||
if (lastRecord === null) {
|
||||
this._mapHead = null;
|
||||
|
@ -189,20 +193,20 @@ export class MapChanges {
|
|||
record = nextRecord;
|
||||
}
|
||||
|
||||
for (var rec:MapChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) {
|
||||
for (var rec:KVChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) {
|
||||
rec._previousValue = rec._currentValue;
|
||||
rec._currentValue = null;
|
||||
MapWrapper.delete(this._records, rec.key);
|
||||
}
|
||||
}
|
||||
|
||||
_isInRemovals(record:MapChangeRecord) {
|
||||
_isInRemovals(record:KVChangeRecord) {
|
||||
return record === this._removalsHead ||
|
||||
record._nextRemoved !== null ||
|
||||
record._prevRemoved !== null;
|
||||
}
|
||||
|
||||
_addToRemovals(record:MapChangeRecord) {
|
||||
_addToRemovals(record:KVChangeRecord) {
|
||||
// todo(vicb) assert
|
||||
//assert(record._next == null);
|
||||
//assert(record._nextAdded == null);
|
||||
|
@ -218,7 +222,7 @@ export class MapChanges {
|
|||
}
|
||||
}
|
||||
|
||||
_removeFromSeq(prev:MapChangeRecord, record:MapChangeRecord) {
|
||||
_removeFromSeq(prev:KVChangeRecord, record:KVChangeRecord) {
|
||||
var next = record._next;
|
||||
if (prev === null) {
|
||||
this._mapHead = next;
|
||||
|
@ -232,7 +236,7 @@ export class MapChanges {
|
|||
//})());
|
||||
}
|
||||
|
||||
_removeFromRemovals(record:MapChangeRecord) {
|
||||
_removeFromRemovals(record:KVChangeRecord) {
|
||||
// todo(vicb) assert
|
||||
//assert(record._next == null);
|
||||
//assert(record._nextAdded == null);
|
||||
|
@ -253,7 +257,7 @@ export class MapChanges {
|
|||
record._prevRemoved = record._nextRemoved = null;
|
||||
}
|
||||
|
||||
_addToAdditions(record:MapChangeRecord) {
|
||||
_addToAdditions(record:KVChangeRecord) {
|
||||
// todo(vicb): assert
|
||||
//assert(record._next == null);
|
||||
//assert(record._nextAdded == null);
|
||||
|
@ -268,7 +272,7 @@ export class MapChanges {
|
|||
}
|
||||
}
|
||||
|
||||
_addToChanges(record:MapChangeRecord) {
|
||||
_addToChanges(record:KVChangeRecord) {
|
||||
// todo(vicb) assert
|
||||
//assert(record._nextAdded == null);
|
||||
//assert(record._nextChanged == null);
|
||||
|
@ -288,7 +292,7 @@ export class MapChanges {
|
|||
var changes = [];
|
||||
var additions = [];
|
||||
var removals = [];
|
||||
var record:MapChangeRecord;
|
||||
var record:KVChangeRecord;
|
||||
|
||||
for (record = this._mapHead; record !== null; record = record._next) {
|
||||
ListWrapper.push(items, stringify(record));
|
||||
|
@ -312,19 +316,29 @@ export class MapChanges {
|
|||
"changes: " + changes.join(', ') + "\n" +
|
||||
"removals: " + removals.join(', ') + "\n";
|
||||
}
|
||||
|
||||
_forEach(obj, fn:Function) {
|
||||
if (obj instanceof Map) {
|
||||
MapWrapper.forEach(obj, fn);
|
||||
} else {
|
||||
StringMapWrapper.forEach(obj, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MapChangeRecord {
|
||||
|
||||
|
||||
export class KVChangeRecord {
|
||||
key;
|
||||
_previousValue;
|
||||
_currentValue;
|
||||
|
||||
_nextPrevious:MapChangeRecord;
|
||||
_next:MapChangeRecord;
|
||||
_nextAdded:MapChangeRecord;
|
||||
_nextRemoved:MapChangeRecord;
|
||||
_prevRemoved:MapChangeRecord;
|
||||
_nextChanged:MapChangeRecord;
|
||||
_nextPrevious:KVChangeRecord;
|
||||
_next:KVChangeRecord;
|
||||
_nextAdded:KVChangeRecord;
|
||||
_nextRemoved:KVChangeRecord;
|
||||
_prevRemoved:KVChangeRecord;
|
||||
_nextChanged:KVChangeRecord;
|
||||
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
|
@ -345,5 +359,4 @@ export class MapChangeRecord {
|
|||
(stringify(this.key) + '[' + stringify(this._previousValue) + '->' +
|
||||
stringify(this._currentValue) + ']');
|
||||
}
|
||||
|
||||
}
|
|
@ -29,6 +29,21 @@ export class EmptyExpr extends AST {
|
|||
}
|
||||
}
|
||||
|
||||
export class Collection extends AST {
|
||||
value:AST;
|
||||
constructor(value:AST) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
eval(context) {
|
||||
return value.eval(context);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitCollection(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
export class ImplicitReceiver extends AST {
|
||||
eval(context) {
|
||||
return context;
|
||||
|
@ -386,20 +401,21 @@ export class TemplateBinding {
|
|||
|
||||
//INTERFACE
|
||||
export class AstVisitor {
|
||||
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) {}
|
||||
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) {}
|
||||
visitFunctionCall(ast:FunctionCall, args) {}
|
||||
visitPrefixNot(ast:PrefixNot, args) {}
|
||||
}
|
||||
|
||||
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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();
|
||||
|
||||
|
@ -10,15 +12,15 @@ 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;
|
||||
export const RECORD_TYPE_LIST = 0x0005;
|
||||
export const RECORD_TYPE_MAP = 0x0006;
|
||||
export const RECORD_TYPE_MARKER = 0x0007;
|
||||
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
|
||||
|
@ -112,7 +114,6 @@ export class Record {
|
|||
this.dest = null;
|
||||
|
||||
this.previousValue = null;
|
||||
this.currentValue = _fresh;
|
||||
|
||||
this.context = null;
|
||||
this.funcOrValue = null;
|
||||
|
@ -125,6 +126,11 @@ export class Record {
|
|||
|
||||
this._mode = protoRecord._mode;
|
||||
|
||||
// Return early for collections, further init delayed until updateContext()
|
||||
if (this.isCollection) return;
|
||||
|
||||
this.currentValue = _fresh;
|
||||
|
||||
var type = this.type;
|
||||
|
||||
if (type === RECORD_TYPE_CONST) {
|
||||
|
@ -150,15 +156,21 @@ export class Record {
|
|||
}
|
||||
}
|
||||
|
||||
get type() {
|
||||
// todo(vicb): getter / setters are much slower than regular methods
|
||||
// todo(vicb): update the whole code base
|
||||
get type():int {
|
||||
return this._mode & RECORD_TYPE_MASK;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
set type(value:int) {
|
||||
this._mode = (this._mode & ~RECORD_TYPE_MASK) | value;
|
||||
}
|
||||
|
||||
get disabled():boolean {
|
||||
return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED;
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
set disabled(value:boolean) {
|
||||
if (value) {
|
||||
this._mode |= RECORD_FLAG_DISABLED;
|
||||
} else {
|
||||
|
@ -166,24 +178,34 @@ export class Record {
|
|||
}
|
||||
}
|
||||
|
||||
get isImplicitReceiver() {
|
||||
get isImplicitReceiver():boolean {
|
||||
return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER;
|
||||
}
|
||||
|
||||
static createMarker(rr:RecordRange) {
|
||||
get 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) {
|
||||
var changed = this._checkCollection();
|
||||
if (changed) {
|
||||
this._notifyDispatcher();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
this.previousValue = this.currentValue;
|
||||
this.currentValue = this._calculateNewValue();
|
||||
|
||||
if (isSame(this.previousValue, this.currentValue)) return false;
|
||||
|
||||
this._updateDestination();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_updateDestination() {
|
||||
// todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
|
||||
|
@ -194,13 +216,38 @@ export class Record {
|
|||
this.dest.updateContext(this.currentValue);
|
||||
}
|
||||
} else {
|
||||
this._notifyDispatcher();
|
||||
}
|
||||
}
|
||||
|
||||
_notifyDispatcher() {
|
||||
this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest);
|
||||
}
|
||||
|
||||
// return whether the content has changed
|
||||
_checkCollection():boolean {
|
||||
switch(this.type) {
|
||||
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.recordRange.disableRecord(this);
|
||||
this.currentValue = null;
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new BaseException(`Unsupported record type (${this.type})`);
|
||||
}
|
||||
}
|
||||
|
||||
_calculateNewValue() {
|
||||
var type = this.type;
|
||||
switch (type) {
|
||||
switch (this.type) {
|
||||
case RECORD_TYPE_PROPERTY:
|
||||
return this.funcOrValue(this.context);
|
||||
|
||||
|
@ -219,17 +266,8 @@ export class Record {
|
|||
this.recordRange.disableRecord(this);
|
||||
return this.funcOrValue;
|
||||
|
||||
case RECORD_TYPE_MARKER:
|
||||
throw new BaseException('Marker not implemented');
|
||||
|
||||
case RECORD_TYPE_MAP:
|
||||
throw new BaseException('Map not implemented');
|
||||
|
||||
case RECORD_TYPE_LIST:
|
||||
throw new BaseException('List not implemented');
|
||||
|
||||
default:
|
||||
throw new BaseException(`Unsupported record type ($type)`);
|
||||
throw new BaseException(`Unsupported record type (${this.type})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,6 +279,34 @@ export class Record {
|
|||
updateContext(value) {
|
||||
this.context = value;
|
||||
this.recordRange.enableRecord(this);
|
||||
|
||||
if (!this.isMarkerRecord) {
|
||||
this.recordRange.enableRecord(this);
|
||||
}
|
||||
|
||||
if (this.isCollection) {
|
||||
if (ArrayChanges.supports(value)) {
|
||||
if (this.type != RECORD_TYPE_ARRAY) {
|
||||
this.type = RECORD_TYPE_ARRAY;
|
||||
this.currentValue = new ArrayChanges();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyValueChanges.supports(value)) {
|
||||
if (this.type != RECORD_TYPE_KEY_VALUE) {
|
||||
this.type = RECORD_TYPE_KEY_VALUE;
|
||||
this.currentValue = new KeyValueChanges();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlank(value)) {
|
||||
this.type = RECORD_TYPE_NULL;
|
||||
} else {
|
||||
throw new BaseException("Collection records must be array like, map like or null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isMarkerRecord() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ProtoRecord,
|
||||
Record,
|
||||
RECORD_FLAG_COLLECTION,
|
||||
RECORD_FLAG_IMPLICIT_RECEIVER,
|
||||
RECORD_TYPE_CONST,
|
||||
RECORD_TYPE_INVOKE_CLOSURE,
|
||||
|
@ -13,10 +14,26 @@ import {
|
|||
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException,
|
||||
NumberWrapper} from 'facade/lang';
|
||||
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive,
|
||||
Binary, Formatter, MethodCall, FunctionCall, PrefixNot, Conditional,
|
||||
LiteralArray, LiteralMap, KeyedAccess, Chain, Assignment} from './parser/ast';
|
||||
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 {
|
||||
headRecord:ProtoRecord;
|
||||
|
@ -32,13 +49,16 @@ export class ProtoRecordRange {
|
|||
* @param ast The expression to watch
|
||||
* @param memento an opaque object which will be passed to WatchGroupDispatcher on
|
||||
* detecting a change.
|
||||
* @param shallow Should collections be shallow watched
|
||||
* @param content Wether to watch collection content (true) or reference (false, default)
|
||||
*/
|
||||
addRecordsFromAST(ast:AST,
|
||||
memento,
|
||||
shallow = false)
|
||||
content:boolean = false)
|
||||
{
|
||||
var creator = new ProtoRecordCreator(this);
|
||||
if (content) {
|
||||
ast = new Collection(ast);
|
||||
}
|
||||
creator.createRecordsFromAST(ast, memento);
|
||||
this._addRecords(creator.headRecord, creator.tailRecord);
|
||||
}
|
||||
|
@ -436,6 +456,12 @@ class ProtoRecordCreator {
|
|||
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, null, dest);
|
||||
ast.condition.visit(this, new Destination(record, 0));
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib';
|
||||
|
||||
import {CollectionChanges} from 'change_detection/collection_changes';
|
||||
|
||||
import {isBlank, NumberWrapper} from 'facade/lang';
|
||||
|
||||
import {ListWrapper} from 'facade/collection';
|
||||
import {ArrayChanges} from 'change_detection/array_changes';
|
||||
import {NumberWrapper} from 'facade/lang';
|
||||
import {ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {TestIterable} from './iterable';
|
||||
import {arrayChangesAsString} from './util';
|
||||
|
||||
// todo(vicb): UnmodifiableListView / frozen object when implemented
|
||||
// todo(vicb): Update the code & tests for object equality
|
||||
export function main() {
|
||||
describe('collection_changes', function() {
|
||||
describe('CollectionChanges', function() {
|
||||
|
@ -15,30 +13,62 @@ export function main() {
|
|||
var l;
|
||||
|
||||
beforeEach(() => {
|
||||
changes = new CollectionChanges();
|
||||
changes = new ArrayChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
changes = null;
|
||||
});
|
||||
|
||||
it('should support list and iterables', () => {
|
||||
expect(ArrayChanges.supports([])).toBeTruthy();
|
||||
expect(ArrayChanges.supports(new TestIterable())).toBeTruthy();
|
||||
expect(ArrayChanges.supports(MapWrapper.create())).toBeFalsy();
|
||||
expect(ArrayChanges.supports(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should support iterables', () => {
|
||||
l = new TestIterable();
|
||||
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: []
|
||||
}));
|
||||
|
||||
l.list = [1];
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['1[null->0]'],
|
||||
additions: ['1[null->0]']
|
||||
}));
|
||||
|
||||
l.list = [2, 1];
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['2[null->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]'],
|
||||
additions: ['2[null->0]'],
|
||||
moves: ['1[0->1]']
|
||||
}));
|
||||
});
|
||||
|
||||
it('should detect additions', () => {
|
||||
l = [];
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: []
|
||||
}));
|
||||
|
||||
ListWrapper.push(l, 'a');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a[null->0]'],
|
||||
additions: ['a[null->0]']
|
||||
}));
|
||||
|
||||
ListWrapper.push(l, 'b');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'b[null->1]'],
|
||||
previous: ['a'],
|
||||
additions: ['b[null->1]']
|
||||
|
@ -51,7 +81,7 @@ export function main() {
|
|||
|
||||
l = [1, 0];
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['1[null->0]', '0[0->1]'],
|
||||
previous: ['0[0->1]'],
|
||||
additions: ['1[null->0]'],
|
||||
|
@ -60,7 +90,7 @@ export function main() {
|
|||
|
||||
l = [2, 1, 0];
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
|
||||
previous: ['1[0->1]', '0[1->2]'],
|
||||
additions: ['2[null->0]'],
|
||||
|
@ -76,7 +106,7 @@ export function main() {
|
|||
ListWrapper.push(l, 2);
|
||||
ListWrapper.push(l, 1);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['2[1->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]', '2[1->0]'],
|
||||
moves: ['2[1->0]', '1[0->1]']
|
||||
|
@ -90,7 +120,7 @@ export function main() {
|
|||
ListWrapper.removeAt(l, 1);
|
||||
ListWrapper.insert(l, 0, 'b');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||
moves: ['b[1->0]', 'a[0->1]']
|
||||
|
@ -99,7 +129,7 @@ export function main() {
|
|||
ListWrapper.removeAt(l, 1);
|
||||
ListWrapper.push(l, 'a');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['b', 'c[2->1]', 'a[1->2]'],
|
||||
previous: ['b', 'a[1->2]', 'c[2->1]'],
|
||||
moves: ['c[2->1]', 'a[1->2]']
|
||||
|
@ -112,14 +142,14 @@ export function main() {
|
|||
|
||||
ListWrapper.push(l, 'a');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a[null->0]'],
|
||||
additions: ['a[null->0]']
|
||||
}));
|
||||
|
||||
ListWrapper.push(l, 'b');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'b[null->1]'],
|
||||
previous: ['a'],
|
||||
additions: ['b[null->1]']
|
||||
|
@ -128,7 +158,7 @@ export function main() {
|
|||
ListWrapper.push(l, 'c');
|
||||
ListWrapper.push(l, 'd');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
|
||||
previous: ['a', 'b'],
|
||||
additions: ['c[null->2]', 'd[null->3]']
|
||||
|
@ -136,7 +166,7 @@ export function main() {
|
|||
|
||||
ListWrapper.removeAt(l, 2);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'b', 'd[3->2]'],
|
||||
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
|
||||
moves: ['d[3->2]'],
|
||||
|
@ -149,7 +179,7 @@ export function main() {
|
|||
ListWrapper.push(l, 'b');
|
||||
ListWrapper.push(l, 'a');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'],
|
||||
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
|
||||
additions: ['c[null->1]'],
|
||||
|
@ -165,7 +195,7 @@ export function main() {
|
|||
var oo = 'oo';
|
||||
ListWrapper.set(l, 1, b + oo);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'boo'],
|
||||
previous: ['a', 'boo']
|
||||
}));
|
||||
|
@ -175,7 +205,7 @@ export function main() {
|
|||
l = [NumberWrapper.NaN];
|
||||
changes.check(l);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: [NumberWrapper.NaN],
|
||||
previous: [NumberWrapper.NaN]
|
||||
}));
|
||||
|
@ -187,7 +217,7 @@ export function main() {
|
|||
|
||||
ListWrapper.insert(l, 0, 'foo');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
|
||||
previous: ['NaN[0->1]', 'NaN[1->2]'],
|
||||
additions: ['foo[null->0]'],
|
||||
|
@ -201,7 +231,7 @@ export function main() {
|
|||
|
||||
ListWrapper.removeAt(l, 1);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'c[2->1]'],
|
||||
previous: ['a', 'b[1->null]', 'c[2->1]'],
|
||||
moves: ['c[2->1]'],
|
||||
|
@ -210,7 +240,7 @@ export function main() {
|
|||
|
||||
ListWrapper.insert(l, 1, 'b');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'b[null->1]', 'c[1->2]'],
|
||||
previous: ['a', 'c[1->2]'],
|
||||
additions: ['b[null->1]'],
|
||||
|
@ -224,7 +254,7 @@ export function main() {
|
|||
|
||||
ListWrapper.removeAt(l, 0);
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
|
||||
previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'],
|
||||
moves: ['b[3->2]', 'b[4->3]'],
|
||||
|
@ -238,7 +268,7 @@ export function main() {
|
|||
|
||||
ListWrapper.insert(l, 0, 'b');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'],
|
||||
previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'],
|
||||
additions: ['b[null->4]'],
|
||||
|
@ -255,7 +285,7 @@ export function main() {
|
|||
ListWrapper.push(l, 'a');
|
||||
ListWrapper.push(l, 'c');
|
||||
changes.check(l);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||
moves: ['b[1->0]', 'a[0->1]']
|
||||
|
@ -265,16 +295,3 @@ export function main() {
|
|||
});
|
||||
}
|
||||
|
||||
function changesAsString({collection, previous, additions, moves, removals}) {
|
||||
if (isBlank(collection)) collection = [];
|
||||
if (isBlank(previous)) previous = [];
|
||||
if (isBlank(additions)) additions = [];
|
||||
if (isBlank(moves)) moves = [];
|
||||
if (isBlank(removals)) removals = [];
|
||||
|
||||
return "collection: " + collection.join(', ') + "\n" +
|
||||
"previous: " + previous.join(', ') + "\n" +
|
||||
"additions: " + additions.join(', ') + "\n" +
|
||||
"moves: " + moves.join(', ') + "\n" +
|
||||
"removals: " + removals.join(', ') + "\n";
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
|
||||
|
||||
import {isPresent} from 'facade/lang';
|
||||
import {isPresent, isBlank, isJsObject} from 'facade/lang';
|
||||
import {List, ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
|
||||
import {Parser} from 'change_detection/parser/parser';
|
||||
import {Lexer} from 'change_detection/parser/lexer';
|
||||
import {arrayChangesAsString, kvChangesAsString} from './util';
|
||||
|
||||
import {
|
||||
ChangeDetector,
|
||||
|
@ -22,9 +23,10 @@ export function main() {
|
|||
return parser.parseBinding(exp).ast;
|
||||
}
|
||||
|
||||
function createChangeDetector(memo:string, exp:string, context = null, formatters = null) {
|
||||
function createChangeDetector(memo:string, exp:string, context = null, formatters = null,
|
||||
content = false) {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast(exp), memo, false);
|
||||
prr.addRecordsFromAST(ast(exp), memo, content);
|
||||
|
||||
var dispatcher = new LoggingDispatcher();
|
||||
var rr = prr.instantiate(dispatcher, formatters);
|
||||
|
@ -35,8 +37,9 @@ export function main() {
|
|||
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
||||
}
|
||||
|
||||
function executeWatch(memo:string, exp:string, context = null, formatters = null) {
|
||||
var res = createChangeDetector(memo, exp, context, formatters);
|
||||
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;
|
||||
}
|
||||
|
@ -194,6 +197,7 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe("ContextWithVariableBindings", () => {
|
||||
it('should read a field from ContextWithVariableBindings', () => {
|
||||
var locals = new ContextWithVariableBindings(null,
|
||||
|
@ -221,6 +225,124 @@ export function main() {
|
|||
.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("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]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import 'dart:collection';
|
||||
|
||||
class TestIterable extends IterableBase<int> {
|
||||
List<int> list = [];
|
||||
Iterator<int> get iterator => list.iterator;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export class TestIterable {
|
||||
constructor() {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.list[Symbol.iterator]();
|
||||
}
|
||||
}
|
|
@ -1,20 +1,18 @@
|
|||
import {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib';
|
||||
|
||||
import {MapChanges} from 'change_detection/map_changes';
|
||||
|
||||
import {isBlank, NumberWrapper} from 'facade/lang';
|
||||
|
||||
import {KeyValueChanges} from 'change_detection/keyvalue_changes';
|
||||
import {NumberWrapper, isJsObject} from 'facade/lang';
|
||||
import {MapWrapper} from 'facade/collection';
|
||||
import {kvChangesAsString} from './util';
|
||||
|
||||
// todo(vicb): Update the code & tests for object equality
|
||||
export function main() {
|
||||
describe('map_changes', function() {
|
||||
describe('MapChanges', function() {
|
||||
describe('keyvalue_changes', function() {
|
||||
describe('KeyValueChanges', function() {
|
||||
var changes;
|
||||
var m;
|
||||
|
||||
beforeEach(() => {
|
||||
changes = new MapChanges();
|
||||
changes = new KeyValueChanges();
|
||||
m = MapWrapper.create();
|
||||
});
|
||||
|
||||
|
@ -27,14 +25,14 @@ export function main() {
|
|||
|
||||
MapWrapper.set(m, 'a', 1);
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a[null->1]'],
|
||||
additions: ['a[null->1]']
|
||||
}));
|
||||
|
||||
MapWrapper.set(m, 'b', 2);
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[null->2]'],
|
||||
previous: ['a'],
|
||||
additions: ['b[null->2]']
|
||||
|
@ -49,7 +47,7 @@ export function main() {
|
|||
MapWrapper.set(m, 2, 10);
|
||||
MapWrapper.set(m, 1, 20);
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['1[10->20]', '2[20->10]'],
|
||||
previous: ['1[10->20]', '2[20->10]'],
|
||||
changes: ['1[10->20]', '2[20->10]']
|
||||
|
@ -61,14 +59,14 @@ export function main() {
|
|||
|
||||
MapWrapper.set(m, 'a', 'A');
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a[null->A]'],
|
||||
additions: ['a[null->A]']
|
||||
}));
|
||||
|
||||
MapWrapper.set(m, 'b', 'B');
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[null->B]'],
|
||||
previous: ['a'],
|
||||
additions: ['b[null->B]']
|
||||
|
@ -77,7 +75,7 @@ export function main() {
|
|||
MapWrapper.set(m, 'b', 'BB');
|
||||
MapWrapper.set(m, 'd', 'D');
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
||||
previous: ['a', 'b[B->BB]'],
|
||||
additions: ['d[null->D]'],
|
||||
|
@ -86,7 +84,7 @@ export function main() {
|
|||
|
||||
MapWrapper.delete(m, 'b');
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'd'],
|
||||
previous: ['a', 'b[BB->null]', 'd'],
|
||||
removals: ['b[BB->null]']
|
||||
|
@ -94,7 +92,7 @@ export function main() {
|
|||
|
||||
MapWrapper.clear(m);
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
previous: ['a[A->null]', 'd[D->null]'],
|
||||
removals: ['a[A->null]', 'd[D->null]']
|
||||
}));
|
||||
|
@ -112,7 +110,7 @@ export function main() {
|
|||
MapWrapper.set(m, f + oo, b + ar);
|
||||
changes.check(m);
|
||||
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['foo'],
|
||||
previous: ['foo']
|
||||
}));
|
||||
|
@ -123,25 +121,70 @@ export function main() {
|
|||
changes.check(m);
|
||||
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(changesAsString({
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['foo'],
|
||||
previous: ['foo']
|
||||
}));
|
||||
});
|
||||
|
||||
// JS specific tests (JS Objects)
|
||||
if (isJsObject({})) {
|
||||
describe('JsObject changes', () => {
|
||||
it('should support JS Object', () => {
|
||||
expect(KeyValueChanges.supports({})).toBeTruthy();
|
||||
expect(KeyValueChanges.supports("not supported")).toBeFalsy();
|
||||
expect(KeyValueChanges.supports(0)).toBeFalsy();
|
||||
expect(KeyValueChanges.supports(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should do basic object watching', () => {
|
||||
m = {};
|
||||
changes.check(m);
|
||||
|
||||
m['a'] = 'A';
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a[null->A]'],
|
||||
additions: ['a[null->A]']
|
||||
}));
|
||||
|
||||
m['b'] = 'B';
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[null->B]'],
|
||||
previous: ['a'],
|
||||
additions: ['b[null->B]']
|
||||
}));
|
||||
|
||||
m['b'] = 'BB';
|
||||
m['d'] = 'D';
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
||||
previous: ['a', 'b[B->BB]'],
|
||||
additions: ['d[null->D]'],
|
||||
changes: ['b[B->BB]']
|
||||
}));
|
||||
|
||||
m = {};
|
||||
m['a'] = 'A';
|
||||
m['d'] = 'D';
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
map: ['a', 'd'],
|
||||
previous: ['a', 'b[BB->null]', 'd'],
|
||||
removals: ['b[BB->null]']
|
||||
}));
|
||||
|
||||
m = {};
|
||||
changes.check(m);
|
||||
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||
previous: ['a[A->null]', 'd[D->null]'],
|
||||
removals: ['a[A->null]', 'd[D->null]']
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changesAsString({map, previous, additions, changes, removals}) {
|
||||
if (isBlank(map)) map = [];
|
||||
if (isBlank(previous)) previous = [];
|
||||
if (isBlank(additions)) additions = [];
|
||||
if (isBlank(changes)) changes = [];
|
||||
if (isBlank(removals)) removals = [];
|
||||
|
||||
return "map: " + map.join(', ') + "\n" +
|
||||
"previous: " + previous.join(', ') + "\n" +
|
||||
"additions: " + additions.join(', ') + "\n" +
|
||||
"changes: " + changes.join(', ') + "\n" +
|
||||
"removals: " + removals.join(', ') + "\n";
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import {isBlank} from 'facade/lang';
|
||||
|
||||
export function arrayChangesAsString({collection, previous, additions, moves, removals}) {
|
||||
if (isBlank(collection)) collection = [];
|
||||
if (isBlank(previous)) previous = [];
|
||||
if (isBlank(additions)) additions = [];
|
||||
if (isBlank(moves)) moves = [];
|
||||
if (isBlank(removals)) removals = [];
|
||||
|
||||
return "collection: " + collection.join(', ') + "\n" +
|
||||
"previous: " + previous.join(', ') + "\n" +
|
||||
"additions: " + additions.join(', ') + "\n" +
|
||||
"moves: " + moves.join(', ') + "\n" +
|
||||
"removals: " + removals.join(', ') + "\n";
|
||||
}
|
||||
|
||||
export function kvChangesAsString({map, previous, additions, changes, removals}) {
|
||||
if (isBlank(map)) map = [];
|
||||
if (isBlank(previous)) previous = [];
|
||||
if (isBlank(additions)) additions = [];
|
||||
if (isBlank(changes)) changes = [];
|
||||
if (isBlank(removals)) removals = [];
|
||||
|
||||
return "map: " + map.join(', ') + "\n" +
|
||||
"previous: " + previous.join(', ') + "\n" +
|
||||
"additions: " + additions.join(', ') + "\n" +
|
||||
"changes: " + changes.join(', ') + "\n" +
|
||||
"removals: " + removals.join(', ') + "\n";
|
||||
}
|
|
@ -51,7 +51,7 @@ class ListWrapper {
|
|||
static filter(List list, fn) => list.where(fn).toList();
|
||||
static find(List list, fn) => list.firstWhere(fn, orElse:() => null);
|
||||
static any(List list, fn) => list.any(fn);
|
||||
static forEach(list, fn) {
|
||||
static forEach(list, Function fn) {
|
||||
list.forEach(fn);
|
||||
}
|
||||
static reduce(List list, Function fn, init) {
|
||||
|
@ -68,6 +68,15 @@ class ListWrapper {
|
|||
static void clear(List l) { l.clear(); }
|
||||
}
|
||||
|
||||
bool isListLikeIterable(obj) => obj is Iterable;
|
||||
|
||||
void iterateListLike(iter, Function fn) {
|
||||
assert(iter is Iterable);
|
||||
for (var item in iter) {
|
||||
fn (item);
|
||||
}
|
||||
}
|
||||
|
||||
class SetWrapper {
|
||||
static Set createFromList(List l) { return new Set.from(l); }
|
||||
static bool has(Set s, key) { return s.contains(key); }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {int} from 'facade/lang';
|
||||
import {int, isJsObject} from 'facade/lang';
|
||||
|
||||
export var List = window.Array;
|
||||
export var Map = window.Map;
|
||||
|
@ -25,12 +25,17 @@ export class MapWrapper {
|
|||
static clear(m) { m.clear(); }
|
||||
}
|
||||
|
||||
// TODO: cannot export StringMap as a type as Dart does not support
|
||||
// renaming types...
|
||||
// TODO: cannot export StringMap as a type as Dart does not support renaming types...
|
||||
/**
|
||||
* Wraps Javascript Objects
|
||||
*/
|
||||
export class StringMapWrapper {
|
||||
static create():Object {
|
||||
// Note: We are not using Object.create(null) here due to
|
||||
// performance!
|
||||
static create():Object { return { }; }
|
||||
// http://jsperf.com/ng2-object-create-null
|
||||
return { };
|
||||
}
|
||||
static get(map, key) {
|
||||
return map.hasOwnProperty(key) ? map[key] : undefined;
|
||||
}
|
||||
|
@ -45,10 +50,12 @@ export class StringMapWrapper {
|
|||
}
|
||||
static forEach(map, callback) {
|
||||
for (var prop in map) {
|
||||
if (map.hasOwnProperty(prop)) {
|
||||
callback(map[prop], prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ListWrapper {
|
||||
static create():List { return new List(); }
|
||||
|
@ -117,6 +124,19 @@ export class ListWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
export function isListLikeIterable(obj):boolean {
|
||||
if (!isJsObject(obj)) return false;
|
||||
return ListWrapper.isList(obj) ||
|
||||
(!(obj instanceof Map) && // JS Map are iterables but return entries as [k, v]
|
||||
Symbol.iterator in obj); // JS Iterable have a Symbol.iterator prop
|
||||
}
|
||||
|
||||
export function iterateListLike(obj, fn:Function) {
|
||||
for (var item of obj) {
|
||||
fn(item);
|
||||
}
|
||||
}
|
||||
|
||||
export class SetWrapper {
|
||||
static createFromList(lst:List) { return new Set(lst); }
|
||||
static has(s:Set, key):boolean { return s.has(key); }
|
||||
|
|
|
@ -161,6 +161,11 @@ dynamic getMapKey(value) {
|
|||
return value.isNaN ? _NAN_KEY : value;
|
||||
}
|
||||
|
||||
normalizeBlank(obj) {
|
||||
dynamic normalizeBlank(obj) {
|
||||
return isBlank(obj) ? null : obj;
|
||||
}
|
||||
|
||||
bool isJsObject(o) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -202,3 +202,7 @@ export function getMapKey(value) {
|
|||
export function normalizeBlank(obj) {
|
||||
return isBlank(obj) ? null : obj;
|
||||
}
|
||||
|
||||
export function isJsObject(o):boolean {
|
||||
return o !== null && (typeof o === "function" || typeof o === "object");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue