Skip to content

Forms

The form of a resource is used for creating, editing and – read-only – for the detail page. You describe it once in form(); Anvil derives the inputs, the validation rules and the data that is saved from it. Dialogs of actions use the same fields.

use Anvil\Schema\Fields\BelongsTo;
use Anvil\Schema\Fields\DateInput;
use Anvil\Schema\Fields\NumberInput;
use Anvil\Schema\Fields\Select;
use Anvil\Schema\Fields\Textarea;
use Anvil\Schema\Fields\TextInput;
use Anvil\Schema\Fields\Toggle;
use Anvil\Schema\Form;
use Anvil\Schema\Layout\Section;
public function form(Form $form): Form
{
return $form->schema([
Section::make('Invoice')->description('Basic data')->columns(2)->schema([
TextInput::make('number')->required()->maxLength(30),
BelongsTo::make('customer_id')->label('Customer')->relationship(CustomerModel::class, 'name')->required(),
Select::make('status')->options(['draft' => 'Draft', 'sent' => 'Sent', 'paid' => 'Paid'])->required()->default('draft'),
NumberInput::make('amount')->currency('EUR')->min(0),
DateInput::make('due_on'),
Toggle::make('reminders')->label('Send reminders'),
Textarea::make('notes')->columnSpanFull(),
]),
]);
}

Every field has a page of its own with all options, the saved value, the validation, the read-only display and the API representation.

Field Use it for Saved value
TextInput single line text, e-mail, URL, phone, plain numbers string
Textarea longer plain text string
MarkdownEditor formatted text with toolbar and preview Markdown string
NumberInput locale aware numbers and money decimal string or integer
Select one value of a fixed list the value
Radio one value of a few options, all visible the value
CheckboxList several values of a fixed list, optionally grouped list of values
Toggle yes / no 0 / 1
DateInput date, or date and time Y-m-d / Y-m-d H:i:s
BelongsTo a record of another model (foreign key) internal id
UserSelect a user of the current tenant internal user id
TagsInput free text tags JSON list
Repeater a short list of small records, or records chosen by search JSON list of objects
FileUpload one file per record and field – (a file of the record)
PasswordInput a password that is never rendered back the password as entered

All fields are in Anvil\Schema\Fields and are created with ::make('column'). The name must be a plain column name ([a-z_][a-z0-9_]*, at most 64 characters, otherwise an InvalidArgumentException); the label defaults to the humanized name (due_on → “Due on”). Own fields: Extending Anvil.

Option Effect
label('…') Label and name in validation messages
required() Adds required, otherwise permit_empty; shows a red asterisk
rules('alpha_dash', 'is_unique[t.c]') Additional CodeIgniter rules; | separated strings work too
default($value) Value of the field when creating (not when editing)
helperText('…') Help text below the input (not on the detail page)
placeholder('…') Placeholder inside the input; for selects the label of the empty option
disabled() Shown, never written
disabledOn('edit') Disabled in the given operations (create, edit)
hiddenOn('create') Not shown, not validated and not written in the given operations (create, edit, view)
virtual() Not a column: validated and passed to Resource::afterSave(), see Virtual fields
columnSpan(2), columnSpanFull() Width in multi column layouts, see Layout

Values are always escaped when they are rendered. Empty values are shown as “—” on the detail page.

A form is rendered for create, edit or view (the detail page). Use the constants of Anvil\Schema\FormContext:

use Anvil\Schema\FormContext;
TextInput::make('cost_center')->required()->disabledOn(FormContext::EDIT), // fixed after creation
Select::make('status')->options($statuses)->disabled()->hiddenOn(FormContext::CREATE), // set by the system
PasswordInput::make('password')->required()->hiddenOn(FormContext::EDIT, FormContext::VIEW),

In view every field shows its value read-only; layouts work the same in all operations. More in Visibility per operation.

The saved data is built from the fields that are writable in the current operation, not from the request: each field converts its validated input (dehydrate), e.g. a NumberInput turns 1.234,50 into "1234.50", a BelongsTo turns a UUID into the internal id. A client cannot write tenant_id, uuid or any other column that is not part of the form, even if it is in $allowedFields of the model. Disabled and hidden fields are left out the same way. Details: Validation.

A field marked virtual() is validated and converted like the others but not written to the model, e.g. the permissions of a role kept in another table. The resource receives its value in afterSave() and fills it when editing with formValues():

CheckboxList::make('items')->groupedOptions($this->itemOptions())->columns(3)->virtual(),

See Extension points. FileUpload is always virtual.

Actions, bulk actions and relation header actions open a dialog when they have a form. It is built with the same fields and rules:

Action::make('reassign')
->form(fn (Form $form, $order) => $form->schema([
BelongsTo::make('user_id')->label('New owner')->relationship(UserAccountModel::class, 'username'),
Textarea::make('reason')->required(),
]))
->handle(function ($order, array $data): string {
service('orders')->reassign($order, $data['user_id'], $data['reason']);
return 'The order has been reassigned.';
}),

The handler receives the validated, converted data, e.g. the internal id for a BelongsTo, and returns a success message or null. See Actions.

The change history of the detail page formats values with the form fields: titles for BelongsTo, labels for options, locale formatted numbers. Columns that are not in the form and look like references (*_id) are listed as “changed” without their value, so internal ids never reach the browser.

With the API enabled for a resource, its form also describes the JSON representation and the accepted input: the same fields, rules and conversions apply. Each field page has a section on its API value. PasswordInput is write-only, FileUpload is not part of the API.