- name
- webiny-form-model
- description
- Building forms with the FormModel system — field types, renderers, layout, validation, conditional rules, computed fields, and dynamic zones. Use this skill when the developer needs to define form fields with the builder API, choose renderers, build layouts with tabs/rows/separators, add validation (Zod or imperative), use conditional visibility/disable rules, create computed fields, or work with object fields and templates (dynamic zones).
# Form Model
## TL;DR
The Form Model is Webiny's declarative form system. Define fields with a fluent builder API (`fields.text()`, `fields.datetime()`, etc.), arrange them with a layout builder (`layout.row()`, `layout.tabs()`, etc.), and validate with Zod schemas or imperative rules. Fields support conditional visibility, computed values, reactive context from other fields (`.context()`), and deeply nested object/list structures with templates (dynamic zones).
## Field Types
All fields are created via the `fields` registry callback. Each builder method returns a chainable builder.
### Text
```typescript
fields.text();
```
Default renderer: `textInput`. Value: `string | null`.
```typescript
fields.text().label("Title").placeholder("Enter title").required("Title is required")
fields.text().renderer("textarea", { rows: 4 })
fields.text().list().renderer("tags").defaultValue([])
fields.text().list().renderer("textInputs", { addItemLabel: "Add text" })
fields.text().list().renderer("textareas", { addItemLabel: "Add description" })
fields.text().renderer("codeEditor", { language: "html", height: 300 })
fields.text().options([
{ label: "Option A", value: "a" },
{ label: "Option B", value: "b" }
]) // auto-switches to "select" renderer
fields.text().options([...]).renderer("radioButtons")
fields.text().list().options([...]).renderer("checkboxes")
// Dynamic options — callback receives { field, form }, re-evaluated reactively
fields.text().options(({ form }) => {
const type = form.field("general.type").getValue();
return getOptionsForType(type);
})
```
### Number
```typescript
fields.number();
```
Default renderer: `numberInput`. Value: `number | null`. Auto-normalizes to number.
```typescript
fields.number().label("Count").placeholder("0").required();
fields.number().list().renderer("numberInputs", { addItemLabel: "Add number" });
fields.number().options([
{ label: "Tier 1", value: 100 },
{ label: "Tier 2", value: 200 }
]);
```
### Boolean
```typescript
fields.boolean();
```
Default renderer: `switch`. Value: `boolean | null`.
```typescript
fields.boolean().label("Featured").defaultValue(false);
```
### DateTime
```typescript
fields.datetime();
```
Default renderer: `dateTimeInput`. Pick a variant method to set the subtype:
| Variant | Value Format | Example |
| ----------------------------------------- | ------------------------------ | -------------------------------------------------------------- |
| `.dateOnly()` | `"2026-05-01"` | Holidays, due dates (birthdays: see the birthday picker below) |
| `.timeOnly()` | `"14:30:00"` | Opening hours |
| `.withTimezone()` | `"2026-05-01T14:30:00+02:00"` | Events tied to a locale |
| `.withoutTimezone()` | `"2026-05-01T14:30:00.000Z"` | Timestamps, logs |
| `.monthOnly()` | `"2026-05"` | Billing cycles |
| `.weekOnly({ startsOn: 1 })` | `"2026-W18"` | Sprint planning |
| `.yearOnly({ range: [2020, 2035] })` | `2026` (number) | Fiscal years |
| `.dateRange()` | `{ from: "...", to: "..." }` | Vacation requests |
| `.multipleDates()` | `["2026-05-01", "2026-05-03"]` | Blackout dates |
| `.multipleMonths()` | `["2026-01", "2026-03"]` | Seasonal availability |
| `.multipleYears({ range: [2020, 2035] })` | `[2024, 2025, 2026]` | Multi-year budgets |
Additional chainable methods:
```typescript
.presets([
{ label: "Today", value: () => new Date() },
{ label: "In a week", value: () => addDays(new Date(), 7) }
])
.displayFormat("dd/MM/yyyy") // date-fns format tokens
.list() // switches renderer to "dateTimeInputs"
.dateGte("2026-01-01", "message?") // value must be on or after
.dateLte({ expression: "today-18y" }, "message?") // value must be on or before
```
`.dateGte()` and `.dateLte()` take a fixed value, or, on a date-only field, `{ expression }`
worked out from the editor's local date every time the field is checked. An expression is
`today`, or `today` plus or minus a number of days (`d`), months (`m`) or years (`y`):
`"today-18y"`, `"today+1d"`. The limits survive a later `.renderer()` or `.schema()` call, and
the check runs after the field's own schema. In the message, `{date}` is the limit in the
admin's date format; without a message, the field says "Must be on or after {date}." or "Must be
on or before {date}.".
For dates far in the past, like a date of birth, a date-only field can use the `birthdayInput`
renderer, a step-by-step picker (day, then month, then year). It stores the same `"YYYY-MM-DD"`
value as the calendar, and has no settings: it offers only the dates the field's `dateGte`/
`dateLte` limits allow (without limits, this year back 120 years). Chain `.renderer()` after the
subtype method, since subtype methods reset the renderer:
```typescript
fields.datetime().dateOnly().renderer("birthdayInput");
fields
.datetime()
.dateOnly()
.dateGte({ expression: "today-100y" }) // not older than 100
.dateLte({ expression: "today-18y" }, "You must be 18 or older.")
.renderer("birthdayInput");
```
### File
```typescript
fields.file();
```
Default renderer: `filePicker`. Value: `FileValue | null` (object with `id`, `name`, `size`, `mimeType`, `src`, `width`, `height`).
```typescript
fields.file().label("Image");
```
### File URL
```typescript
fields.fileUrl();
```
Default renderer: `fileUrlPicker`. Value: `string | null` (URL only).
```typescript
fields.fileUrl().label("Image URL");
```
### Lexical
```typescript
fields.lexical();
```
Default renderer: `lexical`. Value: `RichTextValueWithHtml | null` (`{ state: string; html: string }`).
```typescript
fields.lexical().label("Content").required("Content is required");
```
### Password
```typescript
fields.password();
```
Default renderer: `passwordInput`. Value: `string | null`.
```typescript
fields.password().label("Password").required("Password is required");
```
### Permissions
```typescript
fields.permissions();
```
Default renderer: `permissions`. Value: `Record<string, unknown>[]`. Has a built-in Zod schema requiring at least one permission entry.
```typescript
fields.permissions().label("Permissions");
```
### Roles Multi-Select
```typescript
fields.rolesMultiSelect();
```
Default renderer: `rolesMultiSelect`. Value: `unknown[]`.
```typescript
fields.rolesMultiSelect().label("Roles");
```
### Object
```typescript
fields.object();
```
Default renderer: `objectAccordionSingle`. For nested structures, lists, and dynamic zones.
```typescript
// Simple nested object
fields.object().label("Address").fields(f => ({
street: f.text().label("Street"),
city: f.text().label("City"),
zip: f.text().label("ZIP")
}))
// List of objects
fields.object().list().label("Authors").fields(f => ({
name: f.text().label("Name").required(),
email: f.text().label("Email")
}))
// Dynamic zone (single template selection)
fields.object().label("Content Block")
.template("hero", t => {
t.label("Hero Banner")
.icon({ type: "icon", name: "fas/image" })
.fields(f => ({
heading: f.text().label("Heading").required(),
image: f.file().label("Image")
}));
})
.template("text", t => {
t.label("Rich Text").fields(f => ({
body: f.text().label("Body").renderer("textarea")
}));
})
// Dynamic zone list (multiple items, each picks a template)
fields.object().list().label("Page Sections")
.renderer("dynamicZone", { container: false })
.template("hero", t => { ... })
.template("cta", t => { ... })
// Key-value list
fields.object().list().label("Meta Tags")
.renderer("keyValueTags", { addItemLabel: "Add tag" })
.fields(f => ({
name: f.text().placeholder("Name"),
content: f.text().placeholder("Content")
}))
```
Template visibility can be conditional:
```typescript
.template("premium", t => {
t.label("Premium Widget")
.visible(form => form.field("plan").getValue() === "enterprise")
.fields(f => ({ ... }));
})
```
## Common Builder Methods
These are available on **all** field types:
| Method | Description |
عرض على GitHub