- docs/backend: admin controllers, forms, lists and filters, relation manager, users and permissions, settings, partials and widgets, admin SPA - docs/services: storage, outbound HTTP, realtime, Web Push, search, parity testing and the Frontend and AJAX (not provided) page - Examples for cabana (with testdata/docs YAML), fetchguard, lighthouse and its centrifugo driver, flare, beachcomber and typesense, tide; lighthouse and beachcomber TestDocs* regions run on their Postgres harnesses - concept map rows link their guide pages and the not-provided rows the Frontend and AJAX page; index lists Backend, Database and Services - TestDocsRequiredPages asserts the D-08 section order
106 lines
5.1 KiB
Markdown
106 lines
5.1 KiB
Markdown
---
|
|
title: Forms
|
|
description: Describe admin forms in config_form.yaml and fields.yaml, with the supported field types, spans, tabs, dropdown options and create or update contexts.
|
|
section: backend
|
|
order: 20
|
|
---
|
|
# Forms
|
|
|
|
The Form behaviour's `config_form.yaml` and the model's `fields.yaml` keep their WinterCMS shape. [cabana](../../modules/cabana/README.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 src=modules/cabana/testdata/docs/controllers/posts/config_form.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 src=modules/cabana/testdata/docs/models/post/fields.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 src=modules/cabana/example_test.go#ExampleCompileForm
|
|
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](relation-manager.md). |
|
|
| `relation-manager` | An embedded list of related records; see [Relation manager](relation-manager.md). |
|
|
| `widget` | A plugin custom element with a server action; see [Partials and widgets](partials-and-widgets.md). |
|
|
| `partial` | A server-rendered template; see [Partials and widgets](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](../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.
|