Forms¶
Create and edit forms are generated from the fields. The server validates every write; the form mirrors its rules so most mistakes are caught before a request, and places every server violation on the field it names.
Layout¶
Group a long form into sections and tabs by returning them among the fields:
use Kinetis\Vector\Fields\Layout\{Section, Tabs};
public function fields(): array
{
return [
Id::make(),
Section::make('Details')->fields(['sku', 'name', 'status']),
Section::make('Pricing')->description('In the product\'s currency.')->collapsible()
->fields(['priceCents', 'currency']),
Tabs::make()
->tab('Content', ['description', 'imageKey'])
->tab('Publishing', ['isFeatured', 'releasedAt']),
Text::make('sku')->required(),
// … every field, in its own order
];
}
A section or tab holds fields, or the names of fields declared elsewhere in
fields(). A tab may hold sections; a section holds only fields. A field left at the top level keeps its place.columns(1..4)sets a section’s grid (two by default);->columnSpan(1|2|'full')on a field sets its width, or the UI picks one by its type. A field declaringcolumnSpan()gives the resource a layout even without sections.collapsible()lets a section fold;collapsed()starts it folded.The show page uses the same layout, with the fields it shows.
Layout only groups: validation, serialization, filters and writes are
unchanged. The compiler flattens it to the same field list and
publishes it apart, as the descriptor’s layout (REST API). A
field a user may not see leaves the layout, and a section or tab it
empties disappears. A name the resource does not declare, or one placed
twice, fails the build. When a save is refused, the form opens the
section and switches to the tab holding the field in error, and marks
the tab.
Conditions¶
A field can depend on another field of the same form — a resource’s, an action dialog’s or a settings group’s:
DateTime::make('releasedAt')
->visibleWhen('status', 'ne', 'draft')
->requiredWhen('status', 'eq', 'active'),
Boolean::make('isFeatured')->disabledWhen('status', 'eq', 'archived'),
The operators are eq, ne, in, nin (a list), null and nnull.
Repeated calls of one kind must all hold. A relation compares by its id.
The form re-evaluates conditions as values change, and the server
evaluates them again on the stored record overlaid with the request. A
hidden or disabled field’s submitted value is ignored; a field whose
requiredWhen holds must end up non-empty.
A condition on a field the form does not have, or on the field itself,
fails the build. A collection can only be tested with null or
nnull.
Relation choices¶
A relation input searches the target’s title. When the target declares
searchSubtitle() or searchImage() (see
Resources), each choice shows that value
under its title and the image as a thumbnail — the same as global
search — so records with the same title can be told apart.
Dependent choices¶
A belongs-to can offer only the records that match another field:
BelongsTo::to('team', TeamResource::class),
BelongsTo::to('assignee', AgentResource::class)->dependsOn('team'),
The assignee input waits until a team is chosen and clears itself when
the team changes. Use dependsOn('team', on: 'squad') when the target’s
property is named differently.
The options endpoint narrows by depends[team]=…; without that
parameter it searches every record, as a list filter does. A write that
changes the choice or the team must keep the choice among the agents the
options would offer; otherwise the choice is refused with
invalid_choice. Unchanged values are not re-checked, so records stored
before the rule still save. A dependency on a field the form lacks, or
on a property the target does not map, fails the build.
Repeaters¶
A HasMany the parent owns can be edited inside the parent’s form:
HasMany::to('lines', OrderLineResource::class)
->repeater(['product', 'quantity', 'unitPriceCents'], min: 1, max: 50),
The whole list is sent with the parent and written in one transaction:
{"lines": [{"id": 7, "quantity": 3}, {"product": 4, "quantity": 1, "unitPriceCents": 1200}]}
a row with an
idupdates that child;a row without one creates a child pointing at the parent;
a child left out is deleted.
Each row is validated as the child resource validates a record, and a
violation names its row (lines/1/quantity). The child’s policy
authorizes each create, update and delete; its hooks run and its audit
trail records each change, as if the child had been written on its own.
The parent’s hooks run after the rows are in place, so a total can be
recomputed there. GET /vector/r/{resource}/{id}/{relation}/rows
returns the current rows for an edit form.
Compilation refuses a repeater that is over a relation that is not owned, that leaves out a child field a new row requires, whose child resource soft-deletes, or that carries conditions. Repeaters are not written in bulk, inline or by import.
Inline editing¶
inlineEditable() lets the list edit a field in its cell. Edits across
rows are saved together as one changeset with
POST /vector/r/{resource}/changes, flushed in one transaction. Each
record is authorized, validated and hooked as a PATCH would be, and
checked against the _version it was listed at: one stale record
answers 409 for the whole changeset. A violation’s path is
changes/{index}/{field}. Only simple fields editable on update
qualify: text, email, url, slug, color, number, money, boolean, enum,
currency, date, datetime and belongs-to. Anything else fails the
build.
Optimistic locking¶
When the entity has a #[Version] column, every update carries the
version it was read at. A record changed in the meantime answers 409
instead of losing the other change; the form says so.