Skip to content

Commit 877d100

Browse files
committed
feat(core): implement takeUntilDestroyed in rxjs-interop
This commit implements an RxJS operator `takeUntilDestroyed` which terminates an Observable when the current context (component, directive, etc) is destroyed. `takeUntilDestroyed` will inject the current `DestroyRef` if none is provided, or use one provided as an argument.
1 parent dd35bbb commit 877d100

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

goldens/public-api/core/rxjs-interop/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
55
```ts
66

7+
import { DestroyRef } from '@angular/core';
78
import { Injector } from '@angular/core';
9+
import { MonoTypeOperatorFunction } from 'rxjs';
810
import { Observable } from 'rxjs';
911
import { Signal } from '@angular/core';
1012

@@ -22,6 +24,9 @@ export interface FromSignalOptions {
2224
injector?: Injector;
2325
}
2426

27+
// @public
28+
export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T>;
29+
2530
// (No @packageDocumentation comment for this package)
2631

2732
```

packages/core/rxjs-interop/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88

99
export {fromObservable} from './from_observable';
1010
export {fromSignal, FromSignalOptions} from './from_signal';
11+
export {takeUntilDestroyed} from './take_until_destroyed';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {assertInInjectionContext, DestroyRef, inject} from '@angular/core';
10+
import {MonoTypeOperatorFunction, Observable} from 'rxjs';
11+
import {takeUntil} from 'rxjs/operators';
12+
13+
/**
14+
* Operator which completes the Observable when the calling context (component, directive, service,
15+
* etc) is destroyed.
16+
*
17+
* @param destroyRef optionally, the `DestroyRef` representing the current context. This can be
18+
* passed explicitly to use `takeUntilDestroyed` outside of an injection context. Otherwise, the
19+
* current `DestroyRef` is injected.
20+
*
21+
* @developerPreview
22+
*/
23+
export function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T> {
24+
if (!destroyRef) {
25+
assertInInjectionContext(takeUntilDestroyed);
26+
destroyRef = inject(DestroyRef);
27+
}
28+
29+
const destroyed$ = new Observable<void>(observer => {
30+
destroyRef!.onDestroy(observer.next.bind(observer));
31+
});
32+
33+
return <T>(source: Observable<T>) => {
34+
return source.pipe(takeUntil(destroyed$));
35+
};
36+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {DestroyRef, EnvironmentInjector, Injector, runInInjectionContext} from '@angular/core';
10+
import {BehaviorSubject} from 'rxjs';
11+
12+
import {takeUntilDestroyed} from '../src/take_until_destroyed';
13+
14+
describe('takeUntilDestroyed', () => {
15+
it('should complete an observable when the current context is destroyed', () => {
16+
const injector = Injector.create({providers: []}) as EnvironmentInjector;
17+
const source$ = new BehaviorSubject(0);
18+
const tied$ = runInInjectionContext(injector, () => source$.pipe(takeUntilDestroyed()));
19+
20+
let completed = false;
21+
let last = 0;
22+
23+
tied$.subscribe({
24+
next(value) {
25+
last = value;
26+
},
27+
complete() {
28+
completed = true;
29+
}
30+
});
31+
32+
source$.next(1);
33+
expect(last).toBe(1);
34+
35+
injector.destroy();
36+
expect(completed).toBeTrue();
37+
source$.next(2);
38+
expect(last).toBe(1);
39+
});
40+
41+
it('should allow a manual DestroyRef to be passed', () => {
42+
const injector = Injector.create({providers: []}) as EnvironmentInjector;
43+
const source$ = new BehaviorSubject(0);
44+
const tied$ = source$.pipe(takeUntilDestroyed(injector.get(DestroyRef)));
45+
46+
let completed = false;
47+
let last = 0;
48+
49+
tied$.subscribe({
50+
next(value) {
51+
last = value;
52+
},
53+
complete() {
54+
completed = true;
55+
}
56+
});
57+
58+
source$.next(1);
59+
expect(last).toBe(1);
60+
61+
injector.destroy();
62+
expect(completed).toBeTrue();
63+
source$.next(2);
64+
expect(last).toBe(1);
65+
});
66+
});

0 commit comments

Comments
 (0)