# Form

> A form that wires errors to Nasaq fields by name, with useForm for values, validation, a pending submit and server errors mapped back onto fields, and FormField for label, control, description and error in one line.

Source: https://docs.nasaqui.com/components/form

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/form.json
```

`Form` is Base UI's form: an error keyed by a field's `name` marks that field invalid and shows the message,
and editing the field clears it. `useForm` is a small state hook to go with it; you can also bring your own
form library and pass `errors`.

## When to use

- Any form that submits to a server and can get field errors back (taken email, bad slug).
- Simple forms where a form library would be too much.

## When not to use

- Large, dynamic or nested forms: use a form library and pass its errors to `Form`'s `errors`.
- One inline field that saves on blur: a `Field` alone is enough.

## Import

```tsx
import { Form, FormField, useForm } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, Form, FormField, Input, useForm } from "@fadymondy/nasaq/web";

declare function signUp(v: { email: string }): Promise<{ ok: boolean; taken?: boolean }>;

export function SignUp() {
  const form = useForm({
    defaultValues: { email: "" },
    validate: (v) => (v.email.includes("@") ? {} : { email: "Enter a valid email." }),
    onSubmit: async (v) => {
      const res = await signUp(v);
      if (res.taken) return { fieldErrors: { email: "This email already has an account." } };
      if (!res.ok) return { error: "Sign-up failed. Try again." };
    },
  });
  return (
    <Form {...form.formProps}>
      <FormField name="email" label="Email">
        <Input {...form.register("email")} type="email" />
      </FormField>
      <Button type="submit" variant="primary" loading={form.submitting}>Create account</Button>
    </Form>
  );
}
```

## Anatomy

```
Form                            data-slot="form", noValidate
├─ Alert (danger)               formError, above the fields
└─ FormField                    Field name=…
   ├─ FieldLabel
   ├─ control                   Input, Textarea, NativeSelect…
   ├─ FieldDescription
   └─ FieldError                the error for this name
```

## API

**Form**: every Base UI `Form` prop, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `errors` | `Record<string, string \| undefined>` | | Errors by field name. Empty values are ignored. |
| `formError` | `string \| null` | | One error for the whole form. |

**FormField**: every `Field` prop, plus `name` (required), `label`, `description` and the control as `children`.

**useForm({ defaultValues, validate?, onSubmit })**

- `validate(values)` returns an error per invalid field; an empty object submits.
- `onSubmit(values)` may return `{ fieldErrors?, error? }`. A thrown error becomes `formError`.

Returns `values`, `setValue(name, value)`, `errors`, `setErrors`, `formError`, `submitting`, `dirty`,
`register(name)` (`{ name, value, onChange }` for text controls), `formProps` (spread on `Form`) and
`reset(values?)`.

## Accessibility

- Invalid fields get `aria-invalid` and their error is linked with `aria-describedby` by `Field`.
- `formError` is an `Alert`, announced when it appears.
- The form is `noValidate`, so messages come from you, localised, instead of the browser's bubbles.

## RTL & i18n

- Layout follows the page direction. Write messages in the user's language; nothing is hard-coded.

## Styling & tokens

- A vertical stack with `gap-4`. Target `[data-slot="form"]`.

## Do / Don't

- Do return server field errors with the same keys as the field names.
- Do say how to fix it: "Use 8 characters or more", not "Invalid".
- Don't disable the submit button until the form is valid; let people submit and see what is missing.

## Related

- [`Field`](https://docs.nasaqui.com/components/field)
- [`NativeSelect`](https://docs.nasaqui.com/components/native-select)
- [`Alert`](https://docs.nasaqui.com/components/alert)

## Lab

https://docs.nasaqui.com/?path=/docs/components-forms-form--docs

## Code

### React

```tsx
import { Button, Form, FormField, Input, useForm } from "@fadymondy/nasaq/web";

declare function signUp(v: { email: string }): Promise<{ ok: boolean; taken?: boolean }>;

export function SignUp() {
  const form = useForm({
    defaultValues: { email: "" },
    validate: (v) => (v.email.includes("@") ? {} : { email: "Enter a valid email." }),
    onSubmit: async (v) => {
      const res = await signUp(v);
      if (res.taken) return { fieldErrors: { email: "This email already has an account." } };
      if (!res.ok) return { error: "Sign-up failed. Try again." };
    },
  });
  return (
    <Form {...form.formProps}>
      <FormField name="email" label="Email">
        <Input {...form.register("email")} type="email" />
      </FormField>
      <Button type="submit" variant="primary" loading={form.submitting}>Create account</Button>
    </Form>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { Form, FormField, useForm } from "@/components/ui/form";
import { Input } from "@/components/ui/field";

declare function signUp(v: { email: string }): Promise<{ ok: boolean; taken?: boolean }>;

export function SignUp() {
  const form = useForm({
    defaultValues: { email: "" },
    validate: (v) => (v.email.includes("@") ? {} : { email: "Enter a valid email." }),
    onSubmit: async (v) => {
      const res = await signUp(v);
      if (res.taken) return { fieldErrors: { email: "This email already has an account." } };
      if (!res.ok) return { error: "Sign-up failed. Try again." };
    },
  });
  return (
    <Form {...form.formProps}>
      <FormField name="email" label="Email">
        <Input {...form.register("email")} type="email" />
      </FormField>
      <Button type="submit" variant="primary" loading={form.submitting}>Create account</Button>
    </Form>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqForm, NqFormField, NqInput, useForm } from "@fadymondy/nasaq/vue";

// Pretend server call: the address "taken@example.com" already has an account.
async function signUp(v: { email: string }): Promise<{ ok: boolean; taken?: boolean }> {
  await new Promise((resolve) => setTimeout(resolve, 400));
  return { ok: v.email !== "fail@example.com", taken: v.email === "taken@example.com" };
}

const form = useForm({
  defaultValues: { email: "" },
  validate: (v) => (v.email.includes("@") ? {} : { email: "Enter a valid email." }),
  onSubmit: async (v) => {
    const res = await signUp(v);
    if (res.taken) return { fieldErrors: { email: "This email already has an account." } };
    if (!res.ok) return { error: "Sign-up failed. Try again." };
  },
});
</script>

<template>
  <NqForm v-bind="form.formProps" class="max-w-sm">
    <NqFormField name="email" label="Email">
      <NqInput v-bind="form.register('email')" type="email" />
    </NqFormField>
    <NqButton type="submit" variant="primary" :loading="form.submitting">Create account</NqButton>
  </NqForm>
</template>
```

### Blade

```blade
<x-nq::form class="max-w-sm" action="/sign-up" method="post">
    <x-nq::form.field name="email" label="Email">
        <x-nq::field.input type="email" />
    </x-nq::form.field>
    <x-nq::button type="submit" variant="primary" x-bind:disabled="submitting" x-bind:aria-busy="submitting ? 'true' : null">Create account</x-nq::button>
</x-nq::form>
```

### HTML + Alpine

```html
<form data-slot="form" novalidate x-data="nqForm(JSON.parse('{\u0022errors\u0022:{},\u0022formError\u0022:null}'))" x-on:submit="submit($event)" action="/sign-up" method="post" class="flex flex-col gap-4 max-w-sm">
    <div data-slot="alert" data-tone="danger" role="alert"
        x-show="formError" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="formError"></span></div>
            </div>
    </div>
    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     data-name="email" x-effect="invalid = Boolean(errors[$root.dataset.name])" x-on:input="clear($root.dataset.name)" x-on:change="clear($root.dataset.name)" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Email</label>
        <input data-slot="input" type="email"
         name="email"             class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
        <div data-slot="field-error" role="alert" x-show="invalid"  style="display: none"  class="text-caption text-nq-danger-text"><span x-text="errors[$root.dataset.name] ?? ''"></span></div>
</div>
    <button data-slot="button"
     type="submit"                         x-bind:disabled="submitting" x-bind:aria-busy="submitting ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Create account</button>
</form>
```
