feat(Change Detector): Add support for collection content watch
This commit is contained in:
parent
2d2f44949d
commit
bf71b94bde
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
isListLikeIterable,
|
||||||
|
iterateListLike,
|
||||||
ListWrapper,
|
ListWrapper,
|
||||||
MapWrapper
|
MapWrapper
|
||||||
} from 'facade/collection';
|
} from 'facade/collection';
|
||||||
|
@ -12,20 +14,21 @@ import {
|
||||||
looseIdentical,
|
looseIdentical,
|
||||||
} from 'facade/lang';
|
} from 'facade/lang';
|
||||||
|
|
||||||
export class CollectionChanges {
|
export class ArrayChanges {
|
||||||
_collection;
|
_collection;
|
||||||
_length:int;
|
_length:int;
|
||||||
_linkedRecords:_DuplicateMap;
|
_linkedRecords:_DuplicateMap;
|
||||||
_unlinkedRecords:_DuplicateMap;
|
_unlinkedRecords:_DuplicateMap;
|
||||||
_previousItHead:CollectionChangeRecord<V> ;
|
_previousItHead:CollectionChangeRecord<V>;
|
||||||
_itHead:CollectionChangeRecord<V>;
|
_itHead:CollectionChangeRecord<V>;
|
||||||
_itTail:CollectionChangeRecord<V> ;
|
_itTail:CollectionChangeRecord<V>;
|
||||||
_additionsHead:CollectionChangeRecord<V>;
|
_additionsHead:CollectionChangeRecord<V>;
|
||||||
_additionsTail:CollectionChangeRecord<V> ;
|
_additionsTail:CollectionChangeRecord<V>;
|
||||||
_movesHead:CollectionChangeRecord<V>;
|
_movesHead:CollectionChangeRecord<V>;
|
||||||
_movesTail:CollectionChangeRecord<V> ;
|
_movesTail:CollectionChangeRecord<V> ;
|
||||||
_removalsHead:CollectionChangeRecord<V>;
|
_removalsHead:CollectionChangeRecord<V>;
|
||||||
_removalsTail:CollectionChangeRecord<V> ;
|
_removalsTail:CollectionChangeRecord<V>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._collection = null;
|
this._collection = null;
|
||||||
this._length = null;
|
this._length = null;
|
||||||
|
@ -45,6 +48,10 @@ export class CollectionChanges {
|
||||||
this._removalsTail = null;
|
this._removalsTail = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static supports(obj):boolean {
|
||||||
|
return isListLikeIterable(obj);
|
||||||
|
}
|
||||||
|
|
||||||
get collection() {
|
get collection() {
|
||||||
return this._collection;
|
return this._collection;
|
||||||
}
|
}
|
||||||
|
@ -112,8 +119,19 @@ export class CollectionChanges {
|
||||||
record = record._next;
|
record = record._next;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// todo(vicb) implement iterators
|
index = 0;
|
||||||
throw "NYI";
|
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);
|
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;
|
_records:Map;
|
||||||
_map:Map;
|
_map:any;
|
||||||
|
|
||||||
_mapHead:MapChangeRecord;
|
_mapHead:KVChangeRecord;
|
||||||
_previousMapHead:MapChangeRecord;
|
_previousMapHead:KVChangeRecord;
|
||||||
_changesHead:MapChangeRecord;
|
_changesHead:KVChangeRecord;
|
||||||
_changesTail:MapChangeRecord;
|
_changesTail:KVChangeRecord;
|
||||||
_additionsHead:MapChangeRecord;
|
_additionsHead:KVChangeRecord;
|
||||||
_additionsTail:MapChangeRecord;
|
_additionsTail:KVChangeRecord;
|
||||||
_removalsHead:MapChangeRecord;
|
_removalsHead:KVChangeRecord;
|
||||||
_removalsTail:MapChangeRecord;
|
_removalsTail:KVChangeRecord;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._records = MapWrapper.create();
|
this._records = MapWrapper.create();
|
||||||
|
@ -28,57 +28,61 @@ export class MapChanges {
|
||||||
this._removalsTail = null;
|
this._removalsTail = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static supports(obj):boolean {
|
||||||
|
return obj instanceof Map || isJsObject(obj);
|
||||||
|
}
|
||||||
|
|
||||||
get isDirty():boolean {
|
get isDirty():boolean {
|
||||||
return this._additionsHead !== null ||
|
return this._additionsHead !== null ||
|
||||||
this._changesHead !== null ||
|
this._changesHead !== null ||
|
||||||
this._removalsHead !== null;
|
this._removalsHead !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
forEachItem(fn:Function) {
|
forEachItem(fn:Function) {
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
for (record = this._mapHead; record !== null; record = record._next) {
|
for (record = this._mapHead; record !== null; record = record._next) {
|
||||||
fn(record);
|
fn(record);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forEachPreviousItem(fn:Function) {
|
forEachPreviousItem(fn:Function) {
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
|
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
|
||||||
fn(record);
|
fn(record);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forEachChangedItem(fn:Function) {
|
forEachChangedItem(fn:Function) {
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
||||||
fn(record);
|
fn(record);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forEachAddedItem(fn:Function){
|
forEachAddedItem(fn:Function){
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||||
fn(record);
|
fn(record);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
forEachRemovedItem(fn:Function){
|
forEachRemovedItem(fn:Function){
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||||
fn(record);
|
fn(record);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
check(map):boolean {
|
check(map):boolean {
|
||||||
this._reset();
|
this._reset();
|
||||||
this._map = map;
|
this._map = map;
|
||||||
var records = this._records;
|
var records = this._records;
|
||||||
var oldSeqRecord:MapChangeRecord = this._mapHead;
|
var oldSeqRecord:KVChangeRecord = this._mapHead;
|
||||||
var lastOldSeqRecord:MapChangeRecord = null;
|
var lastOldSeqRecord:KVChangeRecord = null;
|
||||||
var lastNewSeqRecord:MapChangeRecord = null;
|
var lastNewSeqRecord:KVChangeRecord = null;
|
||||||
var seqChanged:boolean = false;
|
var seqChanged:boolean = false;
|
||||||
|
|
||||||
MapWrapper.forEach(map, (value, key) => {
|
this._forEach(map, (value, key) => {
|
||||||
var newSeqRecord;
|
var newSeqRecord;
|
||||||
if (oldSeqRecord !== null && key === oldSeqRecord.key) {
|
if (oldSeqRecord !== null && key === oldSeqRecord.key) {
|
||||||
newSeqRecord = oldSeqRecord;
|
newSeqRecord = oldSeqRecord;
|
||||||
|
@ -97,7 +101,7 @@ export class MapChanges {
|
||||||
if (MapWrapper.contains(records, key)) {
|
if (MapWrapper.contains(records, key)) {
|
||||||
newSeqRecord = MapWrapper.get(records, key);
|
newSeqRecord = MapWrapper.get(records, key);
|
||||||
} else {
|
} else {
|
||||||
newSeqRecord = new MapChangeRecord(key);
|
newSeqRecord = new KVChangeRecord(key);
|
||||||
MapWrapper.set(records, key, newSeqRecord);
|
MapWrapper.set(records, key, newSeqRecord);
|
||||||
newSeqRecord._currentValue = value;
|
newSeqRecord._currentValue = value;
|
||||||
this._addToAdditions(newSeqRecord);
|
this._addToAdditions(newSeqRecord);
|
||||||
|
@ -124,7 +128,7 @@ export class MapChanges {
|
||||||
|
|
||||||
_reset() {
|
_reset() {
|
||||||
if (this.isDirty) {
|
if (this.isDirty) {
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
// Record the state of the mapping
|
// Record the state of the mapping
|
||||||
for (record = this._previousMapHead = this._mapHead;
|
for (record = this._previousMapHead = this._mapHead;
|
||||||
record !== null;
|
record !== null;
|
||||||
|
@ -171,7 +175,7 @@ export class MapChanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_truncate(lastRecord:MapChangeRecord, record:MapChangeRecord) {
|
_truncate(lastRecord:KVChangeRecord, record:KVChangeRecord) {
|
||||||
while (record !== null) {
|
while (record !== null) {
|
||||||
if (lastRecord === null) {
|
if (lastRecord === null) {
|
||||||
this._mapHead = null;
|
this._mapHead = null;
|
||||||
|
@ -189,20 +193,20 @@ export class MapChanges {
|
||||||
record = nextRecord;
|
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._previousValue = rec._currentValue;
|
||||||
rec._currentValue = null;
|
rec._currentValue = null;
|
||||||
MapWrapper.delete(this._records, rec.key);
|
MapWrapper.delete(this._records, rec.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isInRemovals(record:MapChangeRecord) {
|
_isInRemovals(record:KVChangeRecord) {
|
||||||
return record === this._removalsHead ||
|
return record === this._removalsHead ||
|
||||||
record._nextRemoved !== null ||
|
record._nextRemoved !== null ||
|
||||||
record._prevRemoved !== null;
|
record._prevRemoved !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_addToRemovals(record:MapChangeRecord) {
|
_addToRemovals(record:KVChangeRecord) {
|
||||||
// todo(vicb) assert
|
// todo(vicb) assert
|
||||||
//assert(record._next == null);
|
//assert(record._next == null);
|
||||||
//assert(record._nextAdded == 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;
|
var next = record._next;
|
||||||
if (prev === null) {
|
if (prev === null) {
|
||||||
this._mapHead = next;
|
this._mapHead = next;
|
||||||
|
@ -232,7 +236,7 @@ export class MapChanges {
|
||||||
//})());
|
//})());
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeFromRemovals(record:MapChangeRecord) {
|
_removeFromRemovals(record:KVChangeRecord) {
|
||||||
// todo(vicb) assert
|
// todo(vicb) assert
|
||||||
//assert(record._next == null);
|
//assert(record._next == null);
|
||||||
//assert(record._nextAdded == null);
|
//assert(record._nextAdded == null);
|
||||||
|
@ -253,7 +257,7 @@ export class MapChanges {
|
||||||
record._prevRemoved = record._nextRemoved = null;
|
record._prevRemoved = record._nextRemoved = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_addToAdditions(record:MapChangeRecord) {
|
_addToAdditions(record:KVChangeRecord) {
|
||||||
// todo(vicb): assert
|
// todo(vicb): assert
|
||||||
//assert(record._next == null);
|
//assert(record._next == null);
|
||||||
//assert(record._nextAdded == null);
|
//assert(record._nextAdded == null);
|
||||||
|
@ -268,7 +272,7 @@ export class MapChanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addToChanges(record:MapChangeRecord) {
|
_addToChanges(record:KVChangeRecord) {
|
||||||
// todo(vicb) assert
|
// todo(vicb) assert
|
||||||
//assert(record._nextAdded == null);
|
//assert(record._nextAdded == null);
|
||||||
//assert(record._nextChanged == null);
|
//assert(record._nextChanged == null);
|
||||||
|
@ -288,7 +292,7 @@ export class MapChanges {
|
||||||
var changes = [];
|
var changes = [];
|
||||||
var additions = [];
|
var additions = [];
|
||||||
var removals = [];
|
var removals = [];
|
||||||
var record:MapChangeRecord;
|
var record:KVChangeRecord;
|
||||||
|
|
||||||
for (record = this._mapHead; record !== null; record = record._next) {
|
for (record = this._mapHead; record !== null; record = record._next) {
|
||||||
ListWrapper.push(items, stringify(record));
|
ListWrapper.push(items, stringify(record));
|
||||||
|
@ -312,19 +316,29 @@ export class MapChanges {
|
||||||
"changes: " + changes.join(', ') + "\n" +
|
"changes: " + changes.join(', ') + "\n" +
|
||||||
"removals: " + removals.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;
|
key;
|
||||||
_previousValue;
|
_previousValue;
|
||||||
_currentValue;
|
_currentValue;
|
||||||
|
|
||||||
_nextPrevious:MapChangeRecord;
|
_nextPrevious:KVChangeRecord;
|
||||||
_next:MapChangeRecord;
|
_next:KVChangeRecord;
|
||||||
_nextAdded:MapChangeRecord;
|
_nextAdded:KVChangeRecord;
|
||||||
_nextRemoved:MapChangeRecord;
|
_nextRemoved:KVChangeRecord;
|
||||||
_prevRemoved:MapChangeRecord;
|
_prevRemoved:KVChangeRecord;
|
||||||
_nextChanged:MapChangeRecord;
|
_nextChanged:KVChangeRecord;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
@ -345,5 +359,4 @@ export class MapChangeRecord {
|
||||||
(stringify(this.key) + '[' + stringify(this._previousValue) + '->' +
|
(stringify(this.key) + '[' + stringify(this._previousValue) + '->' +
|
||||||
stringify(this._currentValue) + ']');
|
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 {
|
export class ImplicitReceiver extends AST {
|
||||||
eval(context) {
|
eval(context) {
|
||||||
return context;
|
return context;
|
||||||
|
@ -386,20 +401,21 @@ export class TemplateBinding {
|
||||||
|
|
||||||
//INTERFACE
|
//INTERFACE
|
||||||
export class AstVisitor {
|
export class AstVisitor {
|
||||||
visitChain(ast:Chain, args){}
|
|
||||||
visitImplicitReceiver(ast:ImplicitReceiver, args) {}
|
|
||||||
visitConditional(ast:Conditional, args) {}
|
|
||||||
visitAccessMember(ast:AccessMember, 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) {}
|
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) {}
|
visitLiteralArray(ast:LiteralArray, args) {}
|
||||||
visitLiteralMap(ast:LiteralMap, args) {}
|
visitLiteralMap(ast:LiteralMap, args) {}
|
||||||
|
visitLiteralPrimitive(ast:LiteralPrimitive, args) {}
|
||||||
visitMethodCall(ast:MethodCall, 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]];
|
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
|
||||||
|
@ -410,4 +426,4 @@ function evalList(context, exps:List){
|
||||||
result[i] = exps[i].eval(context);
|
result[i] = exps[i].eval(context);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {ProtoRecordRange, RecordRange} from './record_range';
|
import {ProtoRecordRange, RecordRange} from './record_range';
|
||||||
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
|
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
|
||||||
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
|
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
|
||||||
|
import {ArrayChanges} from './array_changes';
|
||||||
|
import {KeyValueChanges} from './keyvalue_changes';
|
||||||
|
|
||||||
var _fresh = new Object();
|
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_FORMATTER = 0x0002;
|
||||||
export const RECORD_TYPE_INVOKE_METHOD = 0x0003;
|
export const RECORD_TYPE_INVOKE_METHOD = 0x0003;
|
||||||
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004;
|
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004;
|
||||||
export const RECORD_TYPE_LIST = 0x0005;
|
const RECORD_TYPE_ARRAY = 0x0005;
|
||||||
export const RECORD_TYPE_MAP = 0x0006;
|
const RECORD_TYPE_KEY_VALUE = 0x0006;
|
||||||
export const RECORD_TYPE_MARKER = 0x0007;
|
const RECORD_TYPE_MARKER = 0x0007;
|
||||||
export const RECORD_TYPE_PROPERTY = 0x0008;
|
export const RECORD_TYPE_PROPERTY = 0x0008;
|
||||||
|
const RECORD_TYPE_NULL= 0x0009;
|
||||||
|
|
||||||
const RECORD_FLAG_DISABLED = 0x0100;
|
const RECORD_FLAG_DISABLED = 0x0100;
|
||||||
export const RECORD_FLAG_IMPLICIT_RECEIVER = 0x0200;
|
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
|
* 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.dest = null;
|
||||||
|
|
||||||
this.previousValue = null;
|
this.previousValue = null;
|
||||||
this.currentValue = _fresh;
|
|
||||||
|
|
||||||
this.context = null;
|
this.context = null;
|
||||||
this.funcOrValue = null;
|
this.funcOrValue = null;
|
||||||
|
@ -125,6 +126,11 @@ export class Record {
|
||||||
|
|
||||||
this._mode = protoRecord._mode;
|
this._mode = protoRecord._mode;
|
||||||
|
|
||||||
|
// Return early for collections, further init delayed until updateContext()
|
||||||
|
if (this.isCollection) return;
|
||||||
|
|
||||||
|
this.currentValue = _fresh;
|
||||||
|
|
||||||
var type = this.type;
|
var type = this.type;
|
||||||
|
|
||||||
if (type === RECORD_TYPE_CONST) {
|
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;
|
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;
|
return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
set disabled(value) {
|
set disabled(value:boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this._mode |= RECORD_FLAG_DISABLED;
|
this._mode |= RECORD_FLAG_DISABLED;
|
||||||
} else {
|
} else {
|
||||||
|
@ -166,23 +178,33 @@ export class Record {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isImplicitReceiver() {
|
get isImplicitReceiver():boolean {
|
||||||
return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER;
|
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);
|
return new Record(rr, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
check():boolean {
|
check():boolean {
|
||||||
this.previousValue = this.currentValue;
|
if (this.isCollection) {
|
||||||
this.currentValue = this._calculateNewValue();
|
var changed = this._checkCollection();
|
||||||
|
if (changed) {
|
||||||
if (isSame(this.previousValue, this.currentValue)) return false;
|
this._notifyDispatcher();
|
||||||
|
return true;
|
||||||
this._updateDestination();
|
}
|
||||||
|
return false;
|
||||||
return true;
|
} else {
|
||||||
|
this.previousValue = this.currentValue;
|
||||||
|
this.currentValue = this._calculateNewValue();
|
||||||
|
if (isSame(this.previousValue, this.currentValue)) return false;
|
||||||
|
this._updateDestination();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateDestination() {
|
_updateDestination() {
|
||||||
|
@ -194,13 +216,38 @@ export class Record {
|
||||||
this.dest.updateContext(this.currentValue);
|
this.dest.updateContext(this.currentValue);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest);
|
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() {
|
_calculateNewValue() {
|
||||||
var type = this.type;
|
switch (this.type) {
|
||||||
switch (type) {
|
|
||||||
case RECORD_TYPE_PROPERTY:
|
case RECORD_TYPE_PROPERTY:
|
||||||
return this.funcOrValue(this.context);
|
return this.funcOrValue(this.context);
|
||||||
|
|
||||||
|
@ -219,17 +266,8 @@ export class Record {
|
||||||
this.recordRange.disableRecord(this);
|
this.recordRange.disableRecord(this);
|
||||||
return this.funcOrValue;
|
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:
|
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) {
|
updateContext(value) {
|
||||||
this.context = value;
|
this.context = value;
|
||||||
this.recordRange.enableRecord(this);
|
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() {
|
get isMarkerRecord() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ProtoRecord,
|
ProtoRecord,
|
||||||
Record,
|
Record,
|
||||||
|
RECORD_FLAG_COLLECTION,
|
||||||
RECORD_FLAG_IMPLICIT_RECEIVER,
|
RECORD_FLAG_IMPLICIT_RECEIVER,
|
||||||
RECORD_TYPE_CONST,
|
RECORD_TYPE_CONST,
|
||||||
RECORD_TYPE_INVOKE_CLOSURE,
|
RECORD_TYPE_INVOKE_CLOSURE,
|
||||||
|
@ -13,10 +14,26 @@ import {
|
||||||
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException,
|
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException,
|
||||||
NumberWrapper} from 'facade/lang';
|
NumberWrapper} from 'facade/lang';
|
||||||
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
|
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 {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 {
|
export class ProtoRecordRange {
|
||||||
headRecord:ProtoRecord;
|
headRecord:ProtoRecord;
|
||||||
|
@ -32,13 +49,16 @@ export class ProtoRecordRange {
|
||||||
* @param ast The expression to watch
|
* @param ast The expression to watch
|
||||||
* @param memento an opaque object which will be passed to WatchGroupDispatcher on
|
* @param memento an opaque object which will be passed to WatchGroupDispatcher on
|
||||||
* detecting a change.
|
* 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,
|
addRecordsFromAST(ast:AST,
|
||||||
memento,
|
memento,
|
||||||
shallow = false)
|
content:boolean = false)
|
||||||
{
|
{
|
||||||
var creator = new ProtoRecordCreator(this);
|
var creator = new ProtoRecordCreator(this);
|
||||||
|
if (content) {
|
||||||
|
ast = new Collection(ast);
|
||||||
|
}
|
||||||
creator.createRecordsFromAST(ast, memento);
|
creator.createRecordsFromAST(ast, memento);
|
||||||
this._addRecords(creator.headRecord, creator.tailRecord);
|
this._addRecords(creator.headRecord, creator.tailRecord);
|
||||||
}
|
}
|
||||||
|
@ -436,6 +456,12 @@ class ProtoRecordCreator {
|
||||||
this.add(record);
|
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) {
|
visitConditional(ast:Conditional, dest) {
|
||||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, null, dest);
|
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, null, dest);
|
||||||
ast.condition.visit(this, new Destination(record, 0));
|
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 {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib';
|
||||||
|
import {ArrayChanges} from 'change_detection/array_changes';
|
||||||
import {CollectionChanges} from 'change_detection/collection_changes';
|
import {NumberWrapper} from 'facade/lang';
|
||||||
|
import {ListWrapper, MapWrapper} from 'facade/collection';
|
||||||
import {isBlank, NumberWrapper} from 'facade/lang';
|
import {TestIterable} from './iterable';
|
||||||
|
import {arrayChangesAsString} from './util';
|
||||||
import {ListWrapper} from 'facade/collection';
|
|
||||||
|
|
||||||
// todo(vicb): UnmodifiableListView / frozen object when implemented
|
// todo(vicb): UnmodifiableListView / frozen object when implemented
|
||||||
// todo(vicb): Update the code & tests for object equality
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('collection_changes', function() {
|
describe('collection_changes', function() {
|
||||||
describe('CollectionChanges', function() {
|
describe('CollectionChanges', function() {
|
||||||
|
@ -15,30 +13,62 @@ export function main() {
|
||||||
var l;
|
var l;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
changes = new CollectionChanges();
|
changes = new ArrayChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
changes = null;
|
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', () => {
|
it('should detect additions', () => {
|
||||||
l = [];
|
l = [];
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: []
|
collection: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ListWrapper.push(l, 'a');
|
ListWrapper.push(l, 'a');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a[null->0]'],
|
collection: ['a[null->0]'],
|
||||||
additions: ['a[null->0]']
|
additions: ['a[null->0]']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ListWrapper.push(l, 'b');
|
ListWrapper.push(l, 'b');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'b[null->1]'],
|
collection: ['a', 'b[null->1]'],
|
||||||
previous: ['a'],
|
previous: ['a'],
|
||||||
additions: ['b[null->1]']
|
additions: ['b[null->1]']
|
||||||
|
@ -51,7 +81,7 @@ export function main() {
|
||||||
|
|
||||||
l = [1, 0];
|
l = [1, 0];
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['1[null->0]', '0[0->1]'],
|
collection: ['1[null->0]', '0[0->1]'],
|
||||||
previous: ['0[0->1]'],
|
previous: ['0[0->1]'],
|
||||||
additions: ['1[null->0]'],
|
additions: ['1[null->0]'],
|
||||||
|
@ -60,7 +90,7 @@ export function main() {
|
||||||
|
|
||||||
l = [2, 1, 0];
|
l = [2, 1, 0];
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
|
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
|
||||||
previous: ['1[0->1]', '0[1->2]'],
|
previous: ['1[0->1]', '0[1->2]'],
|
||||||
additions: ['2[null->0]'],
|
additions: ['2[null->0]'],
|
||||||
|
@ -76,7 +106,7 @@ export function main() {
|
||||||
ListWrapper.push(l, 2);
|
ListWrapper.push(l, 2);
|
||||||
ListWrapper.push(l, 1);
|
ListWrapper.push(l, 1);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['2[1->0]', '1[0->1]'],
|
collection: ['2[1->0]', '1[0->1]'],
|
||||||
previous: ['1[0->1]', '2[1->0]'],
|
previous: ['1[0->1]', '2[1->0]'],
|
||||||
moves: ['2[1->0]', '1[0->1]']
|
moves: ['2[1->0]', '1[0->1]']
|
||||||
|
@ -90,7 +120,7 @@ export function main() {
|
||||||
ListWrapper.removeAt(l, 1);
|
ListWrapper.removeAt(l, 1);
|
||||||
ListWrapper.insert(l, 0, 'b');
|
ListWrapper.insert(l, 0, 'b');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||||
moves: ['b[1->0]', 'a[0->1]']
|
moves: ['b[1->0]', 'a[0->1]']
|
||||||
|
@ -99,7 +129,7 @@ export function main() {
|
||||||
ListWrapper.removeAt(l, 1);
|
ListWrapper.removeAt(l, 1);
|
||||||
ListWrapper.push(l, 'a');
|
ListWrapper.push(l, 'a');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['b', 'c[2->1]', 'a[1->2]'],
|
collection: ['b', 'c[2->1]', 'a[1->2]'],
|
||||||
previous: ['b', 'a[1->2]', 'c[2->1]'],
|
previous: ['b', 'a[1->2]', 'c[2->1]'],
|
||||||
moves: ['c[2->1]', 'a[1->2]']
|
moves: ['c[2->1]', 'a[1->2]']
|
||||||
|
@ -112,14 +142,14 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.push(l, 'a');
|
ListWrapper.push(l, 'a');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a[null->0]'],
|
collection: ['a[null->0]'],
|
||||||
additions: ['a[null->0]']
|
additions: ['a[null->0]']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ListWrapper.push(l, 'b');
|
ListWrapper.push(l, 'b');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'b[null->1]'],
|
collection: ['a', 'b[null->1]'],
|
||||||
previous: ['a'],
|
previous: ['a'],
|
||||||
additions: ['b[null->1]']
|
additions: ['b[null->1]']
|
||||||
|
@ -128,7 +158,7 @@ export function main() {
|
||||||
ListWrapper.push(l, 'c');
|
ListWrapper.push(l, 'c');
|
||||||
ListWrapper.push(l, 'd');
|
ListWrapper.push(l, 'd');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
|
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
|
||||||
previous: ['a', 'b'],
|
previous: ['a', 'b'],
|
||||||
additions: ['c[null->2]', 'd[null->3]']
|
additions: ['c[null->2]', 'd[null->3]']
|
||||||
|
@ -136,7 +166,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.removeAt(l, 2);
|
ListWrapper.removeAt(l, 2);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'b', 'd[3->2]'],
|
collection: ['a', 'b', 'd[3->2]'],
|
||||||
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
|
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
|
||||||
moves: ['d[3->2]'],
|
moves: ['d[3->2]'],
|
||||||
|
@ -149,7 +179,7 @@ export function main() {
|
||||||
ListWrapper.push(l, 'b');
|
ListWrapper.push(l, 'b');
|
||||||
ListWrapper.push(l, 'a');
|
ListWrapper.push(l, 'a');
|
||||||
changes.check(l);
|
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]'],
|
collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'],
|
||||||
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
|
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
|
||||||
additions: ['c[null->1]'],
|
additions: ['c[null->1]'],
|
||||||
|
@ -165,7 +195,7 @@ export function main() {
|
||||||
var oo = 'oo';
|
var oo = 'oo';
|
||||||
ListWrapper.set(l, 1, b + oo);
|
ListWrapper.set(l, 1, b + oo);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'boo'],
|
collection: ['a', 'boo'],
|
||||||
previous: ['a', 'boo']
|
previous: ['a', 'boo']
|
||||||
}));
|
}));
|
||||||
|
@ -175,7 +205,7 @@ export function main() {
|
||||||
l = [NumberWrapper.NaN];
|
l = [NumberWrapper.NaN];
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: [NumberWrapper.NaN],
|
collection: [NumberWrapper.NaN],
|
||||||
previous: [NumberWrapper.NaN]
|
previous: [NumberWrapper.NaN]
|
||||||
}));
|
}));
|
||||||
|
@ -187,7 +217,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.insert(l, 0, 'foo');
|
ListWrapper.insert(l, 0, 'foo');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
|
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
|
||||||
previous: ['NaN[0->1]', 'NaN[1->2]'],
|
previous: ['NaN[0->1]', 'NaN[1->2]'],
|
||||||
additions: ['foo[null->0]'],
|
additions: ['foo[null->0]'],
|
||||||
|
@ -201,7 +231,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.removeAt(l, 1);
|
ListWrapper.removeAt(l, 1);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'c[2->1]'],
|
collection: ['a', 'c[2->1]'],
|
||||||
previous: ['a', 'b[1->null]', 'c[2->1]'],
|
previous: ['a', 'b[1->null]', 'c[2->1]'],
|
||||||
moves: ['c[2->1]'],
|
moves: ['c[2->1]'],
|
||||||
|
@ -210,7 +240,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.insert(l, 1, 'b');
|
ListWrapper.insert(l, 1, 'b');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'b[null->1]', 'c[1->2]'],
|
collection: ['a', 'b[null->1]', 'c[1->2]'],
|
||||||
previous: ['a', 'c[1->2]'],
|
previous: ['a', 'c[1->2]'],
|
||||||
additions: ['b[null->1]'],
|
additions: ['b[null->1]'],
|
||||||
|
@ -224,7 +254,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.removeAt(l, 0);
|
ListWrapper.removeAt(l, 0);
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
|
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
|
||||||
previous: ['a', 'a', 'a[2->null]', '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]'],
|
moves: ['b[3->2]', 'b[4->3]'],
|
||||||
|
@ -238,7 +268,7 @@ export function main() {
|
||||||
|
|
||||||
ListWrapper.insert(l, 0, 'b');
|
ListWrapper.insert(l, 0, 'b');
|
||||||
changes.check(l);
|
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]'],
|
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'],
|
previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'],
|
||||||
additions: ['b[null->4]'],
|
additions: ['b[null->4]'],
|
||||||
|
@ -255,7 +285,7 @@ export function main() {
|
||||||
ListWrapper.push(l, 'a');
|
ListWrapper.push(l, 'a');
|
||||||
ListWrapper.push(l, 'c');
|
ListWrapper.push(l, 'c');
|
||||||
changes.check(l);
|
changes.check(l);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(arrayChangesAsString({
|
||||||
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
collection: ['b[1->0]', 'a[0->1]', 'c'],
|
||||||
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
previous: ['a[0->1]', 'b[1->0]', 'c'],
|
||||||
moves: ['b[1->0]', 'a[0->1]']
|
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 {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 {List, ListWrapper, MapWrapper} from 'facade/collection';
|
||||||
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
|
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
|
||||||
import {Parser} from 'change_detection/parser/parser';
|
import {Parser} from 'change_detection/parser/parser';
|
||||||
import {Lexer} from 'change_detection/parser/lexer';
|
import {Lexer} from 'change_detection/parser/lexer';
|
||||||
|
import {arrayChangesAsString, kvChangesAsString} from './util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetector,
|
ChangeDetector,
|
||||||
|
@ -22,9 +23,10 @@ export function main() {
|
||||||
return parser.parseBinding(exp).ast;
|
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();
|
var prr = new ProtoRecordRange();
|
||||||
prr.addRecordsFromAST(ast(exp), memo, false);
|
prr.addRecordsFromAST(ast(exp), memo, content);
|
||||||
|
|
||||||
var dispatcher = new LoggingDispatcher();
|
var dispatcher = new LoggingDispatcher();
|
||||||
var rr = prr.instantiate(dispatcher, formatters);
|
var rr = prr.instantiate(dispatcher, formatters);
|
||||||
|
@ -35,8 +37,9 @@ export function main() {
|
||||||
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeWatch(memo:string, exp:string, context = null, formatters = null) {
|
function executeWatch(memo:string, exp:string, context = null, formatters = null,
|
||||||
var res = createChangeDetector(memo, exp, context, formatters);
|
content = false) {
|
||||||
|
var res = createChangeDetector(memo, exp, context, formatters, content);
|
||||||
res["changeDetector"].detectChanges();
|
res["changeDetector"].detectChanges();
|
||||||
return res["dispatcher"].log;
|
return res["dispatcher"].log;
|
||||||
}
|
}
|
||||||
|
@ -194,10 +197,11 @@ export function main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("ContextWithVariableBindings", () => {
|
describe("ContextWithVariableBindings", () => {
|
||||||
it('should read a field from ContextWithVariableBindings', () => {
|
it('should read a field from ContextWithVariableBindings', () => {
|
||||||
var locals = new ContextWithVariableBindings(null,
|
var locals = new ContextWithVariableBindings(null,
|
||||||
MapWrapper.createFromPairs([["key", "value"]]));
|
MapWrapper.createFromPairs([["key", "value"]]));
|
||||||
|
|
||||||
expect(executeWatch('key', 'key', locals))
|
expect(executeWatch('key', 'key', locals))
|
||||||
.toEqual(['key=value']);
|
.toEqual(['key=value']);
|
||||||
|
@ -205,7 +209,7 @@ export function main() {
|
||||||
|
|
||||||
it('should handle nested ContextWithVariableBindings', () => {
|
it('should handle nested ContextWithVariableBindings', () => {
|
||||||
var nested = new ContextWithVariableBindings(null,
|
var nested = new ContextWithVariableBindings(null,
|
||||||
MapWrapper.createFromPairs([["key", "value"]]));
|
MapWrapper.createFromPairs([["key", "value"]]));
|
||||||
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
|
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
|
||||||
|
|
||||||
expect(executeWatch('key', 'key', locals))
|
expect(executeWatch('key', 'key', locals))
|
||||||
|
@ -213,14 +217,132 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fall back to a regular field read when ContextWithVariableBindings " +
|
it("should fall back to a regular field read when ContextWithVariableBindings " +
|
||||||
"does not have the requested field", () => {
|
"does not have the requested field", () => {
|
||||||
var locals = new ContextWithVariableBindings(new Person("Jim"),
|
var locals = new ContextWithVariableBindings(new Person("Jim"),
|
||||||
MapWrapper.createFromPairs([["key", "value"]]));
|
MapWrapper.createFromPairs([["key", "value"]]));
|
||||||
|
|
||||||
expect(executeWatch('name', 'name', locals))
|
expect(executeWatch('name', 'name', locals))
|
||||||
.toEqual(['name=Jim']);
|
.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 {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib';
|
||||||
|
import {KeyValueChanges} from 'change_detection/keyvalue_changes';
|
||||||
import {MapChanges} from 'change_detection/map_changes';
|
import {NumberWrapper, isJsObject} from 'facade/lang';
|
||||||
|
|
||||||
import {isBlank, NumberWrapper} from 'facade/lang';
|
|
||||||
|
|
||||||
import {MapWrapper} from 'facade/collection';
|
import {MapWrapper} from 'facade/collection';
|
||||||
|
import {kvChangesAsString} from './util';
|
||||||
|
|
||||||
// todo(vicb): Update the code & tests for object equality
|
// todo(vicb): Update the code & tests for object equality
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('map_changes', function() {
|
describe('keyvalue_changes', function() {
|
||||||
describe('MapChanges', function() {
|
describe('KeyValueChanges', function() {
|
||||||
var changes;
|
var changes;
|
||||||
var m;
|
var m;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
changes = new MapChanges();
|
changes = new KeyValueChanges();
|
||||||
m = MapWrapper.create();
|
m = MapWrapper.create();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,14 +25,14 @@ export function main() {
|
||||||
|
|
||||||
MapWrapper.set(m, 'a', 1);
|
MapWrapper.set(m, 'a', 1);
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a[null->1]'],
|
map: ['a[null->1]'],
|
||||||
additions: ['a[null->1]']
|
additions: ['a[null->1]']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
MapWrapper.set(m, 'b', 2);
|
MapWrapper.set(m, 'b', 2);
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a', 'b[null->2]'],
|
map: ['a', 'b[null->2]'],
|
||||||
previous: ['a'],
|
previous: ['a'],
|
||||||
additions: ['b[null->2]']
|
additions: ['b[null->2]']
|
||||||
|
@ -49,7 +47,7 @@ export function main() {
|
||||||
MapWrapper.set(m, 2, 10);
|
MapWrapper.set(m, 2, 10);
|
||||||
MapWrapper.set(m, 1, 20);
|
MapWrapper.set(m, 1, 20);
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['1[10->20]', '2[20->10]'],
|
map: ['1[10->20]', '2[20->10]'],
|
||||||
previous: ['1[10->20]', '2[20->10]'],
|
previous: ['1[10->20]', '2[20->10]'],
|
||||||
changes: ['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');
|
MapWrapper.set(m, 'a', 'A');
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a[null->A]'],
|
map: ['a[null->A]'],
|
||||||
additions: ['a[null->A]']
|
additions: ['a[null->A]']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
MapWrapper.set(m, 'b', 'B');
|
MapWrapper.set(m, 'b', 'B');
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a', 'b[null->B]'],
|
map: ['a', 'b[null->B]'],
|
||||||
previous: ['a'],
|
previous: ['a'],
|
||||||
additions: ['b[null->B]']
|
additions: ['b[null->B]']
|
||||||
|
@ -77,7 +75,7 @@ export function main() {
|
||||||
MapWrapper.set(m, 'b', 'BB');
|
MapWrapper.set(m, 'b', 'BB');
|
||||||
MapWrapper.set(m, 'd', 'D');
|
MapWrapper.set(m, 'd', 'D');
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
map: ['a', 'b[B->BB]', 'd[null->D]'],
|
||||||
previous: ['a', 'b[B->BB]'],
|
previous: ['a', 'b[B->BB]'],
|
||||||
additions: ['d[null->D]'],
|
additions: ['d[null->D]'],
|
||||||
|
@ -86,7 +84,7 @@ export function main() {
|
||||||
|
|
||||||
MapWrapper.delete(m, 'b');
|
MapWrapper.delete(m, 'b');
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['a', 'd'],
|
map: ['a', 'd'],
|
||||||
previous: ['a', 'b[BB->null]', 'd'],
|
previous: ['a', 'b[BB->null]', 'd'],
|
||||||
removals: ['b[BB->null]']
|
removals: ['b[BB->null]']
|
||||||
|
@ -94,7 +92,7 @@ export function main() {
|
||||||
|
|
||||||
MapWrapper.clear(m);
|
MapWrapper.clear(m);
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
previous: ['a[A->null]', 'd[D->null]'],
|
previous: ['a[A->null]', 'd[D->null]'],
|
||||||
removals: ['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);
|
MapWrapper.set(m, f + oo, b + ar);
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
|
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['foo'],
|
map: ['foo'],
|
||||||
previous: ['foo']
|
previous: ['foo']
|
||||||
}));
|
}));
|
||||||
|
@ -123,25 +121,70 @@ export function main() {
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
|
|
||||||
changes.check(m);
|
changes.check(m);
|
||||||
expect(changes.toString()).toEqual(changesAsString({
|
expect(changes.toString()).toEqual(kvChangesAsString({
|
||||||
map: ['foo'],
|
map: ['foo'],
|
||||||
previous: ['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 filter(List list, fn) => list.where(fn).toList();
|
||||||
static find(List list, fn) => list.firstWhere(fn, orElse:() => null);
|
static find(List list, fn) => list.firstWhere(fn, orElse:() => null);
|
||||||
static any(List list, fn) => list.any(fn);
|
static any(List list, fn) => list.any(fn);
|
||||||
static forEach(list, fn) {
|
static forEach(list, Function fn) {
|
||||||
list.forEach(fn);
|
list.forEach(fn);
|
||||||
}
|
}
|
||||||
static reduce(List list, Function fn, init) {
|
static reduce(List list, Function fn, init) {
|
||||||
|
@ -68,6 +68,15 @@ class ListWrapper {
|
||||||
static void clear(List l) { l.clear(); }
|
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 {
|
class SetWrapper {
|
||||||
static Set createFromList(List l) { return new Set.from(l); }
|
static Set createFromList(List l) { return new Set.from(l); }
|
||||||
static bool has(Set s, key) { return s.contains(key); }
|
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 List = window.Array;
|
||||||
export var Map = window.Map;
|
export var Map = window.Map;
|
||||||
|
@ -25,12 +25,17 @@ export class MapWrapper {
|
||||||
static clear(m) { m.clear(); }
|
static clear(m) { m.clear(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: cannot export StringMap as a type as Dart does not support
|
// TODO: cannot export StringMap as a type as Dart does not support renaming types...
|
||||||
// renaming types...
|
/**
|
||||||
|
* Wraps Javascript Objects
|
||||||
|
*/
|
||||||
export class StringMapWrapper {
|
export class StringMapWrapper {
|
||||||
// Note: We are not using Object.create(null) here due to
|
static create():Object {
|
||||||
// performance!
|
// Note: We are not using Object.create(null) here due to
|
||||||
static create():Object { return { }; }
|
// performance!
|
||||||
|
// http://jsperf.com/ng2-object-create-null
|
||||||
|
return { };
|
||||||
|
}
|
||||||
static get(map, key) {
|
static get(map, key) {
|
||||||
return map.hasOwnProperty(key) ? map[key] : undefined;
|
return map.hasOwnProperty(key) ? map[key] : undefined;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +50,9 @@ export class StringMapWrapper {
|
||||||
}
|
}
|
||||||
static forEach(map, callback) {
|
static forEach(map, callback) {
|
||||||
for (var prop in map) {
|
for (var prop in map) {
|
||||||
callback(map[prop], prop);
|
if (map.hasOwnProperty(prop)) {
|
||||||
|
callback(map[prop], prop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
export class SetWrapper {
|
||||||
static createFromList(lst:List) { return new Set(lst); }
|
static createFromList(lst:List) { return new Set(lst); }
|
||||||
static has(s:Set, key):boolean { return s.has(key); }
|
static has(s:Set, key):boolean { return s.has(key); }
|
||||||
|
|
|
@ -161,6 +161,11 @@ dynamic getMapKey(value) {
|
||||||
return value.isNaN ? _NAN_KEY : value;
|
return value.isNaN ? _NAN_KEY : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeBlank(obj) {
|
dynamic normalizeBlank(obj) {
|
||||||
return isBlank(obj) ? null : obj;
|
return isBlank(obj) ? null : obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isJsObject(o) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,3 +202,7 @@ export function getMapKey(value) {
|
||||||
export function normalizeBlank(obj) {
|
export function normalizeBlank(obj) {
|
||||||
return isBlank(obj) ? null : 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