feat(change_detector): notify directives on property changes

This commit is contained in:
vsavkin
2014-12-02 17:09:46 -08:00
parent 5bdefee6c9
commit 847cefcb7b
9 changed files with 320 additions and 62 deletions

View File

@ -0,0 +1,5 @@
export class OnChange {
onChange(changes) {
throw "not implemented";
}
}

View File

@ -1,5 +1,5 @@
import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facade/dom';
import {ListWrapper, MapWrapper, List} from 'facade/collection';
import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'facade/collection';
import {ProtoRecordRange, RecordRange, WatchGroupDispatcher} from 'change_detection/record_range';
import {Record} from 'change_detection/record';
import {AST} from 'change_detection/parser/ast';
@ -12,6 +12,7 @@ import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
import {Injector} from 'di/di';
import {NgElement} from 'core/dom/element';
import {ViewPort} from './viewport';
import {OnChange} from './interfaces';
const NG_BINDING_CLASS = 'ng-binding';
@ -47,22 +48,55 @@ export class View {
this.viewPorts = null;
}
onRecordChange(record:Record, target) {
onRecordChange(groupMemento, records:List<Record>) {
this._invokeMementoForRecords(records);
if (groupMemento instanceof DirectivePropertyGroupMemento) {
this._notifyDirectiveAboutChanges(groupMemento, records);
}
}
_invokeMementoForRecords(records:List<Record>) {
for(var i = 0; i < records.length; ++i) {
this._invokeMementoFor(records[i]);
}
}
_notifyDirectiveAboutChanges(groupMemento, records:List<Record>) {
var dir = groupMemento.directive(this.elementInjectors);
if (dir instanceof OnChange) {
dir.onChange(this._collectChanges(records));
}
}
// dispatch to element injector or text nodes based on context
if (target instanceof DirectivePropertyMemento) {
_invokeMementoFor(record:Record) {
var memento = record.expressionMemento();
if (memento instanceof DirectivePropertyMemento) {
// we know that it is DirectivePropertyMemento
var directiveMemento:DirectivePropertyMemento = target;
var directiveMemento:DirectivePropertyMemento = memento;
directiveMemento.invoke(record, this.elementInjectors);
} else if (target instanceof ElementPropertyMemento) {
var elementMemento:ElementPropertyMemento = target;
} else if (memento instanceof ElementPropertyMemento) {
var elementMemento:ElementPropertyMemento = memento;
elementMemento.invoke(record, this.bindElements);
} else {
// we know it refers to _textNodes.
var textNodeIndex:number = target;
var textNodeIndex:number = memento;
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
}
}
_collectChanges(records:List) {
var changes = StringMapWrapper.create();
for(var i = 0; i < records.length; ++i) {
var record = records[i];
var propertyUpdate = new PropertyUpdate(record.currentValue, record.previousValue);
StringMapWrapper.set(changes, record.expressionMemento()._setterName, propertyUpdate);
}
return changes;
}
addViewPort(viewPort: ViewPort) {
if (isBlank(this.viewPorts)) this.viewPorts = [];
ListWrapper.push(this.viewPorts, viewPort);
@ -163,7 +197,8 @@ export class ProtoView {
elBinder.textNodeIndices = ListWrapper.create();
}
ListWrapper.push(elBinder.textNodeIndices, indexInParent);
this.protoRecordRange.addRecordsFromAST(expression, this.textNodesWithBindingCount++);
var memento = this.textNodesWithBindingCount++;
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
}
/**
@ -175,12 +210,8 @@ export class ProtoView {
elBinder.hasElementPropertyBindings = true;
this.elementsWithBindingCount++;
}
this.protoRecordRange.addRecordsFromAST(expression,
new ElementPropertyMemento(
this.elementsWithBindingCount-1,
propertyName
)
);
var memento = new ElementPropertyMemento(this.elementsWithBindingCount-1, propertyName);
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
}
/**
@ -202,15 +233,15 @@ export class ProtoView {
expression:AST,
setterName:string,
setter:SetterFn) {
this.protoRecordRange.addRecordsFromAST(
expression,
new DirectivePropertyMemento(
this.elementBinders.length-1,
directiveIndex,
setterName,
setter
)
var expMemento = new DirectivePropertyMemento(
this.elementBinders.length-1,
directiveIndex,
setterName,
setter
);
var groupMemento = DirectivePropertyGroupMemento.get(expMemento);
this.protoRecordRange.addRecordsFromAST(expression, expMemento, groupMemento, false);
}
static _createElementInjectors(elements, binders, hostElementInjector) {
@ -363,6 +394,43 @@ export class DirectivePropertyMemento {
}
}
var _groups = MapWrapper.create();
class DirectivePropertyGroupMemento {
_elementInjectorIndex:number;
_directiveIndex:number;
constructor(elementInjectorIndex:number, directiveIndex:number) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
}
static get(memento:DirectivePropertyMemento) {
var elementInjectorIndex = memento._elementInjectorIndex;
var directiveIndex = memento._directiveIndex;
var id = elementInjectorIndex * 100 + directiveIndex;
if (! MapWrapper.contains(_groups, id)) {
return MapWrapper.set(_groups, id, new DirectivePropertyGroupMemento(elementInjectorIndex, directiveIndex));
}
return MapWrapper.get(_groups, id);
}
directive(elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
return elementInjector.getAtIndex(this._directiveIndex);
}
}
class PropertyUpdate {
currentValue;
previousValue;
constructor(currentValue, previousValue) {
this.currentValue = currentValue;
this.previousValue = previousValue;
}
}
//TODO(tbosch): I don't like to have done be called from a different place than notify

View File

@ -2,6 +2,7 @@
* Define public API for Angular here
*/
export * from './annotations/annotations';
export * from './compiler/interfaces';
export * from './annotations/template_config';
export * from './application';

View File

@ -3,6 +3,7 @@ import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader';
import {Component, Decorator, Template} from 'core/annotations/annotations';
import {OnChange} from 'core/core';
import {ProtoRecordRange} from 'change_detection/record_range';
import {ChangeDetector} from 'change_detection/change_detector';
import {TemplateConfig} from 'core/annotations/template_config';
@ -281,7 +282,7 @@ export function main() {
expect(view.bindElements[0].id).toEqual('buz');
});
it('should consume directive watch expression change.', () => {
it('should consume directive watch expression change', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
@ -292,6 +293,43 @@ export function main() {
cd.detectChanges();
expect(view.elementInjectors[0].get(SomeDirective).prop).toEqual('buz');
});
it('should notify a directive about changes after all its properties have been set', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv);
ctx.a = 100;
ctx.b = 200;
cd.detectChanges();
var directive = view.elementInjectors[0].get(DirectiveImplementingOnChange);
expect(directive.c).toEqual(300);
});
it('should provide a map of updated properties', () => {
var pv = new ProtoView(createElement('<div class="ng-binding"></div>'),
new ProtoRecordRange());
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a'));
pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b'));
createView(pv);
ctx.a = 0;
ctx.b = 0;
cd.detectChanges();
ctx.a = 100;
cd.detectChanges();
var directive = view.elementInjectors[0].get(DirectiveImplementingOnChange);
expect(directive.changes["a"].currentValue).toEqual(100);
expect(directive.changes["b"]).not.toBeDefined();
});
});
});
@ -324,6 +362,18 @@ class SomeDirective {
}
}
class DirectiveImplementingOnChange extends OnChange {
a;
b;
c;
changes;
onChange(changes) {
this.c = this.a + this.b;
this.changes = changes;
}
}
class SomeService {}
@Component({
@ -368,6 +418,8 @@ class AnotherDirective {
class MyEvaluationContext {
foo:string;
a;
b;
constructor() {
this.foo = 'bar';
};