Skip to content

Commit 9da4c25

Browse files
gkalpakchuckjaz
authored andcommitted
feat(upgrade): support the $doCheck() lifecycle hook in UpgradeComponent (#13015)
1 parent fcd116f commit 9da4c25

File tree

2 files changed

+178
-9
lines changed

2 files changed

+178
-9
lines changed

modules/@angular/upgrade/src/aot/upgrade_component.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ interface IBindingDestination {
3535
}
3636

3737
interface IControllerInstance extends IBindingDestination {
38+
$doCheck?: () => void;
3839
$onDestroy?: () => void;
3940
$onInit?: () => void;
4041
$postLink?: () => void;
4142
}
4243

43-
type LifecycleHook = '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
44+
type LifecycleHook = '$doCheck' | '$onChanges' | '$onDestroy' | '$onInit' | '$postLink';
4445

4546
/**
4647
* @whatItDoes
@@ -168,6 +169,13 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
168169

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

172+
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
173+
const callDoCheck = () => this.callLifecycleHook('$doCheck', this.controllerInstance);
174+
175+
this.$componentScope.$parent.$watch(callDoCheck);
176+
callDoCheck();
177+
}
178+
171179
const link = this.directive.link;
172180
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
173181
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
@@ -228,7 +236,7 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
228236
}
229237

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

423431

424432
function getOrCall<T>(property: Function | T): T {
425-
return typeof(property) === 'function' ? property() : property;
433+
return isFunction(property) ? property() : property;
434+
}
435+
436+
function isFunction(value: any): value is Function {
437+
return typeof value === 'function';
426438
}
427439

428440
// NOTE: Only works for `typeof T !== 'object'`.

modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts

Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,6 +2335,155 @@ export function main() {
23352335
}));
23362336

23372337

2338+
it('should call `$doCheck()` on controller', async(() => {
2339+
const controllerDoCheckA = jasmine.createSpy('controllerDoCheckA');
2340+
const controllerDoCheckB = jasmine.createSpy('controllerDoCheckB');
2341+
2342+
// Define `ng1Directive`
2343+
const ng1DirectiveA: angular.IDirective = {
2344+
template: 'ng1A',
2345+
bindToController: false,
2346+
controller: class {$doCheck() { controllerDoCheckA(); }}
2347+
};
2348+
2349+
const ng1DirectiveB: angular.IDirective = {
2350+
template: 'ng1B',
2351+
bindToController: true,
2352+
controller: class {constructor() { (this as any)['$doCheck'] = controllerDoCheckB; }}
2353+
};
2354+
2355+
// Define `Ng1ComponentFacade`
2356+
@Directive({selector: 'ng1A'})
2357+
class Ng1ComponentAFacade extends UpgradeComponent {
2358+
constructor(elementRef: ElementRef, injector: Injector) {
2359+
super('ng1A', elementRef, injector);
2360+
}
2361+
}
2362+
2363+
@Directive({selector: 'ng1B'})
2364+
class Ng1ComponentBFacade extends UpgradeComponent {
2365+
constructor(elementRef: ElementRef, injector: Injector) {
2366+
super('ng1B', elementRef, injector);
2367+
}
2368+
}
2369+
2370+
// Define `Ng2Component`
2371+
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
2372+
class Ng2Component {
2373+
}
2374+
2375+
// Define `ng1Module`
2376+
const ng1Module = angular.module('ng1Module', [])
2377+
.directive('ng1A', () => ng1DirectiveA)
2378+
.directive('ng1B', () => ng1DirectiveB)
2379+
.directive('ng2', downgradeComponent({component: Ng2Component}));
2380+
2381+
// Define `Ng2Module`
2382+
@NgModule({
2383+
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
2384+
entryComponents: [Ng2Component],
2385+
imports: [BrowserModule, UpgradeModule]
2386+
})
2387+
class Ng2Module {
2388+
ngDoBootstrap() {}
2389+
}
2390+
2391+
// Bootstrap
2392+
const element = html(`<ng2></ng2>`);
2393+
2394+
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
2395+
// Initial change
2396+
expect(controllerDoCheckA.calls.count()).toBe(1);
2397+
expect(controllerDoCheckB.calls.count()).toBe(1);
2398+
2399+
// Run a `$digest`
2400+
// (Since it's the first one since the `$doCheck` watcher was added,
2401+
// the `watchFn` will be run twice.)
2402+
digest(adapter);
2403+
expect(controllerDoCheckA.calls.count()).toBe(3);
2404+
expect(controllerDoCheckB.calls.count()).toBe(3);
2405+
2406+
// Run another `$digest`
2407+
digest(adapter);
2408+
expect(controllerDoCheckA.calls.count()).toBe(4);
2409+
expect(controllerDoCheckB.calls.count()).toBe(4);
2410+
});
2411+
}));
2412+
2413+
it('should not call `$doCheck()` on scope', async(() => {
2414+
const scopeDoCheck = jasmine.createSpy('scopeDoCheck');
2415+
2416+
// Define `ng1Directive`
2417+
const ng1DirectiveA: angular.IDirective = {
2418+
template: 'ng1A',
2419+
bindToController: false,
2420+
controller: class {
2421+
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
2422+
}
2423+
};
2424+
2425+
const ng1DirectiveB: angular.IDirective = {
2426+
template: 'ng1B',
2427+
bindToController: true,
2428+
controller: class {
2429+
constructor(private $scope: angular.IScope) { $scope['$doCheck'] = scopeDoCheck; }
2430+
}
2431+
};
2432+
2433+
// Define `Ng1ComponentFacade`
2434+
@Directive({selector: 'ng1A'})
2435+
class Ng1ComponentAFacade extends UpgradeComponent {
2436+
constructor(elementRef: ElementRef, injector: Injector) {
2437+
super('ng1A', elementRef, injector);
2438+
}
2439+
}
2440+
2441+
@Directive({selector: 'ng1B'})
2442+
class Ng1ComponentBFacade extends UpgradeComponent {
2443+
constructor(elementRef: ElementRef, injector: Injector) {
2444+
super('ng1B', elementRef, injector);
2445+
}
2446+
}
2447+
2448+
// Define `Ng2Component`
2449+
@Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
2450+
class Ng2Component {
2451+
}
2452+
2453+
// Define `ng1Module`
2454+
const ng1Module = angular.module('ng1Module', [])
2455+
.directive('ng1A', () => ng1DirectiveA)
2456+
.directive('ng1B', () => ng1DirectiveB)
2457+
.directive('ng2', downgradeComponent({component: Ng2Component}));
2458+
2459+
// Define `Ng2Module`
2460+
@NgModule({
2461+
declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
2462+
entryComponents: [Ng2Component],
2463+
imports: [BrowserModule, UpgradeModule]
2464+
})
2465+
class Ng2Module {
2466+
ngDoBootstrap() {}
2467+
}
2468+
2469+
// Bootstrap
2470+
const element = html(`<ng2></ng2>`);
2471+
2472+
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
2473+
// Initial change
2474+
expect(scopeDoCheck).not.toHaveBeenCalled();
2475+
2476+
// Run a `$digest`
2477+
digest(adapter);
2478+
expect(scopeDoCheck).not.toHaveBeenCalled();
2479+
2480+
// Run another `$digest`
2481+
digest(adapter);
2482+
expect(scopeDoCheck).not.toHaveBeenCalled();
2483+
});
2484+
}));
2485+
2486+
23382487
it('should call `$onDestroy()` on controller', async(() => {
23392488
const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
23402489
const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');
@@ -2525,17 +2674,24 @@ export function main() {
25252674
});
25262675
}));
25272676

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

2536-
$onChanges() { this.calls.push('$onChanges'); } $onInit() {
2537-
this.calls.push('$onInit');
2538-
} $postLink() { this.calls.push('$postLink'); }
2688+
$onChanges() { this.calls.push('$onChanges'); }
2689+
2690+
$onInit() { this.calls.push('$onInit'); }
2691+
2692+
$doCheck() { this.calls.push('$doCheck'); }
2693+
2694+
$postLink() { this.calls.push('$postLink'); }
25392695
}
25402696
};
25412697

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

25752731
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
2576-
expect(multiTrim(element.textContent)).toBe('$onChanges > $onInit > $postLink');
2732+
expect(multiTrim(element.textContent))
2733+
.toBe('$onChanges > $onInit > $doCheck > $postLink');
25772734
});
25782735
}));
25792736
});

0 commit comments

Comments
 (0)