|
1 | 1 | import { computed, untracked } from '@angular/core' |
2 | | -import type { Signal, WritableSignal } from '@angular/core' |
3 | | -import type { Virtualizer } from '@tanstack/virtual-core' |
4 | | -import type { AngularVirtualizer } from './types' |
| 2 | +import type { Signal } from '@angular/core' |
5 | 3 |
|
6 | | -export function proxyVirtualizer< |
7 | | - V extends Virtualizer<any, any>, |
8 | | - S extends Element | Window = V extends Virtualizer<infer U, any> ? U : never, |
9 | | - I extends Element = V extends Virtualizer<any, infer U> ? U : never, |
| 4 | +export type SignalProxy< |
| 5 | + TInput extends Record<string | symbol, any>, |
| 6 | + TMethodsToPassThrough extends keyof TInput, |
| 7 | + TAttributesToTransformToSignals extends keyof TInput, |
| 8 | + TMethodsToTrack extends keyof TInput, |
| 9 | + TMethodsToTransformToSignals extends keyof TInput, |
| 10 | +> = { |
| 11 | + [K in TMethodsToPassThrough]: TInput[K] |
| 12 | +} & { |
| 13 | + [K in TAttributesToTransformToSignals]: Signal<TInput[K]> |
| 14 | + } & { |
| 15 | + [K in TMethodsToTrack]: TInput[K] |
| 16 | + } & { |
| 17 | + [K in TMethodsToTransformToSignals]: Signal<ReturnType<TInput[K]>> |
| 18 | + } |
| 19 | + |
| 20 | +export function signalProxy< |
| 21 | + TInput extends Record<string | symbol, any>, |
| 22 | + TMethodsToPassThrough extends keyof TInput, |
| 23 | + TAttributesToTransformToSignals extends keyof TInput, |
| 24 | + TMethodsToTrack extends keyof TInput, |
| 25 | + TMethodsToTransformToSignals extends keyof TInput, |
10 | 26 | >( |
11 | | - virtualizerSignal: WritableSignal<V>, |
12 | | - lazyInit: () => V, |
13 | | -): AngularVirtualizer<S, I> { |
14 | | - return new Proxy(virtualizerSignal, { |
| 27 | + inputSignal: Signal<TInput>, |
| 28 | + methodsToPassThrough: Array<TMethodsToPassThrough>, |
| 29 | + attributesToTransformToSignals: Array<TAttributesToTransformToSignals>, |
| 30 | + methodsToTrack: Array<TMethodsToTrack>, |
| 31 | + methodsToTransformToSignals: Array<TMethodsToTransformToSignals>, |
| 32 | +): SignalProxy< |
| 33 | + TInput, |
| 34 | + TMethodsToPassThrough, |
| 35 | + TAttributesToTransformToSignals, |
| 36 | + TMethodsToTrack, |
| 37 | + TMethodsToTransformToSignals |
| 38 | +> { |
| 39 | + // Type needed to proxy with the apply handler |
| 40 | + const callableTarget = (() => inputSignal()) as (() => TInput) & |
| 41 | + Record<PropertyKey, unknown> |
| 42 | + |
| 43 | + return new Proxy(callableTarget, { |
15 | 44 | apply() { |
16 | | - return virtualizerSignal() |
| 45 | + return inputSignal() |
17 | 46 | }, |
18 | 47 | get(target, property) { |
19 | | - const untypedTarget = target as any |
20 | | - if (untypedTarget[property]) { |
21 | | - return untypedTarget[property] |
| 48 | + const fieldValue = target[property as keyof typeof callableTarget] |
| 49 | + if (fieldValue !== undefined) return fieldValue |
| 50 | + |
| 51 | + // Methods that pass through: call on the instance without tracking the signal read |
| 52 | + if (methodsToPassThrough.includes(property as TMethodsToPassThrough)) { |
| 53 | + return (target[property] = (...args: Parameters<TInput[typeof property]>) => |
| 54 | + untracked(inputSignal)[property as keyof TInput](...args)) |
22 | 55 | } |
23 | | - let virtualizer = untracked(virtualizerSignal) |
24 | | - if (virtualizer == null) { |
25 | | - virtualizer = lazyInit() |
26 | | - untracked(() => virtualizerSignal.set(virtualizer)) |
| 56 | + |
| 57 | + // Zero-arg methods exposed as computed signals (matches main list A for getTotalSize / getVirtualItems) |
| 58 | + if (methodsToTransformToSignals.includes(property as TMethodsToTransformToSignals)) { |
| 59 | + return (target[property] = computed( |
| 60 | + () => (inputSignal()[property as keyof TInput] as () => unknown)() |
| 61 | + )) |
27 | 62 | } |
28 | 63 |
|
29 | | - // Create computed signals for each property that represents a reactive value |
30 | | - if ( |
31 | | - typeof property === 'string' && |
32 | | - [ |
33 | | - 'getTotalSize', |
34 | | - 'getVirtualItems', |
35 | | - 'isScrolling', |
36 | | - 'options', |
37 | | - 'range', |
38 | | - 'scrollDirection', |
39 | | - 'scrollElement', |
40 | | - 'scrollOffset', |
41 | | - 'scrollRect', |
42 | | - 'measureElementCache', |
43 | | - 'measurementsCache', |
44 | | - ].includes(property) |
45 | | - ) { |
46 | | - const isFunction = |
47 | | - typeof virtualizer[property as keyof V] === 'function' |
48 | | - Object.defineProperty(untypedTarget, property, { |
49 | | - value: isFunction |
50 | | - ? computed(() => (target()[property as keyof V] as Function)()) |
51 | | - : computed(() => target()[property as keyof V]), |
52 | | - configurable: true, |
53 | | - enumerable: true, |
54 | | - }) |
| 64 | + // Methods that need to be tracked, track instance changes and call the method |
| 65 | + if (methodsToTrack.includes(property as TMethodsToTrack)) { |
| 66 | + return (target[property] = (...args: Parameters<TInput[typeof property]>) => |
| 67 | + inputSignal()[property as keyof TInput](...args)) |
55 | 68 | } |
56 | 69 |
|
57 | | - // Create plain signals for functions that accept arguments and return reactive values |
58 | | - if ( |
59 | | - typeof property === 'string' && |
60 | | - [ |
61 | | - 'getOffsetForAlignment', |
62 | | - 'getOffsetForIndex', |
63 | | - 'getVirtualItemForOffset', |
64 | | - 'indexFromElement', |
65 | | - ].includes(property) |
66 | | - ) { |
67 | | - const fn = virtualizer[property as keyof V] as Function |
68 | | - Object.defineProperty(untypedTarget, property, { |
69 | | - value: toComputed(virtualizerSignal, fn), |
70 | | - configurable: true, |
71 | | - enumerable: true, |
72 | | - }) |
| 70 | + // Other values that are tracked as signals |
| 71 | + if (attributesToTransformToSignals.includes(property as TAttributesToTransformToSignals)) { |
| 72 | + return (target[property] = computed(() => inputSignal()[property as keyof TInput])) |
73 | 73 | } |
74 | 74 |
|
75 | | - return untypedTarget[property] || virtualizer[property as keyof V] |
| 75 | + // All other fiels. Any field that is not handled above will fail if the signal includes |
| 76 | + // a input or model from a component and this is accessed before initialization. |
| 77 | + return untracked(inputSignal)[property as keyof TInput] |
76 | 78 | }, |
77 | 79 | has(_, property: string) { |
78 | | - return !!untracked(virtualizerSignal)[property as keyof V] |
| 80 | + return property in untracked(inputSignal) |
79 | 81 | }, |
80 | 82 | ownKeys() { |
81 | | - return Reflect.ownKeys(untracked(virtualizerSignal)) |
| 83 | + return Reflect.ownKeys(untracked(inputSignal)) |
82 | 84 | }, |
83 | 85 | getOwnPropertyDescriptor() { |
84 | 86 | return { |
85 | 87 | enumerable: true, |
86 | 88 | configurable: true, |
| 89 | + writable: true, |
87 | 90 | } |
88 | 91 | }, |
89 | | - }) as unknown as AngularVirtualizer<S, I> |
90 | | -} |
91 | | - |
92 | | -function toComputed<V extends Virtualizer<any, any>>( |
93 | | - signal: Signal<V>, |
94 | | - fn: Function, |
95 | | -) { |
96 | | - const computedCache: Record<string, Signal<unknown>> = {} |
97 | | - |
98 | | - return (...args: Array<any>) => { |
99 | | - // Cache computeds by their arguments to avoid re-creating the computed on each call |
100 | | - const serializedArgs = serializeArgs(...args) |
101 | | - if (computedCache.hasOwnProperty(serializedArgs)) { |
102 | | - return computedCache[serializedArgs]?.() |
103 | | - } |
104 | | - const computedSignal = computed(() => { |
105 | | - void signal() |
106 | | - return fn(...args) |
107 | | - }) |
108 | | - |
109 | | - computedCache[serializedArgs] = computedSignal |
110 | | - |
111 | | - return computedSignal() |
112 | | - } |
113 | | -} |
114 | | - |
115 | | -function serializeArgs(...args: Array<any>) { |
116 | | - return JSON.stringify(args) |
| 92 | + }) as SignalProxy< |
| 93 | + TInput, |
| 94 | + TMethodsToPassThrough, |
| 95 | + TAttributesToTransformToSignals, |
| 96 | + TMethodsToTrack, |
| 97 | + TMethodsToTransformToSignals |
| 98 | + > |
117 | 99 | } |
0 commit comments