tivents / livewire-form-builder
A powerful drag-and-drop form builder for Laravel 12 with Livewire 5, supporting all field types, conditional logic, multi-column layouts, and form submissions.
Requires
- php: ^8.2
- laravel/framework: ^12.0
- livewire/livewire: ^4.2
Requires (Dev)
- laravel/boost: *
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
README
A powerful, drag-and-drop form builder for Laravel 12 and Livewire 5 — a drop-in replacement for form.io.
The package ships no Models and no Migrations. You own your data layer. The package communicates with your app through a clean FormRepositoryContract interface.
Features
| Feature | |
|---|---|
| Drag & Drop builder canvas | ✅ |
| 15 field types | ✅ |
| Multi-column layout (full, 1/2, 1/3, 2/3, 1/4, 3/4) | ✅ |
| Repeater groups with nested columns | ✅ |
| Conditional logic (show/hide per AND/OR rules) | ✅ |
| Real-time per-field validation | ✅ |
| File uploads (single & multiple) | ✅ |
| JSON schema import / export | ✅ |
| CSV submission export | ✅ |
| Repository pattern — bring your own Model | ✅ |
| Artisan scaffolding commands | ✅ |
| Fully publishable views | ✅ |
Field types
| Group | Types |
|---|---|
| Inputs | text, textarea, number, select, checkbox, radio, toggle, datetime, file, repeater, hidden |
| Layout | heading, hint, html, divider, row |
Requirements
- PHP
^8.2 - Laravel
^12.0 - Livewire
^4.0
Installation
composer require tivents/livewire-form-builder
1. Publish config
php artisan vendor:publish --tag=livewire-form-builder-config
2. Publish the repository stub
php artisan livewire-form-builder:publish-stubs
This places the following file in your project:
| File | Purpose |
|---|---|
app/Repositories/LivewireFormBuilderRepository.php |
Eloquent implementation of FormRepositoryContract |
The package ships no migration. Create your own migration for the forms and form_submissions tables (the stub's docblock shows the expected columns) and run php artisan migrate when ready.
You are free to rename tables, add columns, or swap out Eloquent for anything else — as long as your repository implements the contract.
3. Include the package styles (Tailwind CSS)
The builder and renderer use Tailwind CSS classes. When you embed the components inside your own app layout, Tailwind's build step must scan the package views — otherwise the classes will be purged and the UI will be unstyled.
Tailwind v4 — add an @source directive to your resources/css/app.css:
@source "../../vendor/tivents/livewire-form-builder/resources/views";
Tailwind v3 — add the path to the content array in your tailwind.config.js:
content: [ // ... your existing paths './vendor/tivents/livewire-form-builder/resources/views/**/*.blade.php', ],
Then rebuild your assets (npm run dev / npm run build).
Note: The built-in admin routes (
/livewire-form-builder) use the package's own layout, which loads Tailwind via CDN and does not require the above. The step above is only needed when embedding<livewire:livewire-form-builder::builder />or<livewire:livewire-form-builder::renderer />inside your own Blade layouts.
4. Bind the repository
In app/Providers/AppServiceProvider.php:
use Tivents\LivewireFormBuilder\Contracts\FormRepositoryContract; use App\Repositories\LivewireFormBuilderRepository; public function register(): void { $this->app->bind(FormRepositoryContract::class, LivewireFormBuilderRepository::class); }
Or set it in config/livewire-form-builder.php:
'repository' => \App\Repositories\LivewireFormBuilderRepository::class,
Usage
Admin builder UI
Navigate to /livewire-form-builder — requires the middleware configured in config/livewire-form-builder.php (auth by default).
Embed the builder in your own view
{{-- Create new form --}} <livewire:livewire-form-builder::builder /> {{-- Edit existing form --}} <livewire:livewire-form-builder::builder :form-id="$form->id" />
Embed the renderer (public-facing)
{{-- By form ID --}} <livewire:livewire-form-builder::renderer :form-id="$form->id" /> {{-- Inline schema (form-id still used to save the submission) --}} <livewire:livewire-form-builder::renderer :form-id="$product->id" :schema="$schemaArray" /> {{-- Custom success message --}} <livewire:livewire-form-builder::renderer :form-id="$form->id" success-message="Vielen Dank für Ihre Anfrage!" /> {{-- Redirect after submit --}} <livewire:livewire-form-builder::renderer :form-id="$form->id" redirect-url="{{ route('thank-you') }}" /> {{-- Edit mode: pre-fill with existing submission data --}} <livewire:livewire-form-builder::renderer :form-id="$product->id" :schema="$schema" :submission-id="$participant->id" /> {{-- Edit mode with initial data (avoids extra API call) --}} <livewire:livewire-form-builder::renderer :form-id="$product->id" :schema="$schema" :submission-id="$participant->id" :initial-data="$participant->data" /> {{-- Extra fields injected from the backend (not part of the schema) --}} <livewire:livewire-form-builder::renderer :form-id="$product->id" :schema="$schema" :extra-fields="[ ['key' => 'notify_participant', 'type' => 'checkbox', 'label' => 'Teilnehmer benachrichtigen', 'value' => '1', 'width' => 'full'], ]" /> {{-- Show fields marked as hidden in the schema (e.g. admin backend) --}} <livewire:livewire-form-builder::renderer :form-id="$product->id" :schema="$schema" :submission-id="$participant->id" :show-hidden="true" />
Renderer props
| Prop | Type | Default | Description |
|---|---|---|---|
form-id |
int|string|null |
null |
Load schema from repository and associate submissions |
schema |
array |
[] |
Pass schema directly (flat field array or full {name, schema, settings} object). When set, the repository is not called for schema loading. form-id is still used to save submissions. |
submission-id |
int|string|null |
null |
Edit mode — pre-fills the form and calls updateSubmission on submit instead of saveSubmission |
initial-data |
array |
[] |
Pre-fill data for edit mode. Skips the repository findSubmissionOrFail call when provided. |
extra-fields |
array |
[] |
Additional fields injected from the backend, not part of the schema. Values are merged into the submission data. |
show-hidden |
bool |
false |
Show fields marked as hidden: true in the schema (with a "Hidden field" badge). |
success-message |
string |
'Thank you! Your response has been recorded.' |
Message shown after successful submission (no redirect). |
redirect-url |
string |
'' |
Redirect to this URL after submit instead of showing the success message. |
Listen to Livewire events
// In a parent Livewire component #[On('form-submitted')] public function onFormSubmitted(int|string|null $formId, array $data): void { // New submission — $data contains all field values incl. extra-fields } #[On('form-updated')] public function onFormUpdated(int|string $submissionId, int|string|null $formId, array $data): void { // Existing submission was updated } #[On('form-saved')] public function onFormSaved(int|string|null $formId): void { // Builder saved/updated a form schema }
Listen to JS events
document.addEventListener('livewire:init', () => { Livewire.on('form-submitted', ({ formId, data }) => { console.log('New submission', data); }); Livewire.on('form-updated', ({ submissionId, formId, data }) => { console.log('Updated submission', submissionId, data); }); Livewire.on('form-saved', ({ formId }) => { console.log('Builder saved form', formId); }); });
Configuration (config/livewire-form-builder.php)
return [ // Your repository implementation 'repository' => \App\Repositories\LivewireFormBuilderRepository::class, // URL prefix for the built-in admin routes 'route_prefix' => 'livewire-form-builder', 'middleware' => ['web', 'auth'], 'builder_routes' => true, // false to disable built-in CRUD routes // Pagination 'per_page' => 25, // File upload 'disk' => 'public', 'upload_directory' => 'livewire-form-builder/uploads', 'max_file_size' => 10240, // KB // Register custom field types 'field_types' => [ // 'signature' => \App\FormFields\SignatureField::class, ], ];
Adding a Custom Field Type
Use the scaffold command:
php artisan livewire-form-builder:make-field StarRating
This generates:
app/FormFields/StarRatingField.php— implement your logicresources/views/vendor/livewire-form-builder/fields/star_rating.blade.php— renderer viewresources/views/vendor/livewire-form-builder/settings/star_rating.blade.php— builder settings panel
Then register it:
// config/livewire-form-builder.php 'field_types' => [ 'star_rating' => \App\FormFields\StarRatingField::class, ],
Row / Column Layout
Fields support a flat width property (full, 1/2, 1/3, 2/3, 1/4, 3/4) that positions them on a shared 12-column grid. For explicit structural grouping, use the row field type as a container.
A row is always full-width itself and renders a nested 12-column grid for its children. Fields inside a row use the same width values.
When to use row:
- You want to move or delete a group of fields as a unit in the builder
- You need semantically grouped columns (e.g. first name + last name side by side)
- You want to mix different widths within a clearly bounded section
Schema structure:
{
"type": "row",
"key": "name_row",
"width": "full",
"children": [
{ "type": "text", "key": "first_name", "label": "First Name", "required": true, "width": "1/2" },
{ "type": "text", "key": "last_name", "label": "Last Name", "required": true, "width": "1/2" }
]
}
Children support all standard field properties (validation, conditional logic, etc.). The row field itself carries no validation rules.
JSON Schema Format
{
"name": "Kontaktformular",
"schema": [
{ "type": "heading", "key": "h1", "text": "Kontakt", "level": "h2", "width": "full" },
{
"type": "row", "key": "name_row", "width": "full",
"children": [
{ "type": "text", "key": "name_abc", "label": "Name", "required": true, "width": "1/2" },
{ "type": "text", "key": "email_xyz", "label": "E-Mail", "required": true, "width": "1/2", "input_type": "email" }
]
},
{
"type": "select", "key": "topic_def", "label": "Thema", "width": "full",
"options": [{ "label": "Vertrieb", "value": "sales" }, { "label": "Support", "value": "support" }]
},
{
"type": "textarea", "key": "msg_ghi", "label": "Nachricht", "required": true, "rows": 5,
"conditions": {
"action": "show", "logic": "and",
"rules": [{ "field": "topic_def", "operator": "!=", "value": "" }]
}
},
{
"type": "text", "key": "internal_note", "label": "Interne Notiz", "hidden": true, "width": "full"
}
]
}
Field properties
| Property | Description |
|---|---|
type |
Field type (see field types table) |
key |
Unique identifier within the form — used as the data key in submissions |
label |
Display label |
width |
Column width: full, 1/2, 1/3, 2/3, 1/4, 3/4 |
required |
true to make the field mandatory |
hidden |
true to hide the field in the renderer by default. The field is still initialised and its default value is submitted. Visible when :show-hidden="true" is passed to the renderer. Configurable in the builder settings panel. |
default |
Default value pre-filled on mount |
disabled |
true to render the input as read-only |
placeholder |
Input placeholder text |
hint |
Helper text shown below the label |
conditions |
Conditional logic — see below |
Examples
The examples/ directory contains ready-to-copy code for common integration scenarios:
| File | Pattern | Use case |
|---|---|---|
ApiWebhookRepository.php |
Repository decorator | Saves to DB and POSTs a signed JSON payload to a webhook URL |
FormSubmissionListener.php |
Laravel event listener | Reacts to a FormSubmitted event — supports ShouldQueue for background processing |
api-form-page.blade.php |
Client-side JS | Listens to the browser form-submitted event and calls any HTTP endpoint via fetch() |
CentralApiFormRepository.php |
Central API repository | Full repository backed entirely by an HTTP API — no local DB needed; for multi-system setups |
See examples/README.md for setup instructions and payload shapes.
Repository Contract
Your repository must implement Tivents\LivewireFormBuilder\Contracts\FormRepositoryContract:
| Method | Description |
|---|---|
findOrFail(id) |
Return form object with at least id, name, schema, settings |
create(data) |
Persist new form, return it |
update(id, data) |
Update form, return it |
delete(id) |
Delete form |
paginate(perPage) |
Return paginated list of forms |
saveSubmission(formId, data, meta) |
Store a new submission |
updateSubmission(submissionId, data, meta) |
Update an existing submission (called in edit mode) |
paginateSubmissions(formId, perPage) |
Return paginated submissions for a form |
findSubmissionOrFail(formId, submissionId) |
Return single submission object with a data array property |
deleteSubmission(submissionId) |
Delete submission |
Artisan Commands
# Scaffold a new custom field type php artisan livewire-form-builder:make-field MyType # Publish Eloquent repository stub php artisan livewire-form-builder:publish-stubs # Publish config php artisan vendor:publish --tag=livewire-form-builder-config # Publish views (to customise) php artisan vendor:publish --tag=livewire-form-builder-views
License
MIT