feat(lit-virtual): add setOptions to update virtualizer options after construction by itisrmk · Pull Request #1334 · TanStack/virtual · GitHub
Skip to content
Open
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
17 changes: 17 additions & 0 deletions .changeset/lit-controller-set-options.md
25 changes: 25 additions & 0 deletions docs/framework/lit/lit-virtual.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,28 @@ private windowVirtualizerController = new WindowVirtualizerController<TItemEleme
```

This class stands of window-based `Virtualizer` instance configured to work with an HTML element as the scrollElement.

## Updating options

The controller reads options once, in its constructor. When a reactive property changes, pass the new values to `setOptions` from `willUpdate` (before render). `VirtualizerController` and `WindowVirtualizerController` both implement it.

`setOptions` merges that partial over the options the controller already holds, including its defaults, and keeps the `onChange` wrapper that re-renders the host. Keys set to `undefined` are ignored, so a partial update does not erase an option from an earlier call.

```ts
@property({ type: Number })
count = 1000

private virtualizerController = new VirtualizerController(this, {
getScrollElement: () => this.scrollElementRef.value,
count: this.count,
estimateSize: () => 35,
})

protected willUpdate(changed: PropertyValues<this>) {
if (changed.has('count')) {
this.virtualizerController.setOptions({ count: this.count })
}
}
```

Call `setOptions` on the controller. `getVirtualizer().setOptions(...)` replaces options wholesale, drops the host `onChange` wrapper, and leaves the controller's stored options stale.
12 changes: 12 additions & 0 deletions packages/lit-virtual/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ class MyVirtualElement extends LitElement {

Note that a [Ref](https://lit.dev/docs/templates/directives/#ref) is attached to the scrolling container to allow the virtualizer to interact with it.

Options passed to the constructor are captured once. When a reactive property changes, merge the new values in `willUpdate` with `setOptions`. The same method exists on `WindowVirtualizerController`.

```ts
protected willUpdate(changed: PropertyValues<this>) {
if (changed.has('count')) {
this.virtualizerController.setOptions({ count: this.count })
}
}
```

`setOptions` merges a partial update over the controller's current options and keeps the `onChange` wrapper that re-renders the host. `undefined` values are ignored. Use the controller method rather than `getVirtualizer().setOptions(...)`, which replaces options wholesale and drops that wrapper.

## `WindowVirtualizerController`

You can also create a virtualizer controller that attaches to the Window:
Expand Down
71 changes: 62 additions & 9 deletions packages/lit-virtual/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,44 @@ class VirtualizerControllerBase<

private readonly virtualizer: Virtualizer<TScrollElement, TItemElement>

private options: VirtualizerOptions<TScrollElement, TItemElement>

private cleanup: () => void = () => {}

constructor(
host: ReactiveControllerHost,
options: VirtualizerOptions<TScrollElement, TItemElement>,
) {
const resolvedOptions: VirtualizerOptions<TScrollElement, TItemElement> = {
...options,
onChange: (instance, sync) => {
this.host.updateComplete.then(() => this.host.requestUpdate())
options.onChange?.(instance, sync)
},
}
this.virtualizer = new Virtualizer(resolvedOptions)
;(this.host = host).addController(this)
this.host = host
this.options = { ...options }
this.virtualizer = new Virtualizer(this.resolveOptions(this.options))
this.host.addController(this)
}

public getVirtualizer() {
return this.virtualizer
}

/**
* Merge a partial options update into the options this controller already
* holds, push them to the virtualizer, and request a host update. Lit folds
* a request made from `willUpdate` into the update already in progress.
*
* Keys set to `undefined` are ignored, matching core `setOptions`, so a
* partial update cannot wipe an option stored earlier. Defaults supplied by
* `VirtualizerController` / `WindowVirtualizerController` stay in place
* unless a later call replaces them.
*/
public setOptions(
options: Partial<VirtualizerOptions<TScrollElement, TItemElement>>,
) {
if (!this.applyOptions(options)) return

this.virtualizer.setOptions(this.resolveOptions(this.options))
this.virtualizer._willUpdate()
this.host.requestUpdate()
}

hostConnected() {
this.cleanup = this.virtualizer._didMount()
}
Expand All @@ -52,6 +69,42 @@ class VirtualizerControllerBase<
hostDisconnected() {
this.cleanup()
}

private applyOptions(
partial: Partial<VirtualizerOptions<TScrollElement, TItemElement>>,
): boolean {
const next: VirtualizerOptions<TScrollElement, TItemElement> = {
...this.options,
}
let changed = false

for (const key in partial) {
if (!Object.prototype.hasOwnProperty.call(partial, key)) continue

const value = partial[key as keyof typeof partial]
if (value === undefined) continue

if (!Object.is(next[key as keyof typeof next], value)) {
changed = true
Object.assign(next, { [key]: value })
}
}

if (changed) this.options = next
return changed
}

private resolveOptions(
options: VirtualizerOptions<TScrollElement, TItemElement>,
): VirtualizerOptions<TScrollElement, TItemElement> {
return {
...options,
onChange: (instance, sync) => {
this.host.updateComplete.then(() => this.host.requestUpdate())
options.onChange?.(instance, sync)
},
}
}
}

export class VirtualizerController<
Expand Down
204 changes: 204 additions & 0 deletions packages/lit-virtual/tests/set-options.test.ts