Skip to content

Commit 7919982

Browse files
atscottthePunderWoman
authored andcommittedAug 6, 2024·
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
1 parent f9a97c7 commit 7919982

File tree

8 files changed

+40
-27
lines changed

8 files changed

+40
-27
lines changed
 

‎goldens/public-api/core/index.api.md

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export class ApplicationRef {
137137
tick(): void;
138138
get viewCount(): number;
139139
// (undocumented)
140+
whenStable(): Promise<void>;
141+
// (undocumented)
140142
static ɵfac: i0.ɵɵFactoryDeclaration<ApplicationRef, never>;
141143
// (undocumented)
142144
static ɵprov: i0.ɵɵInjectableDeclaration<ApplicationRef>;

‎packages/core/src/application/application_ref.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
setActiveConsumer,
1313
setThrowInvalidWriteToSignalError,
1414
} from '@angular/core/primitives/signals';
15-
import {Observable, Subject} from 'rxjs';
15+
import {Observable, Subject, Subscription} from 'rxjs';
1616
import {first, map} from 'rxjs/operators';
1717

1818
import {ZONELESS_ENABLED} from '../change_detection/scheduling/zoneless_scheduling';
@@ -347,6 +347,24 @@ export class ApplicationRef {
347347
map((pending) => !pending),
348348
);
349349

350+
/**
351+
* @returns A promise that resolves when the application becomes stable
352+
*/
353+
whenStable(): Promise<void> {
354+
let subscription: Subscription;
355+
return new Promise<void>((resolve) => {
356+
subscription = this.isStable.subscribe({
357+
next: (stable) => {
358+
if (stable) {
359+
resolve();
360+
}
361+
},
362+
});
363+
}).finally(() => {
364+
subscription.unsubscribe();
365+
});
366+
}
367+
350368
private readonly _injector = inject(EnvironmentInjector);
351369
/**
352370
* The `EnvironmentInjector` used to create this application.

‎packages/core/test/acceptance/after_render_hook_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1383,7 +1383,7 @@ describe('after render hooks', () => {
13831383
const fixture = TestBed.createComponent(TestCmp);
13841384
const appRef = TestBed.inject(ApplicationRef);
13851385
appRef.attachView(fixture.componentRef.hostView);
1386-
await firstValueFrom(appRef.isStable.pipe(filter((stable) => stable)));
1386+
await appRef.whenStable();
13871387
expect(fixture.nativeElement.innerText).toBe('1');
13881388
});
13891389

‎packages/core/test/change_detection_scheduler_spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ describe('Angular with zoneless enabled', () => {
277277
expect(fixture.nativeElement.innerText).toEqual('');
278278
});
279279

280-
function whenStable(applicationRef = TestBed.inject(ApplicationRef)): Promise<boolean> {
281-
return firstValueFrom(applicationRef.isStable.pipe(filter((stable) => stable)));
280+
function whenStable(): Promise<void> {
281+
return TestBed.inject(ApplicationRef).whenStable();
282282
}
283283

284284
it(
@@ -316,14 +316,14 @@ describe('Angular with zoneless enabled', () => {
316316
],
317317
});
318318
const appViewRef = (applicationRef as any)._views[0] as {context: App; rootNodes: any[]};
319-
await whenStable(applicationRef);
319+
await applicationRef.whenStable();
320320

321321
const component2 = createComponent(DynamicCmp, {
322322
environmentInjector: applicationRef.injector,
323323
});
324324
appViewRef.context.viewContainer.insert(component2.hostView);
325325
expect(isStable(applicationRef.injector)).toBe(false);
326-
await whenStable(applicationRef);
326+
await applicationRef.whenStable();
327327
component2.destroy();
328328

329329
// destroying the view synchronously removes element from DOM when not using animations
@@ -333,7 +333,7 @@ describe('Angular with zoneless enabled', () => {
333333

334334
let checkCountBeforeStable = doCheckCount;
335335
let renderCountBeforeStable = renderHookCalls;
336-
await whenStable(applicationRef);
336+
await applicationRef.whenStable();
337337
// The view should not have refreshed
338338
expect(doCheckCount).toEqual(checkCountBeforeStable);
339339
// but render hooks should have run

‎packages/core/test/event_emitter_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ describe('EventEmitter', () => {
203203
},
204204
});
205205
emitter.emit(1);
206-
await firstValueFrom(TestBed.inject(ApplicationRef).isStable.pipe(filter((stable) => stable)));
206+
await TestBed.inject(ApplicationRef).whenStable();
207207
expect(emitValue!).toBeDefined();
208208
expect(emitValue!).toEqual(1);
209209
});

‎packages/core/testing/src/component_fixture.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
ɵPendingTasks as PendingTasks,
2626
} from '@angular/core';
2727
import {Subject, Subscription} from 'rxjs';
28-
import {first} from 'rxjs/operators';
2928

3029
import {DeferBlockFixture} from './defer';
3130
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common';
@@ -136,13 +135,10 @@ export abstract class ComponentFixture<T> {
136135

137136
return new Promise((resolve, reject) => {
138137
this.appErrorHandler.whenStableRejectFunctions.add(reject);
139-
this._appRef.isStable
140-
.pipe(first((stable) => stable))
141-
.toPromise()
142-
.then((v) => {
143-
this.appErrorHandler.whenStableRejectFunctions.delete(reject);
144-
resolve(v);
145-
});
138+
this._appRef.whenStable().then(() => {
139+
this.appErrorHandler.whenStableRejectFunctions.delete(reject);
140+
resolve(true);
141+
});
146142
});
147143
}
148144

‎packages/service-worker/src/provider.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
NgZone,
1818
PLATFORM_ID,
1919
} from '@angular/core';
20-
import {merge, Observable, of} from 'rxjs';
21-
import {delay, filter, take} from 'rxjs/operators';
20+
import {merge, from, Observable, of} from 'rxjs';
21+
import {delay, take} from 'rxjs/operators';
2222

2323
import {NgswCommChannel} from './low_level';
2424
import {SwPush} from './push';
@@ -76,9 +76,10 @@ export function ngswAppInitializer(
7676
readyToRegister$ = delayWithTimeout(+args[0] || 0);
7777
break;
7878
case 'registerWhenStable':
79+
const whenStable$ = from(injector.get(ApplicationRef).whenStable());
7980
readyToRegister$ = !args[0]
80-
? whenStable(injector)
81-
: merge(whenStable(injector), delayWithTimeout(+args[0]));
81+
? whenStable$
82+
: merge(whenStable$, delayWithTimeout(+args[0]));
8283
break;
8384
default:
8485
// Unknown strategy.
@@ -108,11 +109,6 @@ function delayWithTimeout(timeout: number): Observable<unknown> {
108109
return of(null).pipe(delay(timeout));
109110
}
110111

111-
function whenStable(injector: Injector): Observable<unknown> {
112-
const appRef = injector.get(ApplicationRef);
113-
return appRef.isStable.pipe(filter((stable) => stable));
114-
}
115-
116112
export function ngswCommChannelFactory(
117113
opts: SwRegistrationOptions,
118114
platformId: string,

‎packages/service-worker/test/provider_spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
3030
let swRegisterSpy: jasmine.Spy;
3131

3232
const untilStable = () => {
33-
const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
34-
return appRef.isStable.pipe(filter(Boolean), take(1)).toPromise();
33+
return TestBed.inject(ApplicationRef).whenStable();
3534
};
3635

3736
beforeEach(
@@ -170,6 +169,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
170169
provide: ApplicationRef,
171170
useValue: {
172171
isStable: isStableSub.asObservable(),
172+
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
173173
afterTick: new Subject(),
174174
onDestroy: () => {},
175175
},
@@ -189,6 +189,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
189189
provide: ApplicationRef,
190190
useValue: {
191191
isStable: isStableSub.asObservable(),
192+
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
192193
afterTick: new Subject(),
193194
onDestroy: () => {},
194195
},

0 commit comments

Comments
 (0)