ngx-kit 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/angle-selector/README.md +34 -34
- package/box-shadow/README.md +33 -33
- package/color-picker/README.md +52 -52
- package/data-table/README.md +120 -120
- package/date-picker/README.md +74 -74
- package/dialog/README.md +92 -92
- package/drag-resize/README.md +55 -55
- package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
- package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -1
- package/fesm2022/ngx-kit-drawer-menu.mjs +2 -2
- package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
- package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
- package/fesm2022/ngx-kit-gallery.mjs +2 -2
- package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
- package/fesm2022/ngx-kit-grid-layout.mjs +14 -14
- package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -1
- package/fesm2022/ngx-kit-infinite-scroll.mjs +467 -0
- package/fesm2022/ngx-kit-infinite-scroll.mjs.map +1 -0
- package/fesm2022/ngx-kit-shared.mjs.map +1 -1
- package/fesm2022/ngx-kit-virtual-scroll.mjs +2 -2
- package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -1
- package/fesm2022/ngx-kit-webflow.mjs +38 -38
- package/fesm2022/ngx-kit-webflow.mjs.map +1 -1
- package/gallery/README.md +71 -71
- package/gradient-picker/README.md +41 -41
- package/grid-layout/README.md +64 -64
- package/image-editor/README.md +44 -44
- package/infinite-scroll/README.md +252 -0
- package/menu/README.md +85 -85
- package/message/README.md +70 -70
- package/notify/README.md +56 -56
- package/package.json +5 -1
- package/shared/README.md +117 -117
- package/time-picker/README.md +89 -89
- package/types/ngx-kit-infinite-scroll.d.ts +86 -0
- package/virtual-scroll/README.md +199 -199
package/virtual-scroll/README.md
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
# @ngx-kit/virtual-scroll
|
|
2
|
-
|
|
3
|
-
A zoneless, signal-based virtual scroll viewport for Angular. No `@angular/material`
|
|
4
|
-
or `@angular/cdk` dependency, no `NgZone`, no `ngOnChanges`, no `@Input() itemSize`,
|
|
5
|
-
no `@Input() orientation` — everything that can be derived from the real DOM is
|
|
6
|
-
derived from the real DOM.
|
|
7
|
-
|
|
8
|
-
## Why this rewrite
|
|
9
|
-
|
|
10
|
-
| Old design | This design |
|
|
11
|
-
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
12
|
-
| `NgZone` injected, `zone.run` everywhere | No `NgZone` at all — works natively under `provideZonelessChangeDetection()` |
|
|
13
|
-
| `ngOnChanges` + `SimpleChanges` | `input()` signals + `computed()` / `effect()` |
|
|
14
|
-
| `@Input() itemSize` (broke as soon as your item had margin/border/box-shadow) | Real item size is **measured** from `getBoundingClientRect()` deltas between actually rendered siblings, so margins/borders/shadows are always accounted for |
|
|
15
|
-
| `@Input() orientation` | Axis is **auto-detected** from the real layout of your own item markup |
|
|
16
|
-
| Custom `*ngxVirtualFor` structural directive | Plain native `@for` over a `visibleItems()` signal — you keep full control of `track`, `@empty`, etc. |
|
|
17
|
-
|
|
18
|
-
## Install & import
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { NgxVirtualScrollViewport } from '@ngx-kit/virtual-scroll';
|
|
22
|
-
|
|
23
|
-
@Component({
|
|
24
|
-
standalone: true,
|
|
25
|
-
imports: [NgxVirtualScrollViewport],
|
|
26
|
-
...
|
|
27
|
-
})
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Basic usage — vertical list (auto-detected)
|
|
31
|
-
|
|
32
|
-
```html
|
|
33
|
-
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="items" style="height: 400px">
|
|
34
|
-
@for (item of vs.visibleItems(); track $index; let i = $index) {
|
|
35
|
-
<div class="row">{{ vs.baseIndex() + i | number }} - {{ item.name }}</div>
|
|
36
|
-
}
|
|
37
|
-
</ngx-virtual-scroll-viewport>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This is exactly the case that used to break: `.row` can have `margin`, `border`,
|
|
41
|
-
`box-shadow`, `padding` — anything. The viewport measures the real distance between
|
|
42
|
-
two consecutively rendered rows and uses that, not a number you have to keep in sync
|
|
43
|
-
with your CSS.
|
|
44
|
-
|
|
45
|
-
```css
|
|
46
|
-
.row {
|
|
47
|
-
height: 48px;
|
|
48
|
-
border: 1px rgb(166 166 166 / 51%) solid;
|
|
49
|
-
box-shadow: 0px 0px 20px rgb(122 122 122 / 20%);
|
|
50
|
-
border-radius: 5px;
|
|
51
|
-
display: flex;
|
|
52
|
-
align-items: center;
|
|
53
|
-
margin: 10px;
|
|
54
|
-
padding: 15px;
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Horizontal lists & wrapping grids (auto-detected)
|
|
59
|
-
|
|
60
|
-
The axis is inferred purely from how your own items lay out:
|
|
61
|
-
|
|
62
|
-
- Items stack top-to-bottom (default block flow) → **vertical** list.
|
|
63
|
-
- Items lay out left-to-right in a single line (e.g. `display: inline-flex` on the
|
|
64
|
-
item, or a `flex-wrap: nowrap` row) → **horizontal** list.
|
|
65
|
-
- Items lay out left-to-right and _wrap_ to a new line (e.g. cards with
|
|
66
|
-
`display: flex; flex-wrap: wrap`) → treated as a **vertical** list of "lines",
|
|
67
|
-
where each line holds N items. This is how mixed horizontal + vertical layouts
|
|
68
|
-
(a card grid that scrolls vertically) are supported without a separate 2D data
|
|
69
|
-
model — the cross axis (row width) lays out and scrolls natively, only the line
|
|
70
|
-
axis is virtualized.
|
|
71
|
-
|
|
72
|
-
You don't configure any of this — you only make sure your `.item` CSS actually lays
|
|
73
|
-
out the way you want, and the viewport detects it.
|
|
74
|
-
|
|
75
|
-
## Mixed content (e.g. wide, virtualized table)
|
|
76
|
-
|
|
77
|
-
Both axes of the scroll container are always natively scrollable
|
|
78
|
-
(`overflow: auto` on both). Only the detected axis is virtualized; the cross axis
|
|
79
|
-
(for example, a table wider than its container) scrolls with the browser's native
|
|
80
|
-
scrollbar, so wide tables with virtualized rows work out of the box:
|
|
81
|
-
|
|
82
|
-
```html
|
|
83
|
-
<div class="table-shell">
|
|
84
|
-
<table><thead>...</thead></table>
|
|
85
|
-
<!-- kept outside, sticky/fixed header -->
|
|
86
|
-
|
|
87
|
-
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="rows" style="height: 480px">
|
|
88
|
-
<table>
|
|
89
|
-
<tbody>
|
|
90
|
-
@for (row of vs.visibleItems(); track row.id; let i = $index) {
|
|
91
|
-
<tr>
|
|
92
|
-
<td>{{ vs.baseIndex() + i }}</td>
|
|
93
|
-
...
|
|
94
|
-
</tr>
|
|
95
|
-
}
|
|
96
|
-
</tbody>
|
|
97
|
-
</table>
|
|
98
|
-
</ngx-virtual-scroll-viewport>
|
|
99
|
-
</div>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Very large data sets (1M+ rows)
|
|
103
|
-
|
|
104
|
-
Browsers clamp any element's real CSS `height`/`width` to a hard maximum
|
|
105
|
-
(~33.5M px in Chromium, far less in older WebKit/Gecko). Once a naive
|
|
106
|
-
"spacer" element crosses that ceiling, the browser silently clamps it,
|
|
107
|
-
`scrollHeight` stops growing, and everything past that point becomes
|
|
108
|
-
unreachable — the classic "scroll gets stuck around ~500k rows" symptom.
|
|
109
|
-
|
|
110
|
-
This viewport handles it automatically: the spacer's real DOM size is capped
|
|
111
|
-
at `maxAxisSizePx` (6,000,000px by default — safely under every known
|
|
112
|
-
browser limit), and the DOM scrollbar operates in a _compressed_ coordinate
|
|
113
|
-
space via `scaleFactor()`. All range/index math is done against the real,
|
|
114
|
-
uncompressed content size (`naturalTotalSize()`), so scrolling the
|
|
115
|
-
(compressed) native scrollbar all the way to the end still lands you exactly
|
|
116
|
-
on the last item, no matter how many millions of rows you have. The only
|
|
117
|
-
user-visible trade-off for truly massive lists is that the scrollbar thumb's
|
|
118
|
-
motion is no longer perfectly 1:1 with "one row per pixel" — an unavoidable
|
|
119
|
-
consequence of representing an arbitrarily large data set inside a bounded
|
|
120
|
-
native scrollbar, and the same trade-off every large-scale virtual scroll
|
|
121
|
-
implementation makes.
|
|
122
|
-
|
|
123
|
-
You don't need to configure anything for this — it only engages once
|
|
124
|
-
`naturalTotalSize()` actually exceeds `maxAxisSizePx`, and is a no-op
|
|
125
|
-
(`scaleFactor() === 1`) for the vast majority of real-world lists.
|
|
126
|
-
|
|
127
|
-
## API
|
|
128
|
-
|
|
129
|
-
### Inputs
|
|
130
|
-
|
|
131
|
-
| Input | Type | Default | Notes |
|
|
132
|
-
| --------------- | --------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
133
|
-
| `items` | `readonly T[]` | `[]` | Full data set |
|
|
134
|
-
| `minBufferPx` | `number` | `150` | Buffer left before recomputing the range |
|
|
135
|
-
| `maxBufferPx` | `number` | `300` | Extra px rendered outside the viewport |
|
|
136
|
-
| `measureFn` | `NgxVirtualScrollMeasureFn` | `undefined` | Advanced override for the built-in layout probe |
|
|
137
|
-
| `maxAxisSizePx` | `number` | `6_000_000` | Safe ceiling for the real DOM scroll size — see "Very large data sets" below |
|
|
138
|
-
|
|
139
|
-
### Outputs
|
|
140
|
-
|
|
141
|
-
| Output | Type |
|
|
142
|
-
| --------------------- | ----------------------------------------- |
|
|
143
|
-
| `scrolledIndexChange` | `OutputEmitterRef<number>` |
|
|
144
|
-
| `rangeChange` | `OutputEmitterRef<NgxVirtualScrollRange>` |
|
|
145
|
-
|
|
146
|
-
### Public signals (via `#vs="ngxVirtualScrollViewport"`)
|
|
147
|
-
|
|
148
|
-
| Signal | Type | Meaning |
|
|
149
|
-
| -------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
|
150
|
-
| `visibleItems()` | `readonly T[]` | Slice of `items()` to render right now |
|
|
151
|
-
| `range()` | `{ start, end }` | Currently rendered line range |
|
|
152
|
-
| `baseIndex()` | `number` | Absolute index of `visibleItems()[0]` |
|
|
153
|
-
| `axis()` | `'vertical' \| 'horizontal'` | Auto-detected scroll axis |
|
|
154
|
-
| `crossCount()` | `number` | Items per line (1 for a plain list) |
|
|
155
|
-
| `lineSize()` | `number` | Measured px size of one line |
|
|
156
|
-
| `isMeasured()` | `boolean` | Whether the initial layout probe has completed |
|
|
157
|
-
| `isScrolling()` | `boolean` | True while an active scroll gesture is in progress |
|
|
158
|
-
| `naturalTotalSize()` | `number` | Real, uncompressed total size along the scroll axis |
|
|
159
|
-
| `totalSize()` | `number` | Size actually applied to the spacer (capped at `maxAxisSizePx`) |
|
|
160
|
-
| `scaleFactor()` | `number` | `totalSize() / naturalTotalSize()`; `1` unless the data set is extremely large |
|
|
161
|
-
|
|
162
|
-
### Methods
|
|
163
|
-
|
|
164
|
-
| Method | Description |
|
|
165
|
-
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
166
|
-
| `scrollToIndex(index, behavior?)` | Scroll to a given item index |
|
|
167
|
-
| `scrollToOffset(offsetPx, behavior?)` | Scroll to a given pixel offset |
|
|
168
|
-
| `checkViewportSize()` | Force a re-measure (rarely needed — a `ResizeObserver` already handles container resizes) |
|
|
169
|
-
|
|
170
|
-
## Performance notes
|
|
171
|
-
|
|
172
|
-
- Fully `ChangeDetectionStrategy.OnPush` and zoneless-compatible — no `NgZone`
|
|
173
|
-
import anywhere; signal writes are what schedule change detection.
|
|
174
|
-
- Scroll handling is `requestAnimationFrame`-throttled and the listener is
|
|
175
|
-
registered with `{ passive: true }`.
|
|
176
|
-
- The range is only recomputed once the scroll offset gets within
|
|
177
|
-
`minBufferPx` of the current range's edge — not on every scroll event.
|
|
178
|
-
- Layout is only ever **read** (`getBoundingClientRect`) inside an
|
|
179
|
-
`afterRenderEffect({ read: ... })` block, so DOM measurement never
|
|
180
|
-
interleaves with — and never forces — a layout/style write.
|
|
181
|
-
- `will-change: transform` is only applied to the content wrapper while
|
|
182
|
-
`isScrolling()` is true, and removed ~150ms after the last scroll event, to
|
|
183
|
-
avoid holding a compositor layer alive for the component's entire lifetime.
|
|
184
|
-
- `contain: strict` / `contain: layout style` isolate the viewport's layout
|
|
185
|
-
and paint from the rest of the page.
|
|
186
|
-
|
|
187
|
-
## How auto-measurement works
|
|
188
|
-
|
|
189
|
-
On first render, up to 24 items are rendered "in the open" (un-transformed) purely
|
|
190
|
-
to be measured — see `measure-layout.ts`. Their real `getBoundingClientRect()`
|
|
191
|
-
deltas are used to infer axis, items-per-line, and line size in one pass, all pure
|
|
192
|
-
functions with no Angular dependency (so they are trivially unit-testable). Once
|
|
193
|
-
measured, the real virtualized range replaces the probe range and rendering
|
|
194
|
-
proceeds normally. If your rows can change size at runtime (e.g. responsive
|
|
195
|
-
breakpoints), a `ResizeObserver` on the scroll container triggers `checkViewportSize()`
|
|
196
|
-
automatically to re-fit the viewport dimension; if the _line size itself_ changes at
|
|
197
|
-
runtime you can force a full re-measure by re-creating the component (e.g. keyed with
|
|
198
|
-
`@if`/`*ngIf` + a changing key), which is the same limitation `cdk-virtual-scroll-viewport`
|
|
199
|
-
has with its own `FixedSizeVirtualScrollStrategy`.
|
|
1
|
+
# @ngx-kit/virtual-scroll
|
|
2
|
+
|
|
3
|
+
A zoneless, signal-based virtual scroll viewport for Angular. No `@angular/material`
|
|
4
|
+
or `@angular/cdk` dependency, no `NgZone`, no `ngOnChanges`, no `@Input() itemSize`,
|
|
5
|
+
no `@Input() orientation` — everything that can be derived from the real DOM is
|
|
6
|
+
derived from the real DOM.
|
|
7
|
+
|
|
8
|
+
## Why this rewrite
|
|
9
|
+
|
|
10
|
+
| Old design | This design |
|
|
11
|
+
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
12
|
+
| `NgZone` injected, `zone.run` everywhere | No `NgZone` at all — works natively under `provideZonelessChangeDetection()` |
|
|
13
|
+
| `ngOnChanges` + `SimpleChanges` | `input()` signals + `computed()` / `effect()` |
|
|
14
|
+
| `@Input() itemSize` (broke as soon as your item had margin/border/box-shadow) | Real item size is **measured** from `getBoundingClientRect()` deltas between actually rendered siblings, so margins/borders/shadows are always accounted for |
|
|
15
|
+
| `@Input() orientation` | Axis is **auto-detected** from the real layout of your own item markup |
|
|
16
|
+
| Custom `*ngxVirtualFor` structural directive | Plain native `@for` over a `visibleItems()` signal — you keep full control of `track`, `@empty`, etc. |
|
|
17
|
+
|
|
18
|
+
## Install & import
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { NgxVirtualScrollViewport } from '@ngx-kit/virtual-scroll';
|
|
22
|
+
|
|
23
|
+
@Component({
|
|
24
|
+
standalone: true,
|
|
25
|
+
imports: [NgxVirtualScrollViewport],
|
|
26
|
+
...
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Basic usage — vertical list (auto-detected)
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="items" style="height: 400px">
|
|
34
|
+
@for (item of vs.visibleItems(); track $index; let i = $index) {
|
|
35
|
+
<div class="row">{{ vs.baseIndex() + i | number }} - {{ item.name }}</div>
|
|
36
|
+
}
|
|
37
|
+
</ngx-virtual-scroll-viewport>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is exactly the case that used to break: `.row` can have `margin`, `border`,
|
|
41
|
+
`box-shadow`, `padding` — anything. The viewport measures the real distance between
|
|
42
|
+
two consecutively rendered rows and uses that, not a number you have to keep in sync
|
|
43
|
+
with your CSS.
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
.row {
|
|
47
|
+
height: 48px;
|
|
48
|
+
border: 1px rgb(166 166 166 / 51%) solid;
|
|
49
|
+
box-shadow: 0px 0px 20px rgb(122 122 122 / 20%);
|
|
50
|
+
border-radius: 5px;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
margin: 10px;
|
|
54
|
+
padding: 15px;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Horizontal lists & wrapping grids (auto-detected)
|
|
59
|
+
|
|
60
|
+
The axis is inferred purely from how your own items lay out:
|
|
61
|
+
|
|
62
|
+
- Items stack top-to-bottom (default block flow) → **vertical** list.
|
|
63
|
+
- Items lay out left-to-right in a single line (e.g. `display: inline-flex` on the
|
|
64
|
+
item, or a `flex-wrap: nowrap` row) → **horizontal** list.
|
|
65
|
+
- Items lay out left-to-right and _wrap_ to a new line (e.g. cards with
|
|
66
|
+
`display: flex; flex-wrap: wrap`) → treated as a **vertical** list of "lines",
|
|
67
|
+
where each line holds N items. This is how mixed horizontal + vertical layouts
|
|
68
|
+
(a card grid that scrolls vertically) are supported without a separate 2D data
|
|
69
|
+
model — the cross axis (row width) lays out and scrolls natively, only the line
|
|
70
|
+
axis is virtualized.
|
|
71
|
+
|
|
72
|
+
You don't configure any of this — you only make sure your `.item` CSS actually lays
|
|
73
|
+
out the way you want, and the viewport detects it.
|
|
74
|
+
|
|
75
|
+
## Mixed content (e.g. wide, virtualized table)
|
|
76
|
+
|
|
77
|
+
Both axes of the scroll container are always natively scrollable
|
|
78
|
+
(`overflow: auto` on both). Only the detected axis is virtualized; the cross axis
|
|
79
|
+
(for example, a table wider than its container) scrolls with the browser's native
|
|
80
|
+
scrollbar, so wide tables with virtualized rows work out of the box:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<div class="table-shell">
|
|
84
|
+
<table><thead>...</thead></table>
|
|
85
|
+
<!-- kept outside, sticky/fixed header -->
|
|
86
|
+
|
|
87
|
+
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="rows" style="height: 480px">
|
|
88
|
+
<table>
|
|
89
|
+
<tbody>
|
|
90
|
+
@for (row of vs.visibleItems(); track row.id; let i = $index) {
|
|
91
|
+
<tr>
|
|
92
|
+
<td>{{ vs.baseIndex() + i }}</td>
|
|
93
|
+
...
|
|
94
|
+
</tr>
|
|
95
|
+
}
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
</ngx-virtual-scroll-viewport>
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Very large data sets (1M+ rows)
|
|
103
|
+
|
|
104
|
+
Browsers clamp any element's real CSS `height`/`width` to a hard maximum
|
|
105
|
+
(~33.5M px in Chromium, far less in older WebKit/Gecko). Once a naive
|
|
106
|
+
"spacer" element crosses that ceiling, the browser silently clamps it,
|
|
107
|
+
`scrollHeight` stops growing, and everything past that point becomes
|
|
108
|
+
unreachable — the classic "scroll gets stuck around ~500k rows" symptom.
|
|
109
|
+
|
|
110
|
+
This viewport handles it automatically: the spacer's real DOM size is capped
|
|
111
|
+
at `maxAxisSizePx` (6,000,000px by default — safely under every known
|
|
112
|
+
browser limit), and the DOM scrollbar operates in a _compressed_ coordinate
|
|
113
|
+
space via `scaleFactor()`. All range/index math is done against the real,
|
|
114
|
+
uncompressed content size (`naturalTotalSize()`), so scrolling the
|
|
115
|
+
(compressed) native scrollbar all the way to the end still lands you exactly
|
|
116
|
+
on the last item, no matter how many millions of rows you have. The only
|
|
117
|
+
user-visible trade-off for truly massive lists is that the scrollbar thumb's
|
|
118
|
+
motion is no longer perfectly 1:1 with "one row per pixel" — an unavoidable
|
|
119
|
+
consequence of representing an arbitrarily large data set inside a bounded
|
|
120
|
+
native scrollbar, and the same trade-off every large-scale virtual scroll
|
|
121
|
+
implementation makes.
|
|
122
|
+
|
|
123
|
+
You don't need to configure anything for this — it only engages once
|
|
124
|
+
`naturalTotalSize()` actually exceeds `maxAxisSizePx`, and is a no-op
|
|
125
|
+
(`scaleFactor() === 1`) for the vast majority of real-world lists.
|
|
126
|
+
|
|
127
|
+
## API
|
|
128
|
+
|
|
129
|
+
### Inputs
|
|
130
|
+
|
|
131
|
+
| Input | Type | Default | Notes |
|
|
132
|
+
| --------------- | --------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
133
|
+
| `items` | `readonly T[]` | `[]` | Full data set |
|
|
134
|
+
| `minBufferPx` | `number` | `150` | Buffer left before recomputing the range |
|
|
135
|
+
| `maxBufferPx` | `number` | `300` | Extra px rendered outside the viewport |
|
|
136
|
+
| `measureFn` | `NgxVirtualScrollMeasureFn` | `undefined` | Advanced override for the built-in layout probe |
|
|
137
|
+
| `maxAxisSizePx` | `number` | `6_000_000` | Safe ceiling for the real DOM scroll size — see "Very large data sets" below |
|
|
138
|
+
|
|
139
|
+
### Outputs
|
|
140
|
+
|
|
141
|
+
| Output | Type |
|
|
142
|
+
| --------------------- | ----------------------------------------- |
|
|
143
|
+
| `scrolledIndexChange` | `OutputEmitterRef<number>` |
|
|
144
|
+
| `rangeChange` | `OutputEmitterRef<NgxVirtualScrollRange>` |
|
|
145
|
+
|
|
146
|
+
### Public signals (via `#vs="ngxVirtualScrollViewport"`)
|
|
147
|
+
|
|
148
|
+
| Signal | Type | Meaning |
|
|
149
|
+
| -------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
|
150
|
+
| `visibleItems()` | `readonly T[]` | Slice of `items()` to render right now |
|
|
151
|
+
| `range()` | `{ start, end }` | Currently rendered line range |
|
|
152
|
+
| `baseIndex()` | `number` | Absolute index of `visibleItems()[0]` |
|
|
153
|
+
| `axis()` | `'vertical' \| 'horizontal'` | Auto-detected scroll axis |
|
|
154
|
+
| `crossCount()` | `number` | Items per line (1 for a plain list) |
|
|
155
|
+
| `lineSize()` | `number` | Measured px size of one line |
|
|
156
|
+
| `isMeasured()` | `boolean` | Whether the initial layout probe has completed |
|
|
157
|
+
| `isScrolling()` | `boolean` | True while an active scroll gesture is in progress |
|
|
158
|
+
| `naturalTotalSize()` | `number` | Real, uncompressed total size along the scroll axis |
|
|
159
|
+
| `totalSize()` | `number` | Size actually applied to the spacer (capped at `maxAxisSizePx`) |
|
|
160
|
+
| `scaleFactor()` | `number` | `totalSize() / naturalTotalSize()`; `1` unless the data set is extremely large |
|
|
161
|
+
|
|
162
|
+
### Methods
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
166
|
+
| `scrollToIndex(index, behavior?)` | Scroll to a given item index |
|
|
167
|
+
| `scrollToOffset(offsetPx, behavior?)` | Scroll to a given pixel offset |
|
|
168
|
+
| `checkViewportSize()` | Force a re-measure (rarely needed — a `ResizeObserver` already handles container resizes) |
|
|
169
|
+
|
|
170
|
+
## Performance notes
|
|
171
|
+
|
|
172
|
+
- Fully `ChangeDetectionStrategy.OnPush` and zoneless-compatible — no `NgZone`
|
|
173
|
+
import anywhere; signal writes are what schedule change detection.
|
|
174
|
+
- Scroll handling is `requestAnimationFrame`-throttled and the listener is
|
|
175
|
+
registered with `{ passive: true }`.
|
|
176
|
+
- The range is only recomputed once the scroll offset gets within
|
|
177
|
+
`minBufferPx` of the current range's edge — not on every scroll event.
|
|
178
|
+
- Layout is only ever **read** (`getBoundingClientRect`) inside an
|
|
179
|
+
`afterRenderEffect({ read: ... })` block, so DOM measurement never
|
|
180
|
+
interleaves with — and never forces — a layout/style write.
|
|
181
|
+
- `will-change: transform` is only applied to the content wrapper while
|
|
182
|
+
`isScrolling()` is true, and removed ~150ms after the last scroll event, to
|
|
183
|
+
avoid holding a compositor layer alive for the component's entire lifetime.
|
|
184
|
+
- `contain: strict` / `contain: layout style` isolate the viewport's layout
|
|
185
|
+
and paint from the rest of the page.
|
|
186
|
+
|
|
187
|
+
## How auto-measurement works
|
|
188
|
+
|
|
189
|
+
On first render, up to 24 items are rendered "in the open" (un-transformed) purely
|
|
190
|
+
to be measured — see `measure-layout.ts`. Their real `getBoundingClientRect()`
|
|
191
|
+
deltas are used to infer axis, items-per-line, and line size in one pass, all pure
|
|
192
|
+
functions with no Angular dependency (so they are trivially unit-testable). Once
|
|
193
|
+
measured, the real virtualized range replaces the probe range and rendering
|
|
194
|
+
proceeds normally. If your rows can change size at runtime (e.g. responsive
|
|
195
|
+
breakpoints), a `ResizeObserver` on the scroll container triggers `checkViewportSize()`
|
|
196
|
+
automatically to re-fit the viewport dimension; if the _line size itself_ changes at
|
|
197
|
+
runtime you can force a full re-measure by re-creating the component (e.g. keyed with
|
|
198
|
+
`@if`/`*ngIf` + a changing key), which is the same limitation `cdk-virtual-scroll-viewport`
|
|
199
|
+
has with its own `FixedSizeVirtualScrollStrategy`.
|