# Upload Field (https://v2.s3.dimah.dev/docs/react/ui/components/upload-field)

`UploadField` turns uploads into a form value. Each file that uploads adds
its object key, and every key submits as a hidden `<input name>`. The upload
parts go inside it, as they would inside [`Upload`](https://v2.s3.dimah.dev/docs/react/ui/components/upload).

## Preview [#preview]

Upload up to three files, then submit the form to see the keys it sends.

<DemoPreview name="upload-field-demo.tsx">
  <UploadFieldDemo />
</DemoPreview>

## Install [#install]

Complete [UI Setup](https://v2.s3.dimah.dev/docs/react/ui) first: the stylesheet and one
`<Toaster />`. Then add this component:

<Tabs items={["npm package", "shadcn registry"]}>

<Tab value="npm package">

```tsx
import { UploadField, UploadFieldValue } from "@dimah-s3/ui";
```

</Tab>

<Tab value="shadcn registry">

```npm
npx shadcn@latest add @dimah-s3/upload-field
```

</Tab>

</Tabs>

---

## Usage [#usage]

```tsx
"use client";

import { useUpload } from "@dimah-s3/react";
import {
  UploadDropzone,
  UploadField,
  UploadFieldValue,
  UploadToasts,
} from "@dimah-s3/ui";

export function TicketForm({
  save,
}: {
  save: (formData: FormData) => void | Promise<void>;
}) {
  const upload = useUpload({ route: "attachments", maxFiles: 5 });

  return (
    <form action={save} className="space-y-4">
      <UploadField upload={upload} name="attachments" maxFiles={5}>
        <UploadDropzone />
        <UploadFieldValue />
      </UploadField>
      <UploadToasts upload={upload} />
      <button type="submit">Send</button>
    </form>
  );
}
```

The dropzone shows the files in flight inside it. Once a file uploads it
moves to `UploadFieldValue`, which lists the keys the field holds, so a file
never shows twice. On the server, read the keys with `formData.getAll("attachments")`.
`maxFiles` is a client-side limit, so check the keys and their count in your
own endpoint. See [Forms](https://v2.s3.dimah.dev/docs/react/forms) for the rest of the flow.

## Examples [#examples]

### Attach button [#attach-button]

A message form with an attach button, a key saved earlier as
`defaultValue`, and `UploadStatus` for the files in flight. Remove the saved
file or attach two more to fill the field.

<DemoPreview name="upload-field-button-demo.tsx">
  <UploadFieldButtonDemo />
</DemoPreview>

## Value [#value]

- A file adds its key once it uploads. Files that fail, are cancelled, or
  are still in flight add nothing.
- The value grows across calls, even though the hook replaces its queue.
- Files that uploaded before the field mounted are left out.
- `UploadFieldValue` lists the keys, each with a `Remove {name}` action and
  a thumbnail for images the field uploaded. Removing a key does not delete
  the object.
- Inside the field, `UploadStatus` and `UploadFileList` leave out files that
  uploaded: they show only files in flight or failed. The field sets
  `hideUploaded` on its `Upload` root for that.

Keep the value yourself with `value` and `onValueChange`, or start from
`defaultValue`. `onValueChange` runs after the change commits. Keys the field
did not upload show the last segment of the key as their name.

```tsx
const [keys, setKeys] = useState<string[]>(post.attachmentKeys);

<UploadField
  upload={upload}
  name="attachments"
  value={keys}
  onValueChange={setKeys}
>
  <UploadTrigger />
  <UploadFieldValue />
</UploadField>;
```

## Limit [#limit]

With `maxFiles`, each call takes only the files that still fit: `maxFiles`
minus the keys in the value and the files still uploading. The hook rejects
the rest with `TOO_MANY_FILES`.

Once the field is full, its `UploadTrigger` and `UploadDropzoneTrigger`
stay focusable but are `aria-disabled`, and the picker, drops, and pastes do
nothing. "Maximum files reached." leads the dropzone trigger's description. The
field passes `isFull` to its `Upload` root, so your own parts can read
`useUploadContext().isFull`:

```tsx
function FullHint() {
  const { isFull } = useUploadContext();
  return isFull ? <p>Remove a file to add another.</p> : null;
}
```

## Form reset [#form-reset]

Without `value`, the field restores `defaultValue` when its form resets,
including React 19's automatic reset after a `<form action>`, and reports
it through `onValueChange`. Call `upload.reset()` to clear the status list
too. A controlled field must reset `value` itself.

## Custom parts [#custom-parts]

`useUploadFieldContext()` returns `name`, `value`, `items` (the keys with
the file name, size, and type the field knows), `remaining`, and `remove`.
Pass a function to `UploadFieldValue` to render each item yourself:

```tsx
function AttachmentNames() {
  const { remove } = useUploadFieldContext();

  return (
    <UploadFieldValue>
      {(item) => (
        <span>
          {item.fileName}
          <button type="button" onClick={() => remove(item.key)}>
            Remove
          </button>
        </span>
      )}
    </UploadFieldValue>
  );
}
```

Render custom parts inside `UploadField`.

---

## Props [#props]

```ts
import type {
  UploadFieldProps,
  UploadFieldValueProps,
  UploadFieldContextValue,
} from "@dimah-s3/ui";
```

### UploadField [#uploadfield]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-field.tsx"
  name="UploadFieldProps"
/>

### UploadFieldValue [#uploadfieldvalue]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-field.tsx"
  name="UploadFieldValueProps"
/>

### useUploadFieldContext [#useuploadfieldcontext]

<AutoTypeTable
  path="packages/ui/src/components/dimah-s3/upload/upload-field.tsx"
  name="UploadFieldContextValue"
/>
