Skip to content

Commit

Permalink
feat(core): Add whenStable helper on ApplicationRef (#57190)
Browse files Browse the repository at this point in the history
This commit adds a `whenStable` function to `ApplicationRef` to cover
the most common use-case for the `isStable` observable.

PR Close #57190
  • Loading branch information
atscott authored and thePunderWoman committed Aug 6, 2024
1 parent f9a97c7 commit 7919982
Show file tree
Hide file tree
Showing 8 changed files with 40 additions and 27 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/application/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/acceptance/after_render_hook_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
10 changes: 5 additions & 5 deletions packages/core/test/change_detection_scheduler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/event_emitter_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
12 changes: 4 additions & 8 deletions packages/core/testing/src/component_fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
}

Expand Down
14 changes: 5 additions & 9 deletions packages/service-worker/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions packages/service-worker/test/provider_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -170,6 +169,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
provide: ApplicationRef,
useValue: {
isStable: isStableSub.asObservable(),
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
afterTick: new Subject(),
onDestroy: () => {},
},
Expand All @@ -189,6 +189,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule';
provide: ApplicationRef,
useValue: {
isStable: isStableSub.asObservable(),
whenStable: () => isStableSub.pipe(filter(Boolean), take(1)),
afterTick: new Subject(),
onDestroy: () => {},
},
Expand Down

0 comments on commit 7919982

Please sign in to comment.