feat(Change Detector): Add support for collection content watch

This commit is contained in:
Victor Berchet 2014-11-24 18:42:53 +01:00
parent 2d2f44949d
commit bf71b94bde
15 changed files with 609 additions and 206 deletions

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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]']
})]);
});
});
}
});
}); });
}); });
} }

View File

@ -0,0 +1,6 @@
import 'dart:collection';
class TestIterable extends IterableBase<int> {
List<int> list = [];
Iterator<int> get iterator => list.iterator;
}

View File

@ -0,0 +1,9 @@
export class TestIterable {
constructor() {
this.list = [];
}
[Symbol.iterator]() {
return this.list[Symbol.iterator]();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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");
}