Skip to content

Commit

Permalink
feat(upgrade): support the $doCheck() lifecycle hook in `UpgradeCom…
Browse files Browse the repository at this point in the history
…ponent` (#13015)
  • Loading branch information
gkalpak authored and chuckjaz committed Dec 21, 2016
1 parent fcd116f commit 9da4c25
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 9 deletions.
18 changes: 15 additions & 3 deletions modules/@angular/upgrade/src/aot/upgrade_component.ts
Expand Up @@ -35,12 +35,13 @@ interface IBindingDestination {
}

interface IControllerInstance extends IBindingDestination {
$doCheck?: () => void;
$onDestroy?: () => void;
$onInit?: () => void;
$postLink?: () => void;
}

type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';

/**
* @whatItDoes
Expand Down Expand Up @@ -168,6 +169,13 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {

this.callLifecycleHook('$onInit', this.controllerInstance);

if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
const callDoCheck = () => this.callLifecycleHook('$doCheck', this.controllerInstance);

this.$componentScope.$parent.$watch(callDoCheck);
callDoCheck();
}

const link = this.directive.link;
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
Expand Down Expand Up @@ -228,7 +236,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
}

private callLifecycleHook(method: LifecycleHook, context: IBindingDestination, arg?: any) {
if (context && typeof context[method] === 'function') {
if (context && isFunction(context[method])) {
context[method](arg);
}
}
Expand Down Expand Up @@ -422,7 +430,11 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {


function getOrCall<T>(property: Function | T): T {
return typeof(property) === 'function' ? property() : property;
return isFunction(property) ? property() : property;
}

function isFunction(value: any): value is Function {
return typeof value === 'function';
}

// NOTE: Only works for `typeof T !== 'object'`.
Expand Down
Expand Up @@ -2335,6 +2335,155 @@ export function main() {
}));


it('should call `$doCheck()` on controller', async(() => {
const controllerDoCheckA = jasmine.createSpy('controllerDoCheckA');
const controllerDoCheckB = jasmine.createSpy('controllerDoCheckB');

// Define `ng1Directive`
const ng1DirectiveA: angular.IDirective = {
template: 'ng1A',
bindToController: false,
controller: class {$doCheck() { controllerDoCheckA(); }}
};

const ng1DirectiveB: angular.IDirective = {
template: 'ng1B',
bindToController: true,
controller: class {constructor() { (this as any)['$doCheck'] = controllerDoCheckB; }}
};

// Define `Ng1ComponentFacade`
@Directive({selector: 'ng1A'})
class Ng1ComponentAFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1A', elementRef, injector);
}
}

@Directive({selector: 'ng1B'})
class Ng1ComponentBFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1B', elementRef, injector);
}
}

// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
class Ng2Component {
}

// Define `ng1Module`
const ng1Module = angular.module('ng1Module', [])
.directive('ng1A', () => ng1DirectiveA)
.directive('ng1B', () => ng1DirectiveB)
.directive('ng2', downgradeComponent({component: Ng2Component}));

// Define `Ng2Module`
@NgModule({
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}

// Bootstrap
const element = html(`<ng2></ng2>`);

bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
// Initial change
expect(controllerDoCheckA.calls.count()).toBe(1);
expect(controllerDoCheckB.calls.count()).toBe(1);

// Run a `$digest`
// (Since it's the first one since the `$doCheck` watcher was added,
// the `watchFn` will be run twice.)
digest(adapter);
expect(controllerDoCheckA.calls.count()).toBe(3);
expect(controllerDoCheckB.calls.count()).toBe(3);

// Run another `$digest`
digest(adapter);
expect(controllerDoCheckA.calls.count()).toBe(4);
expect(controllerDoCheckB.calls.count()).toBe(4);
});
}));

it('should not call `$doCheck()` on scope', async(() => {
const scopeDoCheck = jasmine.createSpy('scopeDoCheck');

// Define `ng1Directive`
const ng1DirectiveA: angular.IDirective = {
template: 'ng1A',
bindToController: false,
controller: class {
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
}
};

const ng1DirectiveB: angular.IDirective = {
template: 'ng1B',
bindToController: true,
controller: class {
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
}
};

// Define `Ng1ComponentFacade`
@Directive({selector: 'ng1A'})
class Ng1ComponentAFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1A', elementRef, injector);
}
}

@Directive({selector: 'ng1B'})
class Ng1ComponentBFacade extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
super('ng1B', elementRef, injector);
}
}

// Define `Ng2Component`
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
class Ng2Component {
}

// Define `ng1Module`
const ng1Module = angular.module('ng1Module', [])
.directive('ng1A', () => ng1DirectiveA)
.directive('ng1B', () => ng1DirectiveB)
.directive('ng2', downgradeComponent({component: Ng2Component}));

// Define `Ng2Module`
@NgModule({
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}

// Bootstrap
const element = html(`<ng2></ng2>`);

bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
// Initial change
expect(scopeDoCheck).not.toHaveBeenCalled();

// Run a `$digest`
digest(adapter);
expect(scopeDoCheck).not.toHaveBeenCalled();

// Run another `$digest`
digest(adapter);
expect(scopeDoCheck).not.toHaveBeenCalled();
});
}));


it('should call `$onDestroy()` on controller', async(() => {
const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');
Expand Down Expand Up @@ -2525,17 +2674,24 @@ export function main() {
});
}));

it('should be called in order `$onChanges()` > `$onInit()` > `$postLink()`', async(() => {
it('should be called in order `$onChanges()` > `$onInit()` > `$doCheck()` > `$postLink()`',
async(() => {
// Define `ng1Component`
const ng1Component: angular.IComponent = {
template: '{{ $ctrl.calls.join(" > ") }}',
// `$doCheck()` will keep getting called as long as the interpolated value keeps
// changing (by appending `> $doCheck`). Only care about the first 4 values.
template: '{{ $ctrl.calls.slice(0, 4).join(" > ") }}',
bindings: {value: '<'},
controller: class {
calls: string[] = [];

$onChanges() { this.calls.push('$onChanges'); } $onInit() {
this.calls.push('$onInit');
} $postLink() { this.calls.push('$postLink'); }
$onChanges() { this.calls.push('$onChanges'); }

$onInit() { this.calls.push('$onInit'); }

$doCheck() { this.calls.push('$doCheck'); }

$postLink() { this.calls.push('$postLink'); }
}
};

Expand Down Expand Up @@ -2573,7 +2729,8 @@ export function main() {
const element = html(`<ng2></ng2>`);

bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
expect(multiTrim(element.textContent)).toBe('$onChanges > $onInit > $postLink');
expect(multiTrim(element.textContent))
.toBe('$onChanges > $onInit > $doCheck > $postLink');
});
}));
});
Expand Down

0 comments on commit 9da4c25

Please sign in to comment.