diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..137b199d1 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", + "changelog": [ + "@svitejs/changesets-changelog-github-compact", + { "repo": "TanStack/virtual" } + ], + "commit": false, + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "fixed": [], + "linked": [], + "ignore": [], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..5a0d5e480 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 508727c12..fd99b3b17 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ github: tannerlinsley -custom: https://youtube.com/tannerlinsley diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index db8a6f470..d8f8d4b31 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: '🐛 Bug report' +name: 🐛 Bug Report description: Report a reproducible bug or regression body: - type: markdown @@ -108,7 +108,7 @@ body: description: | If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred. placeholder: | - e.g. v4.5.4 + e.g. v5.2.2 - type: textarea id: additional attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ce23f2f8c..4ec47ee22 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Feature Requests & Questions - url: https://github.com/tanstack/virtual/discussions + - name: 🤔 Feature Requests & Questions + url: https://github.com/TanStack/virtual/discussions about: Please ask and answer questions here. - - name: Community Chat + - name: 💬 Community Chat url: https://discord.gg/mQd7egN about: A dedicated discord server hosted by TanStack + - name: 🦋 TanStack Bluesky + url: https://bsky.app/profile/tanstack.com + about: Stay up to date with new releases of our libraries diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..e6556d7f3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## 🎯 Changes + + + +## ✅ Checklist + +- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/virtual/blob/main/CONTRIBUTING.md). +- [ ] I have tested this code locally with `pnpm run test:pr`. + +## 🚀 Release Impact + +- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). +- [ ] This change is docs/CI/dev-only (no release). diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..aa175e3b7 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "configMigration": true, + "extends": [ + "config:recommended", + "group:allNonMajor", + "schedule:weekly", + ":approveMajorUpdates", + ":automergeMinor", + ":disablePeerDependencies", + ":maintainLockFilesMonthly", + ":semanticCommits", + ":semanticCommitTypeAll(chore)" + ], + "ignorePresets": [":ignoreModulesAndTests"], + "labels": ["dependencies"], + "rangeStrategy": "bump", + "postUpdateOptions": ["pnpmDedupe"], + "ignoreDeps": ["@types/node", "node", "typescript"] +} diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 000000000..d9d590b0a --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,29 @@ +name: autofix.ci # needed to securely identify the workflow + +on: + pull_request: + push: + branches: [main, alpha, beta] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + autofix: + name: autofix + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Fix formatting + run: pnpm format + - name: Apply fixes + uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef + with: + commit-message: 'ci: apply automated fixes' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index f67584240..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: ci - -on: - workflow_dispatch: - inputs: - tag: - description: override release tag - required: false - push: - branches: ['main', 'alpha', 'beta'] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - -jobs: - test-and-publish: - name: Test & Publish - if: github.repository == 'TanStack/virtual' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: '0' - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline - - name: Run Tests - run: pnpm run test:ci - - name: Publish - run: | - git config --global user.name 'Tanner Linsley' - git config --global user.email 'tannerlinsley@users.noreply.github.com' - npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" - pnpm run cipublish - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - TAG: ${{ inputs.tag }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 70a0723ad..4b2bb9f53 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,11 +1,7 @@ -name: pr +name: PR on: pull_request: - paths-ignore: - - 'docs/**' - - 'media/**' - - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} @@ -14,29 +10,58 @@ concurrency: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} +permissions: + contents: read + pull-requests: write + jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline + - name: Setup Tools + uses: tanstack/config/.github/setup@main - name: Get base and head commits for `nx affected` - uses: nrwl/nx-set-shas@v3 + uses: nrwl/nx-set-shas@v4.4.0 with: - main-branch-name: 'main' + main-branch-name: main + - name: Install Playwright browsers + run: pnpm exec playwright install chromium - name: Run Checks run: pnpm run test:pr + preview: + name: Preview + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Build Packages + run: pnpm run build:all + - name: Publish Previews + run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' + provenance: + name: Provenance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Check Provenance + uses: danielroe/provenance-action@v0.1.1 + with: + fail-on-downgrade: true + version-preview: + name: Version Preview + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + - name: Setup Tools + uses: TanStack/config/.github/setup@main + - name: Changeset Preview + uses: TanStack/config/.github/changeset-preview@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..70abf3ba1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + branches: [main, alpha, beta] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: write + id-token: write + pull-requests: write + +jobs: + release: + name: Release + if: github.repository_owner == 'TanStack' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + - name: Setup Tools + uses: tanstack/config/.github/setup@main + - name: Install Playwright browsers + run: pnpm exec playwright install chromium + - name: Run Tests + run: pnpm run test:ci + - name: Run Changesets (version or publish) + id: changesets + uses: changesets/action@v1.7.0 + with: + version: pnpm run changeset:version + publish: pnpm run changeset:publish + commit: 'ci: Version Packages' + title: 'ci: Version Packages' + - name: Comment on PRs about release + if: steps.changesets.outputs.published == 'true' + uses: TanStack/config/.github/comment-on-release@main + with: + published-packages: ${{ steps.changesets.outputs.publishedPackages }} diff --git a/.gitignore b/.gitignore index 62a870248..2a91fb574 100644 --- a/.gitignore +++ b/.gitignore @@ -3,18 +3,13 @@ # dependencies node_modules +package-lock.json +yarn.lock # builds -types build -*/build -dist -lib -es -artifacts -.rpt2_cache coverage -*.tgz +dist # misc .DS_Store @@ -36,11 +31,18 @@ stats.html .vscode/settings.json *.log -.DS_Store .cache +.idea .nx/cache +.nx/workspace-data .pnpm-store +.tsup .svelte-kit vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Playwright test artifacts +test-results/ +playwright-report/ +*.log diff --git a/.npmrc b/.npmrc index 2eb073230..268c392d3 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -prefer-workspace-packages=true +provenance=true diff --git a/.nvmrc b/.nvmrc index eb800ed45..b40402760 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.19.0 +24.8.0 diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml deleted file mode 100644 index 29c58231b..000000000 --- a/.nx/workflows/dynamic-changesets.yaml +++ /dev/null @@ -1,4 +0,0 @@ -distribute-on: - small-changeset: 8 linux-medium-js - medium-changeset: 10 linux-medium-js - large-changeset: 12 linux-medium-js diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index e3b414c7e..000000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "trailingComma": "all" -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..532efc03b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,168 @@ +# Contributing + +## Questions + +If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/virtual/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum. + +## Reporting Issues + +If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/virtual/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/virtual/discussions) + +## Suggesting new features + +If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. + +## Development + +If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started: + +- Fork this repository. +- Install dependencies + + ```bash + pnpm install + ``` + + - We use [pnpm](https://pnpm.io/) v9 for package management (run in case of pnpm-related issues). + + ```bash + corepack enable && corepack prepare + ``` + + - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc` + + ```bash + nvm use + ``` + +- Build all packages. + + ```bash + pnpm build:all + ``` + +- Run development server. + + ```bash + pnpm run watch + ``` + +- Implement your changes and tests to files in the `src/` directory and corresponding test files. +- Document your changes in the appropriate doc page. +- Git stage your required changes and commit (see below commit guidelines). +- Submit PR for review. + +### Editing the docs locally and previewing the changes + +The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/virtual` docs. + +> [!NOTE] +> The website fetches the doc pages from GitHub in production, and searches for them at `../virtual/docs` in development. Your local clone of `TanStack/virtual` needs to be in the same directory as the local clone of `TanStack/tanstack.com`. + +You can follow these steps to set up the docs for local development: + +1. Make a new directory called `tanstack`. + +```sh +mkdir tanstack +``` + +2. Enter that directory and clone the [`TanStack/virtual`](https://github.com/TanStack/virtual) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos. + +```sh +cd tanstack +git clone git@github.com:TanStack/virtual.git +# We probably don't need all the branches and commit history +# from the `tanstack.com` repo, so let's just create a shallow +# clone of the latest version of the `main` branch. +# Read more about shallow clones here: +# https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones +git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main +``` + +> [!NOTE] +> Your `tanstack` directory should look like this: +> +> ``` +> tanstack/ +> | +> +-- virtual/ (<-- this directory cannot be called anything else!) +> | +> +-- tanstack.com/ +> ``` + +3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: + +```sh +cd tanstack.com +pnpm i +# The app will run on https://localhost:3000 by default +pnpm dev +``` + +4. Now you can visit http://localhost:3000/virtual/latest/docs/overview in the browser and see the changes you make in `TanStack/virtual/docs` there. + +> [!WARNING] +> You will need to update the `docs/config.json` file (in `TanStack/virtual`) if you add a new documentation page! + +You can see the whole process in the screen capture below: + +https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af275f7a269b + +### Running examples + +- Make sure you've installed the dependencies in the repo's root directory. + + ```bash + pnpm install + ``` + +- If you want to run the example against your local changes, run below in the repo's root directory. Otherwise, it will be run against the latest TanStack Virtual release. + + ```bash + pnpm run watch + ``` + +- Run below in the selected examples' directory. + + ```bash + pnpm run dev + ``` + +#### Note on standalone execution + +If you want to run an example without installing dependencies for the whole repo, just follow instructions from the example's README.md file. It will be then run against the latest TanStack Virtual release. + +## Online one-click setup + +You can use Gitpod (An Online Open Source VS Code like IDE which is free for Open Source) for developing online. With a single click it will start a workspace and automatically: + +- clone the `TanStack/virtual` repo. +- install all the dependencies in `/` and `/docs`. +- run below in the root(`/`) to Auto-build files. + + ```bash + npm start + ``` + +- run below in `/docs`. + + ```bash + npm run dev + ``` + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/virtual) + +## Changesets + +This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run run `pnpm changeset` and commit the file. If needed, changeset descriptions can be more descriptive, and will be included in the changelog. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. + +## Pull requests + +Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. + +Use an appropriate commit type. Be especially careful with breaking changes. + +## Releases + +For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to NPM. diff --git a/README.md b/README.md index 1c84177fd..2a437f2f9 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,105 @@ -![React Virtual Header](https://github.com/tanstack/virtual/raw/main/media/header.png) - -Headless UI for virtualizing scrollable elements in TS/JS and React - - - #TanStack - - - - - - - - semantic-release - - Join the discussion on Github - - - - - +
+ Tanstack Virtual +

-
-Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [React Query](https://github.com/TanStack/react-query), [TanStack Table](https://github.com/TanStack/table), [React Charts](https://github.com/TanStack/react-charts) +
+ + npm downloads + + + github stars + + + bundle size + +
+ +
+ + semantic-release + + + Best of JS + + + Follow @TanStack + +
+ +
+ +### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) + +
+ +# TanStack Virtual + +A headless, framework‑agnostic virtualization library for rendering massive lists, grids, and tables at 60FPS while giving you full control over markup and styles. + +- Framework‑agnostic & headless +- Virtualizes vertical, horizontal & grid layouts with a single hook/function +- Lightweight (10–15kb) yet powerful, with dynamic & measured sizing support +- Smooth 60FPS scrolling with sticky items and window‑scrolling utilities + +### Read the docs → + +## Get Involved + +- We welcome issues and pull requests! +- Participate in [GitHub discussions](https://github.com/TanStack/virtual/discussions) +- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) +- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions + +## Partners -## Visit [tanstack.com/virtual](https://tanstack.com/virtual) for docs, guides, API and more! + + + + + +
+ + + + + CodeRabbit + + + + + + + + Cloudflare + + +
+ + +
+Virtual & you? +

+We're looking for TanStack Virtual Partners to join our mission! Partner with us to push the boundaries of TanStack Virtual and build amazing things together. +

+LET'S CHAT +
+ +## Explore the TanStack Ecosystem -## Quick Features +- TanStack Config – Tooling for JS/TS packages +- TanStack DB – Reactive sync client store +- TanStack DevTools – Unified devtools panel +- TanStack Form – Type‑safe form state +- TanStack Pacer – Debouncing, throttling, batching
+- TanStack Query – Async state & caching +- TanStack Ranger – Range & slider primitives +- TanStack Router – Type‑safe routing, caching & URL state +- TanStack Start – Full‑stack SSR & streaming +- TanStack Store – Reactive data store +- TanStack Table – Headless datagrids -- Row, Column, and Grid virtualization -- One single **headless** function -- Fixed, variable and dynamic measurement modes -- Imperative scrollTo control for offset, indices and alignment -- Custom scrolling function support (eg. smooth scroll) +… and more at TanStack.com » - + diff --git a/docs/api/virtual-item.md b/docs/api/virtual-item.md index 9ab47448b..2393788b1 100644 --- a/docs/api/virtual-item.md +++ b/docs/api/virtual-item.md @@ -6,7 +6,7 @@ The `VirtualItem` object represents a single item returned by the virtualizer. I ```tsx export interface VirtualItem { - key: string | number + key: string | number | bigint index: number start: number end: number @@ -19,7 +19,7 @@ The following properties and methods are available on each VirtualItem object: ### `key` ```tsx -key: string | number +key: string | number | bigint ``` The unique key for the item. By default this is the item index, but should be configured via the `getItemKey` Virtualizer option. diff --git a/docs/api/virtualizer.md b/docs/api/virtualizer.md index 7ac66accf..cee8b2d19 100644 --- a/docs/api/virtualizer.md +++ b/docs/api/virtualizer.md @@ -34,12 +34,20 @@ A function that returns the scrollable element for the virtualizer. It may retur estimateSize: (index: number) => number ``` -> 🧠 If you are dynamically measuring your elements, it's recommended to estimate the largest possible size (width/height, within comfort) of your items. This will ensure features like smooth-scrolling will have a better chance at working correctly. +> 🧠 If you are dynamically measuring your elements, it's recommended to estimate the largest possible size (width/height, within comfort) of your items. This will help the virtualizer calculate more accurate initial positions. This function is passed the index of each item and should return the actual size (or estimated size if you will be dynamically measuring items with `virtualItem.measureElement`) for each item. This measurement should return either the width or height depending on the orientation of your virtualizer. ## Optional Options +### `enabled` + +```tsx +enabled?: boolean +``` + +Set to `false` to disable scrollElement observers and reset the virtualizer's state + ### `debug` ```tsx @@ -59,10 +67,12 @@ The initial `Rect` of the scrollElement. This is mostly useful if you need to ru ### `onChange` ```tsx -onChange?: (instance: Virtualizer) => void +onChange?: (instance: Virtualizer, sync: boolean) => void ``` -A callback function that fires when the virtualizer's internal state changes. It's passed the virtualizer instance. +A callback function that fires when the virtualizer's internal state changes. It's passed the virtualizer instance and the sync parameter. + +The sync parameter indicates whether scrolling is currently in progress. It is `true` when scrolling is ongoing, and `false` when scrolling has stopped or other actions (such as resizing) are being performed. ### `overscan` @@ -70,7 +80,7 @@ A callback function that fires when the virtualizer's internal state changes. It overscan?: number ``` -The number of items to render above and below the visible area. Increasing this number will increase the amount of time it takes to render the virtualizer, but might decrease the likelihood of seeing slow-rendering blank items at the top and bottom of the virtualizer when scrolling. +The number of items to render above and below the visible area. Increasing this number will increase the amount of time it takes to render the virtualizer, but might decrease the likelihood of seeing slow-rendering blank items at the top and bottom of the virtualizer when scrolling. The default value is `1`. ### `horizontal` @@ -118,7 +128,7 @@ The padding to apply to the end of the virtualizer in pixels when scrolling to a initialOffset?: number | (() => number) ``` -The initial offset to apply to the virtualizer. This is usually only useful if you are rendering the virtualizer in a SSR environment. +The position where the list is scrolled to on render. This is useful if you are rendering the virtualizer in a SSR environment or are conditionally rendering the virtualizer. ### `getItemKey` @@ -126,7 +136,9 @@ The initial offset to apply to the virtualizer. This is usually only useful if y getItemKey?: (index: number) => Key ``` -This function is passed the index of each item and should return a unique key for that item. The default functionality of this function is to return the index of the item, but you should override this when possible to return a unique identifier for each item across the entire set. This function should be memoized to prevent unnecessary re-renders. +This function is passed the index of each item and should return a unique key for that item. The default functionality of this function is to return the index of the item, but you should override this when possible to return a unique identifier for each item across the entire set. + +**Note:** The virtualizer automatically invalidates its measurement cache when measurement-affecting options change, ensuring `getTotalSize()` and other measurements return fresh values. While the virtualizer intelligently tracks which options actually affect measurements, it's still better to memoize `getItemKey` (e.g., using `useCallback` in React) to avoid unnecessary recalculations. ### `rangeExtractor` @@ -141,14 +153,18 @@ This function receives visible range indexes and should return array of indexes ```tsx scrollToFn?: ( offset: number, - canSmooth: boolean, + options: { adjustments?: number; behavior?: 'auto' | 'smooth' }, instance: Virtualizer, ) => void ``` -An optional function that if provided should implement the scrolling behavior for your scrollElement. It will be called with the offset to scroll to, a boolean indicating if the scrolling is allowed to be smoothed, and the virtualizer instance. Built-in scroll implementations are exported as `elementScroll` and `windowScroll` which are automatically configured for you by your framework adapter's exported functions like `useVirtualizer` or `useWindowVirtualizer`. +An optional function that (if provided) should implement the scrolling behavior for your scrollElement. It will be called with the following arguments: -> ⚠️ Attempting to use smoothScroll with dynamically measured elements will not work. +- An `offset` (in pixels) to scroll towards. +- An object indicating whether there was a difference between the estimated size and actual size (`adjustments`) and/or whether scrolling was called with a smooth animation (`behaviour`). +- The virtualizer instance itself. + +Note that built-in scroll implementations are exported as `elementScroll` and `windowScroll`, which are automatically configured by the framework adapter functions like `useVirtualizer` or `useWindowVirtualizer`. ### `observeElementRect` @@ -176,12 +192,13 @@ An optional function that if provided is called when the scrollElement changes a ```tsx measureElement?: ( - el: TItemElement, - instance: Virtualizer + element: TItemElement, + entry: ResizeObserverEntry | undefined, + instance: Virtualizer, ) => number ``` -This optional function is called when the virtualizer needs to dynamically measure the size (width or height) of an item when `virtualItem.measureElement` is called. It's passed the element given when you call `virtualItem.measureElement(TItemElement)` and the virtualizer instance. It should return the size of the element as a `number`. +This optional function is called when the virtualizer needs to dynamically measure the size (width or height) of an item. > 🧠 You can use `instance.options.horizontal` to determine if the width or height of the item should be measured. @@ -191,7 +208,13 @@ This optional function is called when the virtualizer needs to dynamically measu scrollMargin?: number ``` -With this option, you can specify where the scroll offset should originate. Typically, this value represents the space between the beginning of the scrolling element and the start of the list. This is especially useful in common scenarios such as when you have a header preceding a window virtualizer or when multiple virtualizers are utilized within a single scrolling element. +With this option, you can specify where the scroll offset should originate. Typically, this value represents the space between the beginning of the scrolling element and the start of the list. This is especially useful in common scenarios such as when you have a header preceding a window virtualizer or when multiple virtualizers are utilized within a single scrolling element. If you are using absolute positioning of elements, you should take into account the `scrollMargin` in your CSS transform: +```tsx +transform: `translateY(${ + virtualRow.start - rowVirtualizer.options.scrollMargin +}px)` +``` +To dynamically measure value for `scrollMargin` you can use `getBoundingClientRect()` or ResizeObserver. This is helpful in scenarios when items above your virtual list might change their height. ### `gap` @@ -209,6 +232,57 @@ lanes: number The number of lanes the list is divided into (aka columns for vertical lists and rows for horizontal lists). +### `isScrollingResetDelay` + +```tsx +isScrollingResetDelay: number +``` + +This option allows you to specify the duration to wait after the last scroll event before resetting the isScrolling instance property. The default value is 150 milliseconds. + +The implementation of this option is driven by the need for a reliable mechanism to handle scrolling behavior across different browsers. Until all browsers uniformly support the scrollEnd event. + +### `useScrollendEvent` + +```tsx +useScrollendEvent: boolean +``` + +Determines whether to use the native scrollend event to detect when scrolling has stopped. If set to false, a debounced fallback is used to reset the isScrolling instance property after isScrollingResetDelay milliseconds. The default value is `false`. + +The implementation of this option is driven by the need for a reliable mechanism to handle scrolling behavior across different browsers. Until all browsers uniformly support the scrollEnd event. + +### `isRtl` + +```tsx +isRtl: boolean +``` + +Whether to invert horizontal scrolling to support right-to-left language locales. + +### `useAnimationFrameWithResizeObserver` + +```tsx +useAnimationFrameWithResizeObserver: boolean +``` + +**Default:** `false` + +When enabled, defers ResizeObserver measurement processing to the next animation frame using `requestAnimationFrame`. + +**Important:** This option typically **should not be enabled** in most cases. ResizeObserver callbacks already execute at an optimal time in the browser's rendering pipeline (after layout, before paint), and the measurements provided in the callback are pre-computed by the browser without causing additional reflows. + +**Potential use cases:** +- If you're performing heavy DOM mutations in response to size changes and want to batch them with the next render cycle +- As a workaround for the "ResizeObserver loop completed with undelivered notifications" error (though this usually indicates a deeper issue that should be fixed) + +**Tradeoffs:** +- **Adds ~16ms delay:** Measurements are deferred to the next frame, which can cause visual artifacts, stale measurements, or slower time-to-interactive +- **No batching benefit:** ResizeObserver already batches multiple element resizes into a single callback +- **Defeats optimization:** The browser has already computed the measurements synchronously; deferring them provides no performance benefit for reading values + +Only enable this option if you have a specific reason and have measured that it improves your use case. + ## Virtualizer Instance The following properties and methods are available on the virtualizer instance: @@ -237,6 +311,14 @@ type getVirtualItems = () => VirtualItem[] Returns the virtual items for the current state of the virtualizer. +### `getVirtualIndexes` + +```tsx +type getVirtualIndexes = () => number[] +``` + +Returns the virtual row indexes for the current state of the virtualizer. + ### `scrollToOffset` ```tsx @@ -244,7 +326,7 @@ scrollToOffset: ( toOffset: number, options?: { align?: 'start' | 'center' | 'end' | 'auto', - smoothScroll?: boolean + behavior?: 'auto' | 'smooth' } ) => void ``` @@ -258,13 +340,30 @@ scrollToIndex: ( index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto', - smoothScroll?: boolean + behavior?: 'auto' | 'smooth' } ) => void ``` Scrolls the virtualizer to the items of the index provided. You can optionally pass an alignment mode to anchor the scroll to a specific part of the scrollElement. +> 🧠 During smooth scrolling, the virtualizer only measures items within a buffer range around the scroll target. Items far from the target are skipped to prevent their size changes from shifting the target position and breaking the smooth animation. +> +> Because of this, the preferred layout strategy for smooth scrolling is **block translation** — translate the entire rendered block using the first item's `start` offset, rather than positioning each item independently with absolute positioning. This ensures items stay correctly positioned relative to each other even when some measurements are skipped. + +### `scrollBy` + +```tsx +scrollBy: ( + delta: number, + options?: { + behavior?: 'auto' | 'smooth' + } +) => void +``` + +Scrolls the virtualizer by the specified number of pixels relative to the current scroll position. + ### `getTotalSize` ```tsx @@ -303,10 +402,10 @@ By default the `measureElement` virtualizer option is configured to measure elem ### `resizeItem` ```tsx -resizeItem: (item: VirtualItem, size: number) => void +resizeItem: (index: number, size: number) => void ``` -Change the virtualized item's size manually. Use this function to manually set the size calculated for this item. Useful in occations when using some custom morphing transition and you know the morphed item's size beforehand. +Change the virtualized item's size manually. Use this function to manually set the size calculated for this index. Useful in occations when using some custom morphing transition and you know the morphed item's size beforehand. You can also use this method with a throttled ResizeObserver instead of `Virtualizer.measureElement` to reduce re-rendering. @@ -319,3 +418,35 @@ scrollRect: Rect ``` Current `Rect` of the scroll element. + +### `shouldAdjustScrollPositionOnItemSizeChange` + +```tsx +shouldAdjustScrollPositionOnItemSizeChange: undefined | ((item: VirtualItem, delta: number, instance: Virtualizer) => boolean) +``` + +The shouldAdjustScrollPositionOnItemSizeChange method enables fine-grained control over the adjustment of scroll position when the size of dynamically rendered items differs from the estimated size. When jumping in the middle of the list and scrolling backward new elements may have a different size than the initially estimated size. This discrepancy can cause subsequent items to shift, potentially disrupting the user's scrolling experience, particularly when navigating backward through the list. + +### `isScrolling` + +```tsx +isScrolling: boolean +``` + +Boolean flag indicating if list is currently being scrolled. + +### `scrollDirection` + +```tsx +scrollDirection: 'forward' | 'backward' | null +``` + +This option indicates the direction of scrolling, with possible values being 'forward' for scrolling downwards and 'backward' for scrolling upwards. The value is set to null when there is no active scrolling. + +### `scrollOffset` + +```tsx +scrollOffset: number +``` + +This option represents the current scroll position along the scrolling axis. It is measured in pixels from the starting point of the scrollable area. diff --git a/docs/config.json b/docs/config.json index 0143798d5..e65ba6ad2 100644 --- a/docs/config.json +++ b/docs/config.json @@ -22,6 +22,15 @@ } ] }, + { + "label": "angular", + "children": [ + { + "label": "Angular Virtual", + "to": "framework/angular/angular-virtual" + } + ] + }, { "label": "solid", "children": [ @@ -59,6 +68,47 @@ "label": "Examples", "children": [], "frameworks": [ + { + "label": "angular", + "children": [ + { + "to": "framework/angular/examples/fixed", + "label": "Fixed" + }, + { + "to": "framework/angular/examples/variable", + "label": "Variable" + }, + { + "to": "framework/angular/examples/dynamic", + "label": "Dynamic" + }, + { + "to": "framework/angular/examples/padding", + "label": "Padding" + }, + { + "to": "framework/angular/examples/sticky", + "label": "Sticky" + }, + { + "to": "framework/angular/examples/infinite-scroll", + "label": "Infinite Scroll" + }, + { + "to": "framework/angular/examples/smooth-scroll", + "label": "Smooth Scroll" + }, + { + "to": "framework/angular/examples/table", + "label": "Table" + }, + { + "to": "framework/angular/examples/window", + "label": "Window" + } + ] + }, { "label": "react", "children": [ @@ -173,6 +223,19 @@ "label": "Scroll Padding" } ] + }, + { + "label": "lit", + "children": [ + { + "to": "framework/lit/examples/fixed", + "label": "Fixed" + }, + { + "to": "framework/lit/examples/dynamic", + "label": "Dynamic" + } + ] } ] } diff --git a/docs/framework/angular/angular-virtual.md b/docs/framework/angular/angular-virtual.md new file mode 100644 index 000000000..f02fdda95 --- /dev/null +++ b/docs/framework/angular/angular-virtual.md @@ -0,0 +1,34 @@ +--- +title: Angular Virtual +--- + +The `@tanstack/angular-virtual` adapter is a wrapper around the core virtual logic. + +## `injectVirtualizer` + +```ts +function injectVirtualizer( + options: PartialKeys< + Omit, 'getScrollElement'>, + 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' + > & { scrollElement: ElementRef | TScrollElement | undefined }, +): AngularVirtualizer +``` + +This function returns an `AngularVirtualizer` instance configured to work with an HTML element as the scrollElement. + +## `injectWindowVirtualizer` + +```ts +function injectWindowVirtualizer( + options: PartialKeys< + VirtualizerOptions, + | 'getScrollElement' + | 'observeElementRect' + | 'observeElementOffset' + | 'scrollToFn' + >, +): AngularVirtualizer +``` + +This function returns a window-based `AngularVirtualizer` instance configured to work with the window as the scrollElement. diff --git a/docs/framework/lit/lit-virtual.md b/docs/framework/lit/lit-virtual.md new file mode 100644 index 000000000..8700490a4 --- /dev/null +++ b/docs/framework/lit/lit-virtual.md @@ -0,0 +1,36 @@ +--- +title: Lit Virtual +--- + +The `@tanstack/lit-virtual` adapter is a wrapper around the core virtual logic. + +## `createVirtualizer` + +```tsx + +private virtualizerController = new VirtualizerController( + options: PartialKeys< VirtualizerOptions, + 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' +) +``` + +This class stands for a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. +This will create a Lit Controller which can be accessed in the element render method. + +```tsx +render() { + const virtualizer = this.virtualizerController.getVirtualizer(); + const virtualItems = virtualizer.getVirtualItems(); +} +) +``` + +## `createWindowVirtualizer` + +```tsx +private windowVirtualizerController = new WindowVirtualizerController( + options: PartialKeys< VirtualizerOptions, + 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' +``` + +This class stands of window-based `Virtualizer` instance configured to work with an HTML element as the scrollElement. diff --git a/docs/framework/react/react-virtual.md b/docs/framework/react/react-virtual.md index e32a982ee..338d32052 100644 --- a/docs/framework/react/react-virtual.md +++ b/docs/framework/react/react-virtual.md @@ -9,7 +9,7 @@ The `@tanstack/react-virtual` adapter is a wrapper around the core virtual logic ```tsx function useVirtualizer( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer @@ -22,7 +22,7 @@ This function returns a standard `Virtualizer` instance configured to work with ```tsx function useWindowVirtualizer( options: PartialKeys< - VirtualizerOptions, + ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' @@ -32,3 +32,44 @@ function useWindowVirtualizer( ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. + +## React-Specific Options + +### `useFlushSync` + +```tsx +type ReactVirtualizerOptions = + VirtualizerOptions & { + useFlushSync?: boolean + } +``` + +Both `useVirtualizer` and `useWindowVirtualizer` accept a `useFlushSync` option that controls whether React's `flushSync` is used for synchronous updates. + +- **Type**: `boolean` +- **Default**: `true` +- **Description**: When `true`, the virtualizer will use `flushSync` from `react-dom` to ensure synchronous rendering during scroll events. This provides the most accurate scrolling behavior but may impact performance in some scenarios. + +#### When to disable `useFlushSync` + +You may want to set `useFlushSync: false` in the following scenarios: + +- **React 19 compatibility**: In React 19, you may see the following console warning when scrolling: + ``` + flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task. + ``` + Setting `useFlushSync: false` will eliminate this warning by allowing React to batch updates naturally. +- **Performance optimization**: If you experience performance issues with rapid scrolling on lower-end devices +- **Testing environments**: When running tests that don't require synchronous DOM updates +- **Non-critical lists**: When slight visual delays during scrolling are acceptable for better overall performance + +#### Example + +```tsx +const virtualizer = useVirtualizer({ + count: 10000, + getScrollElement: () => parentRef.current, + estimateSize: () => 50, + useFlushSync: false, // Disable synchronous updates +}) +``` diff --git a/docs/framework/vue/vue-virtual.md b/docs/framework/vue/vue-virtual.md index 7b4ba427c..ab0275925 100644 --- a/docs/framework/vue/vue-virtual.md +++ b/docs/framework/vue/vue-virtual.md @@ -1,5 +1,5 @@ --- -title: Vue Virtual (Coming Soon) +title: Vue Virtual --- The `@tanstack/vue-virtual` adapter is a wrapper around the core virtual logic. diff --git a/docs/installation.md b/docs/installation.md index 10937ef40..1b49d7c62 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -9,29 +9,41 @@ Install your TanStack Virtual adapter as a dependency using your favorite npm pa ## React Virtual ```bash -$ npm install @tanstack/react-virtual +npm install @tanstack/react-virtual ``` ## Solid Virtual ```bash -$ npm install @tanstack/solid-virtual +npm install @tanstack/solid-virtual ``` ## Svelte Virtual ```bash -$ npm install @tanstack/svelte-virtual +npm install @tanstack/svelte-virtual ``` ## Vue Virtual ```bash -$ npm install @tanstack/vue-virtual +npm install @tanstack/vue-virtual +``` + +## Lit Virtual + +```bash +npm install @tanstack/lit-virtual +``` + +## Angular Virtual + +```bash +npm install @tanstack/angular-virtual ``` ## Virtual Core (no framework) ```bash -$ npm install @tanstack/virtual-core +npm install @tanstack/virtual-core ``` diff --git a/docs/introduction.md b/docs/introduction.md index fa4cb6b5d..76e0535ff 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -2,7 +2,7 @@ title: Introduction --- -TanStack Virtual is a headless UI utility for virtualizing long lists of elements in JS/TS, React, Vue, Svelte and Solid. It is not a component therefore does not ship with or render any markup or styles for you. While this requires a bit of markup and styles from you, you will retain 100% control over your styles, design and implementation. +TanStack Virtual is a headless UI utility for virtualizing long lists of elements in JS/TS, React, Vue, Svelte, Solid, Lit, and Angular. It is not a component therefore does not ship with or render any markup or styles for you. While this requires a bit of markup and styles from you, you will retain 100% control over your styles, design and implementation. ## The Virtualizer @@ -15,7 +15,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'; function App() { // The scrollable element for your list - const parentRef = React.useRef() + const parentRef = React.useRef(null) // The virtualizer const rowVirtualizer = useVirtualizer({ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..2ccb5ad05 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +// @ts-check + +import { tanstackConfig } from '@tanstack/eslint-config' + +export default [ + ...tanstackConfig, + { + name: 'tanstack/temp', + rules: { + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + 'no-self-assign': 'off', + }, + }, +] diff --git a/examples/angular/dynamic/.devcontainer/devcontainer.json b/examples/angular/dynamic/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/dynamic/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/dynamic/.gitignore b/examples/angular/dynamic/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/dynamic/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/dynamic/README.md b/examples/angular/dynamic/README.md new file mode 100644 index 000000000..d96974ead --- /dev/null +++ b/examples/angular/dynamic/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularDynamic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/dynamic/angular.json b/examples/angular/dynamic/angular.json new file mode 100644 index 000000000..ee1536b48 --- /dev/null +++ b/examples/angular/dynamic/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "@tanstack/virtual-example-angular-dynamic": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-dynamic", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-dynamic:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-dynamic:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-dynamic:build" + } + } + } + } + } +} diff --git a/examples/angular/dynamic/package.json b/examples/angular/dynamic/package.json new file mode 100644 index 000000000..5a7571184 --- /dev/null +++ b/examples/angular/dynamic/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tanstack/virtual-example-angular-dynamic", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@faker-js/faker": "^8.4.1", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/dynamic/src/app/app.component.ts b/examples/angular/dynamic/src/app/app.component.ts new file mode 100644 index 000000000..0081dc3ad --- /dev/null +++ b/examples/angular/dynamic/src/app/app.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core' +import { RouterLink, RouterOutlet } from '@angular/router' + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterLink, RouterOutlet], + template: ` +

+ These components are using dynamic sizes. This means that + each element's exact dimensions are unknown when rendered. An estimated + dimension is used to get an a initial measurement, then this measurement + is readjusted on the fly as each element is rendered. +

+ + + + + `, + styles: [], +}) +export class AppComponent {} diff --git a/examples/angular/dynamic/src/app/app.config.ts b/examples/angular/dynamic/src/app/app.config.ts new file mode 100644 index 000000000..bd9327036 --- /dev/null +++ b/examples/angular/dynamic/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { ApplicationConfig } from '@angular/core' +import { provideRouter } from '@angular/router' + +import { routes } from './app.routes' + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes)], +} diff --git a/examples/angular/dynamic/src/app/app.routes.ts b/examples/angular/dynamic/src/app/app.routes.ts new file mode 100644 index 000000000..f24899755 --- /dev/null +++ b/examples/angular/dynamic/src/app/app.routes.ts @@ -0,0 +1,24 @@ +import { Routes } from '@angular/router' +import { RowVirtualizerDynamic } from './row-virtualizer-dynamic.component' +import { GridVirtualizerDynamic } from './grid-virtualizer-dynamic.component' +import { ColumnVirtualizerDynamic } from './column-virtualizer-dynamic.component' +import { RowVirtualizerDynamicWindow } from './row-virtualizer-dynamic-window.component' + +export const routes: Routes = [ + { + path: '', + component: RowVirtualizerDynamic, + }, + { + path: 'window-list', + component: RowVirtualizerDynamicWindow, + }, + { + path: 'columns', + component: ColumnVirtualizerDynamic, + }, + { + path: 'grid', + component: GridVirtualizerDynamic, + }, +] diff --git a/examples/angular/dynamic/src/app/column-virtualizer-dynamic.component.ts b/examples/angular/dynamic/src/app/column-virtualizer-dynamic.component.ts new file mode 100644 index 000000000..0aaf1d658 --- /dev/null +++ b/examples/angular/dynamic/src/app/column-virtualizer-dynamic.component.ts @@ -0,0 +1,74 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + effect, + viewChild, + viewChildren, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { sentences } from './utils' + +@Component({ + standalone: true, + selector: 'column-virtualizer-dynamic', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Columns

+
+
+ @for (col of virtualizer.getVirtualItems(); track col.index) { +
+
+
Column {{ col.index }}
+
{{ sentences[col.index] }}
+
+
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 400px; + width: 400px; + overflow: auto; + } + `, +}) +export class ColumnVirtualizerDynamic { + scrollElement = viewChild>('scrollElement') + + virtualItems = viewChildren>('virtualItem') + + sentences = sentences + + count = this.sentences.length + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.virtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + virtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: this.count, + estimateSize: () => 100, + overscan: 5, + })) +} diff --git a/examples/angular/dynamic/src/app/grid-virtualizer-dynamic.component.ts b/examples/angular/dynamic/src/app/grid-virtualizer-dynamic.component.ts new file mode 100644 index 000000000..598cfc27d --- /dev/null +++ b/examples/angular/dynamic/src/app/grid-virtualizer-dynamic.component.ts @@ -0,0 +1,125 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + afterNextRender, + computed, + effect, + signal, + viewChild, + viewChildren, +} from '@angular/core' +import { + injectVirtualizer, + injectWindowVirtualizer, +} from '@tanstack/angular-virtual' +import { generateColumns, generateData } from './utils' + +@Component({ + standalone: true, + selector: 'grid-virtualizer-dynamic', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Grid

+
+
+ @for (row of rowVirtualizer.getVirtualItems(); track row.key) { +
+
+ @for (col of columnVirtualizer.getVirtualItems(); track col.key) { +
+
+ {{ + row.index === 0 + ? columns[col.index].name + : data[row.index][col.index] + }} +
+
+ } +
+
+ } +
+
+ `, + styles: ` + .scroll-container { + overflow: auto; + } + `, +}) +export class GridVirtualizerDynamic { + scrollElement = viewChild>('scrollElement') + columns = generateColumns(30) + data = generateData(this.columns) + + parentOffset = signal(0) + + constructor() { + afterNextRender(() => + this.parentOffset.set(this.scrollElement()!.nativeElement.offsetTop), + ) + } + + getColumnWidth = (index: number) => this.columns[index].width + + rowVirtualizer = injectWindowVirtualizer(() => ({ + count: this.data.length, + estimateSize: () => 350, + overscan: 5, + scrollMargin: this.parentOffset(), + })) + + columnVirtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: this.columns.length, + estimateSize: this.getColumnWidth, + overscan: 5, + })) + + width = computed( + () => { + const virtualColumns = this.columnVirtualizer.getVirtualItems() + return virtualColumns.length > 0 + ? [ + virtualColumns[0].start, + this.columnVirtualizer.getTotalSize() - + virtualColumns[virtualColumns.length - 1].end, + ] + : [0, 0] + }, + { equal: (a, b) => a[0] === b[0] && a[1] === b[1] }, + ) + + virtualRows = viewChildren>('virtualRow') + + #measureItems = effect( + () => + this.virtualRows().forEach((el) => { + this.rowVirtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) +} diff --git a/examples/angular/dynamic/src/app/row-virtualizer-dynamic-window.component.ts b/examples/angular/dynamic/src/app/row-virtualizer-dynamic-window.component.ts new file mode 100644 index 000000000..b79f6543a --- /dev/null +++ b/examples/angular/dynamic/src/app/row-virtualizer-dynamic-window.component.ts @@ -0,0 +1,91 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + afterNextRender, + effect, + signal, + untracked, + viewChild, + viewChildren, +} from '@angular/core' +import { injectWindowVirtualizer } from '@tanstack/angular-virtual' +import { sentences } from './utils' + +@Component({ + standalone: true, + selector: 'row-virtualizer-dynamic', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+
+
Row {{ row.index }}
+
{{ sentences[row.index] }}
+
+
+ } +
+
+
+ `, + styles: ` + .scroll-container { + height: 400px; + width: 400px; + overflow-y: auto; + contain: 'strict'; + } + `, +}) +export class RowVirtualizerDynamicWindow { + scrollElement = viewChild>('scrollElement') + + parentOffset = signal(0) + + constructor() { + afterNextRender(() => + this.parentOffset.set(this.scrollElement()!.nativeElement.offsetTop), + ) + } + + virtualItems = viewChildren>('virtualItem') + + sentences = sentences + + count = this.sentences.length + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.virtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + virtualizer = injectWindowVirtualizer(() => ({ + count: this.count, + estimateSize: () => 150, + scrollMargin: this.parentOffset(), + })) +} diff --git a/examples/angular/dynamic/src/app/row-virtualizer-dynamic.component.ts b/examples/angular/dynamic/src/app/row-virtualizer-dynamic.component.ts new file mode 100644 index 000000000..c626305ba --- /dev/null +++ b/examples/angular/dynamic/src/app/row-virtualizer-dynamic.component.ts @@ -0,0 +1,94 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + effect, + viewChild, + viewChildren, +} from '@angular/core' +import { + AngularVirtualizer, + injectVirtualizer, +} from '@tanstack/angular-virtual' +import { sentences } from './utils' + +@Component({ + standalone: true, + selector: 'row-virtualizer-dynamic', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Rows

+ + + +
+
+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+
+
Row {{ row.index }}
+
{{ sentences[row.index] }}
+
+
+ } +
+
+
+ `, + styles: ` + .scroll-container { + height: 400px; + width: 400px; + overflow-y: auto; + contain: 'strict'; + } + `, +}) +export class RowVirtualizerDynamic { + scrollElement = viewChild>('scrollElement') + + virtualItems = viewChildren>('virtualItem') + + sentences = sentences + + count = this.sentences.length + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.virtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.count, + estimateSize: () => 120, + })) +} diff --git a/examples/angular/dynamic/src/app/utils.ts b/examples/angular/dynamic/src/app/utils.ts new file mode 100644 index 000000000..b895889a9 --- /dev/null +++ b/examples/angular/dynamic/src/app/utils.ts @@ -0,0 +1,37 @@ +import { faker } from '@faker-js/faker' + +export const generateRandomNumber = (min: number, max: number) => + faker.number.int({ min, max }) + +// 1000 because 10000 takes many seconds +export const sentences = new Array(1000) + .fill(true) + .map(() => faker.lorem.sentence(generateRandomNumber(20, 70))) + +interface Column { + key: string + name: string + width: number +} + +export const generateColumns = (count: number) => { + return new Array(count).fill(0).map((_, i) => { + const key: string = i.toString() + return { + key, + name: `Column ${i}`, + width: generateRandomNumber(75, 300), + } + }) +} + +export const generateData = (columns: Column[], count = 300) => { + return new Array(count).fill(0).map((_, rowIndex) => + columns.reduce((acc, _curr, colIndex) => { + // simulate dynamic size cells + const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) + acc.push(val) + return acc + }, []), + ) +} diff --git a/examples/angular/dynamic/src/favicon.ico b/examples/angular/dynamic/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/dynamic/src/favicon.ico differ diff --git a/examples/angular/dynamic/src/index.html b/examples/angular/dynamic/src/index.html new file mode 100644 index 000000000..65827a899 --- /dev/null +++ b/examples/angular/dynamic/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularDynamic + + + + + + + + diff --git a/examples/angular/dynamic/src/main.ts b/examples/angular/dynamic/src/main.ts new file mode 100644 index 000000000..c3d8f9af9 --- /dev/null +++ b/examples/angular/dynamic/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/dynamic/src/styles.css b/examples/angular/dynamic/src/styles.css new file mode 100644 index 000000000..01c32b54c --- /dev/null +++ b/examples/angular/dynamic/src/styles.css @@ -0,0 +1,31 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/dynamic/tsconfig.app.json b/examples/angular/dynamic/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/dynamic/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/dynamic/tsconfig.json b/examples/angular/dynamic/tsconfig.json new file mode 100644 index 000000000..c79456978 --- /dev/null +++ b/examples/angular/dynamic/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"], + "paths": { + "@angular/*": ["./node_modules/@angular/*"] + } + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/fixed/.devcontainer/devcontainer.json b/examples/angular/fixed/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/fixed/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/fixed/.gitignore b/examples/angular/fixed/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/fixed/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/fixed/README.md b/examples/angular/fixed/README.md new file mode 100644 index 000000000..36d4d18f6 --- /dev/null +++ b/examples/angular/fixed/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularFixed + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/fixed/angular.json b/examples/angular/fixed/angular.json new file mode 100644 index 000000000..0c44f9c10 --- /dev/null +++ b/examples/angular/fixed/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-fixed": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-fixed", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-fixed:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-fixed:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-fixed:build" + } + } + } + } + } +} diff --git a/examples/angular/fixed/package.json b/examples/angular/fixed/package.json new file mode 100644 index 000000000..ac586f423 --- /dev/null +++ b/examples/angular/fixed/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/virtual-example-angular-fixed", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/fixed/src/app/app.component.ts b/examples/angular/fixed/src/app/app.component.ts new file mode 100644 index 000000000..411a7cbcf --- /dev/null +++ b/examples/angular/fixed/src/app/app.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +import { ColumnVirtualizerFixed } from './column-virtualizer-fixed.component' +import { GridVirtualizerFixed } from './grid-virtualizer-fixed.component' +import { RowVirtualizerFixed } from './row-virtualizer-fixed.component' + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ColumnVirtualizerFixed, GridVirtualizerFixed, RowVirtualizerFixed], + template: ` +

+ These components are using fixed sizes. This means that + every element's dimensions are hard-coded to the same value and never + change. +

+ + + + + `, +}) +export class AppComponent {} diff --git a/examples/angular/fixed/src/app/column-virtualizer-fixed.component.ts b/examples/angular/fixed/src/app/column-virtualizer-fixed.component.ts new file mode 100644 index 000000000..565cdddea --- /dev/null +++ b/examples/angular/fixed/src/app/column-virtualizer-fixed.component.ts @@ -0,0 +1,53 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'column-virtualizer-fixed', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Columns

+
+
+ @for (col of virtualizer.getVirtualItems(); track col.index) { +
+ Col {{ col.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 100px; + width: 400px; + overflow: auto; + } + `, +}) +export class ColumnVirtualizerFixed { + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: () => 100, + overscan: 5, + })) +} diff --git a/examples/angular/fixed/src/app/grid-virtualizer-fixed.component.ts b/examples/angular/fixed/src/app/grid-virtualizer-fixed.component.ts new file mode 100644 index 000000000..a30e510a4 --- /dev/null +++ b/examples/angular/fixed/src/app/grid-virtualizer-fixed.component.ts @@ -0,0 +1,86 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'grid-virtualizer-fixed', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Grid

+
+
+ @for ( + row of rowVirtualizer.getVirtualItems(); + track row.index; + let rowEven = $even + ) { + @for ( + col of columnVirtualizer.getVirtualItems(); + track col.index; + let colEven = $even + ) { +
+ Cell {{ row.index }}, {{ col.index }} +
+ } + } +
+
+ `, + styles: ` + .scroll-container { + height: 500px; + width: 500px; + overflow: auto; + } + `, +}) +export class GridVirtualizerFixed { + scrollElement = viewChild>('scrollElement') + + rowVirtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: () => 35, + overscan: 5, + })) + + columnVirtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: () => 100, + overscan: 5, + })) +} diff --git a/examples/angular/fixed/src/app/row-virtualizer-fixed.component.ts b/examples/angular/fixed/src/app/row-virtualizer-fixed.component.ts new file mode 100644 index 000000000..60ccb2251 --- /dev/null +++ b/examples/angular/fixed/src/app/row-virtualizer-fixed.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'row-virtualizer-fixed', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Rows

+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ Row {{ row.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 200px; + width: 400px; + overflow: auto; + } + `, +}) +export class RowVirtualizerFixed { + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: () => 35, + overscan: 5, + })) +} diff --git a/examples/angular/fixed/src/favicon.ico b/examples/angular/fixed/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/fixed/src/favicon.ico differ diff --git a/examples/angular/fixed/src/index.html b/examples/angular/fixed/src/index.html new file mode 100644 index 000000000..486b6661b --- /dev/null +++ b/examples/angular/fixed/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularFixed + + + + + + + + diff --git a/examples/angular/fixed/src/main.ts b/examples/angular/fixed/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/fixed/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/fixed/src/styles.css b/examples/angular/fixed/src/styles.css new file mode 100644 index 000000000..6b296a08f --- /dev/null +++ b/examples/angular/fixed/src/styles.css @@ -0,0 +1,34 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} diff --git a/examples/angular/fixed/tsconfig.app.json b/examples/angular/fixed/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/fixed/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/fixed/tsconfig.json b/examples/angular/fixed/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/fixed/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/infinite-scroll/.devcontainer/devcontainer.json b/examples/angular/infinite-scroll/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/infinite-scroll/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/infinite-scroll/.gitignore b/examples/angular/infinite-scroll/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/infinite-scroll/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/infinite-scroll/README.md b/examples/angular/infinite-scroll/README.md new file mode 100644 index 000000000..c88691184 --- /dev/null +++ b/examples/angular/infinite-scroll/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularInfiniteScroll + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/infinite-scroll/angular.json b/examples/angular/infinite-scroll/angular.json new file mode 100644 index 000000000..74864635e --- /dev/null +++ b/examples/angular/infinite-scroll/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-infinite-scroll": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-infinite-scroll", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build" + } + } + } + } + } +} diff --git a/examples/angular/infinite-scroll/package.json b/examples/angular/infinite-scroll/package.json new file mode 100644 index 000000000..730f587aa --- /dev/null +++ b/examples/angular/infinite-scroll/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tanstack/virtual-example-angular-infinite-scroll", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-query-experimental": "5.80.7", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/infinite-scroll/src/app/app.component.ts b/examples/angular/infinite-scroll/src/app/app.component.ts new file mode 100644 index 000000000..5bed165b6 --- /dev/null +++ b/examples/angular/infinite-scroll/src/app/app.component.ts @@ -0,0 +1,133 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + computed, + effect, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { + QueryClient, + injectInfiniteQuery, + provideQueryClient, +} from '@tanstack/angular-query-experimental' + +async function fetchServerPage( + limit: number, + offset: number = 0, +): Promise<{ rows: string[]; nextOffset: number }> { + const rows = new Array(limit) + .fill(0) + .map((e, i) => `Async loaded row #${i + offset * limit}`) + + await new Promise((r) => setTimeout(r, 500)) + + return { rows, nextOffset: offset + 1 } +} + +@Component({ + selector: 'infinite-scroll', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

+ This infinite scroll example uses Angular Query's injectInfiniteScroll + function to fetch infinite data from a posts endpoint and then a + rowVirtualizer is used along with a loader-row placed at the bottom of the + list to trigger the next page to load. +

+ @if (query.isLoading()) { +

Loading...

+ } @else if (query.isError()) { + Error: {{ query.error()!.message }} + } @else { +
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ {{ + row.index > allRows().length - 1 + ? query.hasNextPage() + ? 'Loading more...' + : 'Nothing more to load' + : allRows()[row.index] + }} +
+ } +
+
+ } + @if (query.isFetching() && !query.isFetchingNextPage()) { +
Background Updating...
+ } + `, + styles: ` + .scroll-container { + height: 500px; + width: 100%; + overflow: auto; + } + `, + providers: [provideQueryClient(new QueryClient())], +}) +export class InfiniteScrollComponent { + query = injectInfiniteQuery(() => ({ + queryKey: ['rows'], + queryFn: ({ pageParam }) => fetchServerPage(10, pageParam), + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + })) + + allRows = computed( + () => this.query.data()?.pages.flatMap((d) => d.rows) ?? [], + ) + + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.query.hasNextPage() + ? this.allRows().length + 1 + : this.allRows().length, + estimateSize: () => 100, + overscan: 5, + })) + + #fetchNextPage = effect( + () => { + const lastItem = + this.virtualizer.getVirtualItems()[ + this.virtualizer.getVirtualItems().length - 1 + ] + if (!lastItem) { + return + } + if ( + lastItem.index >= this.allRows().length - 1 && + this.query.hasNextPage() && + !this.query.isFetchingNextPage() + ) { + this.query.fetchNextPage() + } + }, + { allowSignalWrites: true }, + ) +} + +@Component({ + selector: 'app-root', + standalone: true, + imports: [InfiniteScrollComponent], + template: '', +}) +export class AppComponent {} diff --git a/examples/angular/infinite-scroll/src/favicon.ico b/examples/angular/infinite-scroll/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/infinite-scroll/src/favicon.ico differ diff --git a/examples/angular/infinite-scroll/src/index.html b/examples/angular/infinite-scroll/src/index.html new file mode 100644 index 000000000..92a444d44 --- /dev/null +++ b/examples/angular/infinite-scroll/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularInfiniteScroll + + + + + + + + diff --git a/examples/angular/infinite-scroll/src/main.ts b/examples/angular/infinite-scroll/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/infinite-scroll/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/infinite-scroll/src/styles.css b/examples/angular/infinite-scroll/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/infinite-scroll/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/infinite-scroll/tsconfig.app.json b/examples/angular/infinite-scroll/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/infinite-scroll/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/infinite-scroll/tsconfig.json b/examples/angular/infinite-scroll/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/infinite-scroll/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/padding/.devcontainer/devcontainer.json b/examples/angular/padding/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/padding/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/padding/.gitignore b/examples/angular/padding/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/padding/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/padding/README.md b/examples/angular/padding/README.md new file mode 100644 index 000000000..064aa3657 --- /dev/null +++ b/examples/angular/padding/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularPadding + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/padding/angular.json b/examples/angular/padding/angular.json new file mode 100644 index 000000000..277f0e7f7 --- /dev/null +++ b/examples/angular/padding/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-padding": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-padding", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-padding:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-padding:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-padding:build" + } + } + } + } + } +} diff --git a/examples/angular/padding/package.json b/examples/angular/padding/package.json new file mode 100644 index 000000000..176730a46 --- /dev/null +++ b/examples/angular/padding/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/virtual-example-angular-padding", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/padding/src/app/app.component.ts b/examples/angular/padding/src/app/app.component.ts new file mode 100644 index 000000000..96d8f2bf3 --- /dev/null +++ b/examples/angular/padding/src/app/app.component.ts @@ -0,0 +1,39 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +import { ColumnVirtualizerPadding } from './column-virtualizer-padding.component' +import { GridVirtualizerPadding } from './grid-virtualizer-padding.component' +import { RowVirtualizerPadding } from './row-virtualizer-padding.component' + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ColumnVirtualizerPadding, + GridVirtualizerPadding, + RowVirtualizerPadding, + ], + template: ` +

+ These components are using dynamic sizes. This means that + each element's exact dimensions are unknown when rendered. An estimated + dimension is used to get an a initial measurement, then this measurement + is readjusted on the fly as each element is rendered. Each component has + padding at the beginning and end of its scroll container. +

+ + + + + `, + styles: [], +}) +export class AppComponent { + rows = new Array(10000) + .fill(true) + .map(() => 25 + Math.round(Math.random() * 100)) + + columns = new Array(10000) + .fill(true) + .map(() => 75 + Math.round(Math.random() * 100)) +} diff --git a/examples/angular/padding/src/app/column-virtualizer-padding.component.ts b/examples/angular/padding/src/app/column-virtualizer-padding.component.ts new file mode 100644 index 000000000..1aa9e52fd --- /dev/null +++ b/examples/angular/padding/src/app/column-virtualizer-padding.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + effect, + input, + viewChild, + viewChildren, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'column-virtualizer-padding', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Columns

+
+
+ @for (col of virtualizer.getVirtualItems(); track col.index) { +
+ Column {{ col.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 400px; + width: 400px; + overflow: auto; + } + `, +}) +export class ColumnVirtualizerPadding { + columns = input.required() + + scrollElement = viewChild>('scrollElement') + + virtualItems = viewChildren>('virtualItem') + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.virtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + virtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: this.columns().length, + estimateSize: () => 100, + overscan: 5, + paddingStart: 100, + paddingEnd: 100, + })) +} diff --git a/examples/angular/padding/src/app/grid-virtualizer-padding.component.ts b/examples/angular/padding/src/app/grid-virtualizer-padding.component.ts new file mode 100644 index 000000000..6ef3ab7c4 --- /dev/null +++ b/examples/angular/padding/src/app/grid-virtualizer-padding.component.ts @@ -0,0 +1,124 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + effect, + input, + signal, + viewChild, + viewChildren, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'grid-virtualizer-padding', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Grid

+ + + + @if (show()) { +
+
+ @for (row of rowVirtualizer.getVirtualItems(); track row.index) { + @for (col of columnVirtualizer.getVirtualItems(); track col.index) { +
+
Cell {{ row.index }}, {{ col.index }}
+
+ } + } +
+
+ } + `, + styles: ` + .scroll-container { + height: 400px; + width: 500px; + overflow: auto; + } + `, +}) +export class GridVirtualizerPadding { + rows = input.required() + columns = input.required() + + scrollElement = viewChild>('scrollElement') + + rowVirtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.rows().length, + estimateSize: (index) => this.rows()[index]!, + overscan: 5, + paddingStart: 200, + paddingEnd: 200, + indexAttribute: 'data-rowindex', + })) + + columnVirtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: this.columns().length, + estimateSize: (index) => this.columns()[index]!, + overscan: 5, + paddingStart: 200, + paddingEnd: 200, + indexAttribute: 'data-colindex', + })) + + virtualItems = viewChildren>('virtualItem') + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.rowVirtualizer.measureElement(el.nativeElement) + this.columnVirtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + show = signal(true) + + toggleShow() { + this.show.update((show) => !show) + } +} diff --git a/examples/angular/padding/src/app/row-virtualizer-padding.component.ts b/examples/angular/padding/src/app/row-virtualizer-padding.component.ts new file mode 100644 index 000000000..3c676aa52 --- /dev/null +++ b/examples/angular/padding/src/app/row-virtualizer-padding.component.ts @@ -0,0 +1,69 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + effect, + input, + viewChild, + viewChildren, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'row-virtualizer-padding', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Rows

+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ Row {{ row.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 200px; + width: 400px; + overflow-y: auto; + } + `, +}) +export class RowVirtualizerPadding { + rows = input.required() + + scrollElement = viewChild>('scrollElement') + + virtualItems = viewChildren>('virtualItem') + + #measureItems = effect( + () => + this.virtualItems().forEach((el) => { + this.virtualizer.measureElement(el.nativeElement) + }), + { allowSignalWrites: true }, + ) + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.rows().length, + estimateSize: () => 50, + paddingStart: 100, + paddingEnd: 100, + })) +} diff --git a/examples/angular/padding/src/favicon.ico b/examples/angular/padding/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/padding/src/favicon.ico differ diff --git a/examples/angular/padding/src/index.html b/examples/angular/padding/src/index.html new file mode 100644 index 000000000..bc36bfa3f --- /dev/null +++ b/examples/angular/padding/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularPadding + + + + + + + + diff --git a/examples/angular/padding/src/main.ts b/examples/angular/padding/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/padding/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/padding/src/styles.css b/examples/angular/padding/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/padding/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/padding/tsconfig.app.json b/examples/angular/padding/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/padding/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/padding/tsconfig.json b/examples/angular/padding/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/padding/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/smooth-scroll/.devcontainer/devcontainer.json b/examples/angular/smooth-scroll/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/smooth-scroll/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/smooth-scroll/.gitignore b/examples/angular/smooth-scroll/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/smooth-scroll/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/smooth-scroll/README.md b/examples/angular/smooth-scroll/README.md new file mode 100644 index 000000000..6958fe82f --- /dev/null +++ b/examples/angular/smooth-scroll/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularSmoothScroll + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/smooth-scroll/angular.json b/examples/angular/smooth-scroll/angular.json new file mode 100644 index 000000000..956fa9402 --- /dev/null +++ b/examples/angular/smooth-scroll/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-smooth-scroll": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-smooth-scroll", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build" + } + } + } + } + } +} diff --git a/examples/angular/smooth-scroll/package.json b/examples/angular/smooth-scroll/package.json new file mode 100644 index 000000000..6f85a2bca --- /dev/null +++ b/examples/angular/smooth-scroll/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/virtual-example-angular-smooth-scroll", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/smooth-scroll/src/app/app.component.ts b/examples/angular/smooth-scroll/src/app/app.component.ts new file mode 100644 index 000000000..27f8bc70b --- /dev/null +++ b/examples/angular/smooth-scroll/src/app/app.component.ts @@ -0,0 +1,97 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + signal, + viewChild, +} from '@angular/core' +import { elementScroll, injectVirtualizer } from '@tanstack/angular-virtual' + +function easeInOutQuint(t: number) { + return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t +} + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

+ This smooth scroll example uses the scrollToFn to implement a + custom scrolling function for the methods like + scrollToIndex and scrollToOffset +

+
+ +
+
+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ Row {{ row.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 200px; + width: 400px; + overflow: auto; + } + `, +}) +export class AppComponent { + scrollElement = viewChild>('scrollElement') + + scrollingTime = signal(0) + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: () => 35, + overscan: 5, + scrollToFn: (offset, options, instance) => { + const duration = 1000 + const start = this.scrollElement()!.nativeElement.scrollTop + const startTime = Date.now() + this.scrollingTime.set(startTime) + + const run = () => { + if (this.scrollingTime() !== startTime) return + const now = Date.now() + const elapsed = now - startTime + const progress = easeInOutQuint(Math.min(elapsed / duration, 1)) + const interpolated = start + (offset - start) * progress + + if (elapsed < duration) { + elementScroll(interpolated, options, instance) + requestAnimationFrame(run) + } else { + elementScroll(interpolated, options, instance) + } + } + requestAnimationFrame(run) + }, + })) + + randomIndex = signal(Math.floor(Math.random() * 10000)) + + scrollToRandomIndex() { + this.virtualizer.scrollToIndex(this.randomIndex()) + this.randomIndex.set(Math.floor(Math.random() * 10000)) + } +} diff --git a/examples/angular/smooth-scroll/src/favicon.ico b/examples/angular/smooth-scroll/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/smooth-scroll/src/favicon.ico differ diff --git a/examples/angular/smooth-scroll/src/index.html b/examples/angular/smooth-scroll/src/index.html new file mode 100644 index 000000000..a12568376 --- /dev/null +++ b/examples/angular/smooth-scroll/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularSmoothScroll + + + + + + + + diff --git a/examples/angular/smooth-scroll/src/main.ts b/examples/angular/smooth-scroll/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/smooth-scroll/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/smooth-scroll/src/styles.css b/examples/angular/smooth-scroll/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/smooth-scroll/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/smooth-scroll/tsconfig.app.json b/examples/angular/smooth-scroll/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/smooth-scroll/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/smooth-scroll/tsconfig.json b/examples/angular/smooth-scroll/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/smooth-scroll/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/sticky/.devcontainer/devcontainer.json b/examples/angular/sticky/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/sticky/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/sticky/.gitignore b/examples/angular/sticky/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/sticky/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/sticky/README.md b/examples/angular/sticky/README.md new file mode 100644 index 000000000..b465e5cfe --- /dev/null +++ b/examples/angular/sticky/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularSticky + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/sticky/angular.json b/examples/angular/sticky/angular.json new file mode 100644 index 000000000..9b07a40fd --- /dev/null +++ b/examples/angular/sticky/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-sticky": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-sticky", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-sticky:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-sticky:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-sticky:build" + } + } + } + } + } +} diff --git a/examples/angular/sticky/package.json b/examples/angular/sticky/package.json new file mode 100644 index 000000000..f0d1190a0 --- /dev/null +++ b/examples/angular/sticky/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tanstack/virtual-example-angular-sticky", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@faker-js/faker": "^8.4.1", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/sticky/src/app/app.component.ts b/examples/angular/sticky/src/app/app.component.ts new file mode 100644 index 000000000..c7dbeba8a --- /dev/null +++ b/examples/angular/sticky/src/app/app.component.ts @@ -0,0 +1,100 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + computed, + viewChild, +} from '@angular/core' +import { faker } from '@faker-js/faker' +import { + injectVirtualizer, + defaultRangeExtractor, +} from '@tanstack/angular-virtual' + +const groupedNames: Record = {} + +Array.from({ length: 1000 }) + .map(() => faker.person.firstName()) + .sort() + .forEach((name) => { + const char = name[0] + if (!groupedNames[char]) { + groupedNames[char] = [] + } + groupedNames[char].push(name) + }) +const groups = Object.keys(groupedNames) +const rows = groups.reduce( + (acc: string[], k) => [...acc, k, ...groupedNames[k]], + [], +) +const stickyIndexes = groups.map((gn) => rows.findIndex((n) => n === gn)) +const stickyIndexesSet = new Set(stickyIndexes) +const reversedStickyIndexes = [...stickyIndexes].reverse() + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ {{ rows[row.index] }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 300px; + width: 400px; + overflow: auto; + } + `, +}) +export class AppComponent { + rows = rows + + isSticky = (index: number) => stickyIndexesSet.has(index) + + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.rows.length, + estimateSize: () => 50, + rangeExtractor: (range) => { + const next = new Set([ + reversedStickyIndexes.find((index) => range.startIndex >= index)!, + ...defaultRangeExtractor(range), + ]) + return [...next].sort((a, b) => a - b) + }, + })) + + activeStickyIndex = computed(() => { + return this.virtualizer.getVirtualItems()[0]?.index + }) + + isActiveSticky = (index: number) => this.activeStickyIndex() === index +} diff --git a/examples/angular/sticky/src/favicon.ico b/examples/angular/sticky/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/sticky/src/favicon.ico differ diff --git a/examples/angular/sticky/src/index.html b/examples/angular/sticky/src/index.html new file mode 100644 index 000000000..366a4b9a7 --- /dev/null +++ b/examples/angular/sticky/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularSticky + + + + + + + + diff --git a/examples/angular/sticky/src/main.ts b/examples/angular/sticky/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/sticky/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/sticky/src/styles.css b/examples/angular/sticky/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/sticky/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/sticky/tsconfig.app.json b/examples/angular/sticky/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/sticky/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/sticky/tsconfig.json b/examples/angular/sticky/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/sticky/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/table/.devcontainer/devcontainer.json b/examples/angular/table/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/table/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/table/.gitignore b/examples/angular/table/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/table/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/table/README.md b/examples/angular/table/README.md new file mode 100644 index 000000000..81e389283 --- /dev/null +++ b/examples/angular/table/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularTable + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/table/angular.json b/examples/angular/table/angular.json new file mode 100644 index 000000000..f2d2dd632 --- /dev/null +++ b/examples/angular/table/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-table": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-table", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-table:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-table:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-table:build" + } + } + } + } + } +} diff --git a/examples/angular/table/package.json b/examples/angular/table/package.json new file mode 100644 index 000000000..99392a977 --- /dev/null +++ b/examples/angular/table/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tanstack/virtual-example-angular-table", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@faker-js/faker": "^8.4.1", + "@tanstack/angular-table": "8.21.3", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/table/src/app/app.component.ts b/examples/angular/table/src/app/app.component.ts new file mode 100644 index 000000000..a90d2752e --- /dev/null +++ b/examples/angular/table/src/app/app.component.ts @@ -0,0 +1,194 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + computed, + signal, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' +import { + ColumnDef, + createAngularTable, + getCoreRowModel, + getSortedRowModel, + SortingState, + FlexRenderDirective, + SortDirection, +} from '@tanstack/angular-table' +import { makeData, type Person } from './make-data' + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FlexRenderDirective], + template: ` +

+ For tables, the basis for the offset of the translate css function is from + the row's initial position itself. Because of this, we need to calculate + the translateY pixel count different and base it off the the index. +

+
+ +
+
+ + + @for ( + headerGroup of table.getHeaderGroups(); + track headerGroup.id + ) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + @for ( + virtualRow of virtualizer.getVirtualItems(); + track data[virtualRow.index].id + ) { + + @for ( + cell of rows()[virtualRow.index].getVisibleCells(); + track cell.id + ) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerText }} + {{ getSortIcon(header.column.getIsSorted()) }} + +
+ } +
+ + {{ cellText }} + +
+
+
+ `, + styles: ` + .scroll-container { + height: 600px; + overflow: auto; + } + `, +}) +export class AppComponent { + data = makeData(50_000) + + scrollElement = viewChild>('scrollElement') + + sorting = signal([]) + + sortIcons = { asc: '🔼', desc: '🔽' } + + getSortIcon(sorting: false | SortDirection) { + return sorting ? this.sortIcons[sorting] : null + } + + columns: ColumnDef[] = [ + { + accessorKey: 'id', + header: 'ID', + size: 60, + }, + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => info.getValue(), + header: 'Last Name', + }, + { + accessorKey: 'age', + header: () => 'Age', + size: 50, + }, + { + accessorKey: 'visits', + header: 'Visits', + size: 50, + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + size: 80, + }, + { + accessorKey: 'createdAt', + header: 'Created At', + cell: (info) => info.getValue().toLocaleString(), + }, + ] + + table = createAngularTable(() => ({ + data: this.data, + columns: this.columns, + state: { + sorting: this.sorting(), + }, + onSortingChange: (updaterOrValue) => + typeof updaterOrValue === 'function' + ? this.sorting.update(updaterOrValue) + : this.sorting.set(updaterOrValue), + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + debugTable: true, + })) + + rows = computed(() => this.table.getRowModel().rows) + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.data.length, + estimateSize: () => 34, + overscan: 20, + })) +} diff --git a/examples/angular/table/src/app/make-data.ts b/examples/angular/table/src/app/make-data.ts new file mode 100644 index 000000000..9bf52ab8f --- /dev/null +++ b/examples/angular/table/src/app/make-data.ts @@ -0,0 +1,50 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: number + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + createdAt: Date +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (index: number): Person => { + return { + id: index + 1, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.datatype.datetime({ max: new Date().getTime() }), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((d): Person => { + return { + ...newPerson(d), + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/table/src/favicon.ico b/examples/angular/table/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/table/src/favicon.ico differ diff --git a/examples/angular/table/src/index.html b/examples/angular/table/src/index.html new file mode 100644 index 000000000..1696073fe --- /dev/null +++ b/examples/angular/table/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularTable + + + + + + + + diff --git a/examples/angular/table/src/main.ts b/examples/angular/table/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/table/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/table/src/styles.css b/examples/angular/table/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/table/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/table/tsconfig.app.json b/examples/angular/table/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/table/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/table/tsconfig.json b/examples/angular/table/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/table/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/variable/.devcontainer/devcontainer.json b/examples/angular/variable/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/variable/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/variable/.gitignore b/examples/angular/variable/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/variable/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/variable/README.md b/examples/angular/variable/README.md new file mode 100644 index 000000000..7ff180ffb --- /dev/null +++ b/examples/angular/variable/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularVariable + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/variable/angular.json b/examples/angular/variable/angular.json new file mode 100644 index 000000000..2b986415a --- /dev/null +++ b/examples/angular/variable/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-variable": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-variable", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-variable:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-variable:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-variable:build" + } + } + } + } + } +} diff --git a/examples/angular/variable/package.json b/examples/angular/variable/package.json new file mode 100644 index 000000000..fbeda0e12 --- /dev/null +++ b/examples/angular/variable/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/virtual-example-angular-variable", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/variable/src/app/app.component.ts b/examples/angular/variable/src/app/app.component.ts new file mode 100644 index 000000000..32ebf2db9 --- /dev/null +++ b/examples/angular/variable/src/app/app.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' + +import { ColumnVirtualizerVariable } from './column-virtualizer-variable.component' +import { GridVirtualizerVariable } from './grid-virtualizer-variable.component' +import { RowVirtualizerVariable } from './row-virtualizer-variable.component' + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ColumnVirtualizerVariable, + GridVirtualizerVariable, + RowVirtualizerVariable, + ], + template: ` +

+ These components are using variable sizes. This means + that each element has a unique, but knowable dimension at render time. +

+ + + + + `, + styles: [], +}) +export class AppComponent { + rows = new Array(10000) + .fill(true) + .map(() => 25 + Math.round(Math.random() * 100)) + + columns = new Array(10000) + .fill(true) + .map(() => 75 + Math.round(Math.random() * 100)) +} diff --git a/examples/angular/variable/src/app/column-virtualizer-variable.component.ts b/examples/angular/variable/src/app/column-virtualizer-variable.component.ts new file mode 100644 index 000000000..2be6967db --- /dev/null +++ b/examples/angular/variable/src/app/column-virtualizer-variable.component.ts @@ -0,0 +1,56 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + input, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'column-virtualizer-variable', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Columns

+
+
+ @for (col of virtualizer.getVirtualItems(); track col.index) { +
+ Col {{ col.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 100px; + width: 400px; + overflow: auto; + } + `, +}) +export class ColumnVirtualizerVariable { + columns = input.required() + + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: (index) => this.columns()[index]!, + overscan: 5, + })) +} diff --git a/examples/angular/variable/src/app/grid-virtualizer-variable.component.ts b/examples/angular/variable/src/app/grid-virtualizer-variable.component.ts new file mode 100644 index 000000000..fbdc4ac80 --- /dev/null +++ b/examples/angular/variable/src/app/grid-virtualizer-variable.component.ts @@ -0,0 +1,91 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + input, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'grid-virtualizer-variable', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Grid

+
+
+ @for ( + row of rowVirtualizer.getVirtualItems(); + track row.index; + let rowEven = $even + ) { + @for ( + col of columnVirtualizer.getVirtualItems(); + track col.index; + let colEven = $even + ) { +
+ Cell {{ row.index }}, {{ col.index }} +
+ } + } +
+
+ `, + styles: ` + .scroll-container { + height: 500px; + width: 500px; + overflow: auto; + } + `, +}) +export class GridVirtualizerVariable { + rows = input.required() + + columns = input.required() + + scrollElement = viewChild>('scrollElement') + + rowVirtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: (index) => this.rows()[index]!, + overscan: 5, + })) + + columnVirtualizer = injectVirtualizer(() => ({ + horizontal: true, + scrollElement: this.scrollElement(), + count: 10000, + estimateSize: (index) => this.columns()[index]!, + overscan: 5, + })) +} diff --git a/examples/angular/variable/src/app/row-virtualizer-variable.component.ts b/examples/angular/variable/src/app/row-virtualizer-variable.component.ts new file mode 100644 index 000000000..71b56717c --- /dev/null +++ b/examples/angular/variable/src/app/row-virtualizer-variable.component.ts @@ -0,0 +1,55 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + input, + viewChild, +} from '@angular/core' +import { injectVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + standalone: true, + selector: 'row-virtualizer-variable', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

Rows

+
+
+ @for (row of virtualizer.getVirtualItems(); track row.index) { +
+ Row {{ row.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 200px; + width: 400px; + overflow: auto; + } + `, +}) +export class RowVirtualizerVariable { + rows = input.required() + + scrollElement = viewChild>('scrollElement') + + virtualizer = injectVirtualizer(() => ({ + scrollElement: this.scrollElement(), + count: this.rows().length, + estimateSize: (index) => this.rows()[index]!, + overscan: 5, + })) +} diff --git a/examples/angular/variable/src/favicon.ico b/examples/angular/variable/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/variable/src/favicon.ico differ diff --git a/examples/angular/variable/src/index.html b/examples/angular/variable/src/index.html new file mode 100644 index 000000000..40724edac --- /dev/null +++ b/examples/angular/variable/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularVariable + + + + + + + + diff --git a/examples/angular/variable/src/main.ts b/examples/angular/variable/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/variable/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/variable/src/styles.css b/examples/angular/variable/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/variable/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/variable/tsconfig.app.json b/examples/angular/variable/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/variable/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/variable/tsconfig.json b/examples/angular/variable/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/variable/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/window/.devcontainer/devcontainer.json b/examples/angular/window/.devcontainer/devcontainer.json new file mode 100644 index 000000000..36f47d876 --- /dev/null +++ b/examples/angular/window/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:18" +} diff --git a/examples/angular/window/.gitignore b/examples/angular/window/.gitignore new file mode 100644 index 000000000..cc7b14135 --- /dev/null +++ b/examples/angular/window/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/window/README.md b/examples/angular/window/README.md new file mode 100644 index 000000000..c884a06de --- /dev/null +++ b/examples/angular/window/README.md @@ -0,0 +1,27 @@ +# @tanstack/virtualExampleAngularWindow + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/window/angular.json b/examples/angular/window/angular.json new file mode 100644 index 000000000..c03dac9ee --- /dev/null +++ b/examples/angular/window/angular.json @@ -0,0 +1,64 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "projects": { + "@tanstack/virtual-example-angular-window": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/tanstack/virtual-example-angular-window", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "@tanstack/virtual-example-angular-window:build:production" + }, + "development": { + "buildTarget": "@tanstack/virtual-example-angular-window:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "@tanstack/virtual-example-angular-window:build" + } + } + } + } + } +} diff --git a/examples/angular/window/package.json b/examples/angular/window/package.json new file mode 100644 index 000000000..710c4cb40 --- /dev/null +++ b/examples/angular/window/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tanstack/virtual-example-angular-window", + "private": true, + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "dependencies": { + "@angular/animations": "^18.1.0", + "@angular/common": "^18.1.0", + "@angular/compiler": "^18.1.0", + "@angular/core": "^18.1.0", + "@angular/forms": "^18.1.0", + "@angular/platform-browser": "^18.1.0", + "@angular/platform-browser-dynamic": "^18.1.0", + "@angular/router": "^18.1.0", + "@tanstack/angular-virtual": "^4.0.11", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.15.1" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.1.0", + "@angular/cli": "^18.1.0", + "@angular/compiler-cli": "^18.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/window/src/app/app.component.ts b/examples/angular/window/src/app/app.component.ts new file mode 100644 index 000000000..ed7f4bb6d --- /dev/null +++ b/examples/angular/window/src/app/app.component.ts @@ -0,0 +1,63 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + viewChild, +} from '@angular/core' +import { injectWindowVirtualizer } from '@tanstack/angular-virtual' + +@Component({ + selector: 'app-root', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

+ In many cases, when implementing a virtualizer with a window as the + scrolling element, developers often find the need to specify a + "scrollMargin." The scroll margin is a crucial setting that defines the + space or gap between the start of the page and the edges of the list. +

+

Window Scroller

+
+
+ @for (row of virtualizer.getVirtualItems(); track row.key) { +
+ Row {{ row.index }} +
+ } +
+
+ `, + styles: ` + .scroll-container { + height: 400px; + width: 400px; + overflow-y: auto; + contain: 'strict'; + } + `, +}) +export class AppComponent { + scrollElement = viewChild>('scrollElement') + + virtualizer = injectWindowVirtualizer(() => ({ + count: 10000, + estimateSize: () => 35, + overscan: 5, + scrollMargin: this.scrollElement()?.nativeElement.offsetTop, + })) +} diff --git a/examples/angular/window/src/favicon.ico b/examples/angular/window/src/favicon.ico new file mode 100644 index 000000000..57614f9c9 Binary files /dev/null and b/examples/angular/window/src/favicon.ico differ diff --git a/examples/angular/window/src/index.html b/examples/angular/window/src/index.html new file mode 100644 index 000000000..57f24a151 --- /dev/null +++ b/examples/angular/window/src/index.html @@ -0,0 +1,13 @@ + + + + + @tanstack/virtualExampleAngularWindow + + + + + + + + diff --git a/examples/angular/window/src/main.ts b/examples/angular/window/src/main.ts new file mode 100644 index 000000000..56773910e --- /dev/null +++ b/examples/angular/window/src/main.ts @@ -0,0 +1,4 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent).catch((err) => console.error(err)) diff --git a/examples/angular/window/src/styles.css b/examples/angular/window/src/styles.css new file mode 100644 index 000000000..222f9f737 --- /dev/null +++ b/examples/angular/window/src/styles.css @@ -0,0 +1,38 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} + +.list { + border: 1px solid #e6e4dc; + max-width: 100%; +} + +.list-item-even, +.list-item-odd { + display: flex; + align-items: center; + justify-content: center; +} + +.list-item-even { + background-color: #e6e4dc; +} + +.list-item-odd { + background-color: #fff; +} + +button { + border: 1px solid gray; +} diff --git a/examples/angular/window/tsconfig.app.json b/examples/angular/window/tsconfig.app.json new file mode 100644 index 000000000..84f1f992d --- /dev/null +++ b/examples/angular/window/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/window/tsconfig.json b/examples/angular/window/tsconfig.json new file mode 100644 index 000000000..67d294437 --- /dev/null +++ b/examples/angular/window/tsconfig.json @@ -0,0 +1,29 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/lit/dynamic/.gitignore b/examples/lit/dynamic/.gitignore new file mode 100644 index 000000000..d451ff16c --- /dev/null +++ b/examples/lit/dynamic/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/lit/dynamic/README.md b/examples/lit/dynamic/README.md new file mode 100644 index 000000000..b09ff6203 --- /dev/null +++ b/examples/lit/dynamic/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `npm` +- `npm run start` or `npm run start` diff --git a/examples/lit/dynamic/index.html b/examples/lit/dynamic/index.html new file mode 100644 index 000000000..09dfe3355 --- /dev/null +++ b/examples/lit/dynamic/index.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + + + diff --git a/examples/lit/dynamic/package.json b/examples/lit/dynamic/package.json new file mode 100644 index 000000000..648ba210a --- /dev/null +++ b/examples/lit/dynamic/package.json @@ -0,0 +1,21 @@ +{ + "name": "tanstack-lit-virtual-example-dynamic", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@tanstack/lit-virtual": "^3.13.24", + "@tanstack/virtual-core": "^3.13.23", + "lit": "^3.3.0" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "typescript": "5.4.5", + "vite": "^5.4.19" + } +} diff --git a/examples/lit/dynamic/src/index.css b/examples/lit/dynamic/src/index.css new file mode 100644 index 000000000..8f38e9d80 --- /dev/null +++ b/examples/lit/dynamic/src/index.css @@ -0,0 +1,8 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} diff --git a/examples/lit/dynamic/src/main.ts b/examples/lit/dynamic/src/main.ts new file mode 100644 index 000000000..866441f58 --- /dev/null +++ b/examples/lit/dynamic/src/main.ts @@ -0,0 +1,357 @@ +import { customElement, property } from 'lit/decorators.js' +import { Ref, createRef, ref } from 'lit/directives/ref.js' +import { html, LitElement } from 'lit' +import { faker } from '@faker-js/faker' +import { repeat } from 'lit/directives/repeat.js' +import { + VirtualizerController, + WindowVirtualizerController, +} from '@tanstack/lit-virtual' + +interface Column { + key: string + name: string + width: number +} + +function randomNumber(min: number, max: number) { + return faker.number.int({ min, max }) +} + +const sentences = new Array(10000) + .fill(true) + .map(() => faker.lorem.sentence(randomNumber(20, 70))) + +const generateColumns = (count: number) => { + return new Array(count).fill(0).map((_, i) => { + const key: string = i.toString() + return { + key, + name: `Column ${i}`, + width: randomNumber(75, 300), + } + }) +} + +const generateData = (columns: Column[], count = 300) => { + return new Array(count).fill(0).map((_, rowIndex) => + columns.reduce((acc, _curr, colIndex) => { + // simulate dynamic size cells + const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) + + acc.push(val) + + return acc + }, []), + ) +} + +@customElement('row-virtualizer-dynamic') +class RowVirtualizerDynamic extends LitElement { + private scrollElementRef: Ref = createRef() + + private virtualizerController: VirtualizerController + + constructor() { + super() + this.virtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: sentences.length, + estimateSize: () => 45, + }) + } + + render() { + const virtualizer = this.virtualizerController.getVirtualizer() + const virtualRows = virtualizer.getVirtualItems() + const count = sentences.length + + return html` +
+ + + +
+
+
+ ${repeat( + virtualRows, + (virtualRow) => virtualRow.key, + (virtualRow) => + html`
+
+
Row ${virtualRow.index}
+
${sentences[virtualRow.index]}
+
+
`, + )} +
+
+
+
+ + ` + } +} + +@customElement('column-virtualizer-dynamic') +class ColumnVirtualizerDynamic extends LitElement { + private scrollElementRef: Ref = createRef() + + private virtualizerController: VirtualizerController + + constructor() { + super() + this.virtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: sentences.length, + estimateSize: () => 45, + horizontal: true, + }) + } + + render() { + const virtualizer = this.virtualizerController.getVirtualizer() + const virtualColumns = virtualizer.getVirtualItems() + + return html` +
+
+
+ ${repeat( + virtualColumns, + (virtualColumn) => virtualColumn.key, + (virtualColumn) => html` +
+
+
Column ${virtualColumn.index}
+
${sentences[virtualColumn.index]}
+
+
+ `, + )} +
+
+
+ + ` + } +} + +@customElement('grid-virtualizer-dynamic') +class GridVirtualizerDynamic extends LitElement { + @property() + private data: string[][] + + @property() + private columns: Column[] + + private parentElementRef: Ref = createRef() + private virtualizerController: WindowVirtualizerController + + private columnVirtualizerController: VirtualizerController< + HTMLDivElement, + Element + > + + private getColumnWidth(index: number) { + return this.columns[index].width + } + + connectedCallback() { + this.columnVirtualizerController = new VirtualizerController(this, { + horizontal: true, + count: this.columns.length, + getScrollElement: () => this.parentElementRef.value, + estimateSize: (index) => this.getColumnWidth(index), + overscan: 5, + }) + this.virtualizerController = new WindowVirtualizerController(this, { + count: this.data.length, + estimateSize: () => 350, + overscan: 5, + }) + super.connectedCallback() + } + + render() { + const virtualizer = this.virtualizerController.getVirtualizer() + const columnVirtualizer = this.columnVirtualizerController.getVirtualizer() + const columnItems = columnVirtualizer.getVirtualItems() + const [before, after] = + columnItems.length > 0 + ? [ + columnItems[0].start, + columnVirtualizer.getTotalSize() - + columnItems[columnItems.length - 1].end, + ] + : [0, 0] + + return html` +
+
+ ${repeat( + virtualizer.getVirtualItems(), + (row) => row.key, + (row) => html` +
+
+ ${columnItems.map( + (column) => + html`
+ ${row.index === 0 + ? html`
${this.columns[column.index].name}
` + : html`
+ ${this.data[row.index][column.index]} +
`} +
`, + )} +
+
+ `, + )} +
+
+ + ` + } +} + +@customElement('my-app') +export class MyApp extends LitElement { + protected render() { + const { pathname } = window.location + + return html` +
+

+ These components are using dynamic sizes. This means + that each element's exact dimensions are unknown when rendered. An + estimated dimension is used as the initial measurement, then this + measurement is readjusted on the fly as each element is rendered. +

+ + + ${(() => { + switch (pathname) { + case '/': + return html`` + case '/columns': + return html`` + case '/grid': { + const columns = generateColumns(30) + const data = generateData(columns) + return html`` + } + default: + return html`
Not found
` + } + })()} +
+ ` + } +} diff --git a/examples/lit/dynamic/tsconfig.json b/examples/lit/dynamic/tsconfig.json new file mode 100644 index 000000000..c2bfe0aac --- /dev/null +++ b/examples/lit/dynamic/tsconfig.json @@ -0,0 +1,13 @@ +{ + "composite": true, + "compilerOptions": { + "outDir": "./build/types", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "files": ["src/main.ts"], + "include": ["src"] +} diff --git a/examples/lit/dynamic/vite.config.js b/examples/lit/dynamic/vite.config.js new file mode 100644 index 000000000..a19ee30ff --- /dev/null +++ b/examples/lit/dynamic/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], +}) diff --git a/examples/lit/fixed/.gitignore b/examples/lit/fixed/.gitignore new file mode 100644 index 000000000..d451ff16c --- /dev/null +++ b/examples/lit/fixed/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/lit/fixed/README.md b/examples/lit/fixed/README.md new file mode 100644 index 000000000..b09ff6203 --- /dev/null +++ b/examples/lit/fixed/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` or `npm` +- `npm run start` or `npm run start` diff --git a/examples/lit/fixed/index.html b/examples/lit/fixed/index.html new file mode 100644 index 000000000..09dfe3355 --- /dev/null +++ b/examples/lit/fixed/index.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + + + diff --git a/examples/lit/fixed/package.json b/examples/lit/fixed/package.json new file mode 100644 index 000000000..b19a41370 --- /dev/null +++ b/examples/lit/fixed/package.json @@ -0,0 +1,21 @@ +{ + "name": "tanstack-lit-virtual-example-fixed", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@tanstack/lit-virtual": "^3.13.24", + "@tanstack/virtual-core": "^3.13.23", + "lit": "^3.3.0" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "typescript": "5.4.5", + "vite": "^5.4.19" + } +} diff --git a/examples/lit/fixed/src/index.css b/examples/lit/fixed/src/index.css new file mode 100644 index 000000000..8f38e9d80 --- /dev/null +++ b/examples/lit/fixed/src/index.css @@ -0,0 +1,8 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +body { + padding: 1rem; +} diff --git a/examples/lit/fixed/src/main.ts b/examples/lit/fixed/src/main.ts new file mode 100644 index 000000000..59decbe0c --- /dev/null +++ b/examples/lit/fixed/src/main.ts @@ -0,0 +1,311 @@ +import { customElement, property } from 'lit/decorators.js' +import { Ref, createRef, ref } from 'lit/directives/ref.js' +import { html, LitElement } from 'lit' +import { faker } from '@faker-js/faker' +import { repeat } from 'lit/directives/repeat.js' +import { + VirtualizerController, + WindowVirtualizerController, +} from '@tanstack/lit-virtual' + +interface Column { + key: string + name: string + width: number +} + +function randomNumber(min: number, max: number) { + return faker.number.int({ min, max }) +} + +const sentences = new Array(10000) + .fill(true) + .map(() => faker.lorem.sentence(randomNumber(20, 70))) + +const generateColumns = (count: number) => { + return new Array(count).fill(0).map((_, i) => { + const key: string = i.toString() + return { + key, + name: `Column ${i}`, + width: randomNumber(75, 300), + } + }) +} + +const generateData = (columns: Column[], count = 300) => { + return new Array(count).fill(0).map((_, rowIndex) => + columns.reduce((acc, _curr, colIndex) => { + // simulate dynamic size cells + const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) + + acc.push(val) + + return acc + }, []), + ) +} + +@customElement('row-virtualizer-fixed') +class RowVirtualizerFixed extends LitElement { + private scrollElementRef: Ref = createRef() + + private virtualizerController: VirtualizerController + + constructor() { + super() + this.virtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: 10000, + estimateSize: () => 35, + overscan: 5, + }) + } + + render() { + const virtualizer = this.virtualizerController.getVirtualizer() + const virtualRows = virtualizer.getVirtualItems() + + return html` +
+
+
+ ${repeat( + virtualRows, + (virtualRow) => virtualRow.key, + (virtualRow) => + html`
+ Row ${virtualRow.index} +
`, + )} +
+
+
+ + ` + } +} + +@customElement('column-virtualizer-fixed') +class ColumnVirtualizerFixed extends LitElement { + private scrollElementRef: Ref = createRef() + + private virtualizerController: VirtualizerController + + constructor() { + super() + this.virtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: sentences.length, + estimateSize: () => 100, + horizontal: true, + }) + } + + render() { + const virtualizer = this.virtualizerController.getVirtualizer() + const virtualColumns = virtualizer.getVirtualItems() + + return html` +
+
+
+ ${repeat( + virtualColumns, + (virtualColumn) => virtualColumn.key, + (virtualColumn) => + html`
+ Column ${virtualColumn.index} +
`, + )} +
+
+
+ + ` + } +} + +@customElement('grid-virtualizer-fixed') +class GridVirtualizerFixed extends LitElement { + private scrollElementRef: Ref = createRef() + + private rowVirtualizerController: VirtualizerController< + HTMLDivElement, + Element + > + private columnVirtualizerController: VirtualizerController< + HTMLDivElement, + Element + > + + constructor() { + super() + this.rowVirtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: sentences.length, + estimateSize: () => 35, + overscan: 5, + }) + + this.columnVirtualizerController = new VirtualizerController(this, { + getScrollElement: () => this.scrollElementRef.value, + count: sentences.length, + estimateSize: () => 100, + horizontal: true, + overscan: 5, + }) + } + + render() { + const rowVirtualizer = this.rowVirtualizerController.getVirtualizer() + const columnVirtualizer = this.columnVirtualizerController.getVirtualizer() + + return html` +
+
+
+ ${repeat( + rowVirtualizer.getVirtualItems(), + (virtualRow) => virtualRow.key, + (virtualRow) => + repeat( + columnVirtualizer.getVirtualItems(), + (virtualColumn) => virtualColumn.key, + (virtualColumn) => html` +
+ Cell ${virtualRow.index}, ${virtualColumn.index} +
+ `, + ), + )} +
+
+
+ + ` + } +} + +@customElement('my-app') +export class MyApp extends LitElement { + protected render() { + const { pathname } = window.location + + return html` +
+

+ These components are using fixed sizes. This means + that every element's dimensions are hard-coded to the same value and + never change. +

+
+
+ +

Rows

+ +
+
+

Columns

+ +
+
+

Grid

+ +
+
+
+ ` + } +} diff --git a/examples/lit/fixed/tsconfig.json b/examples/lit/fixed/tsconfig.json new file mode 100644 index 000000000..c2bfe0aac --- /dev/null +++ b/examples/lit/fixed/tsconfig.json @@ -0,0 +1,13 @@ +{ + "composite": true, + "compilerOptions": { + "outDir": "./build/types", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "files": ["src/main.ts"], + "include": ["src"] +} diff --git a/examples/lit/fixed/vite.config.js b/examples/lit/fixed/vite.config.js new file mode 100644 index 000000000..a19ee30ff --- /dev/null +++ b/examples/lit/fixed/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], +}) diff --git a/examples/react/dynamic/package.json b/examples/react/dynamic/package.json index 8c9df7841..397a840bb 100644 --- a/examples/react/dynamic/package.json +++ b/examples/react/dynamic/package.json @@ -1,24 +1,24 @@ { "name": "tanstack-react-virtual-example-dynamic", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { - "@faker-js/faker": "^7.6.0", - "@tanstack/react-virtual": "^3.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@faker-js/faker": "^8.4.1", + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@types/node": "^18.19.3", - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@vitejs/plugin-react": "^4.2.1", - "typescript": "5.2.2", - "vite": "^5.1.3" + "@types/node": "^24.5.2", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "typescript": "5.4.5", + "vite": "^5.4.19" } } diff --git a/examples/react/dynamic/src/main.tsx b/examples/react/dynamic/src/main.tsx index 2d4c33deb..1a0fe0ae0 100644 --- a/examples/react/dynamic/src/main.tsx +++ b/examples/react/dynamic/src/main.tsx @@ -7,7 +7,7 @@ import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual' import './index.css' const randomNumber = (min: number, max: number) => - faker.datatype.number({ min, max }) + faker.number.int({ min, max }) const sentences = new Array(10000) .fill(true) @@ -16,13 +16,20 @@ const sentences = new Array(10000) function RowVirtualizerDynamic() { const parentRef = React.useRef(null) + const [enabled, setEnabled] = React.useState(true) + const count = sentences.length const virtualizer = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => 45, + enabled, }) + React.useEffect(() => { + virtualizer.scrollToIndex(count - 1, { align: 'end' }) + }, []) + const items = virtualizer.getVirtualItems() return ( @@ -37,7 +44,7 @@ function RowVirtualizerDynamic() { + +
> + columns: Array }) { const parentRef = React.useRef(null) @@ -266,9 +282,9 @@ const generateColumns = (count: number) => { }) } -const generateData = (columns: Column[], count = 300) => { +const generateData = (columns: Array, count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => - columns.reduce((acc, _curr, colIndex) => { + columns.reduce>((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) @@ -279,6 +295,110 @@ const generateData = (columns: Column[], count = 300) => { ) } +function RowVirtualizerExperimental() { + const parentRef = React.useRef(null) + const innerRef = React.useRef(null) + const rowRefsMap = React.useRef(new Map()) + + const [enabled, setEnabled] = React.useState(true) + + const count = sentences.length + const virtualizer = useVirtualizer({ + count, + getScrollElement: () => parentRef.current, + estimateSize: () => 45, + enabled, + onChange: (instance) => { + innerRef.current!.style.height = `${instance.getTotalSize()}px` + instance.getVirtualItems().forEach((virtualRow) => { + const rowRef = rowRefsMap.current.get(virtualRow.index) + if (!rowRef) return + rowRef.style.transform = `translateY(${virtualRow.start}px)` + }) + }, + }) + + const indexes = virtualizer.getVirtualIndexes() + + React.useEffect(() => { + virtualizer.measure() + }, []) + + return ( +
+ + + + + + + +
+
+
+ {indexes.map((index) => ( +
{ + if (el) { + virtualizer.measureElement(el) + rowRefsMap.current.set(index, el) + } + }} + className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} + > +
+
Row {index}
+
{sentences[index]}
+
+
+ ))} +
+
+
+ ) +} + function App() { const pathname = location.pathname return ( @@ -286,7 +406,7 @@ function App() {

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An - estimated dimension is used to get an a initial measurement, then this + estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.

{(() => { @@ -316,6 +436,8 @@ function App() { const data = generateData(columns) return } + case '/experimental': + return default: return
Not found
} @@ -326,15 +448,15 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
) } -const container = document.getElementById('root') -const root = createRoot(container!) +const container = document.getElementById('root')! +const root = createRoot(container) const { StrictMode } = React root.render( diff --git a/examples/react/dynamic/tsconfig.dev.json b/examples/react/dynamic/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/dynamic/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/dynamic/tsconfig.json b/examples/react/dynamic/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/dynamic/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/fixed/package.json b/examples/react/fixed/package.json index 04199cc99..c4ab6c483 100644 --- a/examples/react/fixed/package.json +++ b/examples/react/fixed/package.json @@ -1,23 +1,23 @@ { "name": "tanstack-react-virtual-example-fixed", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { - "@tanstack/react-virtual": "^3.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@types/node": "^18.19.3", - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@vitejs/plugin-react": "^4.2.1", - "typescript": "5.2.2", - "vite": "^5.1.3" + "@types/node": "^24.5.2", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "typescript": "5.4.5", + "vite": "^5.4.19" } } diff --git a/examples/react/fixed/src/main.tsx b/examples/react/fixed/src/main.tsx index a67ed5bf3..e265fae27 100644 --- a/examples/react/fixed/src/main.tsx +++ b/examples/react/fixed/src/main.tsx @@ -32,7 +32,7 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
@@ -212,6 +212,7 @@ function GridVirtualizerFixed() { ) } +// eslint-disable-next-line ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/examples/react/fixed/tsconfig.dev.json b/examples/react/fixed/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/fixed/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/fixed/tsconfig.json b/examples/react/fixed/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/fixed/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/infinite-scroll/package.json b/examples/react/infinite-scroll/package.json index d8a08c3a4..b39fcff96 100644 --- a/examples/react/infinite-scroll/package.json +++ b/examples/react/infinite-scroll/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-infinite-scroll", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,15 +9,15 @@ "start": "vite" }, "dependencies": { - "@tanstack/react-virtual": "^3.2.1", - "axios": "^0.26.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-query": "^3.39.1" + "@tanstack/react-query": "^5.80.7", + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/infinite-scroll/src/main.tsx b/examples/react/infinite-scroll/src/main.tsx index ee8ac9b48..508870093 100644 --- a/examples/react/infinite-scroll/src/main.tsx +++ b/examples/react/infinite-scroll/src/main.tsx @@ -1,7 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom' -import axios from 'axios' -import { QueryClient, QueryClientProvider, useInfiniteQuery } from 'react-query' +import { + QueryClient, + QueryClientProvider, + useInfiniteQuery, +} from '@tanstack/react-query' import './index.css' @@ -12,10 +15,10 @@ const queryClient = new QueryClient() async function fetchServerPage( limit: number, offset: number = 0, -): Promise<{ rows: string[]; nextOffset: number }> { +): Promise<{ rows: Array; nextOffset: number }> { const rows = new Array(limit) .fill(0) - .map((e, i) => `Async loaded row #${i + offset * limit}`) + .map((_, i) => `Async loaded row #${i + offset * limit}`) await new Promise((r) => setTimeout(r, 500)) @@ -31,17 +34,16 @@ function App() { isFetchingNextPage, fetchNextPage, hasNextPage, - } = useInfiniteQuery( - 'projects', - (ctx) => fetchServerPage(10, ctx.pageParam), - { - getNextPageParam: (_lastGroup, groups) => groups.length, - }, - ) + } = useInfiniteQuery({ + queryKey: ['projects'], + queryFn: (ctx) => fetchServerPage(10, ctx.pageParam), + getNextPageParam: (lastGroup) => lastGroup.nextOffset, + initialPageParam: 0, + }) const allRows = data ? data.pages.flatMap((d) => d.rows) : [] - const parentRef = React.useRef() + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: hasNextPage ? allRows.length + 1 : allRows.length, @@ -84,10 +86,10 @@ function App() {

- {status === 'loading' ? ( + {status === 'pending' ? (

Loading...

) : status === 'error' ? ( - Error: {(error as Error).message} + Error: {error.message} ) : (

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
diff --git a/examples/react/infinite-scroll/tsconfig.dev.json b/examples/react/infinite-scroll/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/infinite-scroll/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/infinite-scroll/tsconfig.json b/examples/react/infinite-scroll/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/infinite-scroll/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/infinite-scroll/vite.config.js b/examples/react/infinite-scroll/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/infinite-scroll/vite.config.js +++ b/examples/react/infinite-scroll/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/padding/package.json b/examples/react/padding/package.json index b40e890ac..e3bc979e4 100644 --- a/examples/react/padding/package.json +++ b/examples/react/padding/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-padding", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,13 +9,14 @@ "start": "vite" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@tanstack/react-virtual": "^3.2.1" + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/padding/src/main.tsx b/examples/react/padding/src/main.tsx index 59cf86f69..c3291baae 100644 --- a/examples/react/padding/src/main.tsx +++ b/examples/react/padding/src/main.tsx @@ -19,7 +19,7 @@ function App() {

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An - estimated dimension is used to get an a initial measurement, then this + estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.


@@ -39,8 +39,8 @@ function App() { ) } -function RowVirtualizerDynamic({ rows }) { - const parentRef = React.useRef() +function RowVirtualizerDynamic({ rows }: { rows: Array }) { + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, @@ -70,8 +70,9 @@ function RowVirtualizerDynamic({ rows }) { > {rowVirtualizer.getVirtualItems().map((virtualRow) => (
}) { + const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, @@ -123,8 +124,9 @@ function ColumnVirtualizerDynamic({ columns }) { > {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
+ columns: Array +}) { + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, @@ -155,6 +163,7 @@ function GridVirtualizerDynamic({ rows, columns }) { estimateSize: () => 50, paddingStart: 200, paddingEnd: 200, + indexAttribute: 'data-row-index', }) const columnVirtualizer = useVirtualizer({ @@ -164,6 +173,7 @@ function GridVirtualizerDynamic({ rows, columns }) { estimateSize: () => 50, paddingStart: 200, paddingEnd: 200, + indexAttribute: 'data-column-index', }) const [show, setShow] = React.useState(true) @@ -197,13 +207,15 @@ function GridVirtualizerDynamic({ rows, columns }) { }} > {rowVirtualizer.getVirtualItems().map((virtualRow) => ( - + {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
{ - virtualRow.measureElement(el) - virtualColumn.measureElement(el) + rowVirtualizer.measureElement(el) + columnVirtualizer.measureElement(el) }} className={ virtualColumn.index % 2 @@ -237,7 +249,7 @@ function GridVirtualizerDynamic({ rows, columns }) {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null} diff --git a/examples/react/padding/tsconfig.dev.json b/examples/react/padding/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/padding/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/padding/tsconfig.json b/examples/react/padding/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/padding/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/padding/vite.config.js b/examples/react/padding/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/padding/vite.config.js +++ b/examples/react/padding/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/scroll-padding/package.json b/examples/react/scroll-padding/package.json index bd9c78d1e..4e216c208 100644 --- a/examples/react/scroll-padding/package.json +++ b/examples/react/scroll-padding/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-scroll-padding", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,14 +9,15 @@ "start": "vite" }, "dependencies": { - "@react-hookz/web": "^14.2.2", - "@tanstack/react-virtual": "^3.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@react-hookz/web": "^25.1.1", + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/scroll-padding/src/main.tsx b/examples/react/scroll-padding/src/main.tsx index ea98b9115..b430aeae8 100644 --- a/examples/react/scroll-padding/src/main.tsx +++ b/examples/react/scroll-padding/src/main.tsx @@ -3,11 +3,11 @@ import ReactDOM from 'react-dom' import './index.css' -import { useMeasure } from '@react-hookz/web/esm' +import { useMeasure } from '@react-hookz/web' import { useVirtualizer } from '@tanstack/react-virtual' function App() { - const parentRef = React.useRef() + const parentRef = React.useRef(null) const [theadSize, theadRef] = useMeasure() const rowVirtualizer = useVirtualizer({ @@ -91,7 +91,7 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null} diff --git a/examples/react/scroll-padding/tsconfig.dev.json b/examples/react/scroll-padding/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/scroll-padding/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/scroll-padding/tsconfig.json b/examples/react/scroll-padding/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/scroll-padding/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/scroll-padding/vite.config.js b/examples/react/scroll-padding/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/scroll-padding/vite.config.js +++ b/examples/react/scroll-padding/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/smooth-scroll/package.json b/examples/react/smooth-scroll/package.json index 2840c8bc0..326ce52f6 100644 --- a/examples/react/smooth-scroll/package.json +++ b/examples/react/smooth-scroll/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-smooth-scroll", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,13 +9,14 @@ "start": "vite" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@tanstack/react-virtual": "^3.2.1" + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/smooth-scroll/src/main.tsx b/examples/react/smooth-scroll/src/main.tsx index d040cf92c..e87f019c3 100644 --- a/examples/react/smooth-scroll/src/main.tsx +++ b/examples/react/smooth-scroll/src/main.tsx @@ -3,24 +3,21 @@ import ReactDOM from 'react-dom' import './index.css' -import { - elementScroll, - useVirtualizer, - VirtualizerOptions, -} from '@tanstack/react-virtual' +import { elementScroll, useVirtualizer } from '@tanstack/react-virtual' +import type { VirtualizerOptions } from '@tanstack/react-virtual' -function easeInOutQuint(t) { +function easeInOutQuint(t: number) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t } function App() { - const parentRef = React.useRef() + const parentRef = React.useRef(null) const scrollingRef = React.useRef() const scrollToFn: VirtualizerOptions['scrollToFn'] = React.useCallback((offset, canSmooth, instance) => { const duration = 1000 - const start = parentRef.current.scrollTop + const start = parentRef.current?.scrollTop || 0 const startTime = (scrollingRef.current = Date.now()) const run = () => { @@ -111,7 +108,7 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
diff --git a/examples/react/smooth-scroll/tsconfig.dev.json b/examples/react/smooth-scroll/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/smooth-scroll/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/smooth-scroll/tsconfig.json b/examples/react/smooth-scroll/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/smooth-scroll/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/smooth-scroll/vite.config.js b/examples/react/smooth-scroll/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/smooth-scroll/vite.config.js +++ b/examples/react/smooth-scroll/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/sticky/package.json b/examples/react/sticky/package.json index 9c8457b8e..a12958a0b 100644 --- a/examples/react/sticky/package.json +++ b/examples/react/sticky/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-sticky", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,15 +9,17 @@ "start": "vite" }, "dependencies": { - "@faker-js/faker": "^7.6.0", - "@tanstack/react-virtual": "^3.2.1", + "@faker-js/faker": "^8.4.1", + "@tanstack/react-virtual": "^3.13.23", "lodash": "^4.17.21", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/lodash": "^4.17.17", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/sticky/src/main.tsx b/examples/react/sticky/src/main.tsx index 0870a2303..132e8af4e 100644 --- a/examples/react/sticky/src/main.tsx +++ b/examples/react/sticky/src/main.tsx @@ -3,19 +3,23 @@ import * as React from 'react' import ReactDOM from 'react-dom' import { faker } from '@faker-js/faker' import { findIndex, groupBy } from 'lodash' -import { useVirtualizer, defaultRangeExtractor } from '@tanstack/react-virtual' +import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual' +import type { Range } from '@tanstack/react-virtual' const groupedNames = groupBy( Array.from({ length: 1000 }) - .map(() => faker.name.firstName()) + .map(() => faker.person.firstName()) .sort(), (name) => name[0], ) const groups = Object.keys(groupedNames) -const rows = groups.reduce((acc, k) => [...acc, k, ...groupedNames[k]], []) +const rows = groups.reduce>( + (acc, k) => [...acc, k, ...groupedNames[k]], + [], +) const App = () => { - const parentRef = React.useRef() + const parentRef = React.useRef(null) const activeStickyIndexRef = React.useRef(0) @@ -24,19 +28,21 @@ const App = () => { [], ) - const isSticky = (index) => stickyIndexes.includes(index) + const isSticky = (index: number) => stickyIndexes.includes(index) - const isActiveSticky = (index) => activeStickyIndexRef.current === index + const isActiveSticky = (index: number) => + activeStickyIndexRef.current === index const rowVirtualizer = useVirtualizer({ count: rows.length, estimateSize: () => 50, getScrollElement: () => parentRef.current, rangeExtractor: React.useCallback( - (range) => { - activeStickyIndexRef.current = [...stickyIndexes] - .reverse() - .find((index) => range.startIndex >= index) + (range: Range) => { + activeStickyIndexRef.current = + [...stickyIndexes] + .reverse() + .find((index) => range.startIndex >= index) ?? 0 const next = new Set([ activeStickyIndexRef.current, @@ -102,7 +108,7 @@ const App = () => {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
diff --git a/examples/react/sticky/tsconfig.dev.json b/examples/react/sticky/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/sticky/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/sticky/tsconfig.json b/examples/react/sticky/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/sticky/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/sticky/vite.config.js b/examples/react/sticky/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/sticky/vite.config.js +++ b/examples/react/sticky/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/table/package.json b/examples/react/table/package.json index 5f047f1fb..3c54b5b76 100644 --- a/examples/react/table/package.json +++ b/examples/react/table/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-table", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,16 +9,16 @@ "start": "vite" }, "dependencies": { - "@faker-js/faker": "^7.6.0", - "@tanstack/react-table": "^8.7.9", - "@tanstack/react-virtual": "^3.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@faker-js/faker": "^8.4.1", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/table/src/main.tsx b/examples/react/table/src/main.tsx index 8ed0d807d..0bdbe7d69 100644 --- a/examples/react/table/src/main.tsx +++ b/examples/react/table/src/main.tsx @@ -3,21 +3,20 @@ import { createRoot } from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' import { - ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, - Row, - SortingState, useReactTable, } from '@tanstack/react-table' -import { makeData, Person } from './makeData' +import { makeData } from './makeData' +import type { ColumnDef, Row, SortingState } from '@tanstack/react-table' +import type { Person } from './makeData' import './index.css' function ReactTableVirtualized() { const [sorting, setSorting] = React.useState([]) - const columns = React.useMemo[]>( + const columns = React.useMemo>>( () => [ { accessorKey: 'id', @@ -128,7 +127,7 @@ function ReactTableVirtualized() { {virtualizer.getVirtualItems().map((virtualRow, index) => { - const row = rows[virtualRow.index] as Row + const row = rows[virtualRow.index] return (

For tables, the basis for the offset of the translate css function is from the row's initial position itself. Because of this, we need to - calculate the translateY pixel count different and base it off the the + calculate the translateY pixel count differently and base it off the index.

@@ -175,7 +174,7 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null}
diff --git a/examples/react/table/src/makeData.ts b/examples/react/table/src/makeData.ts index 1d6635238..9bf52ab8f 100644 --- a/examples/react/table/src/makeData.ts +++ b/examples/react/table/src/makeData.ts @@ -22,11 +22,11 @@ const range = (len: number) => { const newPerson = (index: number): Person => { return { id: index + 1, - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), - age: faker.datatype.number(40), - visits: faker.datatype.number(1000), - progress: faker.datatype.number(100), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), createdAt: faker.datatype.datetime({ max: new Date().getTime() }), status: faker.helpers.shuffle([ 'relationship', diff --git a/examples/react/table/tsconfig.dev.json b/examples/react/table/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/table/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/table/tsconfig.json b/examples/react/table/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/table/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/variable/package.json b/examples/react/variable/package.json index 582d850cc..6a5ec6c7c 100644 --- a/examples/react/variable/package.json +++ b/examples/react/variable/package.json @@ -1,7 +1,7 @@ { "name": "tanstack-react-virtual-example-variable", - "version": "0.0.0", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -9,13 +9,14 @@ "start": "vite" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@tanstack/react-virtual": "^3.2.1" + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@vitejs/plugin-react": "^4.2.1", - "vite": "^5.1.3" + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "vite": "^5.4.19" } } diff --git a/examples/react/variable/src/main.tsx b/examples/react/variable/src/main.tsx index b9e2bb268..51878a21f 100644 --- a/examples/react/variable/src/main.tsx +++ b/examples/react/variable/src/main.tsx @@ -47,15 +47,15 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null} ) } -function RowVirtualizerVariable({ rows }) { - const parentRef = React.useRef() +function RowVirtualizerVariable({ rows }: { rows: Array }) { + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, @@ -104,8 +104,8 @@ function RowVirtualizerVariable({ rows }) { ) } -function ColumnVirtualizerVariable({ columns }) { - const parentRef = React.useRef() +function ColumnVirtualizerVariable({ columns }: { columns: Array }) { + const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, @@ -157,8 +157,14 @@ function ColumnVirtualizerVariable({ columns }) { ) } -function GridVirtualizerVariable({ rows, columns }) { - const parentRef = React.useRef() +function GridVirtualizerVariable({ + rows, + columns, +}: { + rows: Array + columns: Array +}) { + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, @@ -227,8 +233,8 @@ function GridVirtualizerVariable({ rows, columns }) { ) } -function MasonryVerticalVirtualizerVariable({ rows }) { - const parentRef = React.useRef() +function MasonryVerticalVirtualizerVariable({ rows }: { rows: Array }) { + const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, @@ -278,8 +284,12 @@ function MasonryVerticalVirtualizerVariable({ rows }) { ) } -function MasonryHorizontalVirtualizerVariable({ rows }) { - const parentRef = React.useRef() +function MasonryHorizontalVirtualizerVariable({ + rows, +}: { + rows: Array +}) { + const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, diff --git a/examples/react/variable/tsconfig.dev.json b/examples/react/variable/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/variable/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/variable/tsconfig.json b/examples/react/variable/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/variable/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/variable/vite.config.js b/examples/react/variable/vite.config.js index d2fa7925b..9ffcc6757 100644 --- a/examples/react/variable/vite.config.js +++ b/examples/react/variable/vite.config.js @@ -1,9 +1,6 @@ -import * as path from 'path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import rollupReplace from '@rollup/plugin-replace' -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) diff --git a/examples/react/window/package.json b/examples/react/window/package.json index 221edb13c..6ae5a8da6 100644 --- a/examples/react/window/package.json +++ b/examples/react/window/package.json @@ -1,23 +1,23 @@ { "name": "tanstack-react-virtual-example-window", "private": true, + "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { - "@tanstack/react-virtual": "^3.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@tanstack/react-virtual": "^3.13.23", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@rollup/plugin-replace": "^5.0.2", - "@types/node": "^18.19.3", - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@vitejs/plugin-react": "^4.2.1", - "typescript": "5.2.2", - "vite": "^5.1.3" + "@types/node": "^24.5.2", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.5.2", + "typescript": "5.4.5", + "vite": "^5.4.19" } } diff --git a/examples/react/window/src/main.tsx b/examples/react/window/src/main.tsx index c201202e0..212988be2 100644 --- a/examples/react/window/src/main.tsx +++ b/examples/react/window/src/main.tsx @@ -7,12 +7,17 @@ import { useWindowVirtualizer } from '@tanstack/react-virtual' function Example() { const listRef = React.useRef(null) + const listOffsetRef = React.useRef(0) + + React.useLayoutEffect(() => { + listOffsetRef.current = listRef.current?.offsetTop ?? 0 + }, []) const virtualizer = useWindowVirtualizer({ count: 10000, estimateSize: () => 35, overscan: 5, - scrollMargin: listRef.current?.offsetTop ?? 0, + scrollMargin: listOffsetRef.current, }) return ( @@ -68,7 +73,7 @@ function App() {

Notice: You are currently running React in development mode. Rendering performance will be slightly degraded - until this application is build for production. + until this application is built for production.

) : null} diff --git a/examples/react/window/tsconfig.dev.json b/examples/react/window/tsconfig.dev.json deleted file mode 100644 index c09bc865f..000000000 --- a/examples/react/window/tsconfig.dev.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "composite": true, - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/types" - }, - "files": ["src/main.tsx"], - "include": [ - "src" - // "__tests__/**/*.test.*" - ] -} diff --git a/examples/react/window/tsconfig.json b/examples/react/window/tsconfig.json new file mode 100644 index 000000000..87318025a --- /dev/null +++ b/examples/react/window/tsconfig.json @@ -0,0 +1,25 @@ +{ + "composite": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/svelte/dynamic/package.json b/examples/svelte/dynamic/package.json index 8ec24df76..2840879b6 100644 --- a/examples/svelte/dynamic/package.json +++ b/examples/svelte/dynamic/package.json @@ -1,7 +1,6 @@ { "name": "tanstack-svelte-virtual-example-dynamic", "private": true, - "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", @@ -10,16 +9,16 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { - "@faker-js/faker": "^7.6.0", - "@tanstack/svelte-virtual": "^3.2.1" + "@faker-js/faker": "^8.4.1", + "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@tsconfig/svelte": "^5.0.0", - "svelte": "^4.2.2", - "svelte-check": "^3.4.6", - "tslib": "^2.6.0", - "typescript": "5.2.2", - "vite": "^5.1.3" + "@sveltejs/vite-plugin-svelte": "^3.1.2", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^4.2.20", + "svelte-check": "^4.2.1", + "tslib": "^2.8.1", + "typescript": "5.4.5", + "vite": "^5.4.19" } } diff --git a/examples/svelte/dynamic/src/App.svelte b/examples/svelte/dynamic/src/App.svelte index e46dafc91..b73b876f1 100644 --- a/examples/svelte/dynamic/src/App.svelte +++ b/examples/svelte/dynamic/src/App.svelte @@ -10,9 +10,9 @@

These components are using dynamic sizes. This means that each - element's exact dimensions are unknown when rendered. An estimated dimension - is used to get an a initial measurement, then this measurement is readjusted - on the fly as each element is rendered. + element's exact dimensions are unknown when rendered. An estimated dimension is + used as the initial measurement, then this measurement is readjusted on the fly + as each element is rendered.