TanStack

React Example: Ui Integration Dnd Kit

import { DragDropProvider } from '@dnd-kit/react'
import { isSortable, useSortable } from '@dnd-kit/react/sortable'
import { formOptions, useForm } from '@tanstack/react-form'
import { createItem, initialItems } from '../sharedForm'
import type { DragEndEvent } from '@dnd-kit/react'
import type { ReactFormType } from '@tanstack/react-form'

const singleListFormOptions = formOptions({
  defaultValues: {
    items: initialItems.map((item) => ({ ...item })),
  },
})

type SingleListForm = ReactFormType<typeof singleListFormOptions>

interface SingleListItemProps {
  form: SingleListForm
  id: string
  index: number
}

function SingleListItem({ form, id, index }: SingleListItemProps) {
  const { handleRef, isDragging, isDropTarget, ref } = useSortable({
    id,
    index,
    group: 'single-list',
    type: 'single-list-item',
    accept: 'single-list-item',
  })
  const fieldName = `items[${index}].label` as const

  return (
    <li
      ref={ref}
      className="sortable-item"
      data-dragging={isDragging}
      data-drop-target={isDropTarget}
    >
      <button
        ref={handleRef}
        type="button"
        className="drag-handle"
        aria-label="Reorder item"
      >
        Drag
      </button>
      <form.Field name={fieldName}>
        {(field) => (
          <label className="item-field">
            <span>Item {index + 1}</span>
            <input
              name={field.name}
              value={field.value}
              onBlur={field.handleBlur}
              onChange={(event) => field.handleChange(event.target.value)}
            />
          </label>
        )}
      </form.Field>
    </li>
  )
}

function formatItems(items: Array<{ label: string }>) {
  return items.map((item, index) => `${index + 1}. ${item.label}`).join('\n')
}

function getSingleListDragEndHandler(
  form: SingleListForm,
): (event: DragEndEvent) => void {
  return (event) => {
    if (event.canceled || !isSortable(event.operation.source)) {
      return
    }

    const { source } = event.operation

    if (source.initialIndex === source.index) {
      return
    }

    form.moveFieldValue('items', source.initialIndex, source.index)
  }
}

export function SingleListExample() {
  const form = useForm({
    ...singleListFormOptions,
    onSubmit: ({ value }) => {
      alert(`Single list order:\n\n${formatItems(value.items)}`)
    },
  })

  return (
    <section className="example-section">
      <header className="example-header">
        <h2>Single list</h2>
      </header>

      <form
        className="array-form"
        onSubmit={(event) => {
          event.preventDefault()
          event.stopPropagation()
          form.handleSubmit()
        }}
      >
        <DragDropProvider onDragEnd={getSingleListDragEndHandler(form)}>
          <div className="toolbar">
            <button type="submit">Submit</button>
            <button
              type="button"
              onClick={() => form.pushFieldValue('items', createItem())}
            >
              Add item
            </button>
          </div>

          <form.ArrayField name="items">
            {(arrayField) => (
              <section className="list-section">
                <h3>Items</h3>
                <ul className="sortable-list">
                  {arrayField.value.map((item, index) => (
                    <SingleListItem
                      key={item.id}
                      form={form}
                      id={item.id}
                      index={index}
                    />
                  ))}
                </ul>
              </section>
            )}
          </form.ArrayField>
        </DragDropProvider>
      </form>
    </section>
  )
}