feat(core): Add whenStable helper on ApplicationRef (#57190)

This commit adds a `whenStable` function to `ApplicationRef` to cover
the most common use-case for the `isStable` observable.

PR Close #57190
This commit is contained in:
Andrew Scott
2024-07-10 16:23:46 -07:00
committed by Jessica Janiuk
parent f9a97c710d
commit 7919982063
8 changed files with 40 additions and 27 deletions
+2
View File
@@ -137,6 +137,8 @@ export class ApplicationRef {
tick(): void;
get viewCount(): number;
// (undocumented)
whenStable(): Promise<void>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ApplicationRef, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<ApplicationRef>;
@@ -12,7 +12,7 @@ import {
setActiveConsumer,
setThrowInvalidWriteToSignalError,
} from '@angular/core/primitives/signals';
import {Observable, Subject} from 'rxjs';
import {Observable, Subject, Subscription} from 'rxjs';
import {first, map} from 'rxjs/operators';
import {ZONELESS_ENABLED} from '../change_detection/scheduling/zoneless_scheduling';
@@ -347,6 +347,24 @@ export class ApplicationRef {
map((pending) => !pending),
);
/**
* @returns A promise that resolves when the application becomes stable
*/
whenStable(): Promise<void> {
let subscription: Subscription;
return new Promise<void>((resolve) => {
subscription = this.isStable.subscribe({
next: (stable) => {
if (stable) {
resolve();
}
},
});
}).finally(() => {
subscription.unsubscribe();
});
}
private readonly _injector = inject(EnvironmentInjector);
/**
* The `EnvironmentInjector` used to create this application.
@@ -1383,7 +1383,7 @@ describe('after render hooks', () => {
const fixture = TestBed.createComponent(TestCmp);
const appRef = TestBed.inject(ApplicationRef);
appRef.attachView(fixture.componentRef.hostView);
await firstValueFrom(appRef.isStable.pipe(filter((stable) => stable)));
await appRef.whenStable();
expect(fixture.nativeElement.innerText).toBe('1');
});
@@ -277,8 +277,8 @@ describe('Angular with zoneless enabled', () => {
expect(fixture.nativeElement.innerText).toEqual('');
});
function whenStable(applicationRef = TestBed.inject(ApplicationRef)): Promise<boolean> {
return firstValueFrom(applicationRef.isStable.pipe(filter((stable) => stable)));
function whenStable(): Promise<void> {
return TestBed.inject(ApplicationRef).whenStable();
}
it(
@@ -316,14 +316,14 @@ describe('Angular with zoneless enabled', () => {
],
});
const appViewRef = (applicationRef as any)._views[0] as {context: App; rootNodes: any[]};
await whenStable(applicationRef);
await applicationRef.whenStable();
const component2 = createComponent(DynamicCmp, {
environmentInjector: applicationRef.injector,
});
appViewRef.context.viewContainer.insert(component2.hostView);
expect(isStable(applicationRef.injector)).toBe(false);
await whenStable(applicationRef);
await applicationRef.whenStable();
component2.destroy();
// destroying the view synchronously removes element from DOM when not using animations
@@ -333,7 +333,7 @@ describe('Angular with zoneless enabled', () => {
let checkCountBeforeStable = doCheckCount;
let renderCountBeforeStable = renderHookCalls;
await whenStable(applicationRef);
await applicationRef.whenStable();
// The view should not have refreshed
expect(doCheckCount).toEqual(checkCountBeforeStable);
// but render hooks should have run
+1 -1
View File
@@ -203,7 +203,7 @@ describe('EventEmitter', () => {
},
});
emitter.emit(1);
await firstValueFrom(TestBed.inject(ApplicationRef).isStable.pipe(filter((stable) => stable)));
await TestBed.inject(ApplicationRef).whenStable();
expect(emitValue!).toBeDefined();
expect(emitValue!).toEqual(1);
});
@@ -25,7 +25,6 @@ import {
ɵPendingTasks as PendingTasks,
} from '@angular/core';
import {Subject, Subscription} from 'rxjs';
import {first} from 'rxjs/operators';
import {DeferBlockFixture} from './defer';
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common';
@@ -136,13 +135,10 @@ export abstract class ComponentFixture<T> {
return new Promise((resolve, reject) => {
this.appErrorHandler.whenStableRejectFunctions.add(reject);
this._appRef.isStable
.pipe(first((stable) => stable))
.toPromise()
.then((v) => {
this.appErrorHandler.whenStableRejectFunctions.delete(reject);
resolve(v);
});
this._appRef.whenStable().then(() => {
this.appErrorHandler.whenStableRejectFunctions.delete(reject);
resolve(true);
});
});
}
+5 -9
View File
@@ -17,8 +17,8 @@ import {
NgZone,
PLATFORM_ID,
} from '@angular/core';
import {merge, Observable, of} from 'rxjs';
import {delay, filter, take} from 'rxjs/operators';
import {merge, from, Observable, of} from 'rxjs';
import {delay, take} from 'rxjs/operators';
import {NgswCommChannel} from './low_level';
import {SwPush} from './push';
@@ -76,9 +76,10 @@ export function ngswAppInitializer(
readyToRegister$ = delayWithTimeout(+args[0] || 0);
break;
case 'registerWhenStable':
const whenStable$ = from(injector.get(ApplicationRef).whenStable());
readyToRegister$ = !args[0]
? whenStable(injector)
: merge(whenStable(injector), delayWithTimeout(+args[0]));
? whenStable$
: merge(whenStable$, delayWithTimeout(+args[0]));
break;
default:
// Unknown strategy.
@@ -108,11 +109,6 @@ function delayWithTimeout(timeout: number): Observable<unknown> {
return of(null).pipe(delay(timeout));
}
function whenStable(injector: Injector): Observable<unknown> {
const appRef = injector.get(ApplicationRef);
return appRef.isStable.pipe(filter((stable) => stable));
}
export function ngswCommChannelFactory(
opts: SwRegistrationOptions,
platformId: string,
@@ -30,8 +30,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
let swRegisterSpy: jasmine.Spy;
const untilStable = () => {
const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
return appRef.isStable.pipe(filter(Boolean), take(1)).toPromise();
return TestBed.inject(ApplicationRef).whenStable();
};
beforeEach(
@@ -170,6 +169,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
provide: ApplicationRef,
useValue: {
isStable: isStableSub.asObservable(),
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
afterTick: new Subject(),
onDestroy: () => {},
},
@@ -189,6 +189,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
provide: ApplicationRef,
useValue: {
isStable: isStableSub.asObservable(),
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
afterTick: new Subject(),
onDestroy: () => {},
},