UNPKG

@tanstack/angular-virtual

Version:

Headless UI for virtualizing scrollable elements in Angular

173 lines (168 loc) 6.5 kB
import { untracked, computed, signal, effect, afterNextRender, AfterRenderPhase, inject, DestroyRef } from '@angular/core'; import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, windowScroll, observeWindowOffset, observeWindowRect } from '@tanstack/virtual-core'; export * from '@tanstack/virtual-core'; function proxyVirtualizer(virtualizerSignal, lazyInit) { return new Proxy(virtualizerSignal, { apply() { return virtualizerSignal(); }, get(target, property) { const untypedTarget = target; if (untypedTarget[property]) { return untypedTarget[property]; } let virtualizer = untracked(virtualizerSignal); if (virtualizer == null) { virtualizer = lazyInit(); untracked(() => virtualizerSignal.set(virtualizer)); } // Create computed signals for each property that represents a reactive value if (typeof property === 'string' && [ 'getTotalSize', 'getVirtualItems', 'isScrolling', 'options', 'range', 'scrollDirection', 'scrollElement', 'scrollOffset', 'scrollRect', 'measureElementCache', 'measurementsCache', ].includes(property)) { const isFunction = typeof virtualizer[property] === 'function'; Object.defineProperty(untypedTarget, property, { value: isFunction ? computed(() => target()[property]()) : computed(() => target()[property]), configurable: true, enumerable: true, }); } // Create plain signals for functions that accept arguments and return reactive values if (typeof property === 'string' && [ 'getOffsetForAlignment', 'getOffsetForIndex', 'getVirtualItemForOffset', 'indexFromElement', ].includes(property)) { const fn = virtualizer[property]; Object.defineProperty(untypedTarget, property, { value: toComputed(virtualizerSignal, fn), configurable: true, enumerable: true, }); } return untypedTarget[property] || virtualizer[property]; }, has(_, property) { return !!untracked(virtualizerSignal)[property]; }, ownKeys() { return Reflect.ownKeys(untracked(virtualizerSignal)); }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true, }; }, }); } function toComputed(signal, fn) { const computedCache = {}; return (...args) => { // Cache computeds by their arguments to avoid re-creating the computed on each call const serializedArgs = serializeArgs(...args); if (computedCache.hasOwnProperty(serializedArgs)) { return computedCache[serializedArgs]?.(); } const computedSignal = computed(() => { void signal(); return fn(...args); }); computedCache[serializedArgs] = computedSignal; return computedSignal(); }; } function serializeArgs(...args) { return JSON.stringify(args); } function createVirtualizerBase(options) { let virtualizer; function lazyInit() { virtualizer ??= new Virtualizer(options()); return virtualizer; } const virtualizerSignal = signal(virtualizer, { equal: () => false }); // two-way sync options effect(() => { const _options = options(); lazyInit(); virtualizerSignal.set(virtualizer); virtualizer.setOptions({ ..._options, onChange: (instance, sync) => { // update virtualizerSignal so that dependent computeds recompute. virtualizerSignal.set(instance); _options.onChange?.(instance, sync); }, }); // update virtualizerSignal so that dependent computeds recompute. virtualizerSignal.set(virtualizer); }, { allowSignalWrites: true }); const scrollElement = computed(() => options().getScrollElement()); // let the virtualizer know when the scroll element is changed effect(() => { const el = scrollElement(); if (el) { untracked(virtualizerSignal)._willUpdate(); } }, { allowSignalWrites: true }); let cleanup; afterNextRender(() => (virtualizer ?? lazyInit())._didMount(), { phase: AfterRenderPhase.Read, }); inject(DestroyRef).onDestroy(() => cleanup?.()); return proxyVirtualizer(virtualizerSignal, lazyInit); } function injectVirtualizer(options) { const resolvedOptions = computed(() => { return { observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, getScrollElement: () => { const elementOrRef = options().scrollElement; return ((isElementRef(elementOrRef) ? elementOrRef.nativeElement : elementOrRef) ?? null); }, ...options(), }; }); return createVirtualizerBase(resolvedOptions); } function isElementRef(elementOrRef) { return elementOrRef != null && 'nativeElement' in elementOrRef; } function injectWindowVirtualizer(options) { const resolvedOptions = computed(() => { return { getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => typeof document !== 'undefined' ? window.scrollY : 0, ...options(), }; }); return createVirtualizerBase(resolvedOptions); } /** * Generated bundle index. Do not edit. */ export { injectVirtualizer, injectWindowVirtualizer }; //# sourceMappingURL=tanstack-angular-virtual.mjs.map