feat(change_detection): added onInit and onCheck hooks

This commit is contained in:
vsavkin
2015-05-27 10:14:37 -07:00
parent 5d2af54730
commit c39c8ebcd0
22 changed files with 504 additions and 72 deletions
@@ -4,13 +4,14 @@ import {AST} from './parser/ast';
import {DirectiveIndex, DirectiveRecord} from './directive_record';
const DIRECTIVE = "directive";
const DIRECTIVE_LIFECYCLE = "directiveLifecycle";
const ELEMENT = "element";
const TEXT_NODE = "textNode";
export class BindingRecord {
constructor(public mode: string, public implicitReceiver: any, public ast: AST,
public elementIndex: number, public propertyName: string, public setter: SetterFn,
public directiveRecord: DirectiveRecord) {}
public lifecycleEvent: string, public directiveRecord: DirectiveRecord) {}
callOnChange() { return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange; }
@@ -20,25 +21,42 @@ export class BindingRecord {
isDirective() { return this.mode === DIRECTIVE; }
isDirectiveLifecycle() { return this.mode === DIRECTIVE_LIFECYCLE; }
isElement() { return this.mode === ELEMENT; }
isTextNode() { return this.mode === TEXT_NODE; }
static createForDirective(ast: AST, propertyName: string, setter: SetterFn,
directiveRecord: DirectiveRecord) {
return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, setter, directiveRecord);
return new BindingRecord(DIRECTIVE, 0, ast, 0, propertyName, setter, null, directiveRecord);
}
static createDirectiveOnCheck(directiveRecord: DirectiveRecord) {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onCheck",
directiveRecord);
}
static createDirectiveOnInit(directiveRecord: DirectiveRecord) {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onInit",
directiveRecord);
}
static createDirectiveOnChange(directiveRecord: DirectiveRecord) {
return new BindingRecord(DIRECTIVE_LIFECYCLE, 0, null, 0, null, null, "onChange",
directiveRecord);
}
static createForElement(ast: AST, elementIndex: number, propertyName: string) {
return new BindingRecord(ELEMENT, 0, ast, elementIndex, propertyName, null, null);
return new BindingRecord(ELEMENT, 0, ast, elementIndex, propertyName, null, null, null);
}
static createForHostProperty(directiveIndex: DirectiveIndex, ast: AST, propertyName: string) {
return new BindingRecord(ELEMENT, directiveIndex, ast, directiveIndex.elementIndex,
propertyName, null, null);
propertyName, null, null, null);
}
static createForTextNode(ast: AST, elementIndex: number) {
return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null);
return new BindingRecord(TEXT_NODE, 0, ast, elementIndex, null, null, null, null);
}
}
@@ -40,6 +40,7 @@ var CHANGES_LOCAL = "changes";
var LOCALS_ACCESSOR = "this.locals";
var MODE_ACCESSOR = "this.mode";
var CURRENT_PROTO = "currentProto";
var ALREADY_CHECKED_ACCESSOR = "this.alreadyChecked";
export class ChangeDetectorJITGenerator {
@@ -86,6 +87,7 @@ export class ChangeDetectorJITGenerator {
${PROTOS_ACCESSOR} = protos;
${DIRECTIVES_ACCESSOR} = directiveRecords;
${LOCALS_ACCESSOR} = null;
${ALREADY_CHECKED_ACCESSOR} = false;
${this._genFieldDefinitions()}
}
@@ -101,6 +103,8 @@ export class ChangeDetectorJITGenerator {
context = ${CONTEXT_ACCESSOR};
${this.records.map((r) => this._genRecord(r)).join("\n")}
${ALREADY_CHECKED_ACCESSOR} = true;
}
${this.typeName}.prototype.callOnAllChangesDone = function() {
@@ -113,6 +117,7 @@ export class ChangeDetectorJITGenerator {
${LOCALS_ACCESSOR} = locals;
${this._genHydrateDirectives()}
${this._genHydrateDetectors()}
${ALREADY_CHECKED_ACCESSOR} = false;
}
${this.typeName}.prototype.dehydrate = function() {
@@ -136,7 +141,7 @@ export class ChangeDetectorJITGenerator {
}
_genGetDirectiveFieldNames(): List<string> {
return this.directiveRecords.map((d) => this._genGetDirective(d.directiveIndex));
return this.directiveRecords.map(d => this._genGetDirective(d.directiveIndex));
}
_genGetDetectorFieldNames(): List<string> {
@@ -212,10 +217,26 @@ export class ChangeDetectorJITGenerator {
}
_genRecord(r: ProtoRecord): string {
if (r.mode === RECORD_TYPE_PIPE || r.mode === RECORD_TYPE_BINDING_PIPE) {
return this._genPipeCheck(r);
var rec;
if (r.isLifeCycleRecord()) {
rec = this._genDirectiveLifecycle(r);
} else if (r.isPipeRecord()) {
rec = this._genPipeCheck(r);
} else {
return this._genReferenceCheck(r);
rec = this._genReferenceCheck(r);
}
return `${rec}${this._genLastInDirective(r)}`;
}
_genDirectiveLifecycle(r: ProtoRecord) {
if (r.name === "onCheck") {
return this._genOnCheck(r);
} else if (r.name === "onInit") {
return this._genOnInit(r);
} else if (r.name === "onChange") {
return this._genOnChange(r);
} else {
throw new BaseException(`Unknown lifecycle event '${r.name}'`);
}
}
@@ -248,7 +269,6 @@ export class ChangeDetectorJITGenerator {
${this._genAddToChanges(r)}
${oldValue} = ${newValue};
}
${this._genLastInDirective(r)}
`;
}
@@ -266,7 +286,6 @@ export class ChangeDetectorJITGenerator {
${this._genAddToChanges(r)}
${oldValue} = ${newValue};
}
${this._genLastInDirective(r)}
`;
if (r.isPureFunction()) {
@@ -390,22 +409,27 @@ export class ChangeDetectorJITGenerator {
}
_genLastInDirective(r: ProtoRecord): string {
if (!r.lastInDirective) return "";
return `
${this._genNotifyOnChanges(r)}
${CHANGES_LOCAL} = null;
${this._genNotifyOnPushDetectors(r)}
${IS_CHANGED_LOCAL} = false;
`;
}
_genNotifyOnChanges(r: ProtoRecord): string {
_genOnCheck(r: ProtoRecord): string {
var br = r.bindingRecord;
if (!r.lastInDirective || !br.callOnChange()) return "";
return `
if(${CHANGES_LOCAL}) {
${this._genGetDirective(br.directiveRecord.directiveIndex)}.onChange(${CHANGES_LOCAL});
${CHANGES_LOCAL} = null;
}
`;
return `if (!throwOnChange) ${this._genGetDirective(br.directiveRecord.directiveIndex)}.onCheck();`;
}
_genOnInit(r: ProtoRecord): string {
var br = r.bindingRecord;
return `if (!throwOnChange && !${ALREADY_CHECKED_ACCESSOR}) ${this._genGetDirective(br.directiveRecord.directiveIndex)}.onInit();`;
}
_genOnChange(r: ProtoRecord): string {
var br = r.bindingRecord;
return `if (!throwOnChange && ${CHANGES_LOCAL}) ${this._genGetDirective(br.directiveRecord.directiveIndex)}.onChange(${CHANGES_LOCAL});`;
}
_genNotifyOnPushDetectors(r: ProtoRecord): string {
@@ -1,6 +1,6 @@
import {isPresent} from 'angular2/src/facade/lang';
import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection';
import {RECORD_TYPE_SELF, ProtoRecord} from './proto_record';
import {RECORD_TYPE_SELF, RECORD_TYPE_DIRECTIVE_LIFECYCLE, ProtoRecord} from './proto_record';
/**
* Removes "duplicate" records. It assuming that record evaluation does not
@@ -44,7 +44,8 @@ function _selfRecord(r: ProtoRecord, contextIndex: number, selfIndex: number): P
}
function _findMatching(r: ProtoRecord, rs: List<ProtoRecord>) {
return ListWrapper.find(rs, (rr) => rr.mode === r.mode && rr.funcOrValue === r.funcOrValue &&
return ListWrapper.find(rs, (rr) => rr.mode !== RECORD_TYPE_DIRECTIVE_LIFECYCLE &&
rr.mode === r.mode && rr.funcOrValue === r.funcOrValue &&
rr.contextIndex === r.contextIndex &&
ListWrapper.equals(rr.args, r.args));
}
@@ -1,5 +1,5 @@
import {ON_PUSH} from './constants';
import {StringWrapper} from 'angular2/src/facade/lang';
import {StringWrapper, normalizeBool} from 'angular2/src/facade/lang';
export class DirectiveIndex {
constructor(public elementIndex: number, public directiveIndex: number) {}
@@ -8,8 +8,29 @@ export class DirectiveIndex {
}
export class DirectiveRecord {
constructor(public directiveIndex: DirectiveIndex, public callOnAllChangesDone: boolean,
public callOnChange: boolean, public changeDetection: string) {}
directiveIndex: DirectiveIndex;
callOnAllChangesDone: boolean;
callOnChange: boolean;
callOnCheck: boolean;
callOnInit: boolean;
changeDetection: string;
constructor({directiveIndex, callOnAllChangesDone, callOnChange, callOnCheck, callOnInit,
changeDetection}: {
directiveIndex?: DirectiveIndex,
callOnAllChangesDone?: boolean,
callOnChange?: boolean,
callOnCheck?: boolean,
callOnInit?: boolean,
changeDetection?: string
} = {}) {
this.directiveIndex = directiveIndex;
this.callOnAllChangesDone = normalizeBool(callOnAllChangesDone);
this.callOnChange = normalizeBool(callOnChange);
this.callOnCheck = normalizeBool(callOnCheck);
this.callOnInit = normalizeBool(callOnInit);
this.changeDetection = changeDetection;
}
isOnPushChangeDetection(): boolean { return StringWrapper.equals(this.changeDetection, ON_PUSH); }
}
@@ -27,12 +27,13 @@ import {
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
export class DynamicChangeDetector extends AbstractChangeDetector {
locals: any;
locals: any = null;
values: List<any>;
changes: List<any>;
pipes: List<any>;
prevContexts: List<any>;
directives: any;
directives: any = null;
alreadyChecked: boolean = false;
constructor(private changeControlStrategy: string, private dispatcher: any,
private pipeRegistry: PipeRegistry, private protos: List<ProtoRecord>,
@@ -47,8 +48,6 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
ListWrapper.fill(this.pipes, null);
ListWrapper.fill(this.prevContexts, uninitialized);
ListWrapper.fill(this.changes, false);
this.locals = null;
this.directives = null;
}
hydrate(context: any, locals: any, directives: any) {
@@ -56,6 +55,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
this.values[0] = context;
this.locals = locals;
this.directives = directives;
this.alreadyChecked = false;
}
dehydrate() {
@@ -87,19 +87,26 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var bindingRecord = proto.bindingRecord;
var directiveRecord = bindingRecord.directiveRecord;
var change = this._check(proto, throwOnChange);
if (isPresent(change)) {
this._updateDirectiveOrElement(change, bindingRecord);
isChanged = true;
changes = this._addChange(bindingRecord, change, changes);
if (proto.isLifeCycleRecord()) {
if (proto.name === "onCheck" && !throwOnChange) {
this._getDirectiveFor(directiveRecord.directiveIndex).onCheck();
} else if (proto.name === "onInit" && !throwOnChange && !this.alreadyChecked) {
this._getDirectiveFor(directiveRecord.directiveIndex).onInit();
} else if (proto.name === "onChange" && isPresent(changes) && !throwOnChange) {
this._getDirectiveFor(directiveRecord.directiveIndex).onChange(changes);
}
} else {
var change = this._check(proto, throwOnChange);
if (isPresent(change)) {
this._updateDirectiveOrElement(change, bindingRecord);
isChanged = true;
changes = this._addChange(bindingRecord, change, changes);
}
}
if (proto.lastInDirective) {
if (isPresent(changes)) {
this._getDirectiveFor(directiveRecord.directiveIndex).onChange(changes);
changes = null;
}
changes = null;
if (isChanged && bindingRecord.isOnPushChangeDetection()) {
this._getDetectorFor(directiveRecord.directiveIndex).markAsCheckOnce();
}
@@ -107,6 +114,8 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
isChanged = false;
}
}
this.alreadyChecked = true;
}
callOnAllChangesDone() {
@@ -142,7 +151,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
_check(proto: ProtoRecord, throwOnChange: boolean): SimpleChange {
try {
if (proto.mode === RECORD_TYPE_PIPE || proto.mode === RECORD_TYPE_BINDING_PIPE) {
if (proto.isPipeRecord()) {
return this._pipeCheck(proto, throwOnChange);
} else {
return this._referenceCheck(proto, throwOnChange);
@@ -53,7 +53,8 @@ import {
RECORD_TYPE_BINDING_PIPE,
RECORD_TYPE_INTERPOLATE,
RECORD_TYPE_SAFE_PROPERTY,
RECORD_TYPE_SAFE_INVOKE_METHOD
RECORD_TYPE_SAFE_INVOKE_METHOD,
RECORD_TYPE_DIRECTIVE_LIFECYCLE
} from './proto_record';
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
@@ -72,7 +73,7 @@ export class DynamicProtoChangeDetector extends ProtoChangeDetector {
_createRecords(definition: ChangeDetectorDefinition) {
var recordBuilder = new ProtoRecordBuilder();
ListWrapper.forEach(definition.bindingRecords,
(b) => { recordBuilder.addAst(b, definition.variableNames); });
(b) => { recordBuilder.add(b, definition.variableNames); });
return coalesce(recordBuilder.records);
}
}
@@ -91,7 +92,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector {
_createFactory(definition: ChangeDetectorDefinition) {
var recordBuilder = new ProtoRecordBuilder();
ListWrapper.forEach(definition.bindingRecords,
(b) => { recordBuilder.addAst(b, definition.variableNames); });
(b) => { recordBuilder.add(b, definition.variableNames); });
var c = _jitProtoChangeDetectorClassCounter++;
var records = coalesce(recordBuilder.records);
var typeName = `ChangeDetector${c}`;
@@ -106,19 +107,30 @@ class ProtoRecordBuilder {
constructor() { this.records = []; }
addAst(b: BindingRecord, variableNames: List<string> = null) {
add(b: BindingRecord, variableNames: List<string> = null) {
var oldLast = ListWrapper.last(this.records);
if (isPresent(oldLast) && oldLast.bindingRecord.directiveRecord == b.directiveRecord) {
oldLast.lastInDirective = false;
}
_ConvertAstIntoProtoRecords.append(this.records, b, variableNames);
this._appendRecords(b, variableNames);
var newLast = ListWrapper.last(this.records);
if (isPresent(newLast) && newLast !== oldLast) {
newLast.lastInBinding = true;
newLast.lastInDirective = true;
}
}
_appendRecords(b: BindingRecord, variableNames: List<string>) {
if (b.isDirectiveLifecycle()) {
;
ListWrapper.push(
this.records,
new ProtoRecord(RECORD_TYPE_DIRECTIVE_LIFECYCLE, b.lifecycleEvent, null, [], [], -1, null,
this.records.length + 1, b, null, false, false));
} else {
_ConvertAstIntoProtoRecords.append(this.records, b, variableNames);
}
}
}
class _ConvertAstIntoProtoRecords {
@@ -15,6 +15,7 @@ export const RECORD_TYPE_BINDING_PIPE = 9;
export const RECORD_TYPE_INTERPOLATE = 10;
export const RECORD_TYPE_SAFE_PROPERTY = 11;
export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12;
export const RECORD_TYPE_DIRECTIVE_LIFECYCLE = 13;
export class ProtoRecord {
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,
@@ -26,4 +27,10 @@ export class ProtoRecord {
isPureFunction(): boolean {
return this.mode === RECORD_TYPE_INTERPOLATE || this.mode === RECORD_TYPE_PRIMITIVE_OP;
}
isPipeRecord(): boolean {
return this.mode === RECORD_TYPE_PIPE || this.mode === RECORD_TYPE_BINDING_PIPE;
}
isLifeCycleRecord(): boolean { return this.mode === RECORD_TYPE_DIRECTIVE_LIFECYCLE; }
}
@@ -8,5 +8,7 @@ export {
Directive as DirectiveAnnotation,
onDestroy,
onChange,
onCheck,
onInit,
onAllChangesDone
} from '../annotations_impl/annotations';
@@ -1077,6 +1077,54 @@ export const onDestroy = CONST_EXPR(new LifecycleEvent("onDestroy"));
*/
export const onChange = CONST_EXPR(new LifecycleEvent("onChange"));
/**
* Notify a directive when it has been checked.
*
* This method is called right after the directive's bindings have been checked,
* and before any of its children's bindings have been checked.
*
* It is invoked every time even when none of the directive's bindings has changed.
*
* ## Example:
*
* ```
* @Directive({
* selector: '[class-set]',
* lifecycle: [onCheck]
* })
* class ClassSet {
* onCheck() {
* }
* }
* ```
* @exportedAs angular2/annotations
*/
export const onCheck = CONST_EXPR(new LifecycleEvent("onCheck"));
/**
* Notify a directive when it has been checked the first itme.
*
* This method is called right after the directive's bindings have been checked,
* and before any of its children's bindings have been checked.
*
* It is invoked only once.
*
* ## Example:
*
* ```
* @Directive({
* selector: '[class-set]',
* lifecycle: [onInit]
* })
* class ClassSet {
* onInit() {
* }
* }
* ```
* @exportedAs angular2/annotations
*/
export const onInit = CONST_EXPR(new LifecycleEvent("onInit"));
/**
* Notify a directive when the bindings of all its children have been changed.
*
@@ -19,6 +19,12 @@ bool hasLifecycleHook(LifecycleEvent e, type, Directive annotation) {
} else if (e == onAllChangesDone) {
interface = OnAllChangesDone;
} else if (e == onCheck) {
interface = OnCheck;
} else if (e == onInit) {
interface = OnInit;
}
return interfaces.contains(interface);
@@ -26,6 +26,8 @@ import {
Component,
onChange,
onDestroy,
onCheck,
onInit,
onAllChangesDone
} from 'angular2/src/core/annotations_impl/annotations';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
@@ -303,6 +305,8 @@ export class DirectiveBinding extends ResolvedBinding {
callOnDestroy: hasLifecycleHook(onDestroy, rb.key.token, ann),
callOnChange: hasLifecycleHook(onChange, rb.key.token, ann),
callOnCheck: hasLifecycleHook(onCheck, rb.key.token, ann),
callOnInit: hasLifecycleHook(onInit, rb.key.token, ann),
callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
changeDetection: ann instanceof
@@ -11,6 +11,16 @@ export interface OnChange { onChange(changes: StringMap<string, any>): void; }
*/
export interface OnDestroy { onDestroy(): void; }
/**
* Defines lifecycle method [onCheck] called when a directive is being checked.
*/
export interface OnCheck { onCheck(): void; }
/**
* Defines lifecycle method [onInit] called when a directive is being checked the first time.
*/
export interface OnInit { onInit(): void; }
/**
* Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have
* been changed.
@@ -85,17 +85,27 @@ class BindingRecordsCreator {
for (var i = 0; i < directiveBinders.length; i++) {
var directiveBinder = directiveBinders[i];
var directiveMetadata = allDirectiveMetadatas[directiveBinder.directiveIndex];
var directiveRecord = this._getDirectiveRecord(boundElementIndex, i, directiveMetadata);
// directive properties
MapWrapper.forEach(directiveBinder.propertyBindings, (astWithSource, propertyName) => {
// TODO: these setters should eventually be created by change detection, to make
// it monomorphic!
var setter = reflector.setter(propertyName);
var directiveRecord = this._getDirectiveRecord(boundElementIndex, i, directiveMetadata);
ListWrapper.push(bindings, BindingRecord.createForDirective(astWithSource, propertyName,
setter, directiveRecord));
});
if (directiveRecord.callOnChange) {
ListWrapper.push(bindings, BindingRecord.createDirectiveOnChange(directiveRecord));
}
if (directiveRecord.callOnInit) {
ListWrapper.push(bindings, BindingRecord.createDirectiveOnInit(directiveRecord));
}
if (directiveRecord.callOnCheck) {
ListWrapper.push(bindings, BindingRecord.createDirectiveOnCheck(directiveRecord));
}
// host properties
MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => {
var dirIndex = new DirectiveIndex(boundElementIndex, i);
@@ -110,12 +120,14 @@ class BindingRecordsCreator {
var id = boundElementIndex * 100 + directiveIndex;
if (!MapWrapper.contains(this._directiveRecordsMap, id)) {
var changeDetection = directiveMetadata.changeDetection;
MapWrapper.set(this._directiveRecordsMap, id,
new DirectiveRecord(new DirectiveIndex(boundElementIndex, directiveIndex),
directiveMetadata.callOnAllChangesDone,
directiveMetadata.callOnChange, changeDetection));
MapWrapper.set(this._directiveRecordsMap, id, new DirectiveRecord({
directiveIndex: new DirectiveIndex(boundElementIndex, directiveIndex),
callOnAllChangesDone: directiveMetadata.callOnAllChangesDone,
callOnChange: directiveMetadata.callOnChange,
callOnCheck: directiveMetadata.callOnCheck,
callOnInit: directiveMetadata.callOnInit,
changeDetection: directiveMetadata.changeDetection
}));
}
return MapWrapper.get(this._directiveRecordsMap, id);
+4
View File
@@ -196,6 +196,10 @@ dynamic normalizeBlank(obj) {
return isBlank(obj) ? null : obj;
}
bool normalizeBool(bool obj) {
return isBlank(obj) ? false : obj;
}
bool isJsObject(o) {
return false;
}
+4
View File
@@ -231,6 +231,10 @@ export function normalizeBlank(obj) {
return isBlank(obj) ? null : obj;
}
export function normalizeBool(obj:boolean):boolean {
return isBlank(obj) ? false : obj;
}
export function isJsObject(o): boolean {
return o !== null && (typeof o === "function" || typeof o === "object");
}
+7 -1
View File
@@ -138,11 +138,13 @@ export class DirectiveMetadata {
type: number;
callOnDestroy: boolean;
callOnChange: boolean;
callOnCheck: boolean;
callOnInit: boolean;
callOnAllChangesDone: boolean;
changeDetection: string;
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
callOnAllChangesDone, changeDetection}: {
callOnCheck, callOnInit, callOnAllChangesDone, changeDetection}: {
id?: string,
selector?: string,
compileChildren?: boolean,
@@ -156,6 +158,8 @@ export class DirectiveMetadata {
type?: number,
callOnDestroy?: boolean,
callOnChange?: boolean,
callOnCheck?: boolean,
callOnInit?: boolean,
callOnAllChangesDone?: boolean,
changeDetection?: string
}) {
@@ -172,6 +176,8 @@ export class DirectiveMetadata {
this.type = type;
this.callOnDestroy = callOnDestroy;
this.callOnChange = callOnChange;
this.callOnCheck = callOnCheck;
this.callOnInit = callOnInit;
this.callOnAllChangesDone = callOnAllChangesDone;
this.changeDetection = changeDetection;
}