This is quick implementation of virtual scroll using existing DOM-structure.
.bg-white role="table" data-controller="scrollable"
.flex.flex-row.border.border-2.border-black role="row"
.border.border-r-2.border-black role="columnheader"
| ID
.border.border-r-2.border-black role="columnheader"
| Content
.h-60.overflow-y-auto data-scrollable-target="wrapper" data-action="scroll->scrollable#scroll"
#items.relative data-scrollable-target="scroll" role="rowgroup"
- if @items
= render @items
- else
.pt-4 data-scrollable-target="empty"
| NO ITEMS
.flex.flex-row.border.h-12 data-scrollable-target="item" role="row"
= item.id
= item.content
import { Controller } from "@hotwired/stimulus";
import debounce from "lodash-es/debounce";
export default class extends Controller {
static get targets() {
return [ "empty", "item", "scroll", "wrapper"];
}
initialize() {
this.scroll = debounce(this.scroll, 10).bind(this);
this.itemHeight = 48; // TODO: calculate!
this.viewpointHeight = this.wrapperTarget.offsetHeight;
this.scrollables = [];
}
emptyTargetDisconnected() {
let items = this.itemTargets;
this.scrollables = [...this.scrollables, ...items];
this.scrollTarget.innerHTML = "";
this.refreshWindow();
}
scroll() {
this.refreshWindow();
}
getItemHeight() {
return this.itemHeight;
}
refreshWindow() {
let firstItem = Math.floor(this.wrapperTarget.scrollTop / this.getItemHeight());
let lastItem = firstItem + Math.ceil(this.viewpointHeight / this.getItemHeight()) + 1;
if (lastItem + 1 >= this.scrollables.length) {
lastItem = this.scrollables.length - 1;
}
this.scrollTarget.innerHTML = "";
this.scrollTarget.append(...this.scrollables.slice(firstItem, lastItem + 1));
this.scrollTarget.style.top = `${firstItem * this.itemHeight}px`;
}
}