@tanstack/angular-virtual
Version:
Headless UI for virtualizing scrollable elements in Angular
173 lines (168 loc) • 6.5 kB
JavaScript
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