Skip to content

Instantly share code, notes, and snippets.

@carellen
Last active October 24, 2021 09:14
Show Gist options
  • Save carellen/d9caa5dbff244e23723d3eff73310673 to your computer and use it in GitHub Desktop.
Save carellen/d9caa5dbff244e23723d3eff73310673 to your computer and use it in GitHub Desktop.
stimulus virtual scroll

This is quick implementation of virtual scroll using existing DOM-structure.

index.html.slim

.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

_item.html.slim

.flex.flex-row.border.h-12 data-scrollable-target="item" role="row"
  = item.id
  = item.content

scrollable_controller.js

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`;
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment