# Forms

> Describe admin forms in config_form.yaml and fields.yaml, with the supported field types, spans, tabs, dropdown options and create or update contexts.

The Form behaviour's `config_form.yaml` and the model's `fields.yaml` keep their WinterCMS shape. [cabana](/docs/api/cabana.md) compiles them strictly at boot: an unknown key, field type, span or size stops the start-up with an error naming the file and the field, so a WinterCMS option that SummerCMS does not implement is never ignored silently.

## config_form.yaml

The controller's form configuration names the fields file with a WinterCMS path, the model class, and where the SPA goes after a save:

```yaml
name: acme.blog::lang.posts.form
form: ~/plugins/acme/blog/models/post/fields.yaml
modelClass: Post
defaultRedirect: acme/blog/posts
create:
    redirect: acme/blog/posts/update/:id
    redirectClose: acme/blog/posts
update:
    redirect: acme/blog/posts
    redirectClose: acme/blog/posts
```

`modelClass` must equal the controller's `pact.AdminController.ModelName`. The `~/plugins/<vendor>/<plugin>/` prefix points into the plugin's own embedded tree.

## fields.yaml

```yaml
fields:
    title:
        label: acme.blog::lang.posts.title_column
        type: text
        span: left
        required: true
    slug:
        label: acme.blog::lang.posts.slug
        type: text
        span: right
        context: update
        comment: acme.blog::lang.posts.slug_comment
    status:
        label: acme.blog::lang.posts.status
        type: dropdown
        span: left
        options:
            draft: acme.blog::lang.posts.draft
            published: acme.blog::lang.posts.published
    published:
        label: acme.blog::lang.posts.published
        type: switch
        span: right
    content:
        label: acme.blog::lang.posts.content
        type: textarea
        size: large
        tab: acme.blog::lang.posts.tab_content
```

The compiled schema keeps the fields in file order, with their labels as translation keys until a request asks for them in its locale:

```go
fsys := os.DirFS("testdata/docs")
form, err := cabana.CompileForm("acme.blog", PostsController{}, fsys)
if err != nil {
	fmt.Println(err)
	return
}
for _, f := range form.Fields {
	fmt.Printf("%s %s span=%q tab=%q required=%v options=%d\n", f.Name, f.Type, f.Span, f.Tab, f.Required, len(f.Options))
}
// Output:
// title text span="left" tab="" required=true options=0
// slug text span="right" tab="" required=false options=0
// status dropdown span="left" tab="" required=false options=2
// published switch span="right" tab="" required=false options=0
// content textarea span="" tab="acme.blog::lang.posts.tab_content" required=false options=0
```

### Field types

| Type | Renders |
|------|---------|
| `text`, `textarea`, `number` | Text inputs. |
| `checkbox`, `switch` | Booleans. |
| `dropdown` | A select. Options are a map in the YAML, or the name of a method the controller answers through `pact.DropdownOptionsProvider`. |
| `relation` | A belongsTo or belongsToMany picker; see [Relation manager](/docs/backend/relation-manager.md). |
| `relation-manager` | An embedded list of related records; see [Relation manager](/docs/backend/relation-manager.md). |
| `widget` | A plugin custom element with a server action; see [Partials and widgets](/docs/backend/partials-and-widgets.md). |
| `partial` | A server-rendered template; see [Partials and widgets](/docs/backend/partials-and-widgets.md). |

The WinterCMS widgets that are not in this list (the rich editor, the media finder, the repeater, the file upload and the others) are not provided. A field with one of those types stops the start-up.

### Field options

A field takes `label`, `comment`, `type`, `required`, `default`, `tab`, `span` (`left`, `right`, `full`, `auto`, `row`), `size` (`tiny`, `small`, `large`, `huge`, `giant`), `context`, `attributes` (scalar HTML attributes for the input), `options` and `emptyOption`, plus `nameFrom` and `relation` on relation fields. WinterCMS keys outside this set, such as `readOnly`, `disabled`, `trigger` or `dependsOn`, are refused.

`context: update` shows a field only on the update form, and `context: create` only on the create form; a list of contexts is also accepted. The context is enforced on the server too: a field that is hidden on a form is never written by that form's save, whatever the request body holds.

## What a save may write

The form's writable fields are bound to model columns at boot. A save passes only those fields that are also in the model's `Fillable` list, drops unknown keys, case variants and nested objects, and fills the model with `lagoon.Fill` (see [Models](/docs/database/models.md)). The model's validation rules (a `Rules` method returning `lagoon.Validate` rule strings) and the form's `required` flags are checked in the save's transaction, and a failure is a 422 with messages per field. A value that does not fit its column is also a 422 on that field.
