feat(change_detection): updated handling ON_PUSH detectors so they get notified when their bindings change

This commit is contained in:
vsavkin 2015-04-14 08:54:09 -07:00
parent dc9c614da2
commit 68faddbf5c
13 changed files with 249 additions and 60 deletions

View File

@ -9,13 +9,13 @@ export class AbstractChangeDetector extends ChangeDetector {
shadowDomChildren:List; shadowDomChildren:List;
parent:ChangeDetector; parent:ChangeDetector;
mode:string; mode:string;
changeDetectorRef:ChangeDetectorRef; ref:ChangeDetectorRef;
constructor() { constructor() {
super(); super();
this.lightDomChildren = []; this.lightDomChildren = [];
this.shadowDomChildren = []; this.shadowDomChildren = [];
this.changeDetectorRef = new ChangeDetectorRef(this); this.ref = new ChangeDetectorRef(this);
this.mode = null; this.mode = null;
} }
@ -80,6 +80,10 @@ export class AbstractChangeDetector extends ChangeDetector {
} }
} }
markAsCheckOnce() {
this.mode = CHECK_ONCE;
}
markPathToRootAsCheckOnce() { markPathToRootAsCheckOnce() {
var c = this; var c = this;
while(isPresent(c) && c.mode != DETACHED) { while(isPresent(c) && c.mode != DETACHED) {

View File

@ -32,6 +32,10 @@ export class BindingRecord {
return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange; return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange;
} }
isOnPushChangeDetection() {
return isPresent(this.directiveRecord) && this.directiveRecord.isOnPushChangeDetection();
}
isDirective() { isDirective() {
return this.mode === DIRECTIVE; return this.mode === DIRECTIVE;
} }

View File

@ -36,7 +36,7 @@ var PIPE_REGISTRY_ACCESSOR = "this.pipeRegistry";
var PROTOS_ACCESSOR = "this.protos"; var PROTOS_ACCESSOR = "this.protos";
var DIRECTIVES_ACCESSOR = "this.directiveRecords"; var DIRECTIVES_ACCESSOR = "this.directiveRecords";
var CONTEXT_ACCESSOR = "this.context"; var CONTEXT_ACCESSOR = "this.context";
var CHANGE_LOCAL = "change"; var IS_CHANGED_LOCAL = "isChanged";
var CHANGES_LOCAL = "changes"; var CHANGES_LOCAL = "changes";
var LOCALS_ACCESSOR = "this.locals"; var LOCALS_ACCESSOR = "this.locals";
var MODE_ACCESSOR = "this.mode"; var MODE_ACCESSOR = "this.mode";
@ -78,10 +78,15 @@ function pipeOnDestroyTemplate(pipeNames:List) {
} }
function hydrateTemplate(type:string, mode:string, fieldDefinitions:string, pipeOnDestroy:string, function hydrateTemplate(type:string, mode:string, fieldDefinitions:string, pipeOnDestroy:string,
directiveFieldNames:List<String>):string { directiveFieldNames:List<String>, detectorFieldNames:List<String>):string {
var directiveInit = ""; var directiveInit = "";
for(var i = 0; i < directiveFieldNames.length; ++i) { for(var i = 0; i < directiveFieldNames.length; ++i) {
directiveInit += `${directiveFieldNames[i]} = directives.directive(this.directiveRecords[${i}]);\n`; directiveInit += `${directiveFieldNames[i]} = directives.getDirectiveFor(this.directiveRecords[${i}]);\n`;
}
var detectorInit = "";
for(var i = 0; i < detectorFieldNames.length; ++i) {
detectorInit += `${detectorFieldNames[i]} = directives.getDetectorFor(this.directiveRecords[${i}]);\n`;
} }
return ` return `
@ -90,6 +95,7 @@ ${type}.prototype.hydrate = function(context, locals, directives) {
${CONTEXT_ACCESSOR} = context; ${CONTEXT_ACCESSOR} = context;
${LOCALS_ACCESSOR} = locals; ${LOCALS_ACCESSOR} = locals;
${directiveInit} ${directiveInit}
${detectorInit}
} }
${type}.prototype.dehydrate = function() { ${type}.prototype.dehydrate = function() {
${pipeOnDestroy} ${pipeOnDestroy}
@ -128,7 +134,7 @@ function detectChangesBodyTemplate(localDefinitions:string, changeDefinitions:st
${localDefinitions} ${localDefinitions}
${changeDefinitions} ${changeDefinitions}
var ${TEMP_LOCAL}; var ${TEMP_LOCAL};
var ${CHANGE_LOCAL}; var ${IS_CHANGED_LOCAL} = false;
var ${CURRENT_PROTO}; var ${CURRENT_PROTO};
var ${CHANGES_LOCAL} = null; var ${CHANGES_LOCAL} = null;
@ -208,6 +214,7 @@ function updateDirectiveTemplate(oldValue:string, newValue:string, directiveProp
return ` return `
if(throwOnChange) ${UTIL}.throwOnChange(${CURRENT_PROTO}, ${UTIL}.simpleChange(${oldValue}, ${newValue})); if(throwOnChange) ${UTIL}.throwOnChange(${CURRENT_PROTO}, ${UTIL}.simpleChange(${oldValue}, ${newValue}));
${directiveProperty} = ${newValue}; ${directiveProperty} = ${newValue};
${IS_CHANGED_LOCAL} = true;
`; `;
} }
@ -227,6 +234,22 @@ if(${CHANGES_LOCAL}) {
`; `;
} }
function notifyOnPushDetectorsTemplate(detector:string):string{
return `
if(${IS_CHANGED_LOCAL}) {
${detector}.markAsCheckOnce();
}
`;
}
function lastInDirectiveTemplate(notifyOnChanges:string, notifyOnPush:string):string{
return `
${notifyOnChanges}
${notifyOnPush}
${IS_CHANGED_LOCAL} = false;
`;
}
export class ChangeDetectorJITGenerator { export class ChangeDetectorJITGenerator {
typeName:string; typeName:string;
@ -285,22 +308,32 @@ export class ChangeDetectorJITGenerator {
genHydrate():string { genHydrate():string {
var mode = ChangeDetectionUtil.changeDetectionMode(this.changeDetectionStrategy); var mode = ChangeDetectionUtil.changeDetectionMode(this.changeDetectionStrategy);
return hydrateTemplate(this.typeName, mode, this.genFieldDefinitions(), return hydrateTemplate(this.typeName, mode, this.genFieldDefinitions(),
pipeOnDestroyTemplate(this.getNonNullPipeNames()), this.getDirectiveFieldNames()); pipeOnDestroyTemplate(this.getNonNullPipeNames()),
this.getDirectiveFieldNames(), this.getDetectorFieldNames());
} }
getDirectiveFieldNames():List<string> { getDirectiveFieldNames():List<string> {
return this.directiveRecords.map((d) => this.getDirective(d)); return this.directiveRecords.map((d) => this.getDirective(d));
} }
getDetectorFieldNames():List<string> {
return this.directiveRecords.filter(r => r.isOnPushChangeDetection()).map((d) => this.getDetector(d));
}
getDirective(d:DirectiveRecord) { getDirective(d:DirectiveRecord) {
return `this.directive_${d.name}`; return `this.directive_${d.name}`;
} }
getDetector(d:DirectiveRecord) {
return `this.detector_${d.name}`;
}
genFieldDefinitions() { genFieldDefinitions() {
var fields = []; var fields = [];
fields = fields.concat(this.fieldNames); fields = fields.concat(this.fieldNames);
fields = fields.concat(this.getNonNullPipeNames()); fields = fields.concat(this.getNonNullPipeNames());
fields = fields.concat(this.getDirectiveFieldNames()); fields = fields.concat(this.getDirectiveFieldNames());
fields = fields.concat(this.getDetectorFieldNames());
return fieldDefinitionsTemplate(fields); return fieldDefinitionsTemplate(fields);
} }
@ -362,11 +395,11 @@ export class ChangeDetectorJITGenerator {
var change = this.changeNames[r.selfIndex]; var change = this.changeNames[r.selfIndex];
var pipe = this.pipeNames[r.selfIndex]; var pipe = this.pipeNames[r.selfIndex];
var cdRef = r.mode === RECORD_TYPE_BINDING_PIPE ? "this.changeDetectorRef" : "null"; var cdRef = r.mode === RECORD_TYPE_BINDING_PIPE ? "this.ref" : "null";
var update = this.genUpdateDirectiveOrElement(r); var update = this.genUpdateDirectiveOrElement(r);
var addToChanges = this.genAddToChanges(r); var addToChanges = this.genAddToChanges(r);
var lastInDirective = this.genNotifyOnChanges(r); var lastInDirective = this.genLastInDirective(r);
return pipeCheckTemplate(r.selfIndex - 1, context, cdRef, pipe, r.name, oldValue, newValue, change, return pipeCheckTemplate(r.selfIndex - 1, context, cdRef, pipe, r.name, oldValue, newValue, change,
update, addToChanges, lastInDirective); update, addToChanges, lastInDirective);
@ -380,7 +413,7 @@ export class ChangeDetectorJITGenerator {
var update = this.genUpdateDirectiveOrElement(r); var update = this.genUpdateDirectiveOrElement(r);
var addToChanges = this.genAddToChanges(r); var addToChanges = this.genAddToChanges(r);
var lastInDirective = this.genNotifyOnChanges(r); var lastInDirective = this.genLastInDirective(r);
var check = referenceCheckTemplate(r.selfIndex - 1, assignment, oldValue, newValue, change, var check = referenceCheckTemplate(r.selfIndex - 1, assignment, oldValue, newValue, change,
update, addToChanges, lastInDirective); update, addToChanges, lastInDirective);
@ -471,6 +504,12 @@ export class ChangeDetectorJITGenerator {
return r.bindingRecord.callOnChange() ? addToChangesTemplate(oldValue, newValue) : ""; return r.bindingRecord.callOnChange() ? addToChangesTemplate(oldValue, newValue) : "";
} }
genLastInDirective(r:ProtoRecord):string{
var onChanges = this.genNotifyOnChanges(r);
var onPush = this.genNotifyOnPushDetectors(r);
return lastInDirectiveTemplate(onChanges, onPush);
}
genNotifyOnChanges(r:ProtoRecord):string{ genNotifyOnChanges(r:ProtoRecord):string{
var br = r.bindingRecord; var br = r.bindingRecord;
if (r.lastInDirective && br.callOnChange()) { if (r.lastInDirective && br.callOnChange()) {
@ -480,6 +519,15 @@ export class ChangeDetectorJITGenerator {
} }
} }
genNotifyOnPushDetectors(r:ProtoRecord):string{
var br = r.bindingRecord;
if (r.lastInDirective && br.isOnPushChangeDetection()) {
return notifyOnPushDetectorsTemplate(this.getDetector(br.directiveRecord));
} else {
return "";
}
}
genArgs(r:ProtoRecord):string { genArgs(r:ProtoRecord):string {
return r.args.map((arg) => this.localNames[arg]).join(", "); return r.args.map((arg) => this.localNames[arg]).join(", ");
} }

View File

@ -1,16 +1,24 @@
import {ON_PUSH} from './constants';
import {StringWrapper} from 'angular2/src/facade/lang';
export class DirectiveRecord { export class DirectiveRecord {
elementIndex:number; elementIndex:number;
directiveIndex:number; directiveIndex:number;
callOnAllChangesDone:boolean; callOnAllChangesDone:boolean;
callOnChange:boolean; callOnChange:boolean;
changeDetection:string;
constructor(elementIndex:number, directiveIndex:number, constructor(elementIndex:number, directiveIndex:number,
callOnAllChangesDone:boolean, callOnAllChangesDone:boolean, callOnChange:boolean, changeDetection:string) {
callOnChange:boolean) {
this.elementIndex = elementIndex; this.elementIndex = elementIndex;
this.directiveIndex = directiveIndex; this.directiveIndex = directiveIndex;
this.callOnAllChangesDone = callOnAllChangesDone; this.callOnAllChangesDone = callOnAllChangesDone;
this.callOnChange = callOnChange; this.callOnChange = callOnChange;
this.changeDetection = changeDetection;
}
isOnPushChangeDetection():boolean {
return StringWrapper.equals(this.changeDetection, ON_PUSH);
} }
get name() { get name() {

View File

@ -95,19 +95,31 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var protos:List<ProtoRecord> = this.protos; var protos:List<ProtoRecord> = this.protos;
var changes = null; var changes = null;
var isChanged = false;
for (var i = 0; i < protos.length; ++i) { for (var i = 0; i < protos.length; ++i) {
var proto:ProtoRecord = protos[i]; var proto:ProtoRecord = protos[i];
var bindingRecord = proto.bindingRecord;
var directiveRecord = bindingRecord.directiveRecord;
var change = this._check(proto); var change = this._check(proto);
if (isPresent(change)) { if (isPresent(change)) {
if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, change); if (throwOnChange) ChangeDetectionUtil.throwOnChange(proto, change);
this._updateDirectiveOrElement(change, proto.bindingRecord); this._updateDirectiveOrElement(change, bindingRecord);
changes = this._addChange(proto.bindingRecord, change, changes); isChanged = true;
changes = this._addChange(bindingRecord, change, changes);
} }
if (proto.lastInDirective && isPresent(changes)) { if (proto.lastInDirective) {
this._directive(proto.bindingRecord.directiveRecord).onChange(changes); if (isPresent(changes)) {
changes = null; this._getDirectiveFor(directiveRecord).onChange(changes);
changes = null;
}
if (isChanged && bindingRecord.isOnPushChangeDetection()) {
this._getDetectorFor(directiveRecord).markAsCheckOnce();
}
isChanged = false;
} }
} }
} }
@ -117,7 +129,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
for (var i = dirs.length - 1; i >= 0; --i) { for (var i = dirs.length - 1; i >= 0; --i) {
var dir = dirs[i]; var dir = dirs[i];
if (dir.callOnAllChangesDone) { if (dir.callOnAllChangesDone) {
this._directive(dir).onAllChangesDone(); this._getDirectiveFor(dir).onAllChangesDone();
} }
} }
} }
@ -126,7 +138,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
if (isBlank(bindingRecord.directiveRecord)) { if (isBlank(bindingRecord.directiveRecord)) {
this.dispatcher.notifyOnBinding(bindingRecord, change.currentValue); this.dispatcher.notifyOnBinding(bindingRecord, change.currentValue);
} else { } else {
bindingRecord.setter(this._directive(bindingRecord.directiveRecord), change.currentValue); bindingRecord.setter(this._getDirectiveFor(bindingRecord.directiveRecord), change.currentValue);
} }
} }
@ -138,8 +150,12 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
} }
} }
_directive(directive:DirectiveRecord) { _getDirectiveFor(directive:DirectiveRecord) {
return this.directives.directive(directive); return this.directives.getDirectiveFor(directive);
}
_getDetectorFor(directive:DirectiveRecord) {
return this.directives.getDetectorFor(directive);
} }
_check(proto:ProtoRecord) { _check(proto:ProtoRecord) {
@ -249,7 +265,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
// //
// In the future, pipes declared in the bind configuration should // In the future, pipes declared in the bind configuration should
// be able to access the changeDetectorRef of that component. // be able to access the changeDetectorRef of that component.
var cdr = proto.mode === RECORD_TYPE_BINDING_PIPE ? this.changeDetectorRef : null; var cdr = proto.mode === RECORD_TYPE_BINDING_PIPE ? this.ref : null;
var pipe = this.pipeRegistry.get(proto.name, context, cdr); var pipe = this.pipeRegistry.get(proto.name, context, cdr);
this._writePipe(proto, pipe); this._writePipe(proto, pipe);
return pipe; return pipe;

View File

@ -1,6 +1,7 @@
import {ABSTRACT, CONST, normalizeBlank, isPresent} from 'angular2/src/facade/lang'; import {ABSTRACT, CONST, normalizeBlank, isPresent} from 'angular2/src/facade/lang';
import {ListWrapper, List} from 'angular2/src/facade/collection'; import {ListWrapper, List} from 'angular2/src/facade/collection';
import {Injectable} from 'angular2/di'; import {Injectable} from 'angular2/di';
import {DEFAULT} from 'angular2/change_detection';
// type StringMap = {[idx: string]: string}; // type StringMap = {[idx: string]: string};
@ -553,7 +554,7 @@ export class Component extends Directive {
hostListeners, hostListeners,
injectables, injectables,
lifecycle, lifecycle,
changeDetection changeDetection = DEFAULT
}:{ }:{
selector:string, selector:string,
properties:Object, properties:Object,

View File

@ -8,7 +8,7 @@ import * as viewModule from 'angular2/src/core/compiler/view';
import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {NgElement} from 'angular2/src/core/compiler/ng_element';
import {Directive, Component, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations'; import {Directive, Component, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations';
import {ChangeDetectorRef} from 'angular2/change_detection'; import {ChangeDetector, ChangeDetectorRef} from 'angular2/change_detection';
import {QueryList} from './query_list'; import {QueryList} from './query_list';
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10; var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
@ -277,6 +277,15 @@ export class DirectiveBinding extends ResolvedBinding {
} }
} }
get changeDetection() {
if (this.annotation instanceof Component) {
var c:Component = this.annotation;
return c.changeDetection;
} else {
return null;
}
}
static createFromBinding(b:Binding, annotation:Directive):DirectiveBinding { static createFromBinding(b:Binding, annotation:Directive):DirectiveBinding {
var rb = b.resolve(); var rb = b.resolve();
var deps = ListWrapper.map(rb.dependencies, DirectiveDependency.createFrom); var deps = ListWrapper.map(rb.dependencies, DirectiveDependency.createFrom);
@ -298,13 +307,13 @@ export class PreBuiltObjects {
view:viewModule.AppView; view:viewModule.AppView;
element:NgElement; element:NgElement;
viewContainer:ViewContainer; viewContainer:ViewContainer;
changeDetectorRef:ChangeDetectorRef; changeDetector:ChangeDetector;
constructor(view, element:NgElement, viewContainer:ViewContainer, constructor(view, element:NgElement, viewContainer:ViewContainer,
changeDetectorRef:ChangeDetectorRef) { changeDetector:ChangeDetector) {
this.view = view; this.view = view;
this.element = element; this.element = element;
this.viewContainer = viewContainer; this.viewContainer = viewContainer;
this.changeDetectorRef = changeDetectorRef; this.changeDetector = changeDetector;
} }
} }
@ -603,6 +612,10 @@ export class ElementInjector extends TreeNode {
return this._preBuiltObjects.element; return this._preBuiltObjects.element;
} }
getChangeDetector() {
return this._preBuiltObjects.changeDetector;
}
getComponent() { getComponent() {
if (this._proto._binding0IsComponent) { if (this._proto._binding0IsComponent) {
return this._obj0; return this._obj0;
@ -885,7 +898,7 @@ export class ElementInjector extends TreeNode {
if (keyId === staticKeys.viewId) return this._preBuiltObjects.view; if (keyId === staticKeys.viewId) return this._preBuiltObjects.view;
if (keyId === staticKeys.ngElementId) return this._preBuiltObjects.element; if (keyId === staticKeys.ngElementId) return this._preBuiltObjects.element;
if (keyId === staticKeys.viewContainerId) return this._preBuiltObjects.viewContainer; if (keyId === staticKeys.viewContainerId) return this._preBuiltObjects.viewContainer;
if (keyId === staticKeys.changeDetectorRefId) return this._preBuiltObjects.changeDetectorRef; if (keyId === staticKeys.changeDetectorRefId) return this._preBuiltObjects.changeDetector.ref;
//TODO add other objects as needed //TODO add other objects as needed
return _undefined; return _undefined;

View File

@ -256,11 +256,16 @@ export class AppView {
} }
} }
directive(directive:DirectiveRecord) { getDirectiveFor(directive:DirectiveRecord) {
var elementInjector:ElementInjector = this.elementInjectors[directive.elementIndex]; var elementInjector = this.elementInjectors[directive.elementIndex];
return elementInjector.getDirectiveAtIndex(directive.directiveIndex); return elementInjector.getDirectiveAtIndex(directive.directiveIndex);
} }
getDetectorFor(directive:DirectiveRecord) {
var elementInjector = this.elementInjectors[directive.elementIndex];
return elementInjector.getChangeDetector();
}
setDynamicComponentChildView(boundElementIndex, view:AppView) { setDynamicComponentChildView(boundElementIndex, view:AppView) {
if (!this.proto.elementBinders[boundElementIndex].hasDynamicComponent()) { if (!this.proto.elementBinders[boundElementIndex].hasDynamicComponent()) {
throw new BaseException(`There is no dynamic component directive at element ${boundElementIndex}`); throw new BaseException(`There is no dynamic component directive at element ${boundElementIndex}`);
@ -458,9 +463,11 @@ export class AppProtoView {
if (!MapWrapper.contains(this._directiveRecordsMap, id)) { if (!MapWrapper.contains(this._directiveRecordsMap, id)) {
var binding = protoElementInjector.getDirectiveBindingAtIndex(directiveIndex); var binding = protoElementInjector.getDirectiveBindingAtIndex(directiveIndex);
var changeDetection = binding.changeDetection;
MapWrapper.set(this._directiveRecordsMap, id, MapWrapper.set(this._directiveRecordsMap, id,
new DirectiveRecord(elementInjectorIndex, directiveIndex, new DirectiveRecord(elementInjectorIndex, directiveIndex,
binding.callOnAllChangesDone, binding.callOnChange)); binding.callOnAllChangesDone, binding.callOnChange, changeDetection));
} }
return MapWrapper.get(this._directiveRecordsMap, id); return MapWrapper.get(this._directiveRecordsMap, id);

View File

@ -5,7 +5,6 @@ import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {NgElement} from 'angular2/src/core/compiler/ng_element';
import * as vcModule from './view_container'; import * as vcModule from './view_container';
import * as viewModule from './view'; import * as viewModule from './view';
import {ChangeDetectorRef} from 'angular2/change_detection';
// TODO(tbosch): Make this an OpaqueToken as soon as our transpiler supports this! // TODO(tbosch): Make this an OpaqueToken as soon as our transpiler supports this!
export const VIEW_POOL_CAPACITY = 'ViewFactory.viewPoolCapacity'; export const VIEW_POOL_CAPACITY = 'ViewFactory.viewPoolCapacity';
@ -77,12 +76,11 @@ export class ViewFactory {
elementInjectors[binderIdx] = elementInjector; elementInjectors[binderIdx] = elementInjector;
// componentChildViews // componentChildViews
var changeDetectorRef = null; var childChangeDetector = null;
if (binder.hasStaticComponent()) { if (binder.hasStaticComponent()) {
var childView = this._createView(binder.nestedProtoView); var childView = this._createView(binder.nestedProtoView);
changeDetector.addShadowDomChild(childView.changeDetector); childChangeDetector = childView.changeDetector;
changeDetector.addShadowDomChild(childChangeDetector);
changeDetectorRef = new ChangeDetectorRef(childView.changeDetector);
componentChildViews[binderIdx] = childView; componentChildViews[binderIdx] = childView;
} }
@ -97,7 +95,7 @@ export class ViewFactory {
// preBuiltObjects // preBuiltObjects
if (isPresent(elementInjector)) { if (isPresent(elementInjector)) {
preBuiltObjects[binderIdx] = new eli.PreBuiltObjects(view, new NgElement(view, binderIdx), viewContainer, preBuiltObjects[binderIdx] = new eli.PreBuiltObjects(view, new NgElement(view, binderIdx), viewContainer,
changeDetectorRef); childChangeDetector);
} }
} }

View File

@ -28,7 +28,7 @@ export function main() {
} }
function dirs(directives:List) { function dirs(directives:List) {
return new FakeDirectives(directives); return new FakeDirectives(directives, []);
} }
function convertLocalsToVariableBindings(locals) { function convertLocalsToVariableBindings(locals) {
@ -246,9 +246,9 @@ export function main() {
}); });
describe("updating directives", () => { describe("updating directives", () => {
var dirRecord1 = new DirectiveRecord(0, 0, true, true); var dirRecord1 = new DirectiveRecord(0, 0, true, true, DEFAULT);
var dirRecord2 = new DirectiveRecord(0, 1, true, true); var dirRecord2 = new DirectiveRecord(0, 1, true, true, DEFAULT);
var dirRecordNoCallbacks = new DirectiveRecord(0, 0, false, false); var dirRecordNoCallbacks = new DirectiveRecord(0, 0, false, false, DEFAULT);
function updateA(exp:string, dirRecord) { function updateA(exp:string, dirRecord) {
return BindingRecord.createForDirective(ast(exp), "a", (o,v) => o.a = v, dirRecord); return BindingRecord.createForDirective(ast(exp), "a", (o,v) => o.a = v, dirRecord);
@ -553,6 +553,44 @@ export function main() {
expect(cd.mode).toEqual(CHECK_ALWAYS); expect(cd.mode).toEqual(CHECK_ALWAYS);
}); });
describe("marking ON_PUSH detectors as CHECK_ONCE after an update", () => {
var checkedDetector;
var dirRecordWithOnPush;
var updateDirWithOnPushRecord;
var directives;
beforeEach(() => {
var proto = createProtoChangeDetector(null, ON_PUSH);
checkedDetector = instantiate(proto, null, [], []);
checkedDetector.hydrate(null, null, null);
checkedDetector.mode = CHECKED;
// this directive is a component with ON_PUSH change detection
dirRecordWithOnPush = new DirectiveRecord(0, 0, false, false, ON_PUSH);
// a record updating a component
updateDirWithOnPushRecord =
BindingRecord.createForDirective(ast("42"), "a", (o,v) => o.a = v, dirRecordWithOnPush);
var targetDirective = new TestData(null);
directives = new FakeDirectives([targetDirective], [checkedDetector]);
});
it("should set the mode to CHECK_ONCE when a binding is updated", () => {
var proto = createProtoChangeDetector(null);
var cd = instantiate(proto, null, [updateDirWithOnPushRecord], [dirRecordWithOnPush]);
cd.hydrate(null, null, directives);
expect(checkedDetector.mode).toEqual(CHECKED);
// evaluate the record, update the targetDirective, and mark its detector as CHECK_ONCE
cd.detectChanges();
expect(checkedDetector.mode).toEqual(CHECK_ONCE);
});
});
}); });
describe("markPathToRootAsCheckOnce", () => { describe("markPathToRootAsCheckOnce", () => {
@ -664,7 +702,7 @@ export function main() {
expect(pipe.destroyCalled).toEqual(true); expect(pipe.destroyCalled).toEqual(true);
}); });
it("should inject the binding propagation configuration " + it("should inject the ChangeDetectorRef " +
"of the encompassing component into a pipe", () => { "of the encompassing component into a pipe", () => {
var registry = new FakePipeRegistry('pipe', () => new IdentityPipe()); var registry = new FakePipeRegistry('pipe', () => new IdentityPipe());
@ -673,7 +711,7 @@ export function main() {
cd.detectChanges(); cd.detectChanges();
expect(registry.cdRef).toBe(cd.changeDetectorRef); expect(registry.cdRef).toBe(cd.ref);
}); });
}); });
@ -853,14 +891,20 @@ class TestData {
class FakeDirectives { class FakeDirectives {
directives:List; directives:List;
detectors:List;
constructor(directives:List) { constructor(directives:List, detectors:List) {
this.directives = directives; this.directives = directives;
this.detectors = detectors;
} }
directive(directiveRecord:DirectiveRecord) { getDirectiveFor(directiveRecord:DirectiveRecord) {
return this.directives[directiveRecord.directiveIndex]; return this.directives[directiveRecord.directiveIndex];
} }
getDetectorFor(directiveRecord:DirectiveRecord) {
return this.detectors[directiveRecord.directiveIndex];
}
} }
class TestDispatcher extends ChangeDispatcher { class TestDispatcher extends ChangeDispatcher {

View File

@ -11,7 +11,7 @@ import {AppProtoView, AppView} from 'angular2/src/core/compiler/view';
import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {NgElement} from 'angular2/src/core/compiler/ng_element';
import {Directive} from 'angular2/src/core/annotations/annotations'; import {Directive} from 'angular2/src/core/annotations/annotations';
import {ChangeDetectorRef, Parser, Lexer} from 'angular2/change_detection'; import {DynamicChangeDetector, ChangeDetectorRef, Parser, Lexer} from 'angular2/change_detection';
import {ViewRef, Renderer, EventBinding} from 'angular2/src/render/api'; import {ViewRef, Renderer, EventBinding} from 'angular2/src/render/api';
import {QueryList} from 'angular2/src/core/compiler/query_list'; import {QueryList} from 'angular2/src/core/compiler/query_list';
@ -621,10 +621,10 @@ export function main() {
}); });
it('should return changeDetectorRef', function () { it('should return changeDetectorRef', function () {
var config = new ChangeDetectorRef(null); var cd = new DynamicChangeDetector(null, null, null, [], []);
var inj = injector([], null, null, new PreBuiltObjects(null, null, null, config)); var inj = injector([], null, null, new PreBuiltObjects(null, null, null, cd));
expect(inj.get(ChangeDetectorRef)).toEqual(config); expect(inj.get(ChangeDetectorRef)).toBe(cd.ref);
}); });
}); });

View File

@ -390,13 +390,13 @@ export function main() {
}) })
})); }));
describe("ChangeDetectorRef", () => { describe("ON_PUSH components", () => {
it("can be used to disable the change detection of the component's template", it("should use ChangeDetectorRef to manually request a check",
inject([TestBed, AsyncTestCompleter], (tb, async) => { inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({ tb.overrideView(MyComp, new View({
template: '<push-cmp #cmp></push-cmp>', template: '<push-cmp-with-ref #cmp></push-cmp-with-ref>',
directives: [[[PushBasedComp]]] directives: [[[PushCmpWithRef]]]
})); }));
tb.createView(MyComp, {context: ctx}).then((view) => { tb.createView(MyComp, {context: ctx}).then((view) => {
@ -417,10 +417,33 @@ export function main() {
}) })
})); }));
it('should not affect updating properties on the component', inject([TestBed, AsyncTestCompleter], (tb, async) => { it("should be checked when its bindings got updated",
inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({ tb.overrideView(MyComp, new View({
template: '<push-cmp [prop]="ctxProp" #cmp></push-cmp>', template: '<push-cmp [prop]="ctxProp" #cmp></push-cmp>',
directives: [[[PushBasedComp]]] directives: [[[PushCmp]]]
}));
tb.createView(MyComp, {context: ctx}).then((view) => {
var cmp = view.rawView.locals.get('cmp');
ctx.ctxProp = "one";
view.detectChanges();
expect(cmp.numberOfChecks).toEqual(1);
ctx.ctxProp = "two";
view.detectChanges();
expect(cmp.numberOfChecks).toEqual(2);
async.done();
})
}));
it('should not affect updating properties on the component', inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(MyComp, new View({
template: '<push-cmp-with-ref [prop]="ctxProp" #cmp></push-cmp-with-ref>',
directives: [[[PushCmpWithRef]]]
})); }));
tb.createView(MyComp, {context: ctx}).then((view) => { tb.createView(MyComp, {context: ctx}).then((view) => {
@ -800,7 +823,29 @@ class MyDir {
changeDetection:ON_PUSH changeDetection:ON_PUSH
}) })
@View({template: '{{field}}'}) @View({template: '{{field}}'})
class PushBasedComp { class PushCmp {
numberOfChecks:number;
prop;
constructor() {
this.numberOfChecks = 0;
}
get field(){
this.numberOfChecks++;
return "fixed";
}
}
@Component({
selector: 'push-cmp-with-ref',
properties: {
'prop': 'prop'
},
changeDetection:ON_PUSH
})
@View({template: '{{field}}'})
class PushCmpWithRef {
numberOfChecks:number; numberOfChecks:number;
ref:ChangeDetectorRef; ref:ChangeDetectorRef;
prop; prop;

View File

@ -12,7 +12,8 @@ import {
dynamicChangeDetection, dynamicChangeDetection,
jitChangeDetection, jitChangeDetection,
BindingRecord, BindingRecord,
DirectiveRecord DirectiveRecord,
DEFAULT
} from 'angular2/change_detection'; } from 'angular2/change_detection';
@ -191,7 +192,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations, objec
var proto = changeDetection.createProtoChangeDetector("proto"); var proto = changeDetection.createProtoChangeDetector("proto");
var directiveRecord = new DirectiveRecord(0, 0, false, false); var directiveRecord = new DirectiveRecord(0, 0, false, false, DEFAULT);
var bindings = [ var bindings = [
BindingRecord.createForDirective(parser.parseBinding('field0', null), "field0", reflector.setter("field0"), directiveRecord), BindingRecord.createForDirective(parser.parseBinding('field0', null), "field0", reflector.setter("field0"), directiveRecord),
BindingRecord.createForDirective(parser.parseBinding('field1', null), "field1", reflector.setter("field1"), directiveRecord), BindingRecord.createForDirective(parser.parseBinding('field1', null), "field1", reflector.setter("field1"), directiveRecord),
@ -306,7 +307,7 @@ class FakeDirectives {
this.targetObj = targetObj; this.targetObj = targetObj;
} }
directive(record) { getDirectiveFor(record) {
return this.targetObj; return this.targetObj;
} }
} }