@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
JavaScript
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