Vue Example: With Tanstack Form

<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { useTanStackTableDevtools } from '@tanstack/vue-table-devtools'
import { ref, watch } from 'vue'
import FormStateIndicator from './FormStateIndicator.vue'
import NumberField from './NumberField.vue'
import RowSubmitTableRow from './RowSubmitTableRow.vue'
import SelectField from './SelectField.vue'
import SubmitButton from './SubmitButton.vue'
import TextField from './TextField.vue'
import { makeData } from './makeData'
import { formSchema } from './schema'
import { createAppColumnHelper, useAppTable } from './table'
import type { FormData, FormRow } from './schema'

const columnHelper = createAppColumnHelper<FormRow>()

const blankRow = (): FormRow => ({
  firstName: '',
  lastName: '',
  age: 0,
  visits: 0,
  progress: 0,
  status: 'single',
})

const fullData = ref<Array<FormRow>>(makeData(100))

const fullTableForm = useForm({
  defaultValues: {
    data: fullData.value,
  } satisfies FormData,
  onSubmit: ({ value }: { value: FormData }) => {
    alert(
      `Submitted ${value.data.length} records!\n\nFirst record: ${JSON.stringify(value.data[0], null, 2)}`,
    )
  },
  validators: {
    onChange: formSchema,
  },
})

watch(
  fullData,
  (data) => {
    fullTableForm.reset({ data })
  },
  { flush: 'sync' },
)

const fullColumns = columnHelper.columns([
  columnHelper.accessor('firstName', {
    header: 'First Name',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('lastName', {
    header: 'Last Name',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('age', {
    header: 'Age',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('visits', {
    header: 'Visits',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('status', {
    header: 'Status',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('progress', {
    header: 'Profile Progress',
    footer: (props) => props.column.id,
  }),
])

const fullTable = useAppTable({
  key: 'with-tanstack-form-full-table',
  columns: fullColumns,
  data: fullData,
  debugTable: true,
})

useTanStackTableDevtools(fullTable)

function refreshFullData() {
  fullData.value = makeData(100)
}

function stressTest() {
  fullData.value = makeData(1_000_000)
}

function addRow() {
  fullData.value = [blankRow(), ...fullTableForm.store.get().values.data]
  fullTable.firstPage()
}

const rowData = ref<Array<FormRow>>(makeData(100))

const rowColumns = columnHelper.columns([
  columnHelper.accessor('firstName', {
    header: 'First Name',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('lastName', {
    header: 'Last Name',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('age', {
    header: 'Age',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('visits', {
    header: 'Visits',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('status', {
    header: 'Status',
    footer: (props) => props.column.id,
  }),
  columnHelper.accessor('progress', {
    header: 'Profile Progress',
    footer: (props) => props.column.id,
  }),
  columnHelper.display({
    id: 'save',
    header: '',
    cell: () => null,
  }),
])

const rowTable = useAppTable({
  key: 'with-tanstack-form-row-submit',
  columns: rowColumns,
  data: rowData,
  debugTable: true,
})

useTanStackTableDevtools(rowTable)

function refreshRowData() {
  rowData.value = makeData(100)
}

function saveRow(originalRow: FormRow, value: FormRow) {
  rowData.value = rowData.value.map((row) => {
    return row === originalRow ? value : row
  })
}

function handleSubmit(event: Event) {
  event.preventDefault()
  event.stopPropagation()
  fullTableForm.handleSubmit()
}

function getSortTitle(column: ReturnType<typeof fullTable.getAllColumns>[number]) {
  if (!column.getCanSort()) return undefined

  const nextOrder = column.getNextSortingOrder()
  if (nextOrder === 'asc') return 'Sort ascending'
  if (nextOrder === 'desc') return 'Sort descending'

  return 'Clear sort'
}
</script>

<template>
  <div class="demo-root">
    <section class="example-section">
      <h2 class="section-title">Single form around the table</h2>
      <form @submit="handleSubmit">
        <div class="form-actions">
          <FormStateIndicator :form="fullTableForm" />
          <SubmitButton :form="fullTableForm" label="Save All Changes" />
          <button
            type="button"
            class="demo-button success-action"
            @click="addRow"
          >
            Add Row
          </button>
          <button
            type="button"
            class="demo-button secondary-action"
            @click="refreshFullData"
          >
            Regenerate Data
          </button>
          <button
            type="button"
            class="demo-button secondary-action"
            @click="stressTest"
          >
            Stress Test (1M rows)
          </button>
        </div>

        <component :is="fullTable.AppTable">
          <div class="spacer-sm" />
          <div class="scroll-container">
            <table>
              <thead>
                <tr
                  v-for="headerGroup in fullTable.getHeaderGroups()"
                  :key="headerGroup.id"
                >
                  <component
                    :is="fullTable.AppHeader"
                    v-for="header in headerGroup.headers"
                    :key="header.id"
                    :header="header"
                    v-slot="{ header: appHeader }"
                  >
                    <th :colSpan="appHeader.colSpan">
                      <div
                        v-if="!appHeader.isPlaceholder"
                        :class="
                          appHeader.column.getCanSort()
                            ? 'sortable-header'
                            : undefined
                        "
                        :title="getSortTitle(appHeader.column)"
                        @click="
                          appHeader.column.getToggleSortingHandler()?.($event)
                        "
                      >
                        <component :is="appHeader.FlexRender" />
                        <component :is="appHeader.SortIndicator" />
                        <component :is="appHeader.ColumnFilter" />
                      </div>
                    </th>
                  </component>
                </tr>
              </thead>
              <tbody>
                <tr v-for="row in fullTable.getRowModel().rows" :key="row.id">
                  <td v-for="cell in row.getAllCells()" :key="cell.id">
                    <TextField
                      v-if="cell.column.id === 'firstName'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].firstName`"
                    />
                    <TextField
                      v-else-if="cell.column.id === 'lastName'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].lastName`"
                    />
                    <NumberField
                      v-else-if="cell.column.id === 'age'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].age`"
                    />
                    <NumberField
                      v-else-if="cell.column.id === 'visits'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].visits`"
                    />
                    <SelectField
                      v-else-if="cell.column.id === 'status'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].status`"
                    />
                    <NumberField
                      v-else-if="cell.column.id === 'progress'"
                      :form="fullTableForm"
                      :name="`data[${row.index}].progress`"
                    />
                  </td>
                </tr>
              </tbody>
            </table>
          </div>

          <component :is="fullTable.PaginationControls" />
          <component :is="fullTable.RowCount" />
        </component>
      </form>
    </section>

    <div class="spacer-md" />

    <section class="example-section">
      <h2 class="section-title">Form submission per row</h2>
      <div class="form-actions">
        <button
          type="button"
          class="demo-button secondary-action"
          @click="refreshRowData"
        >
          Regenerate Data
        </button>
      </div>

      <component :is="rowTable.AppTable">
        <div class="spacer-sm" />
        <div class="scroll-container">
          <table>
            <thead>
              <tr
                v-for="headerGroup in rowTable.getHeaderGroups()"
                :key="headerGroup.id"
              >
                <component
                  :is="rowTable.AppHeader"
                  v-for="header in headerGroup.headers"
                  :key="header.id"
                  :header="header"
                  v-slot="{ header: appHeader }"
                >
                  <th :colSpan="appHeader.colSpan">
                    <div
                      v-if="!appHeader.isPlaceholder"
                      :class="
                        appHeader.column.getCanSort()
                          ? 'sortable-header'
                          : undefined
                      "
                      :title="getSortTitle(appHeader.column)"
                      @click="
                        appHeader.column.getToggleSortingHandler()?.($event)
                      "
                    >
                      <component :is="appHeader.FlexRender" />
                      <component :is="appHeader.SortIndicator" />
                      <component :is="appHeader.ColumnFilter" />
                    </div>
                  </th>
                </component>
              </tr>
            </thead>
            <tbody>
              <RowSubmitTableRow
                v-for="row in rowTable.getRowModel().rows"
                :key="row.id"
                :row="row"
                :save="saveRow"
              />
            </tbody>
          </table>
        </div>

        <component :is="rowTable.PaginationControls" />
        <component :is="rowTable.RowCount" />
      </component>
    </section>
  </div>
</template>