UNPKG

@rx-angular/template

Version:

**Fully** Reactive Component Template Rendering in Angular. @rx-angular/template aims to be a reflection of Angular's built in renderings just reactive.

915 lines (902 loc) 42.5 kB
import * as i0 from '@angular/core'; import { InjectionToken, assertInInjectionContext, inject, Injector, runInInjectionContext, effect, untracked, Injectable, ElementRef, DestroyRef, INJECTOR, computed, isSignal, signal, input, booleanAttribute, output, Directive, ViewContainerRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies'; import { PLATFORM } from '@rx-angular/cdk/ssr'; import { ReplaySubject, NEVER, finalize, EMPTY, BehaviorSubject, of, combineLatest } from 'rxjs'; import { distinctUntilChanged, switchMap, map, tap, finalize as finalize$1 } from 'rxjs/operators'; const VIRTUAL_VIEW_CONFIG_TOKEN = new InjectionToken('VIRTUAL_VIEW_CONFIG_TOKEN', { providedIn: 'root', factory: () => VIRTUAL_VIEW_CONFIG_DEFAULT, }); const VIRTUAL_VIEW_CONFIG_DEFAULT = { enabled: true, keepLastKnownSize: false, useContentVisibility: false, useContainment: true, placeholderStrategy: 'low', contentStrategy: 'normal', startWithPlaceholderAsap: false, cacheEnabled: true, enableAfterHydration: true, scrollMargin: '100px', cache: { contentCacheSize: 20, placeholderCacheSize: 20, }, }; /** * Provides a configuration object for the `VirtualView` service. * * Can be used to customize the behavior of the `VirtualView` service. * * Default configuration: * - contentCacheSize: 20 * - placeholderCacheSize: 20 * * Example usage: * * ```ts * import { provideVirtualViewConfig } from '@rx-angular/template/virtual-view'; * * const appConfig: ApplicationConfig = { * providers: [ * provideVirtualViewConfig({ * contentCacheSize: 50, * placeholderCacheSize: 50, * }), * ], * }; * ``` * * @developerPreview * * @param config - The configuration object. * @returns An object that can be provided to the `VirtualView` service. */ function provideVirtualViewConfig(config) { if (typeof config === 'function') { return { provide: VIRTUAL_VIEW_CONFIG_TOKEN, useFactory: () => { const cfg = config(); return { ...VIRTUAL_VIEW_CONFIG_DEFAULT, ...cfg, cache: { ...VIRTUAL_VIEW_CONFIG_DEFAULT.cache, ...(cfg?.cache ?? {}), }, }; }, }; } return { provide: VIRTUAL_VIEW_CONFIG_TOKEN, useValue: { ...VIRTUAL_VIEW_CONFIG_DEFAULT, ...config, cache: { ...VIRTUAL_VIEW_CONFIG_DEFAULT.cache, ...(config?.cache ?? {}) }, }, }; } /** * @internal */ class _RxVirtualViewObserver { } /** * @internal */ class _RxVirtualView { } // https://ngxtension.dev/utilities/injectors/assert-injector/ function assertInjector(fn, injector, runner) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions !injector && assertInInjectionContext(fn); const assertedInjector = injector ?? inject(Injector); if (!runner) return assertedInjector; return runInInjectionContext(assertedInjector, runner); } // https://ngxtension.dev/utilities/signals/effect-once-if/ function effectOnceIf(condition, execution, options) { const assertedInjector = assertInjector(effectOnceIf, options?.injector); return runInInjectionContext(assertedInjector, () => { const effectRef = effect((onCleanup) => { const hasCondition = condition(); if (hasCondition) { untracked(() => execution(hasCondition, onCleanup)); effectRef.destroy(); } }, options); return effectRef; }); } /** * A service that caches templates and placeholders to optimize view rendering. * It makes sure that all cached resources are cleared when the service is destroyed. * * @developerPreview */ class VirtualViewCache { #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); // Maximum number of content that can be stored in the cache. #contentCacheSize = this.#config.cache.contentCacheSize; // Cache for storing content views, identified by a unique key, which is the directive instance. #contentCache = new Map(); // Maximum number of placeholders that can be stored in the cache. #placeholderCacheSize = this.#config.cache.placeholderCacheSize; // Cache for storing placeholder views, identified by a unique key. #placeholderCache = new Map(); /** * Stores a placeholder view in the cache. When the cache reaches its limit, * the oldest entry is removed. * * @param key - The key used to identify the placeholder in the cache. * @param view - The ViewRef of the placeholder to cache. */ storePlaceholder(key, view) { if (this.#placeholderCacheSize <= 0) { view.destroy(); return; } if (this.#placeholderCache.size >= this.#placeholderCacheSize) { this.#removeOldestEntry(this.#placeholderCache); } this.#placeholderCache.set(key, view); } /** * Retrieves a cached placeholder view using the specified key. * * @param key - The key of the placeholder to retrieve. * @returns The ViewRef of the cached placeholder, or undefined if not found. */ getPlaceholder(key) { const view = this.#placeholderCache.get(key); this.#placeholderCache.delete(key); return view; } /** * Stores a content view in the cache. When the cache reaches its limit, * the oldest entry is removed. * * @param key - The key used to identify the content in the cache. * @param view - The ViewRef of the content to cache. */ storeContent(key, view) { if (this.#contentCacheSize <= 0) { view.destroy(); return; } if (this.#contentCache.size >= this.#contentCacheSize) { this.#removeOldestEntry(this.#contentCache); } this.#contentCache.set(key, view); } /** * Retrieves a cached content view using the specified key. * * @param key - The key of the content to retrieve. * @returns The ViewRef of the cached content, or undefined if not found. */ getContent(key) { const view = this.#contentCache.get(key); this.#contentCache.delete(key); return view; } /** * Clears both content and placeholder caches for a given key. * * @param key - The key of the content and placeholder to remove. */ clear(key) { this.#contentCache.get(key)?.destroy(); this.#contentCache.delete(key); this.#placeholderCache.get(key)?.destroy(); this.#placeholderCache.delete(key); } /** * Clears all cached resources when the service is destroyed. */ ngOnDestroy() { this.#contentCache.forEach((view) => view.destroy()); this.#placeholderCache.forEach((view) => view.destroy()); this.#contentCache.clear(); this.#placeholderCache.clear(); } #removeOldestEntry(cache) { const oldestValue = cache.entries().next().value; if (oldestValue !== undefined) { const [key, view] = oldestValue; view?.destroy(); cache.delete(key); } } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VirtualViewCache, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VirtualViewCache }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VirtualViewCache, decorators: [{ type: Injectable }] }); /** * The RxVirtualView directive is a directive that allows you to create virtual views. * * It can be used on an element/component to create a virtual view. * * It works by using 3 directives: * - `rxVirtualViewContent`: The content to render when the virtual view is visible. * - `rxVirtualViewPlaceholder`: The placeholder to render when the virtual view is not visible. * - `rxVirtualViewObserver`: The directive that observes the virtual view and emits a boolean value indicating whether the virtual view is visible. * * The `rxVirtualViewObserver` directive is mandatory for the `rxVirtualView` directive to work. * And it needs to be a sibling of the `rxVirtualView` directive. * * @example * ```html * <div rxVirtualViewObserver> * <div rxVirtualView> * <div *rxVirtualViewContent>Virtual View 1</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * </div> * ``` * * @developerPreview */ class RxVirtualView { #observer; #elementRef; #strategyProvider; #viewCache; #destroyRef; #config; #platform; #injector; #enabled; #content; #placeholder; /** * Returns the current visibility state of the content and placeholder. * * This getter provides synchronous access to the visibility state, which can be useful * when you need to check the current state imperatively or from the template. * * @returns An object containing: * - `content`: `true` if the content is currently visible, `false` otherwise * - `placeholder`: `true` if the placeholder is currently visible, `false` otherwise * * @example * ```html * <!-- Access visibility state in template using exportAs --> * <div rxVirtualViewObserver> * <div rxVirtualView #virtualView="rxVirtualView"> * <div *rxVirtualViewContent>Virtual View Content</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * * <!-- Display visibility state --> * <div> * Content visible: {{ virtualView.visibility.content }} * Placeholder visible: {{ virtualView.visibility.placeholder }} * </div> * </div> * ``` */ get visibility() { return { content: this.#contentIsShown, placeholder: !this.#contentIsShown, }; } #placeholderVisible; #contentIsShown; /** * The `_RxVirtualViewContent` directive whose template is currently embedded * in the view container. Used to detect runtime content-template swaps (e.g. * an `@switch` / `@if` case change inside `rxVirtualView` that replaces the * active `*rxVirtualViewContent` while the host element stays mounted) so the * new template is rendered instead of leaving the card blank. */ #renderedContent; #visible$; constructor() { this.#observer = inject(_RxVirtualViewObserver, { optional: true }); this.#elementRef = inject(ElementRef); this.#strategyProvider = inject(RxStrategyProvider); this.#viewCache = inject(VirtualViewCache, { optional: true }); this.#destroyRef = inject(DestroyRef); this.#config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); this.#platform = inject(PLATFORM); this.#injector = inject(INJECTOR); this.#enabled = computed(() => isSignal(this.#config.enabled) ? this.#config.enabled() : this.#config.enabled, ...(ngDevMode ? [{ debugName: "#enabled" }] : [])); this.#content = signal(null, ...(ngDevMode ? [{ debugName: "#content" }] : [])); this.#placeholder = signal(null, ...(ngDevMode ? [{ debugName: "#placeholder" }] : [])); /** * Useful when we want to cache the templates and placeholders to optimize view rendering. * * Enabled by default. */ this.cacheEnabled = input(this.#config.cacheEnabled, { ...(ngDevMode ? { debugName: "cacheEnabled" } : {}), transform: booleanAttribute }); /** * Whether to start with the placeholder asap or not. * * If `true`, the placeholder will be rendered immediately, without waiting for the content to be visible. * This is useful when you want to render the placeholder immediately, but you don't want to wait for the content to be visible. * * This is to counter concurrent rendering, and to avoid flickering. */ this.startWithPlaceholderAsap = input(this.#config.startWithPlaceholderAsap, { ...(ngDevMode ? { debugName: "startWithPlaceholderAsap" } : {}), transform: booleanAttribute }); /** * This will keep the last known size of the host element while the content is visible. */ this.keepLastKnownSize = input(this.#config.keepLastKnownSize, { ...(ngDevMode ? { debugName: "keepLastKnownSize" } : {}), transform: booleanAttribute }); /** * Whether to use content visibility or not. * * It will add the `content-visibility` CSS class to the host element, together with * `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. */ this.useContentVisibility = input(this.#config.useContentVisibility, { ...(ngDevMode ? { debugName: "useContentVisibility" } : {}), transform: booleanAttribute }); /** * Whether to use containment or not. * * It will add `contain` css property with: * - `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible * - `content`: if `useContentVisibility` is `false` || content is visible */ this.useContainment = input(this.#config.useContainment, { ...(ngDevMode ? { debugName: "useContainment" } : {}), transform: booleanAttribute }); /** * The strategy to use for rendering the placeholder. */ this.placeholderStrategy = input(this.#config.placeholderStrategy, ...(ngDevMode ? [{ debugName: "placeholderStrategy" }] : [])); /** * The strategy to use for rendering the content. */ this.contentStrategy = input(this.#config.contentStrategy, ...(ngDevMode ? [{ debugName: "contentStrategy" }] : [])); /** * A function extracting width & height from a ResizeObserverEntry */ this.extractSize = input(defaultExtractSize, ...(ngDevMode ? [{ debugName: "extractSize" }] : [])); /** * ResizeObserverOptions */ this.resizeObserverOptions = input(...(ngDevMode ? [undefined, { debugName: "resizeObserverOptions" }] : [])); /** * Emits when the visibility state of the virtual view changes. * * This output fires whenever the virtual view transitions between showing content and showing placeholder. * The emitted value is an object containing the current visibility state of both the content and placeholder. * * @example * ```html * <div rxVirtualViewObserver> * <div rxVirtualView (visibilityChanged)="onVisibilityChanged($event)"> * <div *rxVirtualViewContent>Virtual View Content</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * </div> * ``` * * ```typescript * onVisibilityChanged(event: { content: boolean; placeholder: boolean }) { * console.log('Content visible:', event.content); * console.log('Placeholder visible:', event.placeholder); * } * ``` */ this.visibilityChanged = output(); this.#placeholderVisible = signal(false, ...(ngDevMode ? [{ debugName: "#placeholderVisible" }] : [])); this.#contentIsShown = false; /** * The `_RxVirtualViewContent` directive whose template is currently embedded * in the view container. Used to detect runtime content-template swaps (e.g. * an `@switch` / `@if` case change inside `rxVirtualView` that replaces the * active `*rxVirtualViewContent` while the host element stays mounted) so the * new template is rendered instead of leaving the card blank. */ this.#renderedContent = null; this.#visible$ = new ReplaySubject(1); this.size = signal({ width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "size" }] : [])); this.width = computed(() => this.#enabled() && this.size().width ? `${this.size().width}px` : null, ...(ngDevMode ? [{ debugName: "width" }] : [])); this.height = computed(() => this.#enabled() && this.size().height ? `${this.size().height}px` : null, ...(ngDevMode ? [{ debugName: "height" }] : [])); this.containment = computed(() => { if (!this.useContainment() || !this.#enabled()) { return null; } return this.useContentVisibility() && this.#placeholderVisible() ? 'size layout paint' : 'content'; }, ...(ngDevMode ? [{ debugName: "containment" }] : [])); this.intrinsicWidth = computed(() => { if (!this.useContentVisibility() || !this.#enabled()) { return null; } return this.width() === 'auto' ? 'auto' : `auto ${this.width()}`; }, ...(ngDevMode ? [{ debugName: "intrinsicWidth" }] : [])); this.intrinsicHeight = computed(() => { if (!this.useContentVisibility() || !this.#enabled()) { return null; } return this.height() === 'auto' ? 'auto' : `auto ${this.height()}`; }, ...(ngDevMode ? [{ debugName: "intrinsicHeight" }] : [])); this.minHeight = computed(() => { return this.keepLastKnownSize() && this.#placeholderVisible() && this.#enabled() ? this.height() : null; }, ...(ngDevMode ? [{ debugName: "minHeight" }] : [])); this.minWidth = computed(() => { return this.keepLastKnownSize() && this.#placeholderVisible() && this.#enabled() ? this.width() : null; }, ...(ngDevMode ? [{ debugName: "minWidth" }] : [])); effectOnceIf(() => this.#enabled(), () => { if (this.#platform.isBrowser && !this.#observer) { throw new Error('RxVirtualView expects you to provide a RxVirtualViewObserver'); } }); } ngAfterContentInit() { if (this.#enabled()) { if (!this.#content()) { throw new Error('RxVirtualView expects you to provide a RxVirtualViewContent'); } if (this.startWithPlaceholderAsap()) { this.renderPlaceholder(); } } // when enabled: // - register visibility listener // - hide things that are not visible anymore if (this.#enabled()) { this.#registerRenderingBasedOnVisibility(); } else if (this.#config.enableAfterHydration) { // Disabled now (server / pre-hydration). Once the view becomes enabled // (e.g. after hydration) switch to visibility-based rendering. effectOnceIf(() => this.#enabled(), () => this.#registerRenderingBasedOnVisibility(), { injector: this.#injector }); } // Keep the embedded content in sync with the active *rxVirtualViewContent // directive. A single effect covers every case where #content() changes: // - the disabled/SSR initial render (show content as soon as available), // - deferred content that starts inside an inactive @if/@switch branch, // - runtime content-template swaps (e.g. an @switch case change) while the // host element stays mounted, in both the disabled and enabled states. this.#registerContentSync(); } /** * Reactively renders the active content directive whenever it changes. * * While disabled the current content is always shown. While enabled the * visibility pipeline owns the initial show/hide (and size handling), so this * only re-renders a *swap* that happens while content is already shown — * otherwise it would fight the placeholder/visibility logic. * @private */ #registerContentSync() { effect(() => { const content = this.#content(); if (!content || content === this.#renderedContent) { return; } const shouldShow = this.#enabled() ? this.#contentIsShown : true; if (shouldShow) { this.#renderContent(content); } }, { injector: this.#injector }); } /** * Creates the embedded view for the given content directive and marks it as * the currently rendered content. Used for the disabled render and for * runtime content-template swaps. * @private */ #renderContent(content) { this.#contentIsShown = true; this.#placeholderVisible.set(false); this.#renderedContent = content; const view = content.viewContainerRef.createEmbeddedView(content.templateRef); view.detectChanges(); this.visibilityChanged.emit({ content: true, placeholder: false }); } #registerRenderingBasedOnVisibility() { this.#observer ?.observeElementVisibility(this.#elementRef.nativeElement) .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((visible) => this.#visible$.next(visible)); this.#visible$ .pipe(distinctUntilChanged(), switchMap((visible) => { if (visible) { return this.#contentIsShown ? NEVER : this.showContent$().pipe(switchMap((view) => { const resize$ = this.#observer.observeElementSize(this.#elementRef.nativeElement, this.resizeObserverOptions()); view.detectChanges(); return resize$; }), map(this.extractSize()), tap(({ width, height }) => this.size.set({ width, height }))); } return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); }), finalize(() => { this.#viewCache.clear(this); }), takeUntilDestroyed(this.#destroyRef)) .subscribe(); } ngOnDestroy() { this.#content.set(null); this.#placeholder.set(null); this.#renderedContent = null; this.#viewCache?.clear(this); } registerContent(content) { this.#content.set(content); } registerPlaceholder(placeholder) { this.#placeholder.set(placeholder); } /** * Shows the content using the configured rendering strategy (by default: normal). * @private */ showContent$() { return this.#strategyProvider.schedule(() => { this.#contentIsShown = true; this.#renderedContent = this.#content(); this.#placeholderVisible.set(false); const placeHolder = this.#content().viewContainerRef.detach(); if (this.cacheEnabled() && placeHolder) { this.#viewCache.storePlaceholder(this, placeHolder); } else if (!this.cacheEnabled() && placeHolder) { placeHolder.destroy(); } const contentTpl = this.#viewCache.getContent(this) ?? this.#content().templateRef.createEmbeddedView({}); this.#content().viewContainerRef.insert(contentTpl); placeHolder?.detectChanges(); this.visibilityChanged.emit({ content: true, placeholder: false }); return contentTpl; }, { scope: this, strategy: this.contentStrategy() }); } /** * Shows the placeholder using the configured rendering strategy (by default: low). * @private */ showPlaceholder$() { return this.#strategyProvider.schedule(() => this.renderPlaceholder(), { scope: this, strategy: this.placeholderStrategy(), }); } /** * Renders a placeholder within the view container, and hides the content. * * If we already have a content and cache enabled, we store the content in * the cache, so we can reuse it later. * * When we want to render the placeholder, we try to get it from the cache, * and if it is not available, we create a new one. * * Then insert the placeholder into the view container and trigger a CD. */ renderPlaceholder() { this.#placeholderVisible.set(true); this.#contentIsShown = false; const content = this.#content().viewContainerRef.detach(); if (content) { if (this.cacheEnabled()) { this.#viewCache.storeContent(this, content); } else { content.destroy(); } content?.detectChanges(); } if (this.#placeholder()) { const placeholderRef = this.#viewCache.getPlaceholder(this) ?? this.#placeholder().templateRef.createEmbeddedView({}); this.#content().viewContainerRef.insert(placeholderRef); placeholderRef.detectChanges(); } this.visibilityChanged.emit({ content: false, placeholder: true }); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualView, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: RxVirtualView, isStandalone: true, selector: "[rxVirtualView]", inputs: { cacheEnabled: { classPropertyName: "cacheEnabled", publicName: "cacheEnabled", isSignal: true, isRequired: false, transformFunction: null }, startWithPlaceholderAsap: { classPropertyName: "startWithPlaceholderAsap", publicName: "startWithPlaceholderAsap", isSignal: true, isRequired: false, transformFunction: null }, keepLastKnownSize: { classPropertyName: "keepLastKnownSize", publicName: "keepLastKnownSize", isSignal: true, isRequired: false, transformFunction: null }, useContentVisibility: { classPropertyName: "useContentVisibility", publicName: "useContentVisibility", isSignal: true, isRequired: false, transformFunction: null }, useContainment: { classPropertyName: "useContainment", publicName: "useContainment", isSignal: true, isRequired: false, transformFunction: null }, placeholderStrategy: { classPropertyName: "placeholderStrategy", publicName: "placeholderStrategy", isSignal: true, isRequired: false, transformFunction: null }, contentStrategy: { classPropertyName: "contentStrategy", publicName: "contentStrategy", isSignal: true, isRequired: false, transformFunction: null }, extractSize: { classPropertyName: "extractSize", publicName: "extractSize", isSignal: true, isRequired: false, transformFunction: null }, resizeObserverOptions: { classPropertyName: "resizeObserverOptions", publicName: "resizeObserverOptions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visibilityChanged: "visibilityChanged" }, host: { properties: { "style.--rx-vw-h": "height()", "style.--rx-vw-w": "width()", "style.min-height": "minHeight()", "style.min-width": "minWidth()", "style.contain": "containment()", "style.contain-intrinsic-width": "intrinsicWidth()", "style.contain-intrinsic-height": "intrinsicHeight()", "style.content-visibility": "useContentVisibility() ? \"auto\" : null" } }, providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], exportAs: ["rxVirtualView"], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualView, decorators: [{ type: Directive, args: [{ selector: '[rxVirtualView]', host: { '[style.--rx-vw-h]': 'height()', '[style.--rx-vw-w]': 'width()', '[style.min-height]': 'minHeight()', '[style.min-width]': 'minWidth()', '[style.contain]': 'containment()', '[style.contain-intrinsic-width]': 'intrinsicWidth()', '[style.contain-intrinsic-height]': 'intrinsicHeight()', '[style.content-visibility]': 'useContentVisibility() ? "auto" : null', }, exportAs: 'rxVirtualView', providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], }] }], ctorParameters: () => [], propDecorators: { cacheEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "cacheEnabled", required: false }] }], startWithPlaceholderAsap: [{ type: i0.Input, args: [{ isSignal: true, alias: "startWithPlaceholderAsap", required: false }] }], keepLastKnownSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepLastKnownSize", required: false }] }], useContentVisibility: [{ type: i0.Input, args: [{ isSignal: true, alias: "useContentVisibility", required: false }] }], useContainment: [{ type: i0.Input, args: [{ isSignal: true, alias: "useContainment", required: false }] }], placeholderStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderStrategy", required: false }] }], contentStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentStrategy", required: false }] }], extractSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "extractSize", required: false }] }], resizeObserverOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizeObserverOptions", required: false }] }], visibilityChanged: [{ type: i0.Output, args: ["visibilityChanged"] }] } }); const defaultExtractSize = (entry) => ({ width: entry.borderBoxSize[0].inlineSize, height: entry.borderBoxSize[0].blockSize, }); /** * The RxVirtualViewTemplate directive is a directive that allows you to create a content template for the virtual view. * * It can be used on an element/component to create a content template for the virtual view. * * It needs to be a sibling of the `rxVirtualView` directive. * * @example * ```html * <div rxVirtualViewObserver> * <div rxVirtualView> * <div *rxVirtualViewContent>Virtual View 1</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * </div> * ``` * * @developerPreview */ class RxVirtualViewContent { #virtualView; constructor(templateRef) { this.templateRef = templateRef; this.#virtualView = inject(RxVirtualView); this.viewContainerRef = inject(ViewContainerRef); this.#virtualView.registerContent(this); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewContent, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: RxVirtualViewContent, isStandalone: true, selector: "[rxVirtualViewContent]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewContent, decorators: [{ type: Directive, args: [{ selector: '[rxVirtualViewContent]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * A service that observes the resize of the elements. * * @developerPreview */ class RxaResizeObserver { #destroyRef = inject(DestroyRef); #platform = inject(PLATFORM); #resizeObserver = null; /** @internal */ #elements = new Map(); constructor() { if (this.#platform.isBrowser) { this.#resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { if (this.#elements.has(entry.target)) this.#elements.get(entry.target).next(entry); }); }); } this.#destroyRef.onDestroy(() => { this.#elements.clear(); this.#resizeObserver?.disconnect(); }); } observeElement(element, options) { if (!this.#resizeObserver) { return EMPTY; } const resizeEvent$ = new ReplaySubject(1); this.#elements.set(element, resizeEvent$); this.#resizeObserver.observe(element, options); return resizeEvent$.pipe(distinctUntilChanged(), finalize$1(() => { this.#resizeObserver?.unobserve(element); this.#elements.delete(element); })); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxaResizeObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxaResizeObserver }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxaResizeObserver, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * The RxVirtualViewObserver directive observes the virtual view and emits a boolean value indicating whether the virtual view is visible. * This is the container for the RxVirtualView directives. * * This is a mandatory directive for the RxVirtualView directives to work. * * @example * ```html * <div rxVirtualViewObserver> * <div rxVirtualView> * <div *rxVirtualViewContent>Virtual View 1</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * </div> * ``` * * @developerPreview */ class RxVirtualViewObserver extends _RxVirtualViewObserver { constructor() { super(...arguments); this.#config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); this.#platform = inject(PLATFORM); this.#elementRef = inject(ElementRef); this.#observer = null; this.#resizeObserver = inject(RxaResizeObserver, { self: true }); /** * The root element to observe. * * If not provided, the root element is the element that the directive is attached to. */ this.root = input(...(ngDevMode ? [undefined, { debugName: "root" }] : [])); /** * The root margin to observe. * * This is useful when you want to observe the virtual view in a specific area of the root element. */ this.rootMargin = input('', ...(ngDevMode ? [{ debugName: "rootMargin" }] : [])); /** * The scroll margin to observe. * * This is useful when you want to observe the virtual view in a specific area of the scroll container. */ this.scrollMargin = input(this.#config.scrollMargin, ...(ngDevMode ? [{ debugName: "scrollMargin" }] : [])); /** * The threshold to observe. * * If you want to observe the virtual view when it is partially visible, you can set the threshold to a number between 0 and 1. * * For example, if you set the threshold to 0.5, the virtual view will be observed when it is half visible. */ this.threshold = input(0, ...(ngDevMode ? [{ debugName: "threshold" }] : [])); this.#rootElement = computed(() => { const root = this.root(); if (root) { if (root instanceof ElementRef) { return root.nativeElement; } return root; } else if (root === null) { return null; } return this.#elementRef.nativeElement; }, ...(ngDevMode ? [{ debugName: "#rootElement" }] : [])); this.#elements = new Map(); this.#forcedHidden$ = new BehaviorSubject(false); } #config; #platform; #elementRef; #observer; #resizeObserver; #rootElement; #elements; #forcedHidden$; ngOnInit() { if (this.#platform.isBrowser) { this.#observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (this.#elements.has(entry.target)) this.#elements.get(entry.target)?.next(entry.isIntersecting); }); }, { root: this.#rootElement(), rootMargin: this.rootMargin(), // @ts-expect-error - scrollMargin is not available in the type of IntersectionObserverInit scrollMargin: this.scrollMargin(), threshold: this.threshold(), }); } } ngOnDestroy() { this.#elements.clear(); this.#observer?.disconnect(); this.#observer = null; } /** * Hide all the virtual views. * * This is useful when you want to hide all the virtual views when the user cannot see them. * * For example, when the user opens a modal, you can hide all the virtual views to improve performance. * * **IMPORTANT:** * * Don't forget to call `showAllVisible()` when you want to show the virtual views again. */ hideAll() { this.#forcedHidden$.next(true); } /** * Show all the virtual views that are currently visible. * * This needs to be called if `hideAll()` was called before. */ showAllVisible() { this.#forcedHidden$.next(false); } observeElementVisibility(virtualView) { if (this.#platform.isServer) { return of(true); } const isVisible$ = new ReplaySubject(1); // Store the view and the visibility state in the map. // This allows us to retrieve the visibility state later. this.#elements.set(virtualView, isVisible$); // Start observing the virtual view immediately. this.#observer?.observe(virtualView); return combineLatest([isVisible$, this.#forcedHidden$]).pipe(map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), distinctUntilChanged(), finalize$1(() => { this.#observer?.unobserve(virtualView); this.#elements.delete(virtualView); })); } observeElementSize(element, options) { return this.#resizeObserver.observeElement(element, options); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewObserver, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: RxVirtualViewObserver, isStandalone: true, selector: "[rxVirtualViewObserver]", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: false, transformFunction: null }, rootMargin: { classPropertyName: "rootMargin", publicName: "rootMargin", isSignal: true, isRequired: false, transformFunction: null }, scrollMargin: { classPropertyName: "scrollMargin", publicName: "scrollMargin", isSignal: true, isRequired: false, transformFunction: null }, threshold: { classPropertyName: "threshold", publicName: "threshold", isSignal: true, isRequired: false, transformFunction: null } }, providers: [ VirtualViewCache, RxaResizeObserver, { provide: _RxVirtualViewObserver, useExisting: RxVirtualViewObserver }, ], usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewObserver, decorators: [{ type: Directive, args: [{ selector: '[rxVirtualViewObserver]', standalone: true, providers: [ VirtualViewCache, RxaResizeObserver, { provide: _RxVirtualViewObserver, useExisting: RxVirtualViewObserver }, ], }] }], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: false }] }], rootMargin: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootMargin", required: false }] }], scrollMargin: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollMargin", required: false }] }], threshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "threshold", required: false }] }] } }); /** * The RxVirtualViewPlaceholder directive is a directive that allows you to create a placeholder for the virtual view. * * It can be used on an element/component to create a placeholder for the virtual view. * * It needs to be a sibling of the `rxVirtualView` directive. * * @example * ```html * <div rxVirtualViewObserver> * <div rxVirtualView> * <div *rxVirtualViewContent>Virtual View 1</div> * <div *rxVirtualViewPlaceholder>Loading...</div> * </div> * </div> * ``` * * @developerPreview */ class RxVirtualViewPlaceholder { #virtualView = inject(_RxVirtualView); constructor(templateRef) { this.templateRef = templateRef; this.#virtualView.registerPlaceholder(this); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewPlaceholder, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: RxVirtualViewPlaceholder, isStandalone: true, selector: "[rxVirtualViewPlaceholder]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RxVirtualViewPlaceholder, decorators: [{ type: Directive, args: [{ selector: '[rxVirtualViewPlaceholder]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * Generated bundle index. Do not edit. */ export { RxVirtualView, RxVirtualViewContent, RxVirtualViewObserver, RxVirtualViewPlaceholder, provideVirtualViewConfig }; //# sourceMappingURL=template-virtual-view.mjs.map