Skip to content

Commit

Permalink
feat(core/change_detection): implement ngOnChanges life cycle hook
Browse files Browse the repository at this point in the history
Closes #48
  • Loading branch information
Hotell committed Apr 3, 2016
1 parent f506d17 commit 7a19876
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 18 deletions.
1 change: 1 addition & 0 deletions core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export {
} from './src/core/directives';
export { Pipe, PipeTransform } from './src/core/pipes';
export * from './src/core/linker';
export * from './src/core/change_detection'
1 change: 1 addition & 0 deletions src/core/change_detection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SimpleChange } from './change_detection/change_detection_util';
35 changes: 35 additions & 0 deletions src/core/change_detection/change_detection_util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { isPresent, isBlank } from '../../facade/lang';
/**
* Represents a basic change from a previous to a new value.
*/
export class SimpleChange {
constructor(public previousValue: any, public currentValue: any) {}

/**
* Check whether the new value is the first value assigned.
*/
isFirstChange(): boolean { return this.previousValue === ChangeDetectionUtil.uninitialized; }
}

export class UninitializedValue {}

function _simpleChange(previousValue, currentValue): SimpleChange {
return new SimpleChange(previousValue, currentValue);
}

function _uninitializedValue(){
return new UninitializedValue();
}

/* tslint:disable:requireParameterType */
export class ChangeDetectionUtil {
static uninitialized: UninitializedValue = _uninitializedValue();

static simpleChange(previousValue: any, currentValue: any): SimpleChange {
return _simpleChange(previousValue, currentValue);
}

static isValueBlank(value: any): boolean { return isBlank(value); }

static s(value: any): string { return isPresent(value) ? `${value}` : ''; }
}
63 changes: 63 additions & 0 deletions src/core/change_detection/changes_queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @TODO this needs to be in a singleton
import { isFunction } from '../../facade/lang';

const TTL = 10;
let onChangesTtl = TTL;


export class ChangesQueue{

// The onChanges hooks should all be run together in a single digest
// When changes occur, the call to trigger their hooks will be added to this queue
onChangesQueue: Function[];

flushOnChangesQueue: () => void;

buildFlushOnChanges( $rootScope: ng.IRootScopeService ){

const _context = this;

buildFlushOnChangesCb( $rootScope );

function buildFlushOnChangesCb( $rootScope: ng.IRootScopeService ): () => void {

if(isFunction(_context.flushOnChangesQueue)){
return _context.flushOnChangesQueue;
}
_context.flushOnChangesQueue = getFlushOnChangesQueueCb($rootScope);
return _context.flushOnChangesQueue;

}

function getFlushOnChangesQueueCb( $rootScope: ng.IRootScopeService ): () => void {

// This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
return function _flushOnChangesQueue() {
try {
if (!(--onChangesTtl)) {
// We have hit the TTL limit so reset everything
_context.onChangesQueue = undefined;
throw new Error(`infchng, ${TTL} ngOnChanges() iterations reached. Aborting!\n`);
}
// We must run this hook in an apply since the $$postDigest runs outside apply
$rootScope.$apply(function() {
for (var i = 0, ii = _context.onChangesQueue.length; i < ii; ++i) {
_context.onChangesQueue[i]();
}
// Reset the queue to trigger a new schedule next time there is a change
_context.onChangesQueue = undefined;
});
} finally {
onChangesTtl++;
}
}


}


}

}

export const changesQueueService = new ChangesQueue();
4 changes: 3 additions & 1 deletion src/core/directives/directive_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
OnInit,
OnDestroy,
OnChildrenChanged,
ChildrenChangeHook
ChildrenChangeHook,
OnChanges
} from '../linker/directive_lifecycle_interfaces';
import { DirectiveMetadata, ComponentMetadata, LegacyDirectiveDefinition } from './metadata_directives';
import {
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface DirectiveCtrl extends
AfterViewChecked,
OnInit,
OnDestroy,
OnChanges,
OnChildrenChanged {
__readChildrenOrderScheduled?: boolean
__readViewChildrenOrderScheduled?: boolean
Expand Down
84 changes: 72 additions & 12 deletions src/core/directives/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { DirectiveMetadata, ComponentMetadata } from '../metadata_directives';
import { ListWrapper, StringMapWrapper } from '../../../facade/collections';
import { ChildrenChangeHook } from '../../linker/directive_lifecycle_interfaces';
import { QueryMetadata } from '../metadata_di';
import { DirectiveCtrl, NgmDirective, _setupDestroyHandler } from '../directive_provider';
import { DirectiveCtrl, NgmDirective } from '../directive_provider';
import { StringWrapper } from '../../../facade/primitives';
import { ChangeDetectionUtil } from '../../change_detection/change_detection_util';
import { changesQueueService } from '../../change_detection/changes_queue';

const REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/;

Expand Down Expand Up @@ -258,7 +260,8 @@ export function directiveControllerFactory<T extends DirectiveCtrl,U extends Typ

const _services = {
$parse: $injector.get<ng.IParseService>( '$parse' ),
$interpolate: $injector.get<ng.IInterpolateService>( '$interpolate' )
$interpolate: $injector.get<ng.IInterpolateService>( '$interpolate' ),
$rootScope: $injector.get<ng.IRootScopeService>( '$rootScope' )
};
const { $scope, $element, $attrs } = locals;

Expand All @@ -275,15 +278,15 @@ export function directiveControllerFactory<T extends DirectiveCtrl,U extends Typ
// StringMapWrapper.assign( instance, caller );

// setup @Input/@Output/@Attrs for @Component/@Directive
const _disposables = _createDirectiveBindings(
const { removeWatches, initialChanges } = _createDirectiveBindings(
!isAttrDirective( metadata ),
$scope,
$attrs,
instance,
metadata,
_services
);
_setupDestroyHandler( $scope, $element, instance, false, _disposables.watchers, _disposables.observers );
$scope.$on( '$destroy', ()=> removeWatches );

// change injectables to proper inject directives
// we wanna do this only if we inject some locals/directives
Expand All @@ -306,8 +309,9 @@ export function directiveControllerFactory<T extends DirectiveCtrl,U extends Typ
ddo.ngAfterViewInitBound = instance.ngAfterViewInit.bind(instance);
}*/



if ( isFunction( instance.ngOnChanges ) ) {
instance.ngOnChanges( initialChanges );
}

_ddo._ngOnInitBound = function _ngOnInitBound(){

Expand Down Expand Up @@ -545,7 +549,7 @@ export function _parseBindings({ inputs=[], outputs=[], attrs=[] }): ParsedBindi
* @param attributes
* @param ctrl
* @param metadata
* @param {{$interpolate,$parse}}
* @param {{$interpolate,$parse,$rootScope}}
* @returns {{watchers: Array, observers: Array}}
* @internal
* @private
Expand All @@ -556,9 +560,16 @@ export function _createDirectiveBindings(
attributes: ng.IAttributes,
ctrl: any,
metadata: DirectiveMetadata,
{$interpolate,$parse}:{$interpolate?:ng.IInterpolateService,$parse?:ng.IParseService}
): {watchers:Function[], observers:Function[]} {

{ $interpolate, $parse, $rootScope }: {
$interpolate?: ng.IInterpolateService,
$parse?: ng.IParseService,
$rootScope?: ng.IRootScopeService
}
): {
initialChanges: {[key:string]:any},
removeWatches: Function,
_watchers: {watchers: Function[], observers: Function[]}
} {

/* let BOOLEAN_ATTR = {};
'multiple,selected,checked,disabled,readOnly,required,open'
Expand All @@ -575,6 +586,14 @@ export function _createDirectiveBindings(
const _internalWatchers = [];
const _internalObservers = [];

// onChanges tmp vars
const initialChanges = {};
let changes;

// this will create flush queue internally only once
// we need to call this here because we need $rootScope service
changesQueueService.buildFlushOnChanges( $rootScope );

// setup @Inputs '<' or '='
// by default '='
StringMapWrapper.forEach( parsedBindings.inputs, ( config: ParsedBindingValue, propName: string ) => {
Expand Down Expand Up @@ -622,8 +641,11 @@ export function _createDirectiveBindings(
const parentGet = $parse( attributes[ attrName ] );

ctrl[ propName ] = parentGet( scope );
initialChanges[ propName ] = ChangeDetectionUtil.simpleChange( ChangeDetectionUtil.uninitialized, ctrl[ propName ] );

return scope.$watch( parentGet, function parentValueWatchAction( newParentValue ) {
const oldValue = ctrl[ propName ];
recordChanges( propName, newParentValue, oldValue );
ctrl[ propName ] = newParentValue;
}, parentGet.literal );

Expand Down Expand Up @@ -711,6 +733,8 @@ export function _createDirectiveBindings(

const _disposeObserver = attributes.$observe( attrName, function ( value ) {
if ( isString( value ) ) {
const oldValue = ctrl[ propName ];
recordChanges( propName, value, oldValue );
ctrl[ propName ] = value;
}
} );
Expand All @@ -727,13 +751,49 @@ export function _createDirectiveBindings(
ctrl[ propName ] = lastValue;
}

initialChanges[ propName ] = ChangeDetectionUtil.simpleChange( ChangeDetectionUtil.uninitialized, ctrl[ propName ] );
return _disposeObserver;

}

function recordChanges<T>( key: string, currentValue: T, previousValue: T ): void {
if (isFunction(ctrl.ngOnChanges) && currentValue !== previousValue) {
// If we have not already scheduled the top level onChangesQueue handler then do so now
if (!changesQueueService.onChangesQueue) {
(scope as any).$$postDigest(changesQueueService.flushOnChangesQueue);
changesQueueService.onChangesQueue = [];
}
// If we have not already queued a trigger of onChanges for this controller then do so now
if (!changes) {
changes = {};
changesQueueService.onChangesQueue.push(triggerOnChangesHook);
}
// If the has been a change on this property already then we need to reuse the previous value
if (changes[key]) {
previousValue = changes[key].previousValue;
}
// Store this change
changes[key] = ChangeDetectionUtil.simpleChange(previousValue, currentValue);
}
}

function triggerOnChangesHook(): void {
ctrl.ngOnChanges( changes );
// Now clear the changes so that we schedule onChanges when more changes arrive
changes = undefined;
}

function removeWatches(): void {
const removeWatchCollection = [ ..._internalWatchers, ..._internalObservers ];
for ( var i = 0, ii = removeWatchCollection.length; i < ii; ++i ) {
removeWatchCollection[ i ]();
}
}

return {
watchers: _internalWatchers,
observers: _internalObservers
initialChanges,
removeWatches,
_watchers: { watchers: _internalWatchers, observers: _internalObservers }
};

}
3 changes: 2 additions & 1 deletion src/core/linker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export {
AfterContentChecked,
AfterViewChecked,
OnDestroy,
OnInit
OnInit,
OnChanges
} from './linker/directive_lifecycle_interfaces';
44 changes: 44 additions & 0 deletions src/core/linker/directive_lifecycle_interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SimpleChange } from '../change_detection/change_detection_util';
export enum LifecycleHooks {
OnInit,
OnDestroy,
OnChanges,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
Expand All @@ -14,6 +16,7 @@ export enum LifecycleHooks {
export var LIFECYCLE_HOOKS_VALUES = [
LifecycleHooks.OnInit,
LifecycleHooks.OnDestroy,
LifecycleHooks.OnChanges,
LifecycleHooks.AfterContentInit,
LifecycleHooks.AfterContentChecked,
LifecycleHooks.AfterViewInit,
Expand All @@ -28,6 +31,7 @@ export enum ChildrenChangeHook{

/**
* Lifecycle hooks are guaranteed to be called in the following order:
* - `OnChanges` (if any bindings have changed),
* - `OnInit` (after the first check only),
* - `AfterContentInit`,
* - `AfterContentChecked`,
Expand All @@ -36,6 +40,46 @@ export enum ChildrenChangeHook{
* - `OnDestroy` (at the very end before destruction)
*/

/**
* Implement this interface to get notified when any data-bound property of your directive changes.
*
* `ngOnChanges` is called right after the data-bound properties have been checked and before view
* and content children are checked if at least one of them has changed.
*
* The `changes` parameter contains an entry for each of the changed data-bound property. The key is
* the property name and the value is an instance of {@link SimpleChange}.
*
* ### Example ([live example](http://plnkr.co/edit/AHrB6opLqHDBPkt4KpdT?p=preview)):
*
* ```typescript
* @Component({
* selector: 'my-cmp',
* template: `<p>myProp = {{myProp}}</p>`
* })
* class MyComponent implements OnChanges {
* @Input() myProp: any;
*
* ngOnChanges(changes: {[propName: string]: SimpleChange}) {
* console.log('ngOnChanges - myProp = ' + changes['myProp'].currentValue);
* }
* }
*
* @Component({
* selector: 'app',
* template: `
* <button (click)="value = value + 1">Change MyComponent</button>
* <my-cmp [my-prop]="value"></my-cmp>`,
* directives: [MyComponent]
* })
* export class App {
* value = 0;
* }
*
* bootstrap(App).catch(err => console.error(err));
* ```
*/
export interface OnChanges { ngOnChanges(changes: {[key: string]: SimpleChange}); }

/**
* Implement this interface to execute custom initialization logic after your directive's
* data-bound properties have been initialized.
Expand Down
Loading

0 comments on commit 7a19876

Please sign in to comment.