Skip to content

Commit 847cefc

Browse files
committed
feat(change_detector): notify directives on property changes
1 parent 5bdefee commit 847cefc

File tree

9 files changed

+320
-62
lines changed

9 files changed

+320
-62
lines changed
Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
11
import {ProtoRecordRange, RecordRange} from './record_range';
22
import {ProtoRecord, Record} from './record';
3-
import {FIELD, int, isPresent} from 'facade/lang';
3+
import {int, isPresent, isBlank} from 'facade/lang';
4+
import {ListWrapper, List} from 'facade/collection';
45

56
export * from './record';
67
export * from './record_range'
78

89
export class ChangeDetector {
910
_rootRecordRange:RecordRange;
11+
1012
constructor(recordRange:RecordRange) {
1113
this._rootRecordRange = recordRange;
1214
}
1315

1416
detectChanges():int {
15-
var count:int = 0;
16-
for (var record = this._rootRecordRange.findFirstEnabledRecord();
17-
isPresent(record);
18-
record = record.nextEnabled) {
17+
var count = 0;
18+
var updatedRecords = null;
19+
var record = this._rootRecordRange.findFirstEnabledRecord();
20+
21+
while (isPresent(record)) {
22+
var currentRange = record.recordRange;
23+
var currentGroup = record.groupMemento();
24+
25+
var nextEnabled = record.nextEnabled;
26+
var nextRange = isPresent(nextEnabled) ? nextEnabled.recordRange : null;
27+
var nextGroup = isPresent(nextEnabled) ? nextEnabled.groupMemento() : null;
28+
1929
if (record.check()) {
20-
count++;
30+
count ++;
31+
if (record.terminatesExpression()) {
32+
updatedRecords = this._addRecord(updatedRecords, record);
33+
}
2134
}
35+
36+
if (this._shouldNotifyDispatcher(currentRange, nextRange, currentGroup, nextGroup, updatedRecords)) {
37+
currentRange.dispatcher.onRecordChange(currentGroup, updatedRecords);
38+
updatedRecords = null;
39+
}
40+
41+
record = record.nextEnabled;
2242
}
43+
2344
return count;
2445
}
46+
47+
_groupChanged(currentRange, nextRange, currentGroup, nextGroup) {
48+
return currentRange != nextRange || currentGroup != nextGroup;
49+
}
50+
51+
_shouldNotifyDispatcher(currentRange, nextRange, currentGroup, nextGroup, updatedRecords) {
52+
return this._groupChanged(currentRange, nextRange, currentGroup, nextGroup) && isPresent(updatedRecords);
53+
}
54+
55+
_addRecord(updatedRecords:List, record:Record) {
56+
if (isBlank(updatedRecords)) {
57+
updatedRecords = _singleElementList;
58+
updatedRecords[0] = record;
59+
60+
} else if (updatedRecords === _singleElementList) {
61+
updatedRecords = [_singleElementList[0], record];
62+
63+
} else {
64+
ListWrapper.push(updatedRecords, record);
65+
}
66+
return updatedRecords;
67+
}
2568
}
69+
70+
var _singleElementList = [null];

modules/change_detection/src/record.js

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,27 @@ export class ProtoRecord {
3434
arity:int;
3535
name:string;
3636
dest:any;
37+
groupMemento:any;
38+
3739
next:ProtoRecord;
3840

3941
recordInConstruction:Record;
42+
4043
constructor(recordRange:ProtoRecordRange,
4144
mode:int,
4245
funcOrValue,
4346
arity:int,
4447
name:string,
45-
dest) {
48+
dest,
49+
groupMemento) {
4650

4751
this.recordRange = recordRange;
4852
this._mode = mode;
4953
this.funcOrValue = funcOrValue;
5054
this.arity = arity;
5155
this.name = name;
5256
this.dest = dest;
57+
this.groupMemento = groupMemento;
5358

5459
this.next = null;
5560
// The concrete Record instantiated from this ProtoRecord
@@ -190,21 +195,20 @@ export class Record {
190195

191196
check():boolean {
192197
if (this.isCollection) {
193-
var changed = this._checkCollection();
194-
if (changed) {
195-
this._notifyDispatcher();
196-
return true;
197-
}
198-
return false;
198+
return this._checkCollection();
199199
} else {
200-
this.previousValue = this.currentValue;
201-
this.currentValue = this._calculateNewValue();
202-
if (isSame(this.previousValue, this.currentValue)) return false;
203-
this._updateDestination();
204-
return true;
200+
return this._checkSingleRecord();
205201
}
206202
}
207203

204+
_checkSingleRecord():boolean {
205+
this.previousValue = this.currentValue;
206+
this.currentValue = this._calculateNewValue();
207+
if (isSame(this.previousValue, this.currentValue)) return false;
208+
this._updateDestination();
209+
return true;
210+
}
211+
208212
_updateDestination() {
209213
// todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
210214
if (this.dest instanceof Record) {
@@ -213,15 +217,9 @@ export class Record {
213217
} else {
214218
this.dest.updateContext(this.currentValue);
215219
}
216-
} else {
217-
this._notifyDispatcher();
218220
}
219221
}
220222

221-
_notifyDispatcher() {
222-
this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest);
223-
}
224-
225223
// return whether the content has changed
226224
_checkCollection():boolean {
227225
switch(this.type) {
@@ -307,9 +305,21 @@ export class Record {
307305
}
308306
}
309307

308+
terminatesExpression():boolean {
309+
return !(this.dest instanceof Record);
310+
}
311+
310312
get isMarkerRecord() {
311313
return this.type == RECORD_TYPE_MARKER;
312314
}
315+
316+
expressionMemento() {
317+
return this.protoRecord.dest;
318+
}
319+
320+
groupMemento() {
321+
return isPresent(this.protoRecord) ? this.protoRecord.groupMemento : null;
322+
}
313323
}
314324

315325
function isSame(a, b) {

modules/change_detection/src/record_range.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,24 @@ export class ProtoRecordRange {
4545
* Parses [ast] into [ProtoRecord]s and adds them to [ProtoRecordRange].
4646
*
4747
* @param ast The expression to watch
48-
* @param memento an opaque object which will be passed to WatchGroupDispatcher on
48+
* @param expressionMemento an opaque object which will be passed to WatchGroupDispatcher on
4949
* detecting a change.
5050
* @param content Wether to watch collection content (true) or reference (false, default)
5151
*/
5252
addRecordsFromAST(ast:AST,
53-
memento,
53+
expressionMemento,
54+
groupMemento,
5455
content:boolean = false)
5556
{
5657
if (this.recordCreator === null) {
5758
this.recordCreator = new ProtoRecordCreator(this);
5859
}
60+
5961
if (content) {
6062
ast = new Collection(ast);
6163
}
62-
this.recordCreator.createRecordsFromAST(ast, memento);
64+
65+
this.recordCreator.createRecordsFromAST(ast, expressionMemento, groupMemento);
6366
}
6467

6568
// TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher.
@@ -85,6 +88,8 @@ export class ProtoRecordRange {
8588
for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) {
8689
if (proto.dest instanceof Destination) {
8790
proto.recordInConstruction.dest = proto.dest.record.recordInConstruction;
91+
} else {
92+
proto.recordInConstruction.dest = proto.dest;
8893
}
8994
proto.recordInConstruction = null;
9095
}
@@ -378,6 +383,8 @@ class ProtoRecordCreator {
378383
protoRecordRange:ProtoRecordRange;
379384
headRecord:ProtoRecord;
380385
tailRecord:ProtoRecord;
386+
groupMemento:any;
387+
381388
constructor(protoRecordRange) {
382389
this.protoRecordRange = protoRecordRange;
383390
this.headRecord = null;
@@ -491,12 +498,13 @@ class ProtoRecordCreator {
491498

492499
visitTemplateBindings(ast, dest) {this._unsupported();}
493500

494-
createRecordsFromAST(ast:AST, memento){
495-
ast.visit(this, memento);
501+
createRecordsFromAST(ast:AST, expressionMemento:any, groupMemento:any){
502+
this.groupMemento = groupMemento;
503+
ast.visit(this, expressionMemento);
496504
}
497505

498506
construct(recordType, funcOrValue, arity, name, dest) {
499-
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest);
507+
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest, this.groupMemento);
500508
}
501509

502510
add(protoRecord:ProtoRecord) {

modules/change_detection/test/change_detector_spec.js

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function main() {
2626
function createChangeDetector(memo:string, exp:string, context = null, formatters = null,
2727
content = false) {
2828
var prr = new ProtoRecordRange();
29-
prr.addRecordsFromAST(ast(exp), memo, content);
29+
prr.addRecordsFromAST(ast(exp), memo, memo, content);
3030

3131
var dispatcher = new LoggingDispatcher();
3232
var rr = prr.instantiate(dispatcher, formatters);
@@ -97,21 +97,21 @@ export function main() {
9797
it("should support literal array", () => {
9898
var c = createChangeDetector('array', '[1,2]');
9999
c["changeDetector"].detectChanges();
100-
expect(c["dispatcher"].loggedValues).toEqual([[1,2]]);
100+
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
101101

102102
c = createChangeDetector('array', '[1,a]', new TestData(2));
103103
c["changeDetector"].detectChanges();
104-
expect(c["dispatcher"].loggedValues).toEqual([[1,2]]);
104+
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
105105
});
106106

107107
it("should support literal maps", () => {
108108
var c = createChangeDetector('map', '{z:1}');
109109
c["changeDetector"].detectChanges();
110-
expect(MapWrapper.get(c["dispatcher"].loggedValues[0], 'z')).toEqual(1);
110+
expect(MapWrapper.get(c["dispatcher"].loggedValues[0][0], 'z')).toEqual(1);
111111

112112
c = createChangeDetector('map', '{z:a}', new TestData(1));
113113
c["changeDetector"].detectChanges();
114-
expect(MapWrapper.get(c["dispatcher"].loggedValues[0], 'z')).toEqual(1);
114+
expect(MapWrapper.get(c["dispatcher"].loggedValues[0][0], 'z')).toEqual(1);
115115
});
116116

117117
it("should support binary operations", () => {
@@ -242,6 +242,7 @@ export function main() {
242242

243243
context.a = [0];
244244
cd.detectChanges();
245+
245246
expect(dsp.log).toEqual(["a=" +
246247
arrayChangesAsString({
247248
collection: ['0[null->0]'],
@@ -343,10 +344,69 @@ export function main() {
343344
});
344345
}
345346
});
347+
348+
describe("onGroupChange", () => {
349+
it("should notify the dispatcher when a group of records changes", () => {
350+
var prr = new ProtoRecordRange();
351+
prr.addRecordsFromAST(ast("1 + 2"), "memo", 1);
352+
prr.addRecordsFromAST(ast("10 + 20"), "memo", 1);
353+
prr.addRecordsFromAST(ast("100 + 200"), "memo2", 2);
354+
355+
var dispatcher = new LoggingDispatcher();
356+
var rr = prr.instantiate(dispatcher, null);
357+
358+
var cd = new ChangeDetector(rr);
359+
cd.detectChanges();
360+
361+
expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]);
362+
});
363+
364+
it("should update every instance of a group individually", () => {
365+
var prr = new ProtoRecordRange();
366+
prr.addRecordsFromAST(ast("1 + 2"), "memo", "memo");
367+
368+
var dispatcher = new LoggingDispatcher();
369+
var rr = new RecordRange(null, dispatcher);
370+
rr.addRange(prr.instantiate(dispatcher, null));
371+
rr.addRange(prr.instantiate(dispatcher, null));
372+
373+
var cd = new ChangeDetector(rr);
374+
cd.detectChanges();
375+
376+
expect(dispatcher.loggedValues).toEqual([[3], [3]]);
377+
});
378+
379+
it("should notify the dispatcher before switching to the next group", () => {
380+
var prr = new ProtoRecordRange();
381+
prr.addRecordsFromAST(ast("a()"), "a", 1);
382+
prr.addRecordsFromAST(ast("b()"), "b", 2);
383+
prr.addRecordsFromAST(ast("c()"), "b", 2);
384+
385+
var dispatcher = new LoggingDispatcher();
386+
var rr = prr.instantiate(dispatcher, null);
387+
388+
var tr = new TestRecord();
389+
tr.a = () => {dispatcher.logValue('InvokeA'); return 'a'};
390+
tr.b = () => {dispatcher.logValue('InvokeB'); return 'b'};
391+
tr.c = () => {dispatcher.logValue('InvokeC'); return 'c'};
392+
rr.setContext(tr);
393+
394+
var cd = new ChangeDetector(rr);
395+
cd.detectChanges();
396+
397+
expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]);
398+
});
399+
});
346400
});
347401
});
348402
}
349403

404+
class TestRecord {
405+
a;
406+
b;
407+
c;
408+
}
409+
350410
class Person {
351411
name:string;
352412
address:Address;
@@ -387,6 +447,7 @@ class TestData {
387447
class LoggingDispatcher extends WatchGroupDispatcher {
388448
log:List;
389449
loggedValues:List;
450+
390451
constructor() {
391452
this.log = null;
392453
this.loggedValues = null;
@@ -397,10 +458,18 @@ class LoggingDispatcher extends WatchGroupDispatcher {
397458
this.log = ListWrapper.create();
398459
this.loggedValues = ListWrapper.create();
399460
}
461+
462+
logValue(value) {
463+
ListWrapper.push(this.loggedValues, value);
464+
}
465+
466+
onRecordChange(group, records:List) {
467+
var value = records[0].currentValue;
468+
var dest = records[0].protoRecord.dest;
469+
ListWrapper.push(this.log, dest + '=' + this._asString(value));
400470

401-
onRecordChange(record:Record, context) {
402-
ListWrapper.push(this.loggedValues, record.currentValue);
403-
ListWrapper.push(this.log, context + '=' + this._asString(record.currentValue));
471+
var values = ListWrapper.map(records, (r) => r.currentValue);
472+
ListWrapper.push(this.loggedValues, values);
404473
}
405474

406475
_asString(value) {

modules/change_detection/test/record_range_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function main() {
4646
}
4747

4848
function createRecord(rr) {
49-
return new Record(rr, new ProtoRecord(null, 0, null, null, null, null), null);
49+
return new Record(rr, new ProtoRecord(null, 0, null, null, null, null, null), null);
5050
}
5151

5252
describe('record range', () => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class OnChange {
2+
onChange(changes) {
3+
throw "not implemented";
4+
}
5+
}

0 commit comments

Comments
 (0)