preflow / form
Preflow form builder — field rendering, model binding, hypermedia integration
Requires
- php: >=8.4
- preflow/validation: ^0.1 || @dev
- preflow/view: ^0.1 || @dev
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.76.0
- v0.75.0
- v0.74.0
- v0.73.0
- v0.72.1
- v0.72.0
- v0.71.1
- v0.71.0
- v0.70.0
- v0.69.2
- v0.69.1
- v0.69.0
- v0.68.1
- v0.68.0
- v0.67.0
- v0.66.2
- v0.66.1
- v0.66.0
- v0.65.0
- v0.64.0
- v0.63.2
- v0.63.1
- v0.63.0
- v0.62.0
- v0.61.0
- v0.60.1
- v0.60.0
- v0.59.0
- v0.58.1
- v0.58.0
- v0.57.0
- v0.56.1
- v0.56.0
- v0.55.0
- v0.54.1
- v0.54.0
- v0.53.2
- v0.53.1
- v0.53.0
- v0.52.0
- v0.51.0
- v0.50.0
- v0.49.1
- v0.49.0
- v0.48.5
- v0.48.4
- v0.48.3
- v0.48.2
- v0.48.1
- v0.48.0
- v0.47.1
- v0.47.0
- v0.46.1
- v0.46.0
- v0.45.0
- v0.44.0
- v0.43.0
- v0.42.1
- v0.42.0
- v0.41.2
- v0.41.1
- v0.41.0
- v0.40.0
- v0.39.0
- v0.38.0
- v0.37.0
- v0.36.0
- v0.35.0
- v0.34.1
- v0.34.0
- v0.33.0
- v0.32.0
- v0.31.0
- v0.30.0
- v0.29.0
- v0.28.0
- v0.27.1
- v0.27.0
- v0.26.0
- v0.25.1
- v0.25.0
- v0.24.1
- v0.24.0
- v0.23.0
- v0.22.1
- v0.22.0
- v0.21.0
- v0.20.0
- v0.19.2
- v0.19.1
- v0.19.0
- v0.18.1
- v0.18.0
- v0.17.2
- v0.17.1
- v0.17.0
- v0.16.2
- v0.16.1
- v0.16.0
- v0.15.0
- v0.14.2
- v0.14.1
- v0.14.0
- v0.13.1
- v0.13.0
This package is auto-updated.
Last update: 2026-11-14 19:29:44 UTC
README
Form builder for Preflow. Renders fields with labels, errors, and help text from a simple template API. Binds to models, reads #[Validate] attributes for type inference and required detection, and auto-enhances forms with inline validation when a hypermedia driver is active.
Installation
composer require preflow/form
Requires preflow/validation and preflow/view. Optionally integrates with preflow/data (model binding) and preflow/htmx (inline validation).
Quick Start
Plain form (no model)
{% set form = form_begin({action: '/contact', csrf_token: csrf_token()}) %}
{{ form.begin()|raw }}
{{ form.field('name')|raw }}
{{ form.field('email', {type: 'email'})|raw }}
{{ form.field('message', {type: 'textarea'})|raw }}
{{ form.submit('Send')|raw }}
{{ form_end()|raw }}
Model-bound form (inside a component)
{% set form = form_begin({model: job, errorBag: errors}) %}
<form {{ hd.post('save', componentClass, componentId, props)|raw }}>
{{ form.field('name')|raw }}
{{ form.select('status', {options: {draft: 'Draft', published: 'Published'}})|raw }}
{{ form.field('description', {type: 'textarea'})|raw }}
{{ form.submit('Save')|raw }}
</form>
The form builder reads #[Validate] attributes from the model to infer input types (email rule = type="email"), detect required fields, and populate select options from in: rules.
Field Methods
| Method | Description |
|---|---|
form.field(name, options) |
Text, email, password, number, date, url, tel, hidden |
form.select(name, options) |
Dropdown with options map |
form.checkbox(name, options) |
Single checkbox |
form.radio(name, options) |
One radio button (a group is one call per button, see below) |
form.file(name, options) |
File upload input |
form.hidden(name, value) |
Hidden input (no label/wrapper) |
form.submit(label, options) |
Submit button |
A radio button's value is the value it sends; it is checked only when you say so. A group
is one call per button, each with its own id:
{% for key, label in {draft: 'Draft', published: 'Published'} %}
{{ form.radio('status', {value: key, id: 'status-' ~ key, label: label, attrs: {checked: job.status == key}})|raw }}
{% endfor %}
Field Options
| Option | Description |
|---|---|
type |
Input type (auto-inferred from model rules if bound) |
label |
Label text (auto-generated from field name if omitted) |
value |
Field value; without it: the old input, then the model property |
required |
Required indicator (auto-detected from required rule) |
help |
Help text below the field |
errors |
Error messages (auto-populated from ErrorBag) |
attrs |
Raw HTML attributes passed to the input (escape hatch) |
width |
Width hint for use inside groups ('1/3', '2/3') |
options |
Key-value map for select fields |
Field Groups
Group fields for side-by-side layout:
{{ form.group({class: 'form-row', label: 'Address'})|raw }}
{{ form.field('zip', {width: '1/3'})|raw }}
{{ form.field('city', {width: '2/3'})|raw }}
{{ form.endGroup()|raw }}
Auto-Generation
Generate fields from model #[Validate] attributes:
{# All validated fields #} {{ form.fields()|raw }} {# Cherry-pick #} {{ form.fields({only: ['name', 'email']})|raw }} {# Exclude #} {{ form.fields({except: ['password_hash', 'created_at']})|raw }} {# Override specific fields #} {{ form.fields({only: ['name', 'email', 'role'], overrides: {role: {type: 'select', options: roles}}})|raw }}
Validation Scenarios
Override rules per form for create/update differences:
{% set form = form_begin({
model: user,
rules: isEdit
? {password: ['nullable', 'min:8']}
: {password: ['required', 'min:8']}
}) %}
Or use on: directives on the model:
#[Validate('required', 'min:8', 'on:create')] #[Validate('nullable', 'min:8', 'on:update')] public string $password = '';
{% set form = form_begin({model: user, scenario: 'create'}) %}
Hypermedia Integration
When inside a component with a hypermedia driver active, the form builder can auto-add inline validation attributes. The driver is the app's HypermediaDriver (htmx, or the class app.hypermedia names) when it also implements Preflow\View\FormHypermediaDriver; otherwise forms are plain. The attrs escape hatch lets you pass any driver-specific attributes:
{{ form.field('email', {
attrs: {
'hx-post': validateUrl,
'hx-trigger': 'blur delay:300ms',
'hx-target': 'closest .form-group',
'hx-swap': 'outerHTML'
}
})|raw }}
License
MIT