Files
summercms/docs/backend/forms.md
Jakub Zych 67d4c7ff13 feat(12.2-02): add the datepicker field with server-side bounds and date list columns
- type: datepicker compiles the D-20 keys; format maps to displayFormat with WinterCMS's momentFormat table
- boot fails when the mode does not match the column's Go type (time.Time, lagoon.Date, lagoon.TimeOfDay)
- datepicker is a writable scalar field; minDate and maxDate are rechecked on save
- columns.yaml accepts type: date and type: time; Scanner/Valuer structs are columns, not relations
- conformance fixture carries date and datetime fields; README, forms and lists docs
2026-10-02 18:19:04 +02:00

11 KiB

title, description, section, order
title description section order
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. backend 20

Forms

The Form behaviour's config_form.yaml and the model's fields.yaml keep their WinterCMS shape. cabana 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:

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

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:

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 An embedded list of related records; see Relation manager.
widget A plugin custom element with a server action; see Partials and widgets.
partial A server-rendered template; see Partials and widgets.
fileupload Uploads for an attachOne or attachMany relation; see File uploads.
datepicker A date, date and time, or time of day; see Date pickers.

The WinterCMS widgets that are not in this list (the rich editor, the markdown editor, the code editor, the color picker, the media finder, the repeater, the tag list 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.

Date pickers

A type: datepicker field edits one model column. Its mode decides the column's Go type, and the start-up stops when they do not match:

mode Go type of the column JSON value
datetime (the default) time.Time or *time.Time (a timestamptz column) "2026-10-02T10:30:00Z"
date lagoon.Date or *lagoon.Date (a DATE column) "2026-10-02"
time lagoon.TimeOfDay or *lagoon.TimeOfDay (a TIME column) "14:30:00"

A plugin does not define its own date types; see Casts and validation for the two framework types. Use the pointer types for an optional value: an empty picker stores NULL, and required: true refuses it.

fields:
    published_on:
        label: acme.blog::lang.posts.published_on
        type: datepicker
        mode: date
        minDate: 2000-01-01
    starts_at:
        label: acme.blog::lang.posts.starts_at
        type: datepicker
        format: d.m.Y H:i
        firstDay: 1

The field takes the generic keys plus these WinterCMS keys:

Key Meaning
mode date, datetime or time.
format The display format as a PHP date() format. It is turned into the SPA's format at boot (the schema serves it as displayFormat); a letter with no equivalent, such as T, U or c, stops the start-up. It changes only how the value is shown, not the order of the editable segments, which follows the admin's locale.
minDate, maxDate The earliest and latest calendar date (YYYY-MM-DD). Not valid on mode: time.
yearRange Years either side of today (10) or a [from, to] list for the year selector.
firstDay The first day of the week, 0 (Sunday) to 6.
twelveHour Shows a 12-hour clock.
ignoreTimezone mode: datetime only: the value is shown and saved as entered, with no time zone conversion.

options, emptyOption, nameFrom and every other key stop the start-up.

minDate and maxDate are checked again on save: a value whose calendar date (the UTC date of a datetime, or its wall-clock date with ignoreTimezone) lies outside them is a 422 on the field. A datetime value is stored in UTC and the admin SPA shows and edits it in the browser's time zone; date and time values are never converted.

File uploads

A type: fileupload field edits one attachment relation of the form's model. The model declares its relations with an AttachRelations method (attach.HasRelations) and implements attach.Owner; see Attachments. The field name must be one of the declared relation names, otherwise the start-up stops.

fields:
    photos:
        label: acme.blog::lang.posts.photos
        type: fileupload
        mode: image
        fileTypes: jpg,png,webp
        maxFilesize: 5
        maxFiles: 10
        thumbOptions:
            mode: crop

The field takes the generic keys plus these WinterCMS keys:

Key Meaning
mode image or file (the default). Image mode accepts only jpg, jpeg, png, gif and webp and checks that the bytes decode as such an image.
fileTypes Allowed extensions, as a comma- or pipe-separated string or a list.
mimeTypes Allowed MIME types (image/png, image/*) or extensions.
maxFilesize The largest file in megabytes. It may not exceed http.body_limits.upload_bytes.
maxFiles The most files an attachMany relation may hold. Refused on attachOne.
imageWidth, imageHeight Preview size, 1 to 4096 pixels (240 by 240 when not set).
thumbOptions A mapping with mode: auto, exact, crop (the default) or fit.
useCaption Lets the administrator edit each file's title and description.
prompt The upload button text, a translation key.

options, emptyOption, nameFrom and every other key stop the start-up.

Uploads are deferred until the form is saved, on the create form and the update form alike, as WinterCMS's file upload widget does. The admin SPA makes a random session key when it opens a form and sends it in the X-Session-Key header with every upload and with the save. The upload stores the file unattached and records a pending binding for that key and the signed-in administrator; the record's create or update save attaches every pending file of the form's fileupload fields inside its own transaction. If the save fails with a 422, the uploads stay pending for the next attempt; if the form is left without saving, the daily deferred:purge removes them. A session key is only ever seen by the administrator who used it.

Removing a file is deferred the same way: the file disappears from the form at once and is deleted by the save (a pending upload that is removed is deleted at once). On an attachOne relation, saving a new upload deletes the file it replaces. Captions (with useCaption) and the order of an attachMany field are saved at once, as in WinterCMS. After applying the session's work, the save checks maxFiles and, for a required: true fileupload field, that at least one file is attached; a failure is a 422 on the field and keeps the pending work. Files of a protected relation are shown through authenticated admin routes only; see Protected files in the admin.

The limits are enforced on the server: the upload route caps the request body at the smaller of http.body_limits.upload_bytes and maxFilesize plus 64 KiB (413 payload_too_large past it), and a file that is too large, of a type the field does not allow, or not a valid image in image mode is a 422 on the field.

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). 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.