Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
342 changes: 121 additions & 221 deletions README.md

Large diffs are not rendered by default.

143 changes: 143 additions & 0 deletions docs/adapter-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Adapter methods

[← Adapter properties](adapter.md) · [Documentation index](index.md) · Working draft

## Results, lifecycle and sequencing

Datasource construction creates the Adapter and permits subscriptions. `Workflow` initialization attaches it and sets `init`; returning from the Workflow constructor does not mean that the first cycle has finished. A scroller-control method called before `init` has no effect, although its result can report `success: true`.

For an integration that remains mounted while initialization completes, the native reactive API permits this sequence:

```js
const adapter = datasource.adapter;
if (!adapter.init) {
await new Promise(resolve => adapter.init$.once(resolve));
}
const result = await adapter.relax();
```

The `await` after `init$` allows initialization to finish before `relax()` runs. A pending subscription needs cancellation if its integration is disposed first. The same sequence requires the integration's own subscription API when Adapter reactivity has been replaced.

Except for `showLog()`, every method returns a Promise resolving to `{ success: boolean, immediate: boolean, details: string | null }`.

| Field | Meaning |
| --- | --- |
| `success` | Whether the Adapter operation completed without an Adapter error. `true` can also describe a no-op, including a call before initialization. |
| `immediate` | Whether completion required no workflow-settling wait; it does not mean the Promise callback runs synchronously. |
| `details` | An explanation when the operation was ignored, interrupted or failed; otherwise `null`. |

Adapter validation errors resolve with `success: false` rather than rejecting the Promise. This result is not a transaction or a guarantee that application callbacks cannot throw. Dependent operations require explicit sequencing; concurrent calls do not promise an isolated render per call. `reload` and `reset` can interrupt a pending cycle, but do not abort the application's underlying data request. Workflow disposal settles outstanding Adapter work.

### relax

`relax(callback?: () => void)` waits until the scroller is idle, or completes immediately if it already is. The optional callback runs synchronously at that point; its return value is ignored. A superseding `reload`, `reset` or disposal can end a pending wait with `success: false`.

Idleness does not assert that the datasource request succeeded, that a separate consumer render finished, or that the scroller will remain idle. Dependent Adapter calls follow the returned Promise rather than running inside the callback. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#relax).

## Scroller control

### reload

`reload(reloadIndex?: number | string)` discards the rendered Buffer and fetches again around the supplied integer index. Without a valid index it starts at configured `startIndex`; an out-of-range index is clamped to configured bounds. A one-off reload index does not change the configured start.

The existing datasource `get` and settings remain in effect. The internal cache is cleared unless `devSettings.cacheOnReload` is enabled. `reload` can interrupt a busy cycle, but is ignored while paused. It restarts viewport loading rather than retrying one failed range. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#reload).

### reset

`reset(datasource?: { get?, settings?, devSettings? })` reinitializes scroller state, clears the Buffer and cache, and starts a new load. Without an argument it uses the existing datasource configuration. A supplied `get`, `settings` or `devSettings` section replaces that section; omitted sections retain their current values. Individual settings are not deep-merged.

Unlike `reload`, `reset` can apply datasource configuration changes and clears the cache even when `cacheOnReload` is enabled. It is allowed while paused and starts unpaused. It can interrupt a busy cycle while retaining the datasource's public Adapter context. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#reset).

### pause

`pause()` suspends workflow and scroll processing without clearing the Buffer. It does not drain or abort work already in flight; such work may remain pending until `resume()` or `reset()`. While paused, workflow methods other than `resume` and `reset` are ignored; `relax` and `showLog` are not subject to that workflow-method guard. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#pause-resume).

### resume

`resume()` clears the paused state and runs a cycle to reconcile the current viewport. It also starts a cycle when already unpaused. Datasource configuration and buffered data remain in place. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#pause-resume).

## Buffer maintenance

### check

`check()` remeasures buffered DOM rows after an external layout change, updates cached sizes and adjusts scrolling if needed. It does not change datasource values or render DOM changes itself: the changed rows must already be committed before measurement. Unchanged sizes yield an immediate no-op. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#check-size).

### clip

`clip({ forwardOnly?: boolean, backwardOnly?: boolean } = {})` clips eligible offscreen Buffer items, including in `settings.infinite` mode. The viewport and configured overscan are preserved. Without a directional flag, both sides are eligible; `forwardOnly` and `backwardOnly` select the higher- and lower-index sides respectively.

Clipping replaces rows with virtual padding. It does not delete application data or necessarily discard cached sizes. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#clip).

## Item mutations

Adapter mutations change the scroller's Buffer, indexed cache and virtual geometry; they do not persist data in the application collection or server. Subsequent `get(index, count)` responses must reflect the same additions, removals and index shifts, or a later fetch can restore old data. The [basic](https://dhilt.github.io/ngx-ui-scroll/#adapter#append-prepend) and [synchronized](https://dhilt.github.io/ngx-ui-scroll/#adapter#append-prepend-sync) demos illustrate this distinction.

Indexes increase forward—downward in a vertical list, rightward in a horizontal one. Directional options such as `fixRight` refer to lower or higher indexes, not a fixed screen coordinate. `$index` can shift after a mutation and is not an application record ID.

Mutation `items` arrays must be nonempty and contain values with the same JavaScript `typeof`. Predicate callbacks must declare one parameter. Where alternative selectors are shown, exactly one is accepted. Unless stated otherwise, optional flags default to `false`.

### append

`append({ items: Data[], eof?: boolean, decrease?: boolean, virtualize?: boolean })` adds items after the highest cached index. `eof: true` instead targets the known absolute end: items remain virtual when that end is not currently reached. `virtualize: true` keeps a nonempty Buffer unchanged and updates virtual indexes and space, leaving future delivery to `get`. `eof` and `virtualize` are mutually exclusive.

By default, the lower boundary stays fixed and higher indexes extend forward. `decrease: true` fixes the upper side and shifts preceding indexes downward. Array order is preserved. An empty Buffer takes a direct-fill path even with `virtualize`. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#append-prepend).

### prepend

`prepend({ items: Data[], bof?: boolean, increase?: boolean, virtualize?: boolean })` adds before the lowest cached index. `bof: true` targets the known absolute beginning; `virtualize: true` keeps a nonempty Buffer unchanged and reserves virtual space. `bof` and `virtualize` are mutually exclusive.

By default, the upper boundary stays fixed and indexes extend backward. `increase: true` fixes the lower side and shifts following indexes upward instead. In a nonempty Buffer the input array is processed in reverse order: prepending `['X', 'Y']` displays Y before X. An empty Buffer takes the direct-fill path. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#append-prepend).

### insert

`insert({ items: Data[], before?, after?, beforeIndex?: number, afterIndex?: number, decrease?: boolean })` inserts a block relative to exactly one target. `before` and `after` are predicates on buffered items; numeric targets may also address eligible virtual positions within known boundaries. The first predicate match is used.

Array order is preserved. By default, following indexes shift upward; `decrease: true` shifts preceding indexes downward instead. No matching target is an immediate no-op. With an empty Buffer, an integer numeric target establishes a new indexed range. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#insert).

### remove

`remove({ predicate?: (item) => boolean, indexes?: number[], increase?: boolean })` accepts either a predicate for buffered items or a list of indexes that may also include eligible virtual positions. Indexes must be unique integers.

By default, following indexes decrease and the upper boundary shrinks. `increase: true` shifts preceding indexes upward and fixes the upper side instead. No eligible match is an immediate no-op. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#remove).

### replace

`replace({ items: Data[], predicate: (item) => boolean, fixRight?: boolean })` replaces all matching buffered items with one block. Matches need not be adjacent; unmatched items remain. The block is placed at the lowest match by default, or at the highest match with `fixRight: true`.

`fixRight: false` fixes the lower side and lets the upper boundary absorb the count change; `true` fixes the upper side. An unmatched predicate produces a no-op. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#replace).

### update

`update({ predicate: (item) => unknown, fixRight?: boolean })` applies a synchronous transformation to each buffered item. The callback's return value determines the action:

| Return value | Action |
| --- | --- |
| Falsy value or `[]` | Remove the item. |
| Truthy non-array value | Retain the item. |
| Nonempty data array | Replace the item with that array, preserving its order. |

Including the original `item.data` value by identity (`===`) in a returned array retains that core item; other values create new items. Reusing that value twice is unsupported. A returned object alone retains rather than replaces the item, and an async callback returns a truthy Promise rather than an awaited transformation.

`fixRight` has the boundary effect described for `replace` and reverses callback traversal to high-to-low. If every item is retained, the method may finish without a render cycle. [Interactive demo](https://dhilt.github.io/ngx-ui-scroll/#adapter#update).

The older `append(items, eof?)`, `prepend(items, bof?)` and `remove(predicate)` overloads remain available for compatibility. The options-object signatures above avoid ambiguity when a data object itself has an `items` property.

## Advanced and diagnostics

### fix (experimental)

`fix({ scrollPosition?, minIndex?, maxIndex?, updater?, scrollToItem?, scrollToItemOpt? })` performs direct runtime adjustments. Multiple supplied options are processed in the order shown below. The method result does not wait for a cycle caused by scrolling.

| Option | Effect |
| --- | --- |
| `scrollPosition` | Integer pixel position; `-Infinity` and `Infinity` select the beginning and scrollable end. |
| `minIndex`, `maxIndex` | Change the current absolute bounds without fetching or trimming items. |
| `updater` | Callback on buffered item wrappers; its `update()` argument requests a new Buffer-array reference after a change. |
| `scrollToItem` | Predicate locating the first matching buffered item; missing items are not fetched. |
| `scrollToItemOpt` | `boolean` or `ScrollIntoViewOptions` passed to the item-scroll routine; requires `scrollToItem`. |

Bounds changed through `fix` must remain consistent with datasource indexing; a later `reload` restores the stored configured limits. [Position](https://dhilt.github.io/ngx-ui-scroll/#experimental#adapter-fix-position), [updater](https://dhilt.github.io/ngx-ui-scroll/#experimental#adapter-fix-updater) and [scroll-to-item](https://dhilt.github.io/ngx-ui-scroll/#experimental#adapter-fix-scrollToItem) demos cover the separate options.

### showLog

`showLog(): void` flushes buffered diagnostic messages when `devSettings.debug` is enabled and `immediateLog` is disabled. With immediate logging, messages have already reached the console; with debug disabled, there are none to flush. See [Configuration](configuration.md#development-settings).
81 changes: 81 additions & 0 deletions docs/adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Adapter API

[← Documentation index](index.md) · [Adapter methods →](adapter-methods.md) · Working draft

The Adapter adds runtime observation and control to virtual scrolling. It exposes workflow state, visible items and dataset boundaries, and supports data changes without recreating `Workflow`. It is available as `datasource.adapter` when the datasource is constructed through [`makeDatasource()`](datasource.md#creating-a-datasource-with-an-adapter). The Adapter exists at datasource construction; scroller-control methods take effect after Workflow initialization.

## Properties

Adapter properties are read-only. A property ending in `$` is the reactive counterpart of its scalar property; the core subscription API is described [below](#reactive-subscriptions).

| Property | Type | Meaning |
| --- | --- | --- |
| `init`, `init$` | `boolean`, reactive boolean | Becomes `true` when `Workflow` connects the Adapter to the scroller. This does not signal completion of the first load. |
| `isLoading`, `isLoading$` | `boolean`, reactive boolean | Indicates whether the scroller is busy with a Workflow cycle. [Details below](#cycles-and-inner-loops). |
| `loopPending`, `loopPending$` | `boolean`, reactive boolean | Indicates whether a Workflow cycle is running an inner loop (fetch/render/adjust). [Details below](#cycles-and-inner-loops). |
| `itemsCount` | `number` | Number of non-invisible Buffer items, including offscreen overscan; not the dataset total. |
| `bufferInfo` | `IBufferInfo` | Summarizes the Buffer range, cache, and dataset boundaries. [Details below](#bufferinfo). |
| `firstVisible`, `firstVisible$` | `IAdapterItem<Data>`, reactive item | First item visible in the viewport, even partially. |
| `lastVisible`, `lastVisible$` | `IAdapterItem<Data>`, reactive item | Last item visible in the viewport, even partially. |
| `bof`, `bof$` | `boolean`, reactive boolean | Indicates whether the Buffer has reached the known beginning of the dataset. |
| `eof`, `eof$` | `boolean`, reactive boolean | Indicates whether the Buffer has reached the known end of the dataset. |
| `paused`, `paused$` | `boolean`, reactive boolean | Indicates whether Workflow processing is paused. |
| `packageInfo` | object | Core and consumer `name`/`version` metadata. |
| `version` | `string` | Core version associated with this Adapter context. |

### Reactive subscriptions

The built-in `$` properties provide `get()` for the current value, `on(callback)` for updates, and `once(callback)` for one notification. Both subscription methods return a cancellation function.

Boolean properties do not emit their current value on subscription. A listener installed before `Workflow` construction observes the initial loading cycle without an extra read. Here, `#loading-indicator` starts hidden in the markup:

```js
const indicator = document.getElementById('loading-indicator');
const off = datasource.adapter.isLoading$.on(loading => {
indicator.hidden = !loading;
});
new Workflow({ consumer, element, datasource, run });

// When the integration is removed:
off();
```

For a listener installed after the scroller starts, read the scalar value as well to initialize the indicator.

Notifications are synchronous, and identical (`===`) values are not emitted again. `firstVisible$` and `lastVisible$` differ from the boolean properties: they emit their current value on subscription, possibly `EMPTY_ITEM` before an item is visible. Thus, `once()` on either does not necessarily wait for a visible item. Reactive properties report Adapter state; they do not control it. A [custom reactive configuration](datasource.md#consumer-specific-adapter-reactivity) may replace the built-in subscription API.

### Cycles and inner loops

A Workflow cycle is a processing session started by initialization, a relevant scroll event, or an Adapter operation. It remains active until the scroller has finished the resulting work; `isLoading` marks this whole interval. One cycle may contain several inner loops.

An inner workflow loop is one pass through the needed work: determining missing data, fetching it when needed, rendering and measuring rows, clipping the Buffer, and adjusting scrolling geometry. Some steps may be skipped. If a pass reveals more work—for example, the rendered rows are shorter than estimated—another loop follows within the same cycle. `loopPending` marks each individual pass.

Cycles and inner loops are fundamental to VScroll's internal architecture. The [Workflow wiki page](https://github.com/dhilt/vscroll/wiki/VScroll-Workflow) provides detailed flowcharts of both.

### bufferInfo

`bufferInfo` provides a snapshot of the current Buffer state, computed on access rather than updated in place.

| Field | Meaning |
| --- | --- |
| `firstIndex`, `lastIndex` | Lowest and highest indexes in the current Buffer; `NaN` when it is empty or before initialization. |
| `minIndex`, `maxIndex` | Lowest and highest cached indexes, including previously rendered items; `NaN` before initialization, or `startIndex` when the initialized cache is empty. |
| `absMinIndex`, `absMaxIndex` | Known absolute dataset boundaries, supplied by settings or inferred from datasource responses; Adapter operations may change them. Unknown bounds remain infinite. Before initialization, they are `-Infinity` and `Infinity`, respectively. |
| `defaultSize` | Current estimated item size where an individual size is unknown; `NaN` before initialization. |

### Visible items

`firstVisible` and `lastVisible` identify the first and last items intersecting the viewport, including partially visible rows. Unlike the Buffer's edges, these delimit the visible range. When both are available, the visible item count cannot exceed `itemsCount`, which also includes offscreen buffered items:

```js
const visibleCount = adapter.lastVisible.$index - adapter.firstVisible.$index + 1;
expect(visibleCount).toBeLessThanOrEqual(adapter.itemsCount);
```

Tracking of each edge begins when its scalar or `$` property is first accessed. Until a visible item is available, the value is `EMPTY_ITEM`, not an item with a usable `$index` or DOM element. The reactive properties report changes to the visible edges, not every scroll event or in-place change to an item's data.

## Integration metadata

`id`, `mock` and `augmented` are public Adapter properties used mainly by integrations: they identify the Adapter context, reflect its configured mock flag and indicate connection to the runtime implementation. They are not readiness or scrolling-state indicators.

The package root exports `IAdapter<Data>`, `IAdapterItem<Data>`, `AdapterPropName`, `EMPTY_ITEM` and `getDefaultAdapterProps()`. The last function provides property descriptors for constructing an Adapter context; it does not reset a running Adapter. Custom reactive sources belong to [datasource configuration](datasource.md#consumer-specific-adapter-reactivity), not ordinary Adapter method calls.
Binary file added docs/assets/datasource-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/viewport-animation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/viewport-static.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/vscroll-distribution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading