feat(pipes): replaces iterable and key value diffing pipes with services

BREAKING CHANGE:
    Directives that previously injected Pipes to get iterableDiff or keyvalueDiff, now should inject IterableDiffers and KeyValueDiffers.
This commit is contained in:
vsavkin
2015-07-31 12:23:50 -07:00
parent c20a5d65d8
commit 392de4af67
20 changed files with 595 additions and 260 deletions
@@ -0,0 +1,319 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {
DefaultIterableDiffer,
DefaultIterableDifferFactory
} from 'angular2/src/change_detection/differs/default_iterable_differ';
import {NumberWrapper} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {TestIterable} from '../iterable';
import {iterableChangesAsString} from '../util';
// todo(vicb): UnmodifiableListView / frozen object when implemented
export function main() {
describe('iterable differ', function() {
describe('DefaultIterableDiffer', function() {
var differ;
beforeEach(() => { differ = new DefaultIterableDiffer(); });
it('should support list and iterables', () => {
var f = new DefaultIterableDifferFactory();
expect(f.supports([])).toBeTruthy();
expect(f.supports(new TestIterable())).toBeTruthy();
expect(f.supports(new Map())).toBeFalsy();
expect(f.supports(null)).toBeFalsy();
});
it('should support iterables', () => {
let l = new TestIterable();
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
l.list = [1];
differ.check(l);
expect(differ.toString())
.toEqual(
iterableChangesAsString({collection: ['1[null->0]'], additions: ['1[null->0]']}));
l.list = [2, 1];
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['2[null->0]', '1[0->1]'],
previous: ['1[0->1]'],
additions: ['2[null->0]'],
moves: ['1[0->1]']
}));
});
it('should detect additions', () => {
let l = [];
differ.check(l);
expect(differ.toString()).toEqual(iterableChangesAsString({collection: []}));
l.push('a');
differ.check(l);
expect(differ.toString())
.toEqual(
iterableChangesAsString({collection: ['a[null->0]'], additions: ['a[null->0]']}));
l.push('b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString(
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
});
it('should support changing the reference', () => {
let l = [0];
differ.check(l);
l = [1, 0];
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['1[null->0]', '0[0->1]'],
previous: ['0[0->1]'],
additions: ['1[null->0]'],
moves: ['0[0->1]']
}));
l = [2, 1, 0];
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['2[null->0]', '1[0->1]', '0[1->2]'],
previous: ['1[0->1]', '0[1->2]'],
additions: ['2[null->0]'],
moves: ['1[0->1]', '0[1->2]']
}));
});
it('should handle swapping element', () => {
let l = [1, 2];
differ.check(l);
ListWrapper.clear(l);
l.push(2);
l.push(1);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['2[1->0]', '1[0->1]'],
previous: ['1[0->1]', '2[1->0]'],
moves: ['2[1->0]', '1[0->1]']
}));
});
it('should handle swapping element', () => {
let l = ['a', 'b', 'c'];
differ.check(l);
ListWrapper.removeAt(l, 1);
ListWrapper.insert(l, 0, 'b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['b[1->0]', 'a[0->1]', 'c'],
previous: ['a[0->1]', 'b[1->0]', 'c'],
moves: ['b[1->0]', 'a[0->1]']
}));
ListWrapper.removeAt(l, 1);
l.push('a');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['b', 'c[2->1]', 'a[1->2]'],
previous: ['b', 'a[1->2]', 'c[2->1]'],
moves: ['c[2->1]', 'a[1->2]']
}));
});
it('should detect changes in list', () => {
let l = [];
differ.check(l);
l.push('a');
differ.check(l);
expect(differ.toString())
.toEqual(
iterableChangesAsString({collection: ['a[null->0]'], additions: ['a[null->0]']}));
l.push('b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString(
{collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]']}));
l.push('c');
l.push('d');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['a', 'b', 'c[null->2]', 'd[null->3]'],
previous: ['a', 'b'],
additions: ['c[null->2]', 'd[null->3]']
}));
ListWrapper.removeAt(l, 2);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['a', 'b', 'd[3->2]'],
previous: ['a', 'b', 'c[2->null]', 'd[3->2]'],
moves: ['d[3->2]'],
removals: ['c[2->null]']
}));
ListWrapper.clear(l);
l.push('d');
l.push('c');
l.push('b');
l.push('a');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'],
previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'],
additions: ['c[null->1]'],
moves: ['d[2->0]', 'b[1->2]', 'a[0->3]']
}));
});
it('should test string by value rather than by reference (Dart)', () => {
let l = ['a', 'boo'];
differ.check(l);
var b = 'b';
var oo = 'oo';
ListWrapper.set(l, 1, b + oo);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({collection: ['a', 'boo'], previous: ['a', 'boo']}));
});
it('should ignore [NaN] != [NaN] (JS)', () => {
let l = [NumberWrapper.NaN];
differ.check(l);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString(
{collection: [NumberWrapper.NaN], previous: [NumberWrapper.NaN]}));
});
it('should detect [NaN] moves', () => {
let l = [NumberWrapper.NaN, NumberWrapper.NaN];
differ.check(l);
ListWrapper.insert<any>(l, 0, 'foo');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'],
previous: ['NaN[0->1]', 'NaN[1->2]'],
additions: ['foo[null->0]'],
moves: ['NaN[0->1]', 'NaN[1->2]']
}));
});
it('should remove and add same item', () => {
let l = ['a', 'b', 'c'];
differ.check(l);
ListWrapper.removeAt(l, 1);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['a', 'c[2->1]'],
previous: ['a', 'b[1->null]', 'c[2->1]'],
moves: ['c[2->1]'],
removals: ['b[1->null]']
}));
ListWrapper.insert(l, 1, 'b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['a', 'b[null->1]', 'c[1->2]'],
previous: ['a', 'c[1->2]'],
additions: ['b[null->1]'],
moves: ['c[1->2]']
}));
});
it('should support duplicates', () => {
let l = ['a', 'a', 'a', 'b', 'b'];
differ.check(l);
ListWrapper.removeAt(l, 0);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['a', 'a', 'b[3->2]', 'b[4->3]'],
previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'],
moves: ['b[3->2]', 'b[4->3]'],
removals: ['a[2->null]']
}));
});
it('should support insertions/moves', () => {
let l = ['a', 'a', 'b', 'b'];
differ.check(l);
ListWrapper.insert(l, 0, 'b');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'],
previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'],
additions: ['b[null->4]'],
moves: ['b[2->0]', 'a[0->1]', 'a[1->2]']
}));
});
it('should not report unnecessary moves', () => {
let l = ['a', 'b', 'c'];
differ.check(l);
ListWrapper.clear(l);
l.push('b');
l.push('a');
l.push('c');
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['b[1->0]', 'a[0->1]', 'c'],
previous: ['a[0->1]', 'b[1->0]', 'c'],
moves: ['b[1->0]', 'a[0->1]']
}));
});
describe('diff', () => {
it('should return self when there is a change',
() => { expect(differ.diff(['a', 'b'])).toBe(differ); });
it('should return null when there is no change', () => {
differ.diff(['a', 'b']);
expect(differ.diff(['a', 'b'])).toEqual(null);
});
it('should treat null as an empty list', () => {
differ.diff(['a', 'b']);
expect(differ.diff(null).toString())
.toEqual(iterableChangesAsString({
previous: ['a[0->null]', 'b[1->null]'],
removals: ['a[0->null]', 'b[1->null]']
}));
});
it('should throw when given an invalid collection', () => {
expect(() => differ.diff("invalid")).toThrowErrorWith("Error trying to diff 'invalid'");
});
});
});
});
}
@@ -0,0 +1,218 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {
DefaultKeyValueDiffer,
DefaultKeyValueDifferFactory
} from 'angular2/src/change_detection/differs/default_keyvalue_differ';
import {NumberWrapper, isJsObject} from 'angular2/src/facade/lang';
import {MapWrapper} from 'angular2/src/facade/collection';
import {kvChangesAsString} from '../util';
// todo(vicb): Update the code & tests for object equality
export function main() {
describe('keyvalue differ', function() {
describe('DefaultKeyValueDiffer', function() {
var differ;
var m: Map<any, any>;
beforeEach(() => {
differ = new DefaultKeyValueDiffer();
m = new Map();
});
afterEach(() => { differ = null; });
it('should detect additions', () => {
differ.check(m);
m.set('a', 1);
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->1]'], additions: ['a[null->1]']}));
m.set('b', 2);
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->2]'], previous: ['a'], additions: ['b[null->2]']}));
});
it('should handle changing key/values correctly', () => {
m.set(1, 10);
m.set(2, 20);
differ.check(m);
m.set(2, 10);
m.set(1, 20);
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({
map: ['1[10->20]', '2[20->10]'],
previous: ['1[10->20]', '2[20->10]'],
changes: ['1[10->20]', '2[20->10]']
}));
});
it('should expose previous and current value', () => {
var previous, current;
m.set(1, 10);
differ.check(m);
m.set(1, 20);
differ.check(m);
differ.forEachChangedItem((record) => {
previous = record.previousValue;
current = record.currentValue;
});
expect(previous).toEqual(10);
expect(current).toEqual(20);
});
it('should do basic map watching', () => {
differ.check(m);
m.set('a', 'A');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
m.set('b', 'B');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
m.set('b', 'BB');
m.set('d', 'D');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({
map: ['a', 'b[B->BB]', 'd[null->D]'],
previous: ['a', 'b[B->BB]'],
additions: ['d[null->D]'],
changes: ['b[B->BB]']
}));
MapWrapper.delete(m, 'b');
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'd'], previous: ['a', 'b[BB->null]', 'd'], removals: ['b[BB->null]']}));
m.clear();
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{previous: ['a[A->null]', 'd[D->null]'], removals: ['a[A->null]', 'd[D->null]']}));
});
it('should test string by value rather than by reference (DART)', () => {
m.set('foo', 'bar');
differ.check(m);
var f = 'f';
var oo = 'oo';
var b = 'b';
var ar = 'ar';
m.set(f + oo, b + ar);
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({map: ['foo'], previous: ['foo']}));
});
it('should not see a NaN value as a change (JS)', () => {
m.set('foo', NumberWrapper.NaN);
differ.check(m);
differ.check(m);
expect(differ.toString()).toEqual(kvChangesAsString({map: ['foo'], previous: ['foo']}));
});
// JS specific tests (JS Objects)
if (isJsObject({})) {
describe('JsObject changes', () => {
it('should support JS Object', () => {
var f = new DefaultKeyValueDifferFactory();
expect(f.supports({})).toBeTruthy();
expect(f.supports("not supported")).toBeFalsy();
expect(f.supports(0)).toBeFalsy();
expect(f.supports(null)).toBeFalsy();
});
it('should do basic object watching', () => {
let m = {};
differ.check(m);
m['a'] = 'A';
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({map: ['a[null->A]'], additions: ['a[null->A]']}));
m['b'] = 'B';
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString(
{map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]']}));
m['b'] = 'BB';
m['d'] = 'D';
differ.check(m);
expect(differ.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';
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({
map: ['a', 'd'],
previous: ['a', 'b[BB->null]', 'd'],
removals: ['b[BB->null]']
}));
m = {};
differ.check(m);
expect(differ.toString())
.toEqual(kvChangesAsString({
previous: ['a[A->null]', 'd[D->null]'],
removals: ['a[A->null]', 'd[D->null]']
}));
});
});
describe('diff', () => {
it('should return self when there is a change', () => {
m.set('a', 'A');
expect(differ.diff(m)).toBe(differ);
});
it('should return null when there is no change', () => {
m.set('a', 'A');
differ.diff(m);
expect(differ.diff(m)).toEqual(null);
});
it('should treat null as an empty list', () => {
m.set('a', 'A');
differ.diff(m);
expect(differ.diff(null).toString())
.toEqual(kvChangesAsString({previous: ['a[A->null]'], removals: ['a[A->null]']}));
});
it('should throw when given an invalid collection', () => {
expect(() => differ.diff("invalid")).toThrowErrorWith("Error trying to diff 'invalid'");
});
});
}
});
});
}
@@ -0,0 +1,70 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
SpyIterableDifferFactory
} from 'angular2/test_lib';
import {IterableDiffers} from 'angular2/src/change_detection/differs/iterable_differs';
import {Injector, bind} from 'angular2/di';
export function main() {
describe('IterableDiffers', function() {
var factory1;
var factory2;
var factory3;
beforeEach(() => {
factory1 = new SpyIterableDifferFactory();
factory2 = new SpyIterableDifferFactory();
factory3 = new SpyIterableDifferFactory();
});
it('should throw when no suitable implementation found', () => {
var differs = new IterableDiffers([]);
expect(() => differs.find("some object"))
.toThrowErrorWith("Cannot find a differ supporting object 'some object'")
});
it('should return the first suitable implementation', () => {
factory1.spy("supports").andReturn(false);
factory2.spy("supports").andReturn(true);
factory3.spy("supports").andReturn(true);
var differs = IterableDiffers.create(<any>[factory1, factory2, factory3]);
expect(differs.find("some object")).toBe(factory2);
});
it('should copy over differs from the parent repo', () => {
factory1.spy("supports").andReturn(true);
factory2.spy("supports").andReturn(false);
var parent = IterableDiffers.create(<any>[factory1]);
var child = IterableDiffers.create(<any>[factory2], parent);
expect(child.factories).toEqual([factory2, factory1]);
});
describe(".extend()", () => {
it('should throw if calling extend when creating root injector', () => {
var injector = Injector.resolveAndCreate([IterableDiffers.extend([])]);
expect(() => injector.get(IterableDiffers))
.toThrowErrorWith("Cannot extend IterableDiffers without a parent injector");
});
it('should extend di-inherited diffesr', () => {
var parent = new IterableDiffers([factory1]);
var injector = Injector.resolveAndCreate([bind(IterableDiffers).toValue(parent)]);
var childInjector = injector.resolveAndCreateChild([IterableDiffers.extend([factory2])]);
expect(injector.get(IterableDiffers).factories).toEqual([factory1]);
expect(childInjector.get(IterableDiffers).factories).toEqual([factory2, factory1]);
});
});
});
}