Svelte Example: Table

svelte
<script lang="ts">
  import {
    createSvelteTable,
    getCoreRowModel,
    getSortedRowModel,
    type ColumnDef,
    type SortingState,
    type Updater,
    flexRender,
    type TableOptions,
  } from '@tanstack/svelte-table'
  import { createVirtualizer } from '@tanstack/svelte-virtual'
  import { makeData, type Person } from './makeData'
  import { writable } from 'svelte/store'

  let virtualListEl: HTMLDivElement
  let sorting: SortingState = []

  function setSorting(updater: Updater<SortingState>) {
    if (updater instanceof Function) sorting = updater(sorting)
    else sorting = updater

    tableOptions.update((opts) => ({
      ...opts,
      state: {
        ...opts.state,
        sorting,
      },
    }))
  }

  const columns: ColumnDef<Person>[] = [
    {
      accessorKey: 'id',
      header: 'ID',
      size: 60,
    },
    {
      accessorKey: 'firstName',
      header: 'First Name',
      cell: (info) => info.getValue(),
    },
    {
      accessorFn: (row) => row.lastName,
      id: 'lastName',
      header: 'Last Name',
      cell: (info) => info.getValue(),
    },
    {
      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<Date>().toLocaleString(),
    },
  ]

  const tableOptions = writable<TableOptions<Person>>({
    data: makeData(50_000),
    columns,
    state: {
      sorting,
    },
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    debugTable: true,
  })

  const table = createSvelteTable(tableOptions)

  $: rows = $table.getRowModel().rows

  $: virtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>({
    count: rows.length,
    getScrollElement: () => virtualListEl,
    estimateSize: () => 34,
    overscan: 20,
  })
</script>

<main>
  <p>
    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 differently and base it off the index.
  </p>

  <div class="list scroll-container" bind:this={virtualListEl}>
    <div style="position: relative; height: {$virtualizer.getTotalSize()}px;">
      <table>
        <thead>
          {#each $table.getHeaderGroups() as headerGroup (headerGroup.id)}
            <tr>
              {#each headerGroup.headers as header (header.id)}
                <th
                  colspan={header.colSpan}
                  style="width: {header.getSize()}px;"
                >
                  {#if !header.isPlaceholder}
                    <button
                      class:sortable-header={header.column.getCanSort()}
                      disabled={!header.column.getCanSort()}
                      on:click={header.column.getToggleSortingHandler()}
                    >
                      <svelte:component
                        this={flexRender(
                          header.column.columnDef.header,
                          header.getContext(),
                        )}
                      />
                      {#if header.column.getIsSorted()}
                        {header.column.getIsSorted() === 'desc' ? ' 🔽' : ' 🔼'}
                      {/if}
                    </button>
                  {/if}
                </th>
              {/each}
            </tr>
          {/each}
        </thead>
        <tbody>
          {#each $virtualizer.getVirtualItems() as row, idx (row.index)}
            <tr
              style="height: {row.size}px; transform: translateY({row.start -
                idx * row.size}px);"
            >
              {#each rows[row.index].getVisibleCells() as cell (cell.id)}
                <td>
                  <svelte:component
                    this={flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext(),
                    )}
                  />
                </td>
              {/each}
            </tr>
          {/each}
        </tbody>
      </table>
    </div>
  </div>
</main>

<style>
  button {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    outline: inherit;
  }

  .scroll-container {
    height: 600px;
    width: 100%;
    overflow: auto;
  }
  .sortable-header {
    cursor: pointer;
    user-select: none;
  }
</style>
<script lang="ts">
  import {
    createSvelteTable,
    getCoreRowModel,
    getSortedRowModel,
    type ColumnDef,
    type SortingState,
    type Updater,
    flexRender,
    type TableOptions,
  } from '@tanstack/svelte-table'
  import { createVirtualizer } from '@tanstack/svelte-virtual'
  import { makeData, type Person } from './makeData'
  import { writable } from 'svelte/store'

  let virtualListEl: HTMLDivElement
  let sorting: SortingState = []

  function setSorting(updater: Updater<SortingState>) {
    if (updater instanceof Function) sorting = updater(sorting)
    else sorting = updater

    tableOptions.update((opts) => ({
      ...opts,
      state: {
        ...opts.state,
        sorting,
      },
    }))
  }

  const columns: ColumnDef<Person>[] = [
    {
      accessorKey: 'id',
      header: 'ID',
      size: 60,
    },
    {
      accessorKey: 'firstName',
      header: 'First Name',
      cell: (info) => info.getValue(),
    },
    {
      accessorFn: (row) => row.lastName,
      id: 'lastName',
      header: 'Last Name',
      cell: (info) => info.getValue(),
    },
    {
      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<Date>().toLocaleString(),
    },
  ]

  const tableOptions = writable<TableOptions<Person>>({
    data: makeData(50_000),
    columns,
    state: {
      sorting,
    },
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    debugTable: true,
  })

  const table = createSvelteTable(tableOptions)

  $: rows = $table.getRowModel().rows

  $: virtualizer = createVirtualizer<HTMLDivElement, HTMLTableRowElement>({
    count: rows.length,
    getScrollElement: () => virtualListEl,
    estimateSize: () => 34,
    overscan: 20,
  })
</script>

<main>
  <p>
    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 differently and base it off the index.
  </p>

  <div class="list scroll-container" bind:this={virtualListEl}>
    <div style="position: relative; height: {$virtualizer.getTotalSize()}px;">
      <table>
        <thead>
          {#each $table.getHeaderGroups() as headerGroup (headerGroup.id)}
            <tr>
              {#each headerGroup.headers as header (header.id)}
                <th
                  colspan={header.colSpan}
                  style="width: {header.getSize()}px;"
                >
                  {#if !header.isPlaceholder}
                    <button
                      class:sortable-header={header.column.getCanSort()}
                      disabled={!header.column.getCanSort()}
                      on:click={header.column.getToggleSortingHandler()}
                    >
                      <svelte:component
                        this={flexRender(
                          header.column.columnDef.header,
                          header.getContext(),
                        )}
                      />
                      {#if header.column.getIsSorted()}
                        {header.column.getIsSorted() === 'desc' ? ' 🔽' : ' 🔼'}
                      {/if}
                    </button>
                  {/if}
                </th>
              {/each}
            </tr>
          {/each}
        </thead>
        <tbody>
          {#each $virtualizer.getVirtualItems() as row, idx (row.index)}
            <tr
              style="height: {row.size}px; transform: translateY({row.start -
                idx * row.size}px);"
            >
              {#each rows[row.index].getVisibleCells() as cell (cell.id)}
                <td>
                  <svelte:component
                    this={flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext(),
                    )}
                  />
                </td>
              {/each}
            </tr>
          {/each}
        </tbody>
      </table>
    </div>
  </div>
</main>

<style>
  button {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    outline: inherit;
  }

  .scroll-container {
    height: 600px;
    width: 100%;
    overflow: auto;
  }
  .sortable-header {
    cursor: pointer;
    user-select: none;
  }
</style>
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.