feat(upgrade): support the `$doCheck()` lifecycle hook in `UpgradeComponent` (#13015)

This commit is contained in:
Georgios Kalpakas 2016-12-21 02:18:43 +02:00 committed by Chuck Jazdzewski
parent fcd116fdc0
commit 9da4c259a5
2 changed files with 178 additions and 9 deletions

View File

@ -35,12 +35,13 @@ interface IBindingDestination {
} }
interface IControllerInstance extends IBindingDestination { interface IControllerInstance extends IBindingDestination {
$doCheck?: () => void;
$onDestroy?: () => void; $onDestroy?: () => void;
$onInit?: () => void; $onInit?: () => void;
$postLink?: () => void; $postLink?: () => void;
} }
type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink'; type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
/** /**
* @whatItDoes * @whatItDoes
@ -168,6 +169,13 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
this.callLifecycleHook('$onInit', this.controllerInstance); 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 link = this.directive.link;
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre; const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link; const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
@ -228,7 +236,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
} }
private callLifecycleHook(method: LifecycleHook, context: IBindingDestination, arg?: any) { private callLifecycleHook(method: LifecycleHook, context: IBindingDestination, arg?: any) {
if (context && typeof context[method] === 'function') { if (context && isFunction(context[method])) {
context[method](arg); context[method](arg);
} }
} }
@ -422,7 +430,11 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
function getOrCall<T>(property: Function | T): T { 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'`. // NOTE: Only works for `typeof T !== 'object'`.

View File

@ -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(() => { it('should call `$onDestroy()` on controller', async(() => {
const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA'); const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB'); const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');
@ -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` // Define `ng1Component`
const ng1Component: angular.IComponent = { 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: '<'}, bindings: {value: '<'},
controller: class { controller: class {
calls: string[] = []; calls: string[] = [];
$onChanges() { this.calls.push('$onChanges'); } $onInit() { $onChanges() { this.calls.push('$onChanges'); }
this.calls.push('$onInit');
} $postLink() { this.calls.push('$postLink'); } $onInit() { this.calls.push('$onInit'); }
$doCheck() { this.calls.push('$doCheck'); }
$postLink() { this.calls.push('$postLink'); }
} }
}; };
@ -2573,7 +2729,8 @@ export function main() {
const element = html(`<ng2></ng2>`); const element = html(`<ng2></ng2>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
expect(multiTrim(element.textContent)).toBe('$onChanges > $onInit > $postLink'); expect(multiTrim(element.textContent))
.toBe('$onChanges > $onInit > $doCheck > $postLink');
}); });
})); }));
}); });