diff --git a/.changeset/clean-ends-wish.md b/.changeset/clean-ends-wish.md new file mode 100644 index 000000000..12daf7530 --- /dev/null +++ b/.changeset/clean-ends-wish.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-virtual': patch +--- + +Fixes writing to signal error in angular effect diff --git a/packages/angular-virtual/src/index.ts b/packages/angular-virtual/src/index.ts index 55532654a..22c4549e3 100644 --- a/packages/angular-virtual/src/index.ts +++ b/packages/angular-virtual/src/index.ts @@ -5,7 +5,6 @@ import { effect, inject, signal, - untracked, } from '@angular/core' import { Virtualizer, @@ -16,7 +15,6 @@ import { observeWindowRect, windowScroll, } from '@tanstack/virtual-core' -import { proxyVirtualizer } from './proxy' import type { ElementRef, Signal } from '@angular/core' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' import type { AngularVirtualizer } from './types' @@ -30,52 +28,86 @@ function createVirtualizerBase< >( options: Signal>, ): AngularVirtualizer { - let virtualizer: Virtualizer - function lazyInit() { - virtualizer ??= new Virtualizer(options()) - return virtualizer + const instance = new Virtualizer(options()) + + const virtualItems = signal(instance.getVirtualItems(), { + equal: () => false, + }) + const totalSize = signal(instance.getTotalSize()) + const isScrolling = signal(instance.isScrolling) + const range = signal(instance.range, { equal: () => false }) + const scrollDirection = signal(instance.scrollDirection) + const scrollElement = signal(instance.scrollElement) + const scrollOffset = signal(instance.scrollOffset) + const scrollRect = signal(instance.scrollRect) + + const handler = { + get( + target: Virtualizer, + prop: keyof Virtualizer, + ) { + switch (prop) { + case 'getVirtualItems': + return virtualItems + case 'getTotalSize': + return totalSize + case 'isScrolling': + return isScrolling + case 'options': + return options + case 'range': + return range + case 'scrollDirection': + return scrollDirection + case 'scrollElement': + return scrollElement + case 'scrollOffset': + return scrollOffset + case 'scrollRect': + return scrollRect + default: + return Reflect.get(target, prop) + } + }, } - const virtualizerSignal = signal(virtualizer!, { equal: () => false }) + const virtualizer = new Proxy( + instance, + handler, + ) as unknown as AngularVirtualizer - // two-way sync options effect( () => { const _options = options() - lazyInit() - virtualizerSignal.set(virtualizer) - virtualizer.setOptions({ + instance.setOptions({ ..._options, onChange: (instance, sync) => { - // update virtualizerSignal so that dependent computeds recompute. - virtualizerSignal.set(instance) + virtualItems.set(instance.getVirtualItems()) + totalSize.set(instance.getTotalSize()) + isScrolling.set(instance.isScrolling) + range.set(instance.range) + scrollDirection.set(instance.scrollDirection) + scrollElement.set(instance.scrollElement) + scrollOffset.set(instance.scrollOffset) + scrollRect.set(instance.scrollRect) _options.onChange?.(instance, sync) }, }) - // update virtualizerSignal so that dependent computeds recompute. - virtualizerSignal.set(virtualizer) + instance._willUpdate() }, { 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() - } + let cleanup: (() => void) | undefined + afterNextRender({ + read: () => { + cleanup = instance._didMount() }, - { allowSignalWrites: true }, - ) - - let cleanup: () => void | undefined - afterNextRender({ read: () => (virtualizer ?? lazyInit())._didMount() }) + }) inject(DestroyRef).onDestroy(() => cleanup?.()) - return proxyVirtualizer(virtualizerSignal, lazyInit) + return virtualizer } export function injectVirtualizer< diff --git a/packages/angular-virtual/src/proxy.ts b/packages/angular-virtual/src/proxy.ts deleted file mode 100644 index 1664b58ea..000000000 --- a/packages/angular-virtual/src/proxy.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { computed, untracked } from '@angular/core' -import type { Signal, WritableSignal } from '@angular/core' -import type { Virtualizer } from '@tanstack/virtual-core' -import type { AngularVirtualizer } from './types' - -export function proxyVirtualizer< - V extends Virtualizer, - S extends Element | Window = V extends Virtualizer ? U : never, - I extends Element = V extends Virtualizer ? U : never, ->( - virtualizerSignal: WritableSignal, - lazyInit: () => V, -): AngularVirtualizer { - return new Proxy(virtualizerSignal, { - apply() { - return virtualizerSignal() - }, - get(target, property) { - const untypedTarget = target as any - 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 as keyof V] === 'function' - Object.defineProperty(untypedTarget, property, { - value: isFunction - ? computed(() => (target()[property as keyof V] as Function)()) - : computed(() => target()[property as keyof V]), - 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 as keyof V] as Function - Object.defineProperty(untypedTarget, property, { - value: toComputed(virtualizerSignal, fn), - configurable: true, - enumerable: true, - }) - } - - return untypedTarget[property] || virtualizer[property as keyof V] - }, - has(_, property: string) { - return !!untracked(virtualizerSignal)[property as keyof V] - }, - ownKeys() { - return Reflect.ownKeys(untracked(virtualizerSignal)) - }, - getOwnPropertyDescriptor() { - return { - enumerable: true, - configurable: true, - } - }, - }) as unknown as AngularVirtualizer -} - -function toComputed>( - signal: Signal, - fn: Function, -) { - const computedCache: Record> = {} - - return (...args: Array) => { - // 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: Array) { - return JSON.stringify(args) -}