# Forms

Define forms, validation and submission handlers.

## Forms

The forms module handles front-end form submissions with CSRF protection, honeypot spam filtering, validation, and Alpine.js-powered submission via the REST API.

### Rendering a form: `[#build-form name#]`

`[#build-form contact#]` reads `contact.json` from the same directory as the current route and renders a complete form. The form submits via Alpine.js to the API - no page reload.

Form definition file (`ext/uri/contact/contact.json`):

```json
{
  "name": "contact",
  "resource": "leads",
  "success_message": "Thank you, we will be in touch.",
  "fields": {
    "name": {
      "type": "text",
      "name": "Your name",
      "required": true
    },
    "email": {
      "type": "email",
      "name": "Email address",
      "required": true
    },
    "message": {
      "type": "textarea",
      "name": "Message"
    }
  },
  "buttons": [
    { "type": "submit", "title": "Send" }
  ]
}
```

The `resource` field is the target resource that will receive the new record via the API. Make sure that resource exists in `ext/data/` with a matching `.meta`.

### Form styling hooks

`[#build-form#]` adds stable classes to the rendered form and field wrappers.
By default the class suffix is the form definition's `name`:

```html
<form class="nb-form nb-form-contact ...">
```

Each rendered field wrapper receives:

```html
<div class="nb-field nb-form-field-contact relative my-10">
```

Set `class_name` in the form JSON to share one styling hook across multiple
forms. The value is treated as a class suffix, not a full class list:

```json
{
  "name": "contact",
  "class_name": "truesim",
  "resource": "leads",
  "fields": {}
}
```

This renders `nb-form-truesim` on the form and `nb-form-field-truesim` on each
field wrapper. The default field wrapper style remains `relative my-10`.

To add classes for a specific render, set `form-field-wrapper-class` before
calling `[#build-form#]`:

```html
[#set form-field-wrapper-class="my-2"#]
[#build-form contact#]
```

Or pass the class directly to the shortcode:

```html
[#build-form contact field-wrapper-class="my-2"#]
```

These classes replace the default wrapper style slot (`relative my-10`). The
generated hook classes `nb-field` and `nb-form-field-{suffix}` are always kept.
With `field-wrapper-class="my-2"`, the field wrapper becomes:

```html
<div class="nb-field nb-form-field-contact my-2">
```

For a public form, explicitly allow the API POST with a route such as:

```
ext/uri/api/v1/leads/index.tpl
```

```html
[#api-allow post leads#]
```

This allows anonymous submissions for that one method/resource pair while keeping the rest of the API protected.

### Public submission form checklist

Contact forms, questionnaires, signup forms, and lead forms all use the same resource-write pattern:

1. Create a target resource for submitted records, e.g. `ext/data/questionnaire_entries/.meta`.
2. Create the form definition JSON next to the route, e.g. `questionnaire.json`.
3. Render it with `[#module forms#]` and `[#build-form questionnaire#]`.
4. Open only anonymous `POST` for the target resource:

```html
[#module api#]
[#api-allow post questionnaire_entries#]
```

5. If the submission should send an email or trigger other side effects, attach an explicit create event to the target resource `.meta`:

```json
"events": {
  "create": ["questionnaire-entry-created"]
}
```

Each submission is stored as a normal record in the target resource and appears in the admin like any other resource record.

### Field types in forms

Same type names as resource fields: `text`, `textarea`, `email`, `url`, `select`, `upload`. Add `"help": "Hint text"` to any field for a help label below the input.

**Select field:**
```json
"status": {
  "type": "select",
  "name": "Type",
  "options": {
    "general": "General question",
    "support": "Support request"
  }
}
```

**Upload field:**
```json
"attachment": {
  "type": "upload",
  "name": "Attachment",
  "accept": ".pdf,.doc"
}
```

For forms with file upload, also set `"upload_field": "attachment"` at the root of the form definition.

### Grouping fields

Use `group_start` / `group_end` to visually group fields side by side:

```json
"fields": {
  "_g1": { "type": "group_start", "name": "Name" },
  "first_name": { "type": "text", "name": "First name" },
  "last_name":  { "type": "text", "name": "Last name" },
  "_g1e": { "type": "group_end" }
}
```

### CSRF and honeypot

Both are handled automatically by `[#build-form#]`. The form includes a hidden `form_key` (session-matched token) and a honeypot field. No extra configuration needed.

### Custom form handlers

For forms that need server-side processing beyond writing to a resource (e.g. file imports or enqueueing a background job), add handler files alongside the route:

```
ext/uri/contact/contact.json       ← form definition
ext/uri/contact/post_contact.inc   ← runs on submission
ext/uri/contact/validate_contact.inc  ← runs before post, return false to abort
```

The files are named `post_{name}.inc` and `validate_{name}.inc`, where `{name}` matches the form's `name` field. Both are optional - use one or both as needed.

---

## Frontend forms

For simple public forms, prefer Alpine.js with `nb.api.post()` or `build-form` over route `post_*.inc`.

- Normalize values in the frontend when the stored value should be canonical, for example `trim().toLowerCase()` for email.
- If duplicate submissions should be treated as success in the UX, map `RESOURCE_EXISTS` to the same success state as `RESOURCE_CREATED`.

---
