diff --git a/adev/shared-docs/components/search-dialog/search-dialog.component.ts b/adev/shared-docs/components/search-dialog/search-dialog.component.ts index a1aea27fe71..bc4a02cc9eb 100644 --- a/adev/shared-docs/components/search-dialog/search-dialog.component.ts +++ b/adev/shared-docs/components/search-dialog/search-dialog.component.ts @@ -30,12 +30,12 @@ import {TextField} from '../text-field/text-field.component'; import {FormsModule} from '@angular/forms'; import {ActiveDescendantKeyManager} from '@angular/cdk/a11y'; import {SearchItem} from '../../directives/search-item/search-item.directive'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {Router, RouterLink} from '@angular/router'; -import {filter, fromEvent} from 'rxjs'; +import {fromEvent} from 'rxjs'; import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component'; import {RelativeLink} from '../../pipes/relative-link.pipe'; import {SearchResult, SnippetResult} from '../../interfaces'; +import {takeUntilDestroyed} from '../../services/rxjs-interop'; @Component({ selector: 'docs-search-dialog', diff --git a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts index 4c17dd08b5e..36c87e252e8 100644 --- a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts +++ b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts @@ -28,7 +28,6 @@ import { ɵPendingTasksInternal as PendingTasks, output, } from '@angular/core'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {TOC_SKIP_CONTENT_MARKER, NavigationState} from '../../../services/index'; import {TableOfContents} from '../../table-of-contents/table-of-contents.component'; import {IconComponent} from '../../icon/icon.component'; @@ -40,6 +39,7 @@ import {fromEvent} from 'rxjs'; import {Breadcrumb} from '../../breadcrumb/breadcrumb.component'; import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component'; import {ExampleViewer} from '../example-viewer/example-viewer.component'; +import {takeUntilDestroyed} from '../../../services/rxjs-interop'; /// diff --git a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts index 226b3cd52c2..e93dac718b2 100644 --- a/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts +++ b/adev/shared-docs/components/viewers/example-viewer/example-viewer.component.ts @@ -29,8 +29,8 @@ import {Clipboard} from '@angular/cdk/clipboard'; import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component'; import {ExampleMetadata, Snippet} from '../../../interfaces/index'; import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {DocViewer} from '../docs-viewer/docs-viewer.component'; +import {takeUntilDestroyed} from '../../../services/rxjs-interop'; export enum CodeExampleViewMode { SNIPPET = 'snippet', diff --git a/adev/shared-docs/services/BUILD.bazel b/adev/shared-docs/services/BUILD.bazel index fec7953c401..639d3f3d03e 100644 --- a/adev/shared-docs/services/BUILD.bazel +++ b/adev/shared-docs/services/BUILD.bazel @@ -30,7 +30,6 @@ ng_module( "//adev/shared-docs/utils", "//packages/common", "//packages/core", - "//packages/core/rxjs-interop", "//packages/router", "@npm//algoliasearch", "@npm//rxjs", diff --git a/adev/shared-docs/services/rxjs-interop.ts b/adev/shared-docs/services/rxjs-interop.ts new file mode 100644 index 00000000000..641cb656412 --- /dev/null +++ b/adev/shared-docs/services/rxjs-interop.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { + assertInInjectionContext, + DestroyRef, + effect, + inject, + Injector, + Signal, + untracked, +} from '@angular/core'; +import {Observable, ReplaySubject, MonoTypeOperatorFunction, takeUntil} from 'rxjs'; + +// ******************************* + +// TODO: Remove the file and revert the commit it belonged to in https://github.com/angular/angular/pull/60234 +// Once the next version of core package has been released + +// ******************************* + +/** + * Operator which completes the Observable when the calling context (component, directive, service, + * etc) is destroyed. + * + * @param destroyRef optionally, the `DestroyRef` representing the current context. This can be + * passed explicitly to use `takeUntilDestroyed` outside of an [injection + * context](guide/di/dependency-injection-context). Otherwise, the current `DestroyRef` is injected. + * + * @publicApi + */ +export function takeUntilDestroyed(destroyRef?: DestroyRef): MonoTypeOperatorFunction { + if (!destroyRef) { + assertInInjectionContext(takeUntilDestroyed); + destroyRef = inject(DestroyRef); + } + + const destroyed$ = new Observable((observer) => { + const unregisterFn = destroyRef!.onDestroy(observer.next.bind(observer)); + return unregisterFn; + }); + + return (source: Observable) => { + return source.pipe(takeUntil(destroyed$)); + }; +} + +/** + * Options for `toObservable`. + * + * @developerPreview + */ +export interface ToObservableOptions { + /** + * The `Injector` to use when creating the underlying `effect` which watches the signal. + * + * If this isn't specified, the current [injection context](guide/di/dependency-injection-context) + * will be used. + */ + injector?: Injector; +} + +/** + * Exposes the value of an Angular `Signal` as an RxJS `Observable`. + * + * The signal's value will be propagated into the `Observable`'s subscribers using an `effect`. + * + * `toObservable` must be called in an injection context unless an injector is provided via options. + * + * @developerPreview + */ +export function toObservable(source: Signal, options?: ToObservableOptions): Observable { + !options?.injector && assertInInjectionContext(toObservable); + const injector = options?.injector ?? inject(Injector); + const subject = new ReplaySubject(1); + + const watcher = effect( + () => { + let value: T; + try { + value = source(); + } catch (err) { + untracked(() => subject.error(err)); + return; + } + untracked(() => subject.next(value)); + }, + {injector, manualCleanup: true}, + ); + + injector.get(DestroyRef).onDestroy(() => { + watcher.destroy(); + subject.complete(); + }); + + return subject.asObservable(); +} diff --git a/adev/shared-docs/services/search.service.ts b/adev/shared-docs/services/search.service.ts index 4fa61f1279e..a11da1638df 100644 --- a/adev/shared-docs/services/search.service.ts +++ b/adev/shared-docs/services/search.service.ts @@ -9,10 +9,10 @@ import {Injectable, afterNextRender, inject, signal} from '@angular/core'; import {ENVIRONMENT} from '../providers/index'; import {SearchResult} from '../interfaces/index'; -import {toObservable} from '@angular/core/rxjs-interop'; import {debounceTime, filter, from, of, switchMap} from 'rxjs'; import {liteClient as algoliasearch} from 'algoliasearch/lite'; import {NavigationEnd, Router} from '@angular/router'; +import {toObservable} from './rxjs-interop'; export const SEARCH_DELAY = 200; // Maximum number of facet values to return for each facet during a regular search.