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

View File

@ -4,13 +4,14 @@ import {AST} from './parser/ast';
import {DirectiveIndex, DirectiveRecord} from './directive_record'; import {DirectiveIndex, DirectiveRecord} from './directive_record';
const DIRECTIVE = "directive"; const DIRECTIVE = "directive";
const DIRECTIVE_LIFECYCLE = "directiveLifecycle";
const ELEMENT = "element"; const ELEMENT = "element";
const TEXT_NODE = "textNode"; const TEXT_NODE = "textNode";
export class BindingRecord { export class BindingRecord {
constructor(public mode: string, public implicitReceiver: any, public ast: AST, constructor(public mode: string, public implicitReceiver: any, public ast: AST,
public elementIndex: number, public propertyName: string, public setter: SetterFn, 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; } callOnChange() { return isPresent(this.directiveRecord) && this.directiveRecord.callOnChange; }
@ -20,25 +21,42 @@ export class BindingRecord {
isDirective() { return this.mode === DIRECTIVE; } isDirective() { return this.mode === DIRECTIVE; }
isDirectiveLifecycle() { return this.mode === DIRECTIVE_LIFECYCLE; }
isElement() { return this.mode === ELEMENT; } isElement() { return this.mode === ELEMENT; }
isTextNode() { return this.mode === TEXT_NODE; } isTextNode() { return this.mode === TEXT_NODE; }
static createForDirective(ast: AST, propertyName: string, setter: SetterFn, static createForDirective(ast: AST, propertyName: string, setter: SetterFn,
directiveRecord: DirectiveRecord) { 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) { 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) { static createForHostProperty(directiveIndex: DirectiveIndex, ast: AST, propertyName: string) {
return new BindingRecord(ELEMENT, directiveIndex, ast, directiveIndex.elementIndex, return new BindingRecord(ELEMENT, directiveIndex, ast, directiveIndex.elementIndex,
propertyName, null, null); propertyName, null, null, null);
} }
static createForTextNode(ast: AST, elementIndex: number) { 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);
} }
} }

View File

@ -40,6 +40,7 @@ var CHANGES_LOCAL = "changes";
var LOCALS_ACCESSOR = "this.locals"; var LOCALS_ACCESSOR = "this.locals";
var MODE_ACCESSOR = "this.mode"; var MODE_ACCESSOR = "this.mode";
var CURRENT_PROTO = "currentProto"; var CURRENT_PROTO = "currentProto";
var ALREADY_CHECKED_ACCESSOR = "this.alreadyChecked";
export class ChangeDetectorJITGenerator { export class ChangeDetectorJITGenerator {
@ -86,6 +87,7 @@ export class ChangeDetectorJITGenerator {
${PROTOS_ACCESSOR} = protos; ${PROTOS_ACCESSOR} = protos;
${DIRECTIVES_ACCESSOR} = directiveRecords; ${DIRECTIVES_ACCESSOR} = directiveRecords;
${LOCALS_ACCESSOR} = null; ${LOCALS_ACCESSOR} = null;
${ALREADY_CHECKED_ACCESSOR} = false;
${this._genFieldDefinitions()} ${this._genFieldDefinitions()}
} }
@ -101,6 +103,8 @@ export class ChangeDetectorJITGenerator {
context = ${CONTEXT_ACCESSOR}; context = ${CONTEXT_ACCESSOR};
${this.records.map((r) => this._genRecord(r)).join("\n")} ${this.records.map((r) => this._genRecord(r)).join("\n")}
${ALREADY_CHECKED_ACCESSOR} = true;
} }
${this.typeName}.prototype.callOnAllChangesDone = function() { ${this.typeName}.prototype.callOnAllChangesDone = function() {
@ -113,6 +117,7 @@ export class ChangeDetectorJITGenerator {
${LOCALS_ACCESSOR} = locals; ${LOCALS_ACCESSOR} = locals;
${this._genHydrateDirectives()} ${this._genHydrateDirectives()}
${this._genHydrateDetectors()} ${this._genHydrateDetectors()}
${ALREADY_CHECKED_ACCESSOR} = false;
} }
${this.typeName}.prototype.dehydrate = function() { ${this.typeName}.prototype.dehydrate = function() {
@ -136,7 +141,7 @@ export class ChangeDetectorJITGenerator {
} }
_genGetDirectiveFieldNames(): List<string> { _genGetDirectiveFieldNames(): List<string> {
return this.directiveRecords.map((d) => this._genGetDirective(d.directiveIndex)); return this.directiveRecords.map(d => this._genGetDirective(d.directiveIndex));
} }
_genGetDetectorFieldNames(): List<string> { _genGetDetectorFieldNames(): List<string> {
@ -212,10 +217,26 @@ export class ChangeDetectorJITGenerator {
} }
_genRecord(r: ProtoRecord): string { _genRecord(r: ProtoRecord): string {
if (r.mode === RECORD_TYPE_PIPE || r.mode === RECORD_TYPE_BINDING_PIPE) { var rec;
return this._genPipeCheck(r); if (r.isLifeCycleRecord()) {
rec = this._genDirectiveLifecycle(r);
} else if (r.isPipeRecord()) {
rec = this._genPipeCheck(r);
} else { } 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)} ${this._genAddToChanges(r)}
${oldValue} = ${newValue}; ${oldValue} = ${newValue};
} }
${this._genLastInDirective(r)}
`; `;
} }
@ -266,7 +286,6 @@ export class ChangeDetectorJITGenerator {
${this._genAddToChanges(r)} ${this._genAddToChanges(r)}
${oldValue} = ${newValue}; ${oldValue} = ${newValue};
} }
${this._genLastInDirective(r)}
`; `;
if (r.isPureFunction()) { if (r.isPureFunction()) {
@ -390,22 +409,27 @@ export class ChangeDetectorJITGenerator {
} }
_genLastInDirective(r: ProtoRecord): string { _genLastInDirective(r: ProtoRecord): string {
if (!r.lastInDirective) return "";
return ` return `
${this._genNotifyOnChanges(r)} ${CHANGES_LOCAL} = null;
${this._genNotifyOnPushDetectors(r)} ${this._genNotifyOnPushDetectors(r)}
${IS_CHANGED_LOCAL} = false; ${IS_CHANGED_LOCAL} = false;
`; `;
} }
_genNotifyOnChanges(r: ProtoRecord): string { _genOnCheck(r: ProtoRecord): string {
var br = r.bindingRecord; var br = r.bindingRecord;
if (!r.lastInDirective || !br.callOnChange()) return ""; return `if (!throwOnChange) ${this._genGetDirective(br.directiveRecord.directiveIndex)}.onCheck();`;
return `
if(${CHANGES_LOCAL}) {
${this._genGetDirective(br.directiveRecord.directiveIndex)}.onChange(${CHANGES_LOCAL});
${CHANGES_LOCAL} = null;
} }
`;
_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 { _genNotifyOnPushDetectors(r: ProtoRecord): string {

View File

@ -1,6 +1,6 @@
import {isPresent} from 'angular2/src/facade/lang'; import {isPresent} from 'angular2/src/facade/lang';
import {List, ListWrapper, Map, MapWrapper} from 'angular2/src/facade/collection'; 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 * 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>) { 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 && rr.contextIndex === r.contextIndex &&
ListWrapper.equals(rr.args, r.args)); ListWrapper.equals(rr.args, r.args));
} }

View File

@ -1,5 +1,5 @@
import {ON_PUSH} from './constants'; import {ON_PUSH} from './constants';
import {StringWrapper} from 'angular2/src/facade/lang'; import {StringWrapper, normalizeBool} from 'angular2/src/facade/lang';
export class DirectiveIndex { export class DirectiveIndex {
constructor(public elementIndex: number, public directiveIndex: number) {} constructor(public elementIndex: number, public directiveIndex: number) {}
@ -8,8 +8,29 @@ export class DirectiveIndex {
} }
export class DirectiveRecord { export class DirectiveRecord {
constructor(public directiveIndex: DirectiveIndex, public callOnAllChangesDone: boolean, directiveIndex: DirectiveIndex;
public callOnChange: boolean, public changeDetection: string) {} 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); } isOnPushChangeDetection(): boolean { return StringWrapper.equals(this.changeDetection, ON_PUSH); }
} }

View File

@ -27,12 +27,13 @@ import {
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions'; import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
export class DynamicChangeDetector extends AbstractChangeDetector { export class DynamicChangeDetector extends AbstractChangeDetector {
locals: any; locals: any = null;
values: List<any>; values: List<any>;
changes: List<any>; changes: List<any>;
pipes: List<any>; pipes: List<any>;
prevContexts: List<any>; prevContexts: List<any>;
directives: any; directives: any = null;
alreadyChecked: boolean = false;
constructor(private changeControlStrategy: string, private dispatcher: any, constructor(private changeControlStrategy: string, private dispatcher: any,
private pipeRegistry: PipeRegistry, private protos: List<ProtoRecord>, private pipeRegistry: PipeRegistry, private protos: List<ProtoRecord>,
@ -47,8 +48,6 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
ListWrapper.fill(this.pipes, null); ListWrapper.fill(this.pipes, null);
ListWrapper.fill(this.prevContexts, uninitialized); ListWrapper.fill(this.prevContexts, uninitialized);
ListWrapper.fill(this.changes, false); ListWrapper.fill(this.changes, false);
this.locals = null;
this.directives = null;
} }
hydrate(context: any, locals: any, directives: any) { hydrate(context: any, locals: any, directives: any) {
@ -56,6 +55,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
this.values[0] = context; this.values[0] = context;
this.locals = locals; this.locals = locals;
this.directives = directives; this.directives = directives;
this.alreadyChecked = false;
} }
dehydrate() { dehydrate() {
@ -87,19 +87,26 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
var bindingRecord = proto.bindingRecord; var bindingRecord = proto.bindingRecord;
var directiveRecord = bindingRecord.directiveRecord; var directiveRecord = bindingRecord.directiveRecord;
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); var change = this._check(proto, throwOnChange);
if (isPresent(change)) { if (isPresent(change)) {
this._updateDirectiveOrElement(change, bindingRecord); this._updateDirectiveOrElement(change, bindingRecord);
isChanged = true; isChanged = true;
changes = this._addChange(bindingRecord, change, changes); changes = this._addChange(bindingRecord, change, changes);
} }
if (proto.lastInDirective) {
if (isPresent(changes)) {
this._getDirectiveFor(directiveRecord.directiveIndex).onChange(changes);
changes = null;
} }
if (proto.lastInDirective) {
changes = null;
if (isChanged && bindingRecord.isOnPushChangeDetection()) { if (isChanged && bindingRecord.isOnPushChangeDetection()) {
this._getDetectorFor(directiveRecord.directiveIndex).markAsCheckOnce(); this._getDetectorFor(directiveRecord.directiveIndex).markAsCheckOnce();
} }
@ -107,6 +114,8 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
isChanged = false; isChanged = false;
} }
} }
this.alreadyChecked = true;
} }
callOnAllChangesDone() { callOnAllChangesDone() {
@ -142,7 +151,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
_check(proto: ProtoRecord, throwOnChange: boolean): SimpleChange { _check(proto: ProtoRecord, throwOnChange: boolean): SimpleChange {
try { try {
if (proto.mode === RECORD_TYPE_PIPE || proto.mode === RECORD_TYPE_BINDING_PIPE) { if (proto.isPipeRecord()) {
return this._pipeCheck(proto, throwOnChange); return this._pipeCheck(proto, throwOnChange);
} else { } else {
return this._referenceCheck(proto, throwOnChange); return this._referenceCheck(proto, throwOnChange);

View File

@ -53,7 +53,8 @@ import {
RECORD_TYPE_BINDING_PIPE, RECORD_TYPE_BINDING_PIPE,
RECORD_TYPE_INTERPOLATE, RECORD_TYPE_INTERPOLATE,
RECORD_TYPE_SAFE_PROPERTY, RECORD_TYPE_SAFE_PROPERTY,
RECORD_TYPE_SAFE_INVOKE_METHOD RECORD_TYPE_SAFE_INVOKE_METHOD,
RECORD_TYPE_DIRECTIVE_LIFECYCLE
} from './proto_record'; } from './proto_record';
export class DynamicProtoChangeDetector extends ProtoChangeDetector { export class DynamicProtoChangeDetector extends ProtoChangeDetector {
@ -72,7 +73,7 @@ export class DynamicProtoChangeDetector extends ProtoChangeDetector {
_createRecords(definition: ChangeDetectorDefinition) { _createRecords(definition: ChangeDetectorDefinition) {
var recordBuilder = new ProtoRecordBuilder(); var recordBuilder = new ProtoRecordBuilder();
ListWrapper.forEach(definition.bindingRecords, ListWrapper.forEach(definition.bindingRecords,
(b) => { recordBuilder.addAst(b, definition.variableNames); }); (b) => { recordBuilder.add(b, definition.variableNames); });
return coalesce(recordBuilder.records); return coalesce(recordBuilder.records);
} }
} }
@ -91,7 +92,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector {
_createFactory(definition: ChangeDetectorDefinition) { _createFactory(definition: ChangeDetectorDefinition) {
var recordBuilder = new ProtoRecordBuilder(); var recordBuilder = new ProtoRecordBuilder();
ListWrapper.forEach(definition.bindingRecords, ListWrapper.forEach(definition.bindingRecords,
(b) => { recordBuilder.addAst(b, definition.variableNames); }); (b) => { recordBuilder.add(b, definition.variableNames); });
var c = _jitProtoChangeDetectorClassCounter++; var c = _jitProtoChangeDetectorClassCounter++;
var records = coalesce(recordBuilder.records); var records = coalesce(recordBuilder.records);
var typeName = `ChangeDetector${c}`; var typeName = `ChangeDetector${c}`;
@ -106,19 +107,30 @@ class ProtoRecordBuilder {
constructor() { this.records = []; } constructor() { this.records = []; }
addAst(b: BindingRecord, variableNames: List<string> = null) { add(b: BindingRecord, variableNames: List<string> = null) {
var oldLast = ListWrapper.last(this.records); var oldLast = ListWrapper.last(this.records);
if (isPresent(oldLast) && oldLast.bindingRecord.directiveRecord == b.directiveRecord) { if (isPresent(oldLast) && oldLast.bindingRecord.directiveRecord == b.directiveRecord) {
oldLast.lastInDirective = false; oldLast.lastInDirective = false;
} }
this._appendRecords(b, variableNames);
_ConvertAstIntoProtoRecords.append(this.records, b, variableNames);
var newLast = ListWrapper.last(this.records); var newLast = ListWrapper.last(this.records);
if (isPresent(newLast) && newLast !== oldLast) { if (isPresent(newLast) && newLast !== oldLast) {
newLast.lastInBinding = true; newLast.lastInBinding = true;
newLast.lastInDirective = 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 { class _ConvertAstIntoProtoRecords {

View File

@ -15,6 +15,7 @@ export const RECORD_TYPE_BINDING_PIPE = 9;
export const RECORD_TYPE_INTERPOLATE = 10; export const RECORD_TYPE_INTERPOLATE = 10;
export const RECORD_TYPE_SAFE_PROPERTY = 11; export const RECORD_TYPE_SAFE_PROPERTY = 11;
export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12; export const RECORD_TYPE_SAFE_INVOKE_METHOD = 12;
export const RECORD_TYPE_DIRECTIVE_LIFECYCLE = 13;
export class ProtoRecord { export class ProtoRecord {
constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>, constructor(public mode: number, public name: string, public funcOrValue, public args: List<any>,
@ -26,4 +27,10 @@ export class ProtoRecord {
isPureFunction(): boolean { isPureFunction(): boolean {
return this.mode === RECORD_TYPE_INTERPOLATE || this.mode === RECORD_TYPE_PRIMITIVE_OP; 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; }
} }

View File

@ -8,5 +8,7 @@ export {
Directive as DirectiveAnnotation, Directive as DirectiveAnnotation,
onDestroy, onDestroy,
onChange, onChange,
onCheck,
onInit,
onAllChangesDone onAllChangesDone
} from '../annotations_impl/annotations'; } from '../annotations_impl/annotations';

View File

@ -1077,6 +1077,54 @@ export const onDestroy = CONST_EXPR(new LifecycleEvent("onDestroy"));
*/ */
export const onChange = CONST_EXPR(new LifecycleEvent("onChange")); 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. * Notify a directive when the bindings of all its children have been changed.
* *

View File

@ -19,6 +19,12 @@ bool hasLifecycleHook(LifecycleEvent e, type, Directive annotation) {
} else if (e == onAllChangesDone) { } else if (e == onAllChangesDone) {
interface = OnAllChangesDone; interface = OnAllChangesDone;
} else if (e == onCheck) {
interface = OnCheck;
} else if (e == onInit) {
interface = OnInit;
} }
return interfaces.contains(interface); return interfaces.contains(interface);

View File

@ -26,6 +26,8 @@ import {
Component, Component,
onChange, onChange,
onDestroy, onDestroy,
onCheck,
onInit,
onAllChangesDone onAllChangesDone
} from 'angular2/src/core/annotations_impl/annotations'; } from 'angular2/src/core/annotations_impl/annotations';
import {hasLifecycleHook} from './directive_lifecycle_reflector'; import {hasLifecycleHook} from './directive_lifecycle_reflector';
@ -303,6 +305,8 @@ export class DirectiveBinding extends ResolvedBinding {
callOnDestroy: hasLifecycleHook(onDestroy, rb.key.token, ann), callOnDestroy: hasLifecycleHook(onDestroy, rb.key.token, ann),
callOnChange: hasLifecycleHook(onChange, 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), callOnAllChangesDone: hasLifecycleHook(onAllChangesDone, rb.key.token, ann),
changeDetection: ann instanceof changeDetection: ann instanceof

View File

@ -11,6 +11,16 @@ export interface OnChange { onChange(changes: StringMap<string, any>): void; }
*/ */
export interface OnDestroy { onDestroy(): 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 * Defines lifecycle method [onAllChangesDone ] called when the bindings of all its children have
* been changed. * been changed.

View File

@ -85,17 +85,27 @@ class BindingRecordsCreator {
for (var i = 0; i < directiveBinders.length; i++) { for (var i = 0; i < directiveBinders.length; i++) {
var directiveBinder = directiveBinders[i]; var directiveBinder = directiveBinders[i];
var directiveMetadata = allDirectiveMetadatas[directiveBinder.directiveIndex]; var directiveMetadata = allDirectiveMetadatas[directiveBinder.directiveIndex];
var directiveRecord = this._getDirectiveRecord(boundElementIndex, i, directiveMetadata);
// directive properties // directive properties
MapWrapper.forEach(directiveBinder.propertyBindings, (astWithSource, propertyName) => { MapWrapper.forEach(directiveBinder.propertyBindings, (astWithSource, propertyName) => {
// TODO: these setters should eventually be created by change detection, to make // TODO: these setters should eventually be created by change detection, to make
// it monomorphic! // it monomorphic!
var setter = reflector.setter(propertyName); var setter = reflector.setter(propertyName);
var directiveRecord = this._getDirectiveRecord(boundElementIndex, i, directiveMetadata);
ListWrapper.push(bindings, BindingRecord.createForDirective(astWithSource, propertyName, ListWrapper.push(bindings, BindingRecord.createForDirective(astWithSource, propertyName,
setter, directiveRecord)); 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 // host properties
MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => { MapWrapper.forEach(directiveBinder.hostPropertyBindings, (astWithSource, propertyName) => {
var dirIndex = new DirectiveIndex(boundElementIndex, i); var dirIndex = new DirectiveIndex(boundElementIndex, i);
@ -110,12 +120,14 @@ class BindingRecordsCreator {
var id = boundElementIndex * 100 + directiveIndex; var id = boundElementIndex * 100 + directiveIndex;
if (!MapWrapper.contains(this._directiveRecordsMap, id)) { if (!MapWrapper.contains(this._directiveRecordsMap, id)) {
var changeDetection = directiveMetadata.changeDetection; MapWrapper.set(this._directiveRecordsMap, id, new DirectiveRecord({
directiveIndex: new DirectiveIndex(boundElementIndex, directiveIndex),
MapWrapper.set(this._directiveRecordsMap, id, callOnAllChangesDone: directiveMetadata.callOnAllChangesDone,
new DirectiveRecord(new DirectiveIndex(boundElementIndex, directiveIndex), callOnChange: directiveMetadata.callOnChange,
directiveMetadata.callOnAllChangesDone, callOnCheck: directiveMetadata.callOnCheck,
directiveMetadata.callOnChange, changeDetection)); callOnInit: directiveMetadata.callOnInit,
changeDetection: directiveMetadata.changeDetection
}));
} }
return MapWrapper.get(this._directiveRecordsMap, id); return MapWrapper.get(this._directiveRecordsMap, id);

View File

@ -196,6 +196,10 @@ dynamic normalizeBlank(obj) {
return isBlank(obj) ? null : obj; return isBlank(obj) ? null : obj;
} }
bool normalizeBool(bool obj) {
return isBlank(obj) ? false : obj;
}
bool isJsObject(o) { bool isJsObject(o) {
return false; return false;
} }

View File

@ -231,6 +231,10 @@ export function normalizeBlank(obj) {
return isBlank(obj) ? null : obj; return isBlank(obj) ? null : obj;
} }
export function normalizeBool(obj:boolean):boolean {
return isBlank(obj) ? false : obj;
}
export function isJsObject(o): boolean { export function isJsObject(o): boolean {
return o !== null && (typeof o === "function" || typeof o === "object"); return o !== null && (typeof o === "function" || typeof o === "object");
} }

View File

@ -138,11 +138,13 @@ export class DirectiveMetadata {
type: number; type: number;
callOnDestroy: boolean; callOnDestroy: boolean;
callOnChange: boolean; callOnChange: boolean;
callOnCheck: boolean;
callOnInit: boolean;
callOnAllChangesDone: boolean; callOnAllChangesDone: boolean;
changeDetection: string; changeDetection: string;
constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes, constructor({id, selector, compileChildren, events, hostListeners, hostProperties, hostAttributes,
hostActions, properties, readAttributes, type, callOnDestroy, callOnChange, hostActions, properties, readAttributes, type, callOnDestroy, callOnChange,
callOnAllChangesDone, changeDetection}: { callOnCheck, callOnInit, callOnAllChangesDone, changeDetection}: {
id?: string, id?: string,
selector?: string, selector?: string,
compileChildren?: boolean, compileChildren?: boolean,
@ -156,6 +158,8 @@ export class DirectiveMetadata {
type?: number, type?: number,
callOnDestroy?: boolean, callOnDestroy?: boolean,
callOnChange?: boolean, callOnChange?: boolean,
callOnCheck?: boolean,
callOnInit?: boolean,
callOnAllChangesDone?: boolean, callOnAllChangesDone?: boolean,
changeDetection?: string changeDetection?: string
}) { }) {
@ -172,6 +176,8 @@ export class DirectiveMetadata {
this.type = type; this.type = type;
this.callOnDestroy = callOnDestroy; this.callOnDestroy = callOnDestroy;
this.callOnChange = callOnChange; this.callOnChange = callOnChange;
this.callOnCheck = callOnCheck;
this.callOnInit = callOnInit;
this.callOnAllChangesDone = callOnAllChangesDone; this.callOnAllChangesDone = callOnAllChangesDone;
this.changeDetection = changeDetection; this.changeDetection = changeDetection;
} }

View File

@ -311,10 +311,26 @@ export function main() {
}); });
describe("updating directives", () => { describe("updating directives", () => {
var dirRecord1 = new DirectiveRecord(new DirectiveIndex(0, 0), true, true, DEFAULT); var dirRecord1 = new DirectiveRecord({
var dirRecord2 = new DirectiveRecord(new DirectiveIndex(0, 1), true, true, DEFAULT); directiveIndex: new DirectiveIndex(0, 0),
var dirRecordNoCallbacks = callOnChange: true,
new DirectiveRecord(new DirectiveIndex(0, 0), false, false, DEFAULT); callOnCheck: true,
callOnAllChangesDone: true
});
var dirRecord2 = new DirectiveRecord({
directiveIndex: new DirectiveIndex(0, 1),
callOnChange: true,
callOnCheck: true,
callOnAllChangesDone: true
});
var dirRecordNoCallbacks = new DirectiveRecord({
directiveIndex: new DirectiveIndex(0, 0),
callOnChange: false,
callOnCheck: false,
callOnAllChangesDone: false
});
function updateA(exp: string, dirRecord) { function updateA(exp: string, dirRecord) {
return BindingRecord.createForDirective(ast(exp), "a", (o, v) => o.a = v, return BindingRecord.createForDirective(ast(exp), "a", (o, v) => o.a = v,
@ -353,7 +369,9 @@ export function main() {
var pcd = createProtoChangeDetector([ var pcd = createProtoChangeDetector([
updateA("1", dirRecord1), updateA("1", dirRecord1),
updateB("2", dirRecord1), updateB("2", dirRecord1),
updateA("3", dirRecord2) BindingRecord.createDirectiveOnChange(dirRecord1),
updateA("3", dirRecord2),
BindingRecord.createDirectiveOnChange(dirRecord2)
], ],
[], [dirRecord1, dirRecord2]); [], [dirRecord1, dirRecord2]);
@ -366,10 +384,13 @@ export function main() {
expect(directive1.changes).toEqual({'a': 1, 'b': 2}); expect(directive1.changes).toEqual({'a': 1, 'b': 2});
expect(directive2.changes).toEqual({'a': 3}); expect(directive2.changes).toEqual({'a': 3});
}); });
});
describe("onCheck", () => {
it("should notify the directive when it is checked", () => {
var pcd = createProtoChangeDetector(
[BindingRecord.createDirectiveOnCheck(dirRecord1)], [], [dirRecord1]);
it("should not call onChange when callOnChange is false", () => {
var pcd = createProtoChangeDetector([updateA("1", dirRecordNoCallbacks)], [],
[dirRecordNoCallbacks]);
var cd = pcd.instantiate(dispatcher); var cd = pcd.instantiate(dispatcher);
@ -377,7 +398,63 @@ export function main() {
cd.detectChanges(); cd.detectChanges();
expect(directive1.changes).toEqual(null); expect(directive1.onCheckCalled).toBe(true);
directive1.onCheckCalled = false;
cd.detectChanges();
expect(directive1.onCheckCalled).toBe(true);
});
it("should not call onCheck in detectNoChanges", () => {
var pcd = createProtoChangeDetector(
[BindingRecord.createDirectiveOnCheck(dirRecord1)], [], [dirRecord1]);
var cd = pcd.instantiate(dispatcher);
cd.hydrate(null, null, dirs([directive1]));
cd.checkNoChanges();
expect(directive1.onCheckCalled).toBe(false);
});
});
describe("onInit", () => {
it("should notify the directive after it has been checked the first time", () => {
var pcd = createProtoChangeDetector(
[BindingRecord.createDirectiveOnInit(dirRecord1)], [], [dirRecord1]);
var cd = pcd.instantiate(dispatcher);
cd.hydrate(null, null, dirs([directive1]));
cd.detectChanges();
expect(directive1.onInitCalled).toBe(true);
directive1.onInitCalled = false;
cd.detectChanges();
expect(directive1.onInitCalled).toBe(false);
});
it("should not call onInit in detectNoChanges", () => {
var pcd = createProtoChangeDetector(
[BindingRecord.createDirectiveOnInit(dirRecord1)], [], [dirRecord1]);
var cd = pcd.instantiate(dispatcher);
cd.hydrate(null, null, dirs([directive1]));
cd.checkNoChanges();
expect(directive1.onInitCalled).toBe(false);
}); });
}); });
@ -466,7 +543,7 @@ export function main() {
describe("reading directives", () => { describe("reading directives", () => {
var index = new DirectiveIndex(0, 0); var index = new DirectiveIndex(0, 0);
var dirRecord = new DirectiveRecord(index, false, false, DEFAULT); var dirRecord = new DirectiveRecord({directiveIndex: new DirectiveIndex(0, 0)});
it("should read directive properties", () => { it("should read directive properties", () => {
var directive = new TestDirective(); var directive = new TestDirective();
@ -688,8 +765,8 @@ export function main() {
checkedDetector.mode = CHECKED; checkedDetector.mode = CHECKED;
// this directive is a component with ON_PUSH change detection // this directive is a component with ON_PUSH change detection
dirRecordWithOnPush = dirRecordWithOnPush = new DirectiveRecord(
new DirectiveRecord(new DirectiveIndex(0, 0), false, false, ON_PUSH); {directiveIndex: new DirectiveIndex(0, 0), changeDetection: ON_PUSH});
// a record updating a component // a record updating a component
updateDirWithOnPushRecord = BindingRecord.createForDirective( updateDirWithOnPushRecord = BindingRecord.createForDirective(
@ -943,15 +1020,23 @@ class TestDirective {
changes; changes;
onChangesDoneCalled; onChangesDoneCalled;
onChangesDoneSpy; onChangesDoneSpy;
onCheckCalled;
onInitCalled;
constructor(onChangesDoneSpy = null) { constructor(onChangesDoneSpy = null) {
this.onChangesDoneCalled = false; this.onChangesDoneCalled = false;
this.onCheckCalled = false;
this.onInitCalled = false;
this.onChangesDoneSpy = onChangesDoneSpy; this.onChangesDoneSpy = onChangesDoneSpy;
this.a = null; this.a = null;
this.b = null; this.b = null;
this.changes = null; this.changes = null;
} }
onCheck() { this.onCheckCalled = true; }
onInit() { this.onInitCalled = true; }
onChange(changes) { onChange(changes) {
var r = {}; var r = {};
StringMapWrapper.forEach(changes, (c, key) => r[key] = c.currentValue); StringMapWrapper.forEach(changes, (c, key) => r[key] = c.currentValue);

View File

@ -1,12 +1,16 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib'; import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib';
import {coalesce} from 'angular2/src/change_detection/coalesce'; import {coalesce} from 'angular2/src/change_detection/coalesce';
import {RECORD_TYPE_SELF, ProtoRecord} from 'angular2/src/change_detection/proto_record'; import {
RECORD_TYPE_SELF,
RECORD_TYPE_DIRECTIVE_LIFECYCLE,
ProtoRecord
} from 'angular2/src/change_detection/proto_record';
export function main() { export function main() {
function r(funcOrValue, args, contextIndex, selfIndex, lastInBinding = false) { function r(funcOrValue, args, contextIndex, selfIndex, lastInBinding = false, mode = 99) {
return new ProtoRecord(99, "name", funcOrValue, args, null, contextIndex, null, selfIndex, null, return new ProtoRecord(mode, "name", funcOrValue, args, null, contextIndex, null, selfIndex,
null, lastInBinding, false); null, null, lastInBinding, false);
} }
describe("change detection - coalesce", () => { describe("change detection - coalesce", () => {
@ -54,5 +58,14 @@ export function main() {
expect(rs[1]).toEqual(new ProtoRecord(RECORD_TYPE_SELF, "self", null, [], null, 1, null, 2, expect(rs[1]).toEqual(new ProtoRecord(RECORD_TYPE_SELF, "self", null, [], null, 1, null, 2,
null, null, true, false)); null, null, true, false));
}); });
it("should not coalesce directive lifecycle records", () => {
var rs = coalesce([
r("onCheck", [], 0, 1, true, RECORD_TYPE_DIRECTIVE_LIFECYCLE),
r("onCheck", [], 0, 1, true, RECORD_TYPE_DIRECTIVE_LIFECYCLE)
]);
expect(rs.length).toEqual(2);
});
}); });
} }

View File

@ -42,6 +42,34 @@ main() {
}); });
}); });
describe("onCheck", () {
it("should be true when the directive implements OnCheck", () {
expect(metadata(DirectiveImplementingOnCheck, new Directive()).callOnCheck).toBe(true);
});
it("should be true when the lifecycle includes onCheck", () {
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onCheck])).callOnCheck).toBe(true);
});
it("should be false otherwise", () {
expect(metadata(DirectiveNoHooks, new Directive()).callOnCheck).toBe(false);
});
});
describe("onInit", () {
it("should be true when the directive implements OnInit", () {
expect(metadata(DirectiveImplementingOnInit, new Directive()).callOnInit).toBe(true);
});
it("should be true when the lifecycle includes onInit", () {
expect(metadata(DirectiveNoHooks, new Directive(lifecycle: [onInit])).callOnInit).toBe(true);
});
it("should be false otherwise", () {
expect(metadata(DirectiveNoHooks, new Directive()).callOnInit).toBe(false);
});
});
describe("onAllChangesDone", () { describe("onAllChangesDone", () {
it("should be true when the directive implements OnAllChangesDone", () { it("should be true when the directive implements OnAllChangesDone", () {
expect(metadata(DirectiveImplementingOnAllChangesDone, new Directive()).callOnAllChangesDone).toBe(true); expect(metadata(DirectiveImplementingOnAllChangesDone, new Directive()).callOnAllChangesDone).toBe(true);
@ -66,6 +94,14 @@ class DirectiveImplementingOnChange implements OnChange {
onChange(_){} onChange(_){}
} }
class DirectiveImplementingOnCheck implements OnCheck {
onCheck(){}
}
class DirectiveImplementingOnInit implements OnInit {
onInit(){}
}
class DirectiveImplementingOnDestroy implements OnDestroy { class DirectiveImplementingOnDestroy implements OnDestroy {
onDestroy(){} onDestroy(){}
} }

View File

@ -18,6 +18,8 @@ import {
Directive, Directive,
onChange, onChange,
onDestroy, onDestroy,
onCheck,
onInit,
onAllChangesDone onAllChangesDone
} from 'angular2/src/core/annotations_impl/annotations'; } from 'angular2/src/core/annotations_impl/annotations';
import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector'; import {DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
@ -64,6 +66,34 @@ export function main() {
}); });
}); });
describe("onInit", () => {
it("should be true when the directive has the onInit method", () => {
expect(metadata(DirectiveWithOnInitMethod, new Directive({})).callOnInit).toBe(true);
});
it("should be true when the lifecycle includes onDestroy", () => {
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onInit]})).callOnInit)
.toBe(true);
});
it("should be false otherwise",
() => { expect(metadata(DirectiveNoHooks, new Directive()).callOnInit).toBe(false); });
});
describe("onCheck", () => {
it("should be true when the directive has the onCheck method", () => {
expect(metadata(DirectiveWithOnCheckMethod, new Directive({})).callOnCheck).toBe(true);
});
it("should be true when the lifecycle includes onCheck", () => {
expect(metadata(DirectiveNoHooks, new Directive({lifecycle: [onCheck]})).callOnCheck)
.toBe(true);
});
it("should be false otherwise",
() => { expect(metadata(DirectiveNoHooks, new Directive()).callOnCheck).toBe(false); });
});
describe("onAllChangesDone", () => { describe("onAllChangesDone", () => {
it("should be true when the directive has the onAllChangesDone method", () => { it("should be true when the directive has the onAllChangesDone method", () => {
expect( expect(
@ -91,10 +121,18 @@ class DirectiveWithOnChangeMethod {
onChange(_) {} onChange(_) {}
} }
class DirectiveWithOnInitMethod {
onInit() {}
}
class DirectiveWithOnCheckMethod {
onCheck() {}
}
class DirectiveWithOnDestroyMethod { class DirectiveWithOnDestroyMethod {
onDestroy(_) {} onDestroy(_) {}
} }
class DirectiveWithOnAllChangesDoneMethod { class DirectiveWithOnAllChangesDoneMethod {
onAllChangesDone(_) {} onAllChangesDone() {}
} }

View File

@ -0,0 +1,72 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit,
IS_DARTIUM
} from 'angular2/test_lib';
import {ListWrapper} from 'angular2/src/facade/collection';
import {TestBed} from 'angular2/src/test_lib/test_bed';
import {Directive, Component, View, onCheck, onInit, onChange} from 'angular2/angular2';
import * as viewAnn from 'angular2/src/core/annotations_impl/view';
export function main() {
describe('directive lifecycle integration spec', () => {
var ctx;
beforeEach(() => { ctx = new MyComp(); });
it('should invoke lifecycle methods onChanges > onInit > onCheck',
inject([TestBed, AsyncTestCompleter], (tb, async) => {
tb.overrideView(
MyComp,
new viewAnn.View(
{template: '<div [field]="123" [lifecycle]></div>', directives: [LifecycleDir]}));
tb.createView(MyComp, {context: ctx})
.then((view) => {
var dir = view.rawView.elementInjectors[0].get(LifecycleDir);
view.detectChanges();
expect(dir.log).toEqual(["onChanges", "onInit", "onCheck"]);
view.detectChanges();
expect(dir.log).toEqual(["onChanges", "onInit", "onCheck", "onCheck"]);
async.done();
});
}));
});
}
@Directive({
selector: "[lifecycle]",
properties: {'field': 'field'},
lifecycle: [onChange, onCheck, onInit]
})
class LifecycleDir {
field;
log: List<string>;
constructor() { this.log = []; }
onChange(_) { ListWrapper.push(this.log, "onChanges"); }
onInit() { ListWrapper.push(this.log, "onInit"); }
onCheck() { ListWrapper.push(this.log, "onCheck"); }
}
@Component({selector: 'my-comp'})
@View({directives: []})
class MyComp {
}

View File

@ -191,7 +191,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations, objec
var parentProto = changeDetection.createProtoChangeDetector(new ChangeDetectorDefinition('parent', null, [], [], [])); var parentProto = changeDetection.createProtoChangeDetector(new ChangeDetectorDefinition('parent', null, [], [], []));
var parentCd = parentProto.instantiate(dispatcher); var parentCd = parentProto.instantiate(dispatcher);
var directiveRecord = new DirectiveRecord(new DirectiveIndex(0, 0), false, false, DEFAULT); var directiveRecord = new DirectiveRecord({directiveIndex: new DirectiveIndex(0, 0)});
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),