Forms
Flashboard form payloads are generated from form() definitions on a resource. The preferred authoring path is schema-first: start with $form->schema([...]) for simple create and edit screens, then introduce grouped layout only when operators genuinely need it.
Simple forms first
<?php
use Pepperfm\Flashboard\Contracts\Forms\FieldRenderer;
use Pepperfm\Flashboard\Contracts\Forms\FormContract;
use Pepperfm\Flashboard\Core\Forms\Fields\Select;
use Pepperfm\Flashboard\Core\Forms\Fields\TextInput;
public static function form(FormContract $form): FormContract
{
return $form
->schema([
Select::make('status')
->label('Status')
->required(),
TextInput::make('notes')
->label('Notes')
->renderer(FieldRenderer::Textarea),
])
->rules([
'status' => ['required', 'string'],
]);
}
This path renders as a single centered UPageCard shell in the package UI without an artificial wrapper section.
Field layout
Schema-first forms can place multiple fields on one row without introducing nested layout nodes.
<?php
use Pepperfm\Flashboard\Core\Forms\Fields\TextInput;
use Pepperfm\Flashboard\Contracts\Forms\FormContract;
public static function form(FormContract $form): FormContract
{
return $form
->columns(2)
->gap(4)
->schema([
TextInput::make('first_name')
->label('First name'),
TextInput::make('last_name')
->label('Last name'),
TextInput::make('email')
->label('Email')
->email()
->columnSpan(2),
]);
}
Available layout helpers:
columns(int|array)for grid containersgap(int|array)for grid or flex spacingcolumnSpan(int|array)for grid itemsfullWidth()as a shorthand for spanning the full grid width
Scalar columns(2) and columns(3) remain mobile-safe by default: Flashboard normalizes them to one column on small screens and activates multiple columns from md upward.
Grouped layouts
Use Section, Tabs, and Tab nodes inside schema() when the form has meaningful visual grouping.
<?php
use Pepperfm\Flashboard\Core\Forms\Fields\Select;
use Pepperfm\Flashboard\Core\Forms\Fields\TextInput;
use Pepperfm\Flashboard\Core\Forms\Fields\Toggle;
use Pepperfm\Flashboard\Contracts\Forms\FormContract;
use Pepperfm\Flashboard\Core\Forms\Layout\Section;
use Pepperfm\Flashboard\Core\Forms\Layout\Tab;
use Pepperfm\Flashboard\Core\Forms\Layout\Tabs;
public static function form(FormContract $form): FormContract
{
return $form
->schema([
Section::make('content')
->label('Content')
->schema([
TextInput::make('name')
->label('Name')
->required(),
TextInput::make('slug')
->label('Slug'),
]),
Tabs::make('settings')
->tabs([
Tab::make('general')
->label('General')
->schema([
Select::make('status')
->label('Status'),
Toggle::make('is_active')
->label('Is active'),
]),
]),
]);
}
Sections and tabs support the same layout API as the root form schema.
Renderer contract
Normalized form payloads expose an explicit renderer hint for every field.
- typed fields set a stable renderer automatically, such as
TextInput->input - override renderer intent explicitly when the visual control differs from the base field type
- legacy arrays can opt into the same contract with
['key' => 'notes', 'renderer' => 'textarea'] - the frontend maps these hints through package-owned wrappers such as
FBInput,FBTextarea,FBSelect, andFBSwitch
Layout contract
Normalized form payloads also expose package-owned layout metadata:
- container layout lives on
form.layout,section.layout, andtab.layout - field sizing lives on
field.layout.column_span - grid and flex settings are validated during normalization and fail fast on invalid combinations
- legacy arrays can opt into the same behavior with
layout,columns,gap,direction,justify,align,wrap, andcolumn_span
Invalid combinations such as columns() plus direction() on the same container raise an exception instead of being silently ignored.
Runtime flow
- Flashboard resolves the create or edit route.
ResourceFormDataSourcehydrates field state.ResourceFormPersistervalidates and saves data.afterSave()hooks and runtime hooks run.- The user is redirected to the detail screen.
Validation and hooks
- create rules:
creationRules() - update rules:
updateRules($record) - shared rules:
formRules() - builder-level mutation:
mutateDataUsing() - resource-level mutation:
mutateFormDataBeforeSave() - post-persist hook:
afterSave()