From 723e8fde936023c5720763b68e4bc364e474a186 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Thu, 26 Mar 2015 14:36:25 -0700 Subject: [PATCH] feat(change_detection): added a directive lifecycle hook that is called after children are checked --- .../abstract_change_detector.js | 2 + .../change_detection_jit_generator.dart | 2 +- .../change_detection_jit_generator.es6 | 47 +++++++- .../dynamic_change_detector.js | 14 ++- .../change_detection/proto_change_detector.js | 14 +-- .../src/core/annotations/annotations.js | 20 ++++ .../src/core/compiler/element_injector.js | 34 +++--- modules/angular2/src/core/compiler/view.js | 70 +++++++++--- .../change_detection/change_detection_spec.js | 108 ++++++++++++++---- .../core/compiler/element_injector_spec.js | 23 ++-- .../test/core/compiler/view_container_spec.js | 2 +- .../angular2/test/core/compiler/view_spec.js | 25 +++- .../change_detection_benchmark.js | 4 +- 13 files changed, 283 insertions(+), 82 deletions(-) diff --git a/modules/angular2/src/change_detection/abstract_change_detector.js b/modules/angular2/src/change_detection/abstract_change_detector.js index e223259e61..746ad700d4 100644 --- a/modules/angular2/src/change_detection/abstract_change_detector.js +++ b/modules/angular2/src/change_detection/abstract_change_detector.js @@ -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; diff --git a/modules/angular2/src/change_detection/change_detection_jit_generator.dart b/modules/angular2/src/change_detection/change_detection_jit_generator.dart index 32905f20c9..0e9428d676 100644 --- a/modules/angular2/src/change_detection/change_detection_jit_generator.dart +++ b/modules/angular2/src/change_detection/change_detection_jit_generator.dart @@ -1,7 +1,7 @@ library change_detectoin.change_detection_jit_generator; class ChangeDetectorJITGenerator { - ChangeDetectorJITGenerator(typeName, records) { + ChangeDetectorJITGenerator(typeName, records, directiveMementos) { } generate() { diff --git a/modules/angular2/src/change_detection/change_detection_jit_generator.es6 b/modules/angular2/src/change_detection/change_detection_jit_generator.es6 index f4bbb79a85..64076cc5c5 100644 --- a/modules/angular2/src/change_detection/change_detection_jit_generator.es6 +++ b/modules/angular2/src/change_detection/change_detection_jit_generator.es6 @@ -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; + directiveMementos:List; localNames:List; changeNames:List; fieldNames:List; pipeNames:List; - constructor(typeName:string, records:List) { + constructor(typeName:string, records:List, 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); diff --git a/modules/angular2/src/change_detection/dynamic_change_detector.js b/modules/angular2/src/change_detection/dynamic_change_detector.js index a7ca7194b3..0adcf8c78b 100644 --- a/modules/angular2/src/change_detection/dynamic_change_detector.js +++ b/modules/angular2/src/change_detection/dynamic_change_detector.js @@ -34,8 +34,9 @@ export class DynamicChangeDetector extends AbstractChangeDetector { prevContexts:List; protos:List; + directiveMementos:List; - constructor(dispatcher:any, pipeRegistry:PipeRegistry, protoRecords:List) { + constructor(dispatcher:any, pipeRegistry:PipeRegistry, protoRecords:List, 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) { diff --git a/modules/angular2/src/change_detection/proto_change_detector.js b/modules/angular2/src/change_detection/proto_change_detector.js index cdefbd87ea..9dda224198 100644 --- a/modules/angular2/src/change_detection/proto_change_detector.js +++ b/modules/angular2/src/change_detection/proto_change_detector.js @@ -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(); } } } diff --git a/modules/angular2/src/core/annotations/annotations.js b/modules/angular2/src/core/annotations/annotations.js index b8f95c06ca..72614ae709 100644 --- a/modules/angular2/src/core/annotations/annotations.js +++ b/modules/angular2/src/core/annotations/annotations.js @@ -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"; diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index fa5408af0a..a8c65945a3 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -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() { diff --git a/modules/angular2/src/core/compiler/view.js b/modules/angular2/src/core/compiler/view.js index f0e7849ec4..c433ba59f5 100644 --- a/modules/angular2/src/core/compiler/view.js +++ b/modules/angular2/src/core/compiler/view.js @@ -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 , // 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) { diff --git a/modules/angular2/test/change_detection/change_detection_spec.js b/modules/angular2/test/change_detection/change_detection_spec.js index eb9648b068..4ea2b061cb 100644 --- a/modules/angular2/test/change_detection/change_detection_spec.js +++ b/modules/angular2/test/change_detection/change_detection_spec.js @@ -44,7 +44,8 @@ export function main() { var dispatcher = new TestDispatcher(); var variableBindings = convertLocalsToVariableBindings(locals); - var cd = pcd.instantiate(dispatcher, [new BindingRecord(ast(exp), memo, memo)], variableBindings); + var records = [new BindingRecord(ast(exp), memo, new FakeDirectiveMemento(memo, false))]; + var cd = pcd.instantiate(dispatcher, records, variableBindings, []); cd.hydrate(context, locals); return {"changeDetector" : cd, "dispatcher" : dispatcher}; @@ -56,6 +57,10 @@ export function main() { return res["dispatcher"].log; } + function instantiate(protoChangeDetector, dispatcher, bindings) { + return protoChangeDetector.instantiate(dispatcher, bindings, null, []); + } + describe(`${name} change detection`, () => { it('should do simple watching', () => { var person = new Person("misko"); @@ -193,7 +198,7 @@ export function main() { var ast = parser.parseInterpolation("B{{a}}A", "location"); var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, [new BindingRecord(ast, "memo", "memo")], null); + var cd = instantiate(pcd, dispatcher, [new BindingRecord(ast, "memo", null)]); cd.hydrate(new TestData("value"), null); cd.detectChanges(); @@ -239,15 +244,18 @@ export function main() { }); describe("group changes", () => { + var dirMemento1 = new FakeDirectiveMemento(1); + var dirMemento2 = new FakeDirectiveMemento(2); + it("should notify the dispatcher when a group of records changes", () => { var pcd = createProtoChangeDetector(); var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, [ - new BindingRecord(ast("1 + 2"), "memo", "1"), - new BindingRecord(ast("10 + 20"), "memo", "1"), - new BindingRecord(ast("100 + 200"), "memo", "2") - ], null); + var cd = instantiate(pcd, dispatcher, [ + new BindingRecord(ast("1 + 2"), "memo", dirMemento1), + new BindingRecord(ast("10 + 20"), "memo", dirMemento1), + new BindingRecord(ast("100 + 200"), "memo", dirMemento2) + ]); cd.detectChanges(); @@ -257,11 +265,11 @@ export function main() { it("should notify the dispatcher before switching to the next group", () => { var pcd = createProtoChangeDetector(); var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, [ - new BindingRecord(ast("a()"), "a", "1"), - new BindingRecord(ast("b()"), "b", "2"), - new BindingRecord(ast("c()"), "c", "2") - ], null); + var cd = instantiate(pcd, dispatcher, [ + new BindingRecord(ast("a()"), "a", dirMemento1), + new BindingRecord(ast("b()"), "b", dirMemento2), + new BindingRecord(ast("c()"), "c", dirMemento2) + ]); var tr = new TestRecord(); tr.a = () => { @@ -283,6 +291,46 @@ export function main() { expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]); }); }); + + describe("onAllChangesDone", () => { + it("should notify the dispatcher about processing all the children", () => { + var pcd = createProtoChangeDetector(); + var dispatcher = new TestDispatcher(); + + var memento1 = new FakeDirectiveMemento(1, false); + var memento2 = new FakeDirectiveMemento(2, true); + + var cd = pcd.instantiate(dispatcher, [ + new BindingRecord(ast("1"), "a", memento1), + new BindingRecord(ast("2"), "b", memento2) + ], null, [memento1, memento2]); + + cd.hydrate(null, null); + + cd.detectChanges(); + + expect(dispatcher.loggedOnAllChangesDone).toEqual([memento2]); + }); + + it("should notify in reverse order so the child is always notified before the parent", () => { + var pcd = createProtoChangeDetector(); + var dispatcher = new TestDispatcher(); + + var memento1 = new FakeDirectiveMemento(1, true); + var memento2 = new FakeDirectiveMemento(2, true); + + var cd = pcd.instantiate(dispatcher, [ + new BindingRecord(ast("1"), "a", memento1), + new BindingRecord(ast("2"), "b", memento2) + ], null, [memento1, memento2]); + + cd.hydrate(null, null); + + cd.detectChanges(); + + expect(dispatcher.loggedOnAllChangesDone).toEqual([memento2, memento1]); + }); + }); }); describe("enforce no new changes", () => { @@ -291,9 +339,9 @@ export function main() { pcd.addAst(ast("a"), "a", 1); var dispatcher = new TestDispatcher(); - var cd = pcd.instantiate(dispatcher, [ + var cd = instantiate(pcd, dispatcher, [ new BindingRecord(ast("a"), "a", 1) - ], null); + ]); cd.hydrate(new TestData('value'), null); expect(() => { @@ -308,7 +356,7 @@ export function main() { var pcd = createProtoChangeDetector(); var cd = pcd.instantiate(new TestDispatcher(), [ new BindingRecord(ast("invalidProp", "someComponent"), "a", 1) - ], null); + ], null, []); cd.hydrate(null, null); try { @@ -363,10 +411,10 @@ export function main() { beforeEach(() => { var protoParent = createProtoChangeDetector(); - parent = protoParent.instantiate(null, [], null); + parent = instantiate(protoParent, null, []); var protoChild = createProtoChangeDetector(); - child = protoChild.instantiate(null, [], null); + child = instantiate(protoChild, null, []); }); it("should add children", () => { @@ -409,7 +457,7 @@ export function main() { }); it("should change CHECK_ONCE to CHECKED", () => { - var cd = createProtoChangeDetector().instantiate(null, [], null); + var cd = instantiate(createProtoChangeDetector(), null, []); cd.mode = CHECK_ONCE; cd.detectChanges(); @@ -418,7 +466,7 @@ export function main() { }); it("should not change the CHECK_ALWAYS", () => { - var cd = createProtoChangeDetector().instantiate(null, [], null); + var cd = instantiate(createProtoChangeDetector(), null, []); cd.mode = CHECK_ALWAYS; cd.detectChanges(); @@ -429,7 +477,7 @@ export function main() { describe("markPathToRootAsCheckOnce", () => { function changeDetector(mode, parent) { - var cd = createProtoChangeDetector().instantiate(null, [], null); + var cd = instantiate(createProtoChangeDetector(), null, []); cd.mode = mode; if (isPresent(parent)) parent.addChild(cd); return cd; @@ -700,16 +748,28 @@ class TestData { } } +class FakeDirectiveMemento { + value:any; + notifyOnAllChangesDone:boolean; + + constructor(value, notifyOnAllChangesDone:boolean = false) { + this.value = value; + this.notifyOnAllChangesDone = notifyOnAllChangesDone; + } +} + class TestDispatcher extends ChangeDispatcher { log:List; loggedValues:List; changeRecords:List; + loggedOnAllChangesDone:List; onChange:Function; constructor() { super(); this.log = null; this.loggedValues = null; + this.loggedOnAllChangesDone = null; this.onChange = (_, __) => {}; this.clear(); } @@ -717,6 +777,7 @@ class TestDispatcher extends ChangeDispatcher { clear() { this.log = ListWrapper.create(); this.loggedValues = ListWrapper.create(); + this.loggedOnAllChangesDone = ListWrapper.create(); this.changeRecords = ListWrapper.create(); } @@ -724,7 +785,7 @@ class TestDispatcher extends ChangeDispatcher { ListWrapper.push(this.loggedValues, value); } - onRecordChange(group, changeRecords:List) { + onRecordChange(directiveMemento, changeRecords:List) { var value = changeRecords[0].change.currentValue; var memento = changeRecords[0].bindingMemento; ListWrapper.push(this.log, memento + '=' + this._asString(value)); @@ -734,9 +795,12 @@ class TestDispatcher extends ChangeDispatcher { ListWrapper.push(this.changeRecords, changeRecords); - this.onChange(group, changeRecords); + this.onChange(directiveMemento, changeRecords); } + onAllChangesDone(directiveMemento) { + ListWrapper.push(this.loggedOnAllChangesDone, directiveMemento); + } _asString(value) { return (isBlank(value) ? 'null' : value.toString()); diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index f28e171b4f..62317fca62 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -222,6 +222,18 @@ export function main() { expect(protoChild.directParent()).toEqual(null); }); + + it("should allow for direct access using getDirectiveBindingAtIndex", function () { + var binding = DirectiveBinding.createFromBinding( + bind(SimpleDirective).toClass(SimpleDirective), null); + var proto = new ProtoElementInjector(null, 0, [binding]); + + expect(proto.getDirectiveBindingAtIndex(0)).toBeAnInstanceOf(DirectiveBinding); + expect(() => proto.getDirectiveBindingAtIndex(-1)).toThrowError( + 'Index -1 is out-of-bounds.'); + expect(() => proto.getDirectiveBindingAtIndex(10)).toThrowError( + 'Index 10 is out-of-bounds.'); + }); }); }); @@ -419,17 +431,6 @@ export function main() { 'Index 10 is out-of-bounds.'); }); - it("should allow for direct access using getBindingAtIndex", function () { - var inj = injector([ - DirectiveBinding.createFromBinding(bind(SimpleDirective).toClass(SimpleDirective), null) - ]); - expect(inj.getDirectiveBindingAtIndex(0)).toBeAnInstanceOf(DirectiveBinding); - expect(() => inj.getDirectiveBindingAtIndex(-1)).toThrowError( - 'Index -1 is out-of-bounds.'); - expect(() => inj.getDirectiveBindingAtIndex(10)).toThrowError( - 'Index 10 is out-of-bounds.'); - }); - it("should handle cyclic dependencies", function () { expect(() => { var bAneedsB = bind(A_Needs_B).toFactory((a) => new A_Needs_B(a), [B_Needs_A]); diff --git a/modules/angular2/test/core/compiler/view_container_spec.js b/modules/angular2/test/core/compiler/view_container_spec.js index 8b8c184bec..a1b2b2cda5 100644 --- a/modules/angular2/test/core/compiler/view_container_spec.js +++ b/modules/angular2/test/core/compiler/view_container_spec.js @@ -11,7 +11,7 @@ import {DynamicProtoChangeDetector, ChangeDetector, Lexer, Parser} from 'angular function createView(nodes) { var view = new View(null, nodes, MapWrapper.create()); - var cd = new DynamicProtoChangeDetector(null).instantiate(view, [], null); + var cd = new DynamicProtoChangeDetector(null).instantiate(view, [], null, []); view.init(cd, [], [], [], [], [], [], [], [], []); return view; } diff --git a/modules/angular2/test/core/compiler/view_spec.js b/modules/angular2/test/core/compiler/view_spec.js index dc5d86e357..7d6b4e6cac 100644 --- a/modules/angular2/test/core/compiler/view_spec.js +++ b/modules/angular2/test/core/compiler/view_spec.js @@ -3,7 +3,7 @@ import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'angul import {ProtoElementInjector, ElementInjector, DirectiveBinding} from 'angular2/src/core/compiler/element_injector'; import {EmulatedScopedShadowDomStrategy, NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_strategy'; import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader'; -import {Component, Decorator, Viewport, Directive, onChange} from 'angular2/src/core/annotations/annotations'; +import {Component, Decorator, Viewport, Directive, onChange, onAllChangesDone} from 'angular2/src/core/annotations/annotations'; import {Lexer, Parser, DynamicProtoChangeDetector, ChangeDetector} from 'angular2/change_detection'; import {EventEmitter} from 'angular2/src/core/annotations/di'; @@ -627,6 +627,21 @@ export function main() { cd.detectChanges(); expect(directive.changes).toEqual({"a" : new PropertyUpdate(100, 0)}); }); + + it('should invoke the onAllChangesDone callback', () => { + var pv = new ProtoView(el('
'), + new DynamicProtoChangeDetector(null), null); + + pv.bindElement(null, 0, new ProtoElementInjector(null, 0, [ + DirectiveBinding.createFromType(DirectiveImplementingOnAllChangesDone, new Directive({lifecycle: [onAllChangesDone]})) + ])); + + createViewAndChangeDetector(pv); + cd.detectChanges(); + + var directive = view.elementInjectors[0].get(DirectiveImplementingOnAllChangesDone); + expect(directive.onAllChangesDoneCalled).toBe(true); + }); }); }); @@ -678,6 +693,14 @@ class DirectiveImplementingOnChange { } } +class DirectiveImplementingOnAllChangesDone { + onAllChangesDoneCalled; + + onAllChangesDone() { + this.onAllChangesDoneCalled = true; + } +} + class SomeService {} @Component({services: [SomeService]}) diff --git a/modules/benchmarks/src/change_detection/change_detection_benchmark.js b/modules/benchmarks/src/change_detection/change_detection_benchmark.js index 1b2baabe93..19e285aa48 100644 --- a/modules/benchmarks/src/change_detection/change_detection_benchmark.js +++ b/modules/benchmarks/src/change_detection/change_detection_benchmark.js @@ -105,7 +105,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations) { var parser = new Parser(new Lexer()); var parentProto = changeDetection.createProtoChangeDetector('parent'); - var parentCd = parentProto.instantiate(dispatcher, [], []); + var parentCd = parentProto.instantiate(dispatcher, [], [], []); var proto = changeDetection.createProtoChangeDetector("proto"); var bindingRecords = [ @@ -126,7 +126,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations) { for (var j = 0; j < 10; ++j) { obj.setField(j, i); } - var cd = proto.instantiate(dispatcher, bindingRecords, []); + var cd = proto.instantiate(dispatcher, bindingRecords, [], []); cd.hydrate(obj, null); parentCd.addChild(cd); }