feat(change_detection): added a directive lifecycle hook that is called after children are checked

This commit is contained in:
vsavkin
2015-03-26 14:36:25 -07:00
parent 507f7ea70a
commit 723e8fde93
13 changed files with 283 additions and 82 deletions
@@ -42,11 +42,13 @@ export class AbstractChangeDetector extends ChangeDetector {
this.detectChangesInRecords(throwOnChange);
this._detectChangesInChildren(throwOnChange);
this.notifyOnAllChangesDone();
if (this.mode === CHECK_ONCE) this.mode = CHECKED;
}
detectChangesInRecords(throwOnChange:boolean){}
notifyOnAllChangesDone(){}
_detectChangesInChildren(throwOnChange:boolean) {
var children = this.children;
@@ -1,7 +1,7 @@
library change_detectoin.change_detection_jit_generator;
class ChangeDetectorJITGenerator {
ChangeDetectorJITGenerator(typeName, records) {
ChangeDetectorJITGenerator(typeName, records, directiveMementos) {
}
generate() {
@@ -64,6 +64,7 @@ import {
* }
* }
*
* ChangeDetector0.prototype.notifyOnAllChangesDone = function() {}
*
* ChangeDetector0.prototype.hydrate = function(context, locals) {
* this.context = context;
@@ -96,31 +97,35 @@ var UTIL = "ChangeDetectionUtil";
var DISPATCHER_ACCESSOR = "this.dispatcher";
var PIPE_REGISTRY_ACCESSOR = "this.pipeRegistry";
var PROTOS_ACCESSOR = "this.protos";
var MEMENTOS_ACCESSOR = "this.directiveMementos";
var CONTEXT_ACCESSOR = "this.context";
var CHANGE_LOCAL = "change";
var CHANGES_LOCAL = "changes";
var LOCALS_ACCESSOR = "this.locals";
var TEMP_LOCAL = "temp";
function typeTemplate(type:string, cons:string, detectChanges:string, setContext:string):string {
function typeTemplate(type:string, cons:string, detectChanges:string,
notifyOnAllChangesDone:string, setContext:string):string {
return `
${cons}
${detectChanges}
${notifyOnAllChangesDone}
${setContext};
return function(dispatcher, pipeRegistry) {
return new ${type}(dispatcher, pipeRegistry, protos);
return new ${type}(dispatcher, pipeRegistry, protos, directiveMementos);
}
`;
}
function constructorTemplate(type:string, fieldsDefinitions:string):string {
return `
var ${type} = function ${type}(dispatcher, pipeRegistry, protos) {
var ${type} = function ${type}(dispatcher, pipeRegistry, protos, directiveMementos) {
${ABSTRACT_CHANGE_DETECTOR}.call(this);
${DISPATCHER_ACCESSOR} = dispatcher;
${PIPE_REGISTRY_ACCESSOR} = pipeRegistry;
${PROTOS_ACCESSOR} = protos;
${MEMENTOS_ACCESSOR} = directiveMementos;
${fieldsDefinitions}
}
@@ -157,6 +162,18 @@ ${type}.prototype.detectChangesInRecords = function(throwOnChange) {
`;
}
function notifyOnAllChangesDoneTemplate(type:string, body:string):string {
return `
${type}.prototype.notifyOnAllChangesDone = function() {
${body}
}
`;
}
function onAllChangesDoneTemplate(index:number):string {
return `${DISPATCHER_ACCESSOR}.onAllChangesDone(${MEMENTOS_ACCESSOR}[${index}]);`;
}
function bodyTemplate(localDefinitions:string, changeDefinitions:string, records:string):string {
return `
@@ -247,14 +264,16 @@ function addSimpleChangeRecordTemplate(protoIndex:number, oldValue:string, newVa
export class ChangeDetectorJITGenerator {
typeName:string;
records:List<ProtoRecord>;
directiveMementos:List;
localNames:List<String>;
changeNames:List<String>;
fieldNames:List<String>;
pipeNames:List<String>;
constructor(typeName:string, records:List<ProtoRecord>) {
constructor(typeName:string, records:List<ProtoRecord>, directiveMementos:List) {
this.typeName = typeName;
this.records = records;
this.directiveMementos = directiveMementos;
this.localNames = this.getLocalNames(records);
this.changeNames = this.getChangeNames(this.localNames);
@@ -284,8 +303,10 @@ export class ChangeDetectorJITGenerator {
}
generate():Function {
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genHydrate());
return new Function('AbstractChangeDetector', 'ChangeDetectionUtil', 'protos', text)(AbstractChangeDetector, ChangeDetectionUtil, this.records);
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(),
this.genNotifyOnAllChangesDone(), this.genHydrate());
return new Function('AbstractChangeDetector', 'ChangeDetectionUtil', 'protos', 'directiveMementos', text)
(AbstractChangeDetector, ChangeDetectionUtil, this.records, this.directiveMementos);
}
genConstructor():string {
@@ -319,6 +340,20 @@ export class ChangeDetectorJITGenerator {
return detectChangesTemplate(this.typeName, body);
}
genNotifyOnAllChangesDone():string {
var notifications = [];
var mementos = this.directiveMementos;
for (var i = mementos.length - 1; i >= 0; --i) {
var memento = mementos[i];
if (memento.notifyOnAllChangesDone) {
notifications.push(onAllChangesDoneTemplate(i));
}
}
return notifyOnAllChangesDoneTemplate(this.typeName, notifications.join(";\n"));
}
genBody():string {
var rec = this.records.map((r) => this.genRecord(r)).join("\n");
return bodyTemplate(this.genLocalDefinitions(), this.genChangeDefinitions(), rec);
@@ -34,8 +34,9 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
prevContexts:List;
protos:List<ProtoRecord>;
directiveMementos:List;
constructor(dispatcher:any, pipeRegistry:PipeRegistry, protoRecords:List<ProtoRecord>) {
constructor(dispatcher:any, pipeRegistry:PipeRegistry, protoRecords:List<ProtoRecord>, directiveMementos:List) {
super();
this.dispatcher = dispatcher;
this.pipeRegistry = pipeRegistry;
@@ -52,6 +53,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
this.locals = null;
this.protos = protoRecords;
this.directiveMementos = directiveMementos;
}
hydrate(context:any, locals:any) {
@@ -102,6 +104,16 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
}
}
notifyOnAllChangesDone() {
var mementos = this.directiveMementos;
for (var i = mementos.length - 1; i >= 0; --i) {
var memento = mementos[i];
if (memento.notifyOnAllChangesDone) {
this.dispatcher.onAllChangesDone(memento);
}
}
}
_check(proto:ProtoRecord) {
try {
if (proto.mode === RECORD_TYPE_PIPE || proto.mode === RECORD_TYPE_BINDING_PIPE) {
@@ -47,7 +47,7 @@ import {
export class ProtoChangeDetector {
addAst(ast:AST, bindingMemento:any, directiveMemento:any = null){}
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List):ChangeDetector{
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List, directiveMemento:List):ChangeDetector{
return null;
}
}
@@ -73,9 +73,9 @@ export class DynamicProtoChangeDetector extends ProtoChangeDetector {
this._pipeRegistry = pipeRegistry;
}
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List) {
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List, directiveMementos:List) {
this._createRecordsIfNecessary(bindingRecords, variableBindings);
return new DynamicChangeDetector(dispatcher, this._pipeRegistry, this._records);
return new DynamicChangeDetector(dispatcher, this._pipeRegistry, this._records, directiveMementos);
}
_createRecordsIfNecessary(bindingRecords:List, variableBindings:List) {
@@ -100,12 +100,12 @@ export class JitProtoChangeDetector extends ProtoChangeDetector {
this._factory = null;
}
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List) {
this._createFactoryIfNecessary(bindingRecords, variableBindings);
instantiate(dispatcher:any, bindingRecords:List, variableBindings:List, directiveMementos:List) {
this._createFactoryIfNecessary(bindingRecords, variableBindings, directiveMementos);
return this._factory(dispatcher, this._pipeRegistry);
}
_createFactoryIfNecessary(bindingRecords:List, variableBindings:List) {
_createFactoryIfNecessary(bindingRecords:List, variableBindings:List, directiveMementos:List) {
if (isBlank(this._factory)) {
var recordBuilder = new ProtoRecordBuilder();
ListWrapper.forEach(bindingRecords, (r) => {
@@ -114,7 +114,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector {
var c = _jitProtoChangeDetectorClassCounter++;
var records = coalesce(recordBuilder.records);
var typeName = `ChangeDetector${c}`;
this._factory = new ChangeDetectorJITGenerator(typeName, records).generate();
this._factory = new ChangeDetectorJITGenerator(typeName, records, directiveMementos).generate();
}
}
}
+20
View File
@@ -900,3 +900,23 @@ export const onDestroy = "onDestroy";
* @publicModule angular2/annotations
*/
export const onChange = "onChange";
/**
* Notify a directive when the bindings of all its children have been changed.
*
* ## Example:
*
* ```
* @Decorator({
* selector: '[class-set]',
* })
* class ClassSet {
*
* onAllChangesDone() {
* }
*
* }
* ```
* @publicModule angular2/annotations
*/
export const onAllChangesDone = "onAllChangesDone";
+21 -13
View File
@@ -7,7 +7,7 @@ import {EventEmitter, PropertySetter, Attribute} from 'angular2/src/core/annotat
import * as viewModule from 'angular2/src/core/compiler/view';
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/dom/element';
import {Directive, onChange, onDestroy} from 'angular2/src/core/annotations/annotations';
import {Directive, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations';
import {BindingPropagationConfig} from 'angular2/change_detection';
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
import {setterFactory} from './property_setter_factory';
@@ -131,11 +131,13 @@ export class DirectiveDependency extends Dependency {
export class DirectiveBinding extends Binding {
callOnDestroy:boolean;
callOnChange:boolean;
callOnAllChangesDone:boolean;
constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean, annotation:Directive) {
super(key, factory, dependencies, providedAsPromise);
this.callOnDestroy = isPresent(annotation) && annotation.hasLifecycleHook(onDestroy);
this.callOnChange = isPresent(annotation) && annotation.hasLifecycleHook(onChange);
this.callOnAllChangesDone = isPresent(annotation) && annotation.hasLifecycleHook(onAllChangesDone);
}
static createFromBinding(b:Binding, annotation:Directive):Binding {
@@ -216,6 +218,8 @@ export class ProtoElementInjector {
distanceToParent:number;
attributes:Map;
numberOfDirectives:number;
/** Whether the element is exported as $implicit. */
exportElement:boolean;
@@ -244,6 +248,7 @@ export class ProtoElementInjector {
this._binding8 = null; this._keyId8 = null;
this._binding9 = null; this._keyId9 = null;
this.numberOfDirectives = bindings.length;
var length = bindings.length;
if (length > 0) {this._binding0 = this._createBinding(bindings[0]); this._keyId0 = this._binding0.key.id;}
@@ -282,6 +287,20 @@ export class ProtoElementInjector {
return isPresent(this._binding0);
}
getDirectiveBindingAtIndex(index:int) {
if (index == 0) return this._binding0;
if (index == 1) return this._binding1;
if (index == 2) return this._binding2;
if (index == 3) return this._binding3;
if (index == 4) return this._binding4;
if (index == 5) return this._binding5;
if (index == 6) return this._binding6;
if (index == 7) return this._binding7;
if (index == 8) return this._binding8;
if (index == 9) return this._binding9;
throw new OutOfBoundsAccess(index);
}
hasEventEmitter(eventName: string) {
var p = this;
if (isPresent(p._binding0) && DirectiveBinding._hasEventEmitter(eventName, p._binding0)) return true;
@@ -648,18 +667,7 @@ export class ElementInjector extends TreeNode {
}
getDirectiveBindingAtIndex(index:int) {
var p = this._proto;
if (index == 0) return p._binding0;
if (index == 1) return p._binding1;
if (index == 2) return p._binding2;
if (index == 3) return p._binding3;
if (index == 4) return p._binding4;
if (index == 5) return p._binding5;
if (index == 6) return p._binding6;
if (index == 7) return p._binding7;
if (index == 8) return p._binding8;
if (index == 9) return p._binding9;
throw new OutOfBoundsAccess(index);
return this._proto.getDirectiveBindingAtIndex(index);
}
hasInstances() {
+53 -17
View File
@@ -236,6 +236,11 @@ export class View {
}
}
onAllChangesDone(directiveMemento) {
var dir = directiveMemento.directive(this.elementInjectors);
dir.onAllChangesDone();
}
_invokeMementos(records:List) {
for(var i = 0; i < records.length; ++i) {
this._invokeMementoFor(records[i]);
@@ -303,6 +308,9 @@ export class ProtoView {
parentProtoView:ProtoView;
_variableBindings:List;
_directiveMementosMap:Map;
_directiveMementos:List;
constructor(
template,
protoChangeDetector:ProtoChangeDetector,
@@ -324,7 +332,9 @@ export class ProtoView {
this.stylePromises = [];
this.eventHandlers = [];
this.bindingRecords = [];
this._directiveMementosMap = MapWrapper.create();
this._variableBindings = null;
this._directiveMementos = null;
}
// TODO(rado): hostElementInjector should be moved to hydrate phase.
@@ -357,6 +367,27 @@ export class ProtoView {
return this._variableBindings;
}
// this work should be done the constructor of ProtoView once we separate
// ProtoView and ProtoViewBuilder
_getDirectiveMementos() {
if (isPresent(this._directiveMementos)) {
return this._directiveMementos;
}
this._directiveMementos = [];
for (var injectorIndex = 0; injectorIndex < this.elementBinders.length; ++injectorIndex) {
var pei = this.elementBinders[injectorIndex].protoElementInjector;
if (isPresent(pei)) {
for (var directiveIndex = 0; directiveIndex < pei.numberOfDirectives; ++directiveIndex) {
ListWrapper.push(this._directiveMementos, this._getDirectiveMemento(injectorIndex, directiveIndex));
}
}
}
return this._directiveMementos;
}
_instantiate(hostElementInjector: ElementInjector, eventManager: EventManager): View {
var rootElementClone = this.instantiateInPlace ? this.element : DOM.importIntoDoc(this.element);
var elementsWithBindingsDynamic;
@@ -385,7 +416,9 @@ export class ProtoView {
}
var view = new View(this, viewNodes, this.protoLocals);
var changeDetector = this.protoChangeDetector.instantiate(view, this.bindingRecords, this._getVariableBindings());
var changeDetector = this.protoChangeDetector.instantiate(view, this.bindingRecords,
this._getVariableBindings(), this._getDirectiveMementos());
var binders = this.elementBinders;
var elementInjectors = ListWrapper.createFixedSize(binders.length);
var eventHandlers = ListWrapper.createFixedSize(binders.length);
@@ -610,15 +643,29 @@ export class ProtoView {
setterName:string,
setter:SetterFn) {
var elementIndex = this.elementBinders.length-1;
var bindingMemento = new DirectiveBindingMemento(
this.elementBinders.length-1,
elementIndex,
directiveIndex,
setterName,
setter
);
var directiveMemento = DirectiveMemento.get(bindingMemento);
var directiveMemento = this._getDirectiveMemento(elementIndex, directiveIndex);
ListWrapper.push(this.bindingRecords, new BindingRecord(expression, bindingMemento, directiveMemento));
}
_getDirectiveMemento(elementInjectorIndex:number, directiveIndex:number) {
var id = elementInjectorIndex * 100 + directiveIndex;
var protoElementInjector = this.elementBinders[elementInjectorIndex].protoElementInjector;
if (!MapWrapper.contains(this._directiveMementosMap, id)) {
var binding = protoElementInjector.getDirectiveBindingAtIndex(directiveIndex);
MapWrapper.set(this._directiveMementosMap, id,
new DirectiveMemento(elementInjectorIndex, directiveIndex, binding.callOnAllChangesDone));
}
return MapWrapper.get(this._directiveMementosMap, id);
}
// Create a rootView as if the compiler encountered <rootcmp></rootcmp>,
// and the component template is already compiled into protoView.
@@ -688,26 +735,15 @@ export class DirectiveBindingMemento {
}
}
var _directiveMementos = MapWrapper.create();
class DirectiveMemento {
_elementInjectorIndex:number;
_directiveIndex:number;
notifyOnAllChangesDone:boolean;
constructor(elementInjectorIndex:number, directiveIndex:number) {
constructor(elementInjectorIndex:number, directiveIndex:number, notifyOnAllChangesDone:boolean) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
}
static get(memento:DirectiveBindingMemento) {
var elementInjectorIndex = memento._elementInjectorIndex;
var directiveIndex = memento._directiveIndex;
var id = elementInjectorIndex * 100 + directiveIndex;
if (!MapWrapper.contains(_directiveMementos, id)) {
MapWrapper.set(_directiveMementos, id, new DirectiveMemento(elementInjectorIndex, directiveIndex));
}
return MapWrapper.get(_directiveMementos, id);
this.notifyOnAllChangesDone = notifyOnAllChangesDone;
}
directive(elementInjectors:List<ElementInjector>) {