feat(upgrade): support the `$doCheck()` lifecycle hook in `UpgradeComponent` (#13015)
This commit is contained in:
parent
fcd116fdc0
commit
9da4c259a5
|
@ -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'`.
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue