|
1 | 1 | import { |
2 | 2 | ApplicationRef, |
3 | 3 | DestroyRef, |
| 4 | + Injector, |
4 | 5 | afterRenderEffect, |
| 6 | + assertInInjectionContext, |
5 | 7 | computed, |
6 | 8 | inject, |
7 | 9 | linkedSignal, |
| 10 | + runInInjectionContext, |
8 | 11 | untracked, |
9 | 12 | } from '@angular/core' |
10 | 13 | import { |
@@ -35,6 +38,14 @@ export type AngularVirtualizerOptions< |
35 | 38 | useApplicationRefTick?: boolean |
36 | 39 | } |
37 | 40 |
|
| 41 | +export type AngularExtensionOptions = { |
| 42 | + /** |
| 43 | + * The injector to use for the virtualizer. |
| 44 | + * @default inject(Injector) |
| 45 | + */ |
| 46 | + injector?: Injector |
| 47 | +} |
| 48 | + |
38 | 49 | // Flush CD after virtual-core updates so template bindings hit the DOM |
39 | 50 | // before the next frame's scroll reconciliation reads `scrollHeight`. |
40 | 51 | function injectScheduleDomFlushViaAppRefTick() { |
@@ -62,81 +73,90 @@ function injectVirtualizerBase< |
62 | 73 | TItemElement extends Element, |
63 | 74 | >( |
64 | 75 | options: () => AngularVirtualizerOptions<TScrollElement, TItemElement>, |
| 76 | + extensions: AngularExtensionOptions = {}, |
65 | 77 | ) { |
66 | | - const scheduleDomFlush = injectScheduleDomFlushViaAppRefTick() |
| 78 | + let injector = extensions.injector; |
| 79 | + if (!injector) { |
| 80 | + assertInInjectionContext(injectVirtualizerBase) |
| 81 | + injector = inject(Injector) |
| 82 | + } |
67 | 83 |
|
68 | | - const resolvedOptions = computed<VirtualizerOptions<TScrollElement, TItemElement>>(() => { |
69 | | - const { useApplicationRefTick = true, ..._options } = options() |
70 | | - return { |
71 | | - ..._options, |
72 | | - onChange: (instance, sync) => { |
73 | | - reactiveVirtualizer.set(instance) |
74 | | - if (useApplicationRefTick) { |
75 | | - scheduleDomFlush() |
76 | | - } |
77 | | - _options.onChange?.(instance, sync) |
78 | | - }, |
79 | | - } |
80 | | - }) |
| 84 | + return runInInjectionContext(injector, () => { |
| 85 | + const scheduleDomFlush = injectScheduleDomFlushViaAppRefTick() |
81 | 86 |
|
82 | | - const lazyVirtualizer = computed(() => new Virtualizer(untracked(resolvedOptions))) |
| 87 | + const resolvedOptions = computed<VirtualizerOptions<TScrollElement, TItemElement>>(() => { |
| 88 | + const { useApplicationRefTick = true, ..._options } = options() |
| 89 | + return { |
| 90 | + ..._options, |
| 91 | + onChange: (instance, sync) => { |
| 92 | + reactiveVirtualizer.set(instance) |
| 93 | + if (useApplicationRefTick) { |
| 94 | + scheduleDomFlush() |
| 95 | + } |
| 96 | + _options.onChange?.(instance, sync) |
| 97 | + }, |
| 98 | + } |
| 99 | + }) |
83 | 100 |
|
84 | | - const reactiveVirtualizer = linkedSignal(() => { |
85 | | - const virtualizer = lazyVirtualizer() |
86 | | - // If setOptions does not call onChange, it's safe to call it here |
87 | | - virtualizer.setOptions(resolvedOptions()) |
88 | | - return virtualizer |
89 | | - }, { equal: () => false }) |
| 101 | + const lazyVirtualizer = computed(() => new Virtualizer(untracked(resolvedOptions))) |
90 | 102 |
|
91 | | - afterRenderEffect((cleanup) => { |
92 | | - cleanup(lazyVirtualizer()._didMount()) |
93 | | - }) |
| 103 | + const reactiveVirtualizer = linkedSignal(() => { |
| 104 | + const virtualizer = lazyVirtualizer() |
| 105 | + // If setOptions does not call onChange, it's safe to call it here |
| 106 | + virtualizer.setOptions(resolvedOptions()) |
| 107 | + return virtualizer |
| 108 | + }, { equal: () => false }) |
94 | 109 |
|
95 | | - afterRenderEffect(() => { |
96 | | - reactiveVirtualizer()._willUpdate() |
97 | | - }) |
| 110 | + afterRenderEffect((cleanup) => { |
| 111 | + cleanup(lazyVirtualizer()._didMount()) |
| 112 | + }) |
| 113 | + |
| 114 | + afterRenderEffect(() => { |
| 115 | + reactiveVirtualizer()._willUpdate() |
| 116 | + }) |
98 | 117 |
|
99 | | - return signalProxy( |
100 | | - reactiveVirtualizer, |
101 | | - // Methods that pass through: call on the instance without tracking the signal read |
102 | | - [ |
103 | | - '_didMount', |
104 | | - '_willUpdate', |
105 | | - 'calculateRange', |
106 | | - 'getVirtualIndexes', |
107 | | - 'measure', |
108 | | - 'measureElement', |
109 | | - 'resizeItem', |
110 | | - 'scrollBy', |
111 | | - 'scrollToIndex', |
112 | | - 'scrollToOffset', |
113 | | - 'setOptions', |
114 | | - ], |
115 | | - // Attributes that will be transformed to signals |
116 | | - [ |
117 | | - 'isScrolling', |
118 | | - 'measurementsCache', |
119 | | - 'options', |
120 | | - 'range', |
121 | | - 'scrollDirection', |
122 | | - 'scrollElement', |
123 | | - 'scrollOffset', |
124 | | - 'scrollRect', |
125 | | - ], |
126 | | - // Methods that will be tracked to the virtualizer signal |
127 | | - [ |
128 | | - 'getOffsetForAlignment', |
129 | | - 'getOffsetForIndex', |
130 | | - 'getVirtualItemForOffset', |
131 | | - 'indexFromElement', |
132 | | - ], |
133 | | - // Zero-arg methods exposed as computed signals |
134 | | - [ |
135 | | - 'getTotalSize', |
136 | | - 'getVirtualItems' |
137 | | - ], |
138 | | - // The rest is passed as is, and can be accessed or called before initialization |
139 | | - ) as unknown as AngularVirtualizer<TScrollElement, TItemElement> |
| 118 | + return signalProxy( |
| 119 | + reactiveVirtualizer, |
| 120 | + // Methods that pass through: call on the instance without tracking the signal read |
| 121 | + [ |
| 122 | + '_didMount', |
| 123 | + '_willUpdate', |
| 124 | + 'calculateRange', |
| 125 | + 'getVirtualIndexes', |
| 126 | + 'measure', |
| 127 | + 'measureElement', |
| 128 | + 'resizeItem', |
| 129 | + 'scrollBy', |
| 130 | + 'scrollToIndex', |
| 131 | + 'scrollToOffset', |
| 132 | + 'setOptions', |
| 133 | + ], |
| 134 | + // Attributes that will be transformed to signals |
| 135 | + [ |
| 136 | + 'isScrolling', |
| 137 | + 'measurementsCache', |
| 138 | + 'options', |
| 139 | + 'range', |
| 140 | + 'scrollDirection', |
| 141 | + 'scrollElement', |
| 142 | + 'scrollOffset', |
| 143 | + 'scrollRect', |
| 144 | + ], |
| 145 | + // Methods that will be tracked to the virtualizer signal |
| 146 | + [ |
| 147 | + 'getOffsetForAlignment', |
| 148 | + 'getOffsetForIndex', |
| 149 | + 'getVirtualItemForOffset', |
| 150 | + 'indexFromElement', |
| 151 | + ], |
| 152 | + // Zero-arg methods exposed as computed signals |
| 153 | + [ |
| 154 | + 'getTotalSize', |
| 155 | + 'getVirtualItems' |
| 156 | + ], |
| 157 | + // The rest is passed as is, and can be accessed or called before initialization |
| 158 | + ) as unknown as AngularVirtualizer<TScrollElement, TItemElement> |
| 159 | + }) |
140 | 160 | } |
141 | 161 |
|
142 | 162 | export function injectVirtualizer< |
|
0 commit comments