diff --git a/modules/angular2/src/change_detection/binding_record.ts b/modules/angular2/src/change_detection/binding_record.ts index 01130b9c77..616e4d02be 100644 --- a/modules/angular2/src/change_detection/binding_record.ts +++ b/modules/angular2/src/change_detection/binding_record.ts @@ -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); } } \ No newline at end of file diff --git a/modules/angular2/src/change_detection/change_detection_jit_generator.ts b/modules/angular2/src/change_detection/change_detection_jit_generator.ts index 233ce2682f..bb933d658e 100644 --- a/modules/angular2/src/change_detection/change_detection_jit_generator.ts +++ b/modules/angular2/src/change_detection/change_detection_jit_generator.ts @@ -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 { - return this.directiveRecords.map((d) => this._genGetDirective(d.directiveIndex)); + return this.directiveRecords.map(d => this._genGetDirective(d.directiveIndex)); } _genGetDetectorFieldNames(): List { @@ -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 { diff --git a/modules/angular2/src/change_detection/coalesce.ts b/modules/angular2/src/change_detection/coalesce.ts index 77d9baefa8..3eb640ff1f 100644 --- a/modules/angular2/src/change_detection/coalesce.ts +++ b/modules/angular2/src/change_detection/coalesce.ts @@ -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) { - 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)); } diff --git a/modules/angular2/src/change_detection/directive_record.ts b/modules/angular2/src/change_detection/directive_record.ts index 7f8cd0bceb..edfadd2668 100644 --- a/modules/angular2/src/change_detection/directive_record.ts +++ b/modules/angular2/src/change_detection/directive_record.ts @@ -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); } } \ No newline at end of file diff --git a/modules/angular2/src/change_detection/dynamic_change_detector.ts b/modules/angular2/src/change_detection/dynamic_change_detector.ts index 12d6e342b8..8bdcc6ff89 100644 --- a/modules/angular2/src/change_detection/dynamic_change_detector.ts +++ b/modules/angular2/src/change_detection/dynamic_change_detector.ts @@ -27,12 +27,13 @@ import { import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions'; export class DynamicChangeDetector extends AbstractChangeDetector { - locals: any; + locals: any = null; values: List; changes: List; pipes: List; prevContexts: List; - directives: any; + directives: any = null; + alreadyChecked: boolean = false; constructor(private changeControlStrategy: string, private dispatcher: any, private pipeRegistry: PipeRegistry, private protos: List, @@ -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); diff --git a/modules/angular2/src/change_detection/proto_change_detector.ts b/modules/angular2/src/change_detection/proto_change_detector.ts index 9b0dd76e97..e951df5de9 100644 --- a/modules/angular2/src/change_detection/proto_change_detector.ts +++ b/modules/angular2/src/change_detection/proto_change_detector.ts @@ -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 = null) { + add(b: BindingRecord, variableNames: List = 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) { + 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 { diff --git a/modules/angular2/src/change_detection/proto_record.ts b/modules/angular2/src/change_detection/proto_record.ts index 68e2625ee4..e4be84df3b 100644 --- a/modules/angular2/src/change_detection/proto_record.ts +++ b/modules/angular2/src/change_detection/proto_record.ts @@ -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, @@ -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; } } diff --git a/modules/angular2/src/core/annotations/annotations.ts b/modules/angular2/src/core/annotations/annotations.ts index e9dde7d412..7f0a1b8873 100644 --- a/modules/angular2/src/core/annotations/annotations.ts +++ b/modules/angular2/src/core/annotations/annotations.ts @@ -8,5 +8,7 @@ export { Directive as DirectiveAnnotation, onDestroy, onChange, + onCheck, + onInit, onAllChangesDone } from '../annotations_impl/annotations'; diff --git a/modules/angular2/src/core/annotations_impl/annotations.ts b/modules/angular2/src/core/annotations_impl/annotations.ts index 841b190857..f02497390f 100644 --- a/modules/angular2/src/core/annotations_impl/annotations.ts +++ b/modules/angular2/src/core/annotations_impl/annotations.ts @@ -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. * diff --git a/modules/angular2/src/core/compiler/directive_lifecycle_reflector.dart b/modules/angular2/src/core/compiler/directive_lifecycle_reflector.dart index 42b7d16857..cae92deeb0 100644 --- a/modules/angular2/src/core/compiler/directive_lifecycle_reflector.dart +++ b/modules/angular2/src/core/compiler/directive_lifecycle_reflector.dart @@ -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); diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 637dc09ecb..df7d326aec 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -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 diff --git a/modules/angular2/src/core/compiler/interfaces.ts b/modules/angular2/src/core/compiler/interfaces.ts index 0c08334353..3df4f04d54 100644 --- a/modules/angular2/src/core/compiler/interfaces.ts +++ b/modules/angular2/src/core/compiler/interfaces.ts @@ -11,6 +11,16 @@ export interface OnChange { onChange(changes: StringMap): 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. diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index bd13a408e4..7025b6a0ec 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -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); diff --git a/modules/angular2/src/facade/lang.dart b/modules/angular2/src/facade/lang.dart index 3d3db10deb..4c1736b423 100644 --- a/modules/angular2/src/facade/lang.dart +++ b/modules/angular2/src/facade/lang.dart @@ -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; } diff --git a/modules/angular2/src/facade/lang.ts b/modules/angular2/src/facade/lang.ts index 49aba2896d..357fce3b03 100644 --- a/modules/angular2/src/facade/lang.ts +++ b/modules/angular2/src/facade/lang.ts @@ -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"); } diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 2365c58913..c4c57cc387 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -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; } diff --git a/modules/angular2/test/change_detection/change_detector_spec.ts b/modules/angular2/test/change_detection/change_detector_spec.ts index 758be0e06f..7414291f82 100644 --- a/modules/angular2/test/change_detection/change_detector_spec.ts +++ b/modules/angular2/test/change_detection/change_detector_spec.ts @@ -311,10 +311,26 @@ export function main() { }); describe("updating directives", () => { - var dirRecord1 = new DirectiveRecord(new DirectiveIndex(0, 0), true, true, DEFAULT); - var dirRecord2 = new DirectiveRecord(new DirectiveIndex(0, 1), true, true, DEFAULT); - var dirRecordNoCallbacks = - new DirectiveRecord(new DirectiveIndex(0, 0), false, false, DEFAULT); + var dirRecord1 = new DirectiveRecord({ + directiveIndex: new DirectiveIndex(0, 0), + callOnChange: true, + 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) { return BindingRecord.createForDirective(ast(exp), "a", (o, v) => o.a = v, @@ -353,7 +369,9 @@ export function main() { var pcd = createProtoChangeDetector([ updateA("1", dirRecord1), updateB("2", dirRecord1), - updateA("3", dirRecord2) + BindingRecord.createDirectiveOnChange(dirRecord1), + updateA("3", dirRecord2), + BindingRecord.createDirectiveOnChange(dirRecord2) ], [], [dirRecord1, dirRecord2]); @@ -366,10 +384,13 @@ export function main() { expect(directive1.changes).toEqual({'a': 1, 'b': 2}); 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); @@ -377,7 +398,63 @@ export function main() { 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", () => { 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", () => { var directive = new TestDirective(); @@ -688,8 +765,8 @@ export function main() { checkedDetector.mode = CHECKED; // this directive is a component with ON_PUSH change detection - dirRecordWithOnPush = - new DirectiveRecord(new DirectiveIndex(0, 0), false, false, ON_PUSH); + dirRecordWithOnPush = new DirectiveRecord( + {directiveIndex: new DirectiveIndex(0, 0), changeDetection: ON_PUSH}); // a record updating a component updateDirWithOnPushRecord = BindingRecord.createForDirective( @@ -943,15 +1020,23 @@ class TestDirective { changes; onChangesDoneCalled; onChangesDoneSpy; + onCheckCalled; + onInitCalled; constructor(onChangesDoneSpy = null) { this.onChangesDoneCalled = false; + this.onCheckCalled = false; + this.onInitCalled = false; this.onChangesDoneSpy = onChangesDoneSpy; this.a = null; this.b = null; this.changes = null; } + onCheck() { this.onCheckCalled = true; } + + onInit() { this.onInitCalled = true; } + onChange(changes) { var r = {}; StringMapWrapper.forEach(changes, (c, key) => r[key] = c.currentValue); diff --git a/modules/angular2/test/change_detection/coalesce_spec.ts b/modules/angular2/test/change_detection/coalesce_spec.ts index 620e0ad561..c08fb6e2f2 100644 --- a/modules/angular2/test/change_detection/coalesce_spec.ts +++ b/modules/angular2/test/change_detection/coalesce_spec.ts @@ -1,12 +1,16 @@ import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'angular2/test_lib'; 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() { - function r(funcOrValue, args, contextIndex, selfIndex, lastInBinding = false) { - return new ProtoRecord(99, "name", funcOrValue, args, null, contextIndex, null, selfIndex, null, - null, lastInBinding, false); + function r(funcOrValue, args, contextIndex, selfIndex, lastInBinding = false, mode = 99) { + return new ProtoRecord(mode, "name", funcOrValue, args, null, contextIndex, null, selfIndex, + null, null, lastInBinding, false); } 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, 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); + }); }); } diff --git a/modules/angular2/test/core/compiler/directive_lifecycle_spec.dart b/modules/angular2/test/core/compiler/directive_lifecycle_spec.dart index 598f0985de..bff7a9e23b 100644 --- a/modules/angular2/test/core/compiler/directive_lifecycle_spec.dart +++ b/modules/angular2/test/core/compiler/directive_lifecycle_spec.dart @@ -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", () { it("should be true when the directive implements OnAllChangesDone", () { expect(metadata(DirectiveImplementingOnAllChangesDone, new Directive()).callOnAllChangesDone).toBe(true); @@ -66,6 +94,14 @@ class DirectiveImplementingOnChange implements OnChange { onChange(_){} } +class DirectiveImplementingOnCheck implements OnCheck { + onCheck(){} +} + +class DirectiveImplementingOnInit implements OnInit { + onInit(){} +} + class DirectiveImplementingOnDestroy implements OnDestroy { onDestroy(){} } diff --git a/modules/angular2/test/core/compiler/directive_lifecycle_spec.ts b/modules/angular2/test/core/compiler/directive_lifecycle_spec.ts index eb14d08680..8e5bc5f739 100644 --- a/modules/angular2/test/core/compiler/directive_lifecycle_spec.ts +++ b/modules/angular2/test/core/compiler/directive_lifecycle_spec.ts @@ -18,6 +18,8 @@ import { Directive, onChange, onDestroy, + onCheck, + onInit, onAllChangesDone } from 'angular2/src/core/annotations_impl/annotations'; 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", () => { it("should be true when the directive has the onAllChangesDone method", () => { expect( @@ -91,10 +121,18 @@ class DirectiveWithOnChangeMethod { onChange(_) {} } +class DirectiveWithOnInitMethod { + onInit() {} +} + +class DirectiveWithOnCheckMethod { + onCheck() {} +} + class DirectiveWithOnDestroyMethod { onDestroy(_) {} } class DirectiveWithOnAllChangesDoneMethod { - onAllChangesDone(_) {} + onAllChangesDone() {} } \ No newline at end of file diff --git a/modules/angular2/test/core/directive_lifecycle_integration_spec.ts b/modules/angular2/test/core/directive_lifecycle_integration_spec.ts new file mode 100644 index 0000000000..03eee424f9 --- /dev/null +++ b/modules/angular2/test/core/directive_lifecycle_integration_spec.ts @@ -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: '
', 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; + + 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 { +} \ No newline at end of file diff --git a/modules/benchmarks/src/change_detection/change_detection_benchmark.js b/modules/benchmarks/src/change_detection/change_detection_benchmark.js index 90f1fa109e..8954214ae6 100644 --- a/modules/benchmarks/src/change_detection/change_detection_benchmark.js +++ b/modules/benchmarks/src/change_detection/change_detection_benchmark.js @@ -191,7 +191,7 @@ function setUpChangeDetection(changeDetection:ChangeDetection, iterations, objec var parentProto = changeDetection.createProtoChangeDetector(new ChangeDetectorDefinition('parent', null, [], [], [])); 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 = [ BindingRecord.createForDirective(parser.parseBinding('field0', null), "field0", reflector.setter("field0"), directiveRecord), BindingRecord.createForDirective(parser.parseBinding('field1', null), "field1", reflector.setter("field1"), directiveRecord),