You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dynamic form builder: form definitions and entries stored in the database, builder and filling UIs in Blade and React, sections, repeating groups, wizards, cascading and conditional inputs, read-only rendering, import/export, extensible input types, optional AI assistant.
Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.
Laravel package javaabu/form-builder, namespace Javaabu\FormBuilder\, structured like javaabu/widgets: PHP + TypeScript in resources/js shipped through javaabu/manifest, with a committed dist/ for the Blade scripts.
form_sections: a version's sections, steps and groups, in order, with conditions.
form_fields: key, type, label, help, settings, rules, conditions, position, and parent_id for fields inside a repeating group.
A form has at most one draft version, which the builder edits. Publishing freezes it, and published versions are immutable (the model refuses updates). Editing a published form copies it into a new draft.
Entries (Form entry storage #3). Each form_entries row points at the form_version_id it was filled with. It holds:
submitter morph and subject morph (what the entry is about, e.g. a workflow step)
status, submitted_at, locale
data (the full answers)
form_entry_values has one row per answer (field key, path such as references.0.name, typed value/number/date columns), so entries are queryable: FormEntry::whereAnswer('age', '>=', 18). CSV export comes from the version's columns.
Compiling to stencil.EntryForm extends Javaabu\Stencil\Form gets the version through with(['version' => $version]) and compiles it in definition(). That gives us:
Blade and React rendering
validation rules, visibleWhen/hiddenWhen conditions and live fields from stencil
a rebuildable form class, so stencil's live route works
Input types (Extensible input types #9).InputType classes in a registry (FormBuilder::inputTypes(), config('form-builder.input_types')). Each one has:
a key, label and icon
its builder settings (a stencil schema)
toStencil() (field → stencil component)
extra rules
storage cast (string/number/date/json/file)
display and export formatting
Built-in types: text, textarea, email, number, phone, url, select, radio, checkboxes, checkbox, date, time, datetime, file, cascading select, and paragraph/heading (display only). Apps add types, or replace and extend built-ins (FormBuilder::extendInputType('text', fn …)). Custom React inputs use @javaabu/forms' custom-field hooks and are registered with StencilProvider.
Sections compile to stencil Section. In wizard mode each section is a Step.
Conditions on sections and fields: in, not_in, checked, not_checked, combined with AND. They compile to stencil's declarative conditions, so they work in the browser and on the server.
Cascading selects use @javaabu/forms' Select2 cascade (parent + ajaxUrl + filterField) against a package options endpoint, or any URL such as a javaabu/locations API. The server validates the pair.
Repeating groups are implemented here as a stencil field, Javaabu\FormBuilder\Stencil\Repeater. Its state is a list of rows, and it handles its own per-row rules, attributes, conditions and dehydration. It renders a template row plus the existing rows, with a Blade view and a small script, and a React component. I'll open a cross-package issue on stencil proposing to upstream it once it's settled.
Rendering entries (Filling forms and read-only rendering #5). A read-only rendering compiles the version into stencil TextEntry components formatted by input type. The same code serves the Blade and React pages and a printable page with print CSS.
Section::make(), Field::text('name')->label()->required()->thaana(), Field::repeater('refs')->schema([...]), and so on. sync() is idempotent: it publishes a new version only when the structure changes. Forms can be locked (code-only), and form-builder.builder can turn the builder UI off.
Export/import (Export and import form structure #8). A versioned JSON format, {"form_builder": 1, "form": {…}, "sections": [...]}, validated on import. Importing an existing key makes a new draft. Available as builder actions and as the form-builder:export / form-builder:import commands.
Route::formBuilder([...]) serves the admin side: forms CRUD, the builder (sections and fields: add, configure, reorder, delete), preview, publish, versions, export/import, entries list and CSV, entry view/print, and the AI assistant.
Route::formBuilderFill([...]) serves filling (show, submit, thank-you). It can be public or behind any guard.
Controllers come from config. FormPolicy/FormEntryPolicy and the permissions view_forms, edit_forms, delete_forms, publish_forms, view_form_entries, delete_form_entries and export_form_entries are registered through javaabu/permissions.
form-builder.views chooses blade or inertia. In the Blade builder every action is a server round trip, with a script for keyboard-accessible reordering. The React builder uses the same endpoints.
Translatable (Optional javaabu/translatable support #16). Optional, feature-detected in Support\Translations. Form names/descriptions and field labels, help and options become locale maps, and the builder shows one input per active locale.
AI (AI assistant to build forms (optional) #10). Optional, with laravel/ai. An agent with structured output returns the export format. It goes through the importer's validation and is saved as a draft, either a new form or an edit of an existing one. Tests use laravel/ai's fakes.
Public API sketch (for javaabu/workflows)
$form = FormBuilder::form('job-application'); // Form model$version = $form->publishedVersion; // FormVersion$stencil = EntryForm::for($version)->action($url); // a stencil Form: {{ $stencil }} or Inertia prop$entry = FormBuilder::submit($version, $request, submitter: $user, subject: $step); // validates, stores FormEntry$entry->answers(); $entry->answer('full_name');
FormBuilder::display($entry); // read-only stencil Form (Blade/React)
API for javaabu/workflows (definitions, entries, rendering)
On main. Install with a VCS repository for Javaabu/form-builder and "javaabu/form-builder": "^1.0@dev". The full reference is docs/advanced-usage/php-api.md. Everything below is a public contract.
Definitions and versions
useJavaabu\FormBuilder\Facades\FormBuilder;
$form = FormBuilder::form('leave-request'); // Models\Form by key (FormBuilderException if none)$version = $form->publishedVersion; // Models\FormVersion; immutable once published$definition = $version->definition(); // Definitions\FormDefinition: getFields(), getField('key' | 'group.key')FormBuilder::define('leave-request')->sections([...])->sync(); // define/update in code (idempotent)
A workflow step should store the form_version_id it was configured with, or always use the form's current publishedVersion. Versions never change once published.
Rendering a step's form
$stencil = FormBuilder::fill($version)->action($url)->method('POST'); // Stencil\EntryForm, a Javaabu\Stencil\Form$stencil = Stencil\EntryForm::for($version)->model($entry->answers()); // to edit a step's data
Blade:{{ $stencil }}.
Inertia: pass it as a prop and render <StencilForm schema={form} components={formBuilderComponents} /> (formBuilderComponents from @javaabu/form-builder, needed for repeating groups and headings).
Storing step data
$entry = FormBuilder::submit($version, $request, submitter: $user, subject: $stepInstance); // validates; throws ValidationExceptionapp(Entries\EntryRecorder::class)->submit($stencil, $version, $input, $user, $stepInstance, status: FormEntry::DRAFT); // saved, not submittedapp(Entries\EntryRecorder::class)->update($entry, $input); // re-validated with its own version
Events Events\EntrySubmitted and Events\EntryUpdated carry $event->entry.
javaabu/form-builder v1.0 is on main and CI is green:
the full matrix: PHP 8.4/8.5 × Laravel 12/13 × lowest/stable
Pint
a leg without Inertia, translatable and laravel/ai
the JS job: lint, format, typecheck, Vitest with coverage, the examples build and the dist freshness check
That's 112 PHPUnit tests and 31 Vitest tests. Every next-gen issue is closed except #17 (needs a human).
What was built
Definitions. These are stored in forms, form_versions, form_sections and form_fields.
Each form has drafts and immutable published versions, and entries keep the version they were filled with.
Fluent definitions (FormBuilder::define()->…->sync()) are idempotent and can be locked to code. form-builder.builder turns the builder off.
17 input types in an extensible registry: registerInputType(), extendInputType(), forgetInputType(). One test type has its own stencil field and a React component built with @javaabu/forms' custom-field hooks.
Compiled to stencil (EntryForm, DisplayForm):
sections and wizards
declarative conditions (in, not in, checked, not checked)
cascading selects, through the select2 cascade and an options endpoint, with the server checking the pair
Entries. These are stored in form_entries and form_entry_values.
FormBuilder::submit() validates and stores, and EntryRecorder and its events are there for other packages.
whereAnswer() queries indexed answers.
Entries can be shown read-only or printed, exported to CSV, and their files downloaded.
Builder pages (Route::formBuilder()):
the forms list, creating forms and their settings
the builder itself: add, configure, move and delete sections and fields, and reorder them by drag and drop or the keyboard (announced to screen readers)
preview, publish, discard draft, versions, import and export, entries
Fill pages come from Route::formBuilderFill(). Every page is in Blade and in Inertia React, and every route also answers JSON.
Controllers and permissions.
Controllers are overridable and have hooks, and routes take guard, prefix, name and middleware.
FormPolicy and FormEntryPolicy check seven permissions through javaabu/permissions, for any guard or user model.
JSON export and import (format 1), validated, from the builder or form-builder:export / form-builder:import.
Optional packages.
javaabu/translatable: form names in every language, and per-locale inputs throughout the builder.
laravel/ai: an assistant that builds or changes a draft from a description. Its answer is validated, it gets one retry, and the tests use laravel/ai's fakes.
Dhivehi and RTL. Every string is translated into Dhivehi, Thaana inputs work, and the CSS uses logical properties only.
Docs and tooling.llms.txt, the Boost guideline and skill, runnable examples, and AGENTS.md.
For workflows. The API is in the comment above.
Intentionally out of scope for v1
Nesting. Repeating groups can't be nested.
Live fields in repeater rows. Stencil live fields (server re-evaluation) aren't supported inside repeater rows. Conditions and cascading selects work there.
Cascading options from your own URL (options_url) can't be checked against a list on the server. The docs say so and show how to add a rule.
Save-and-resume and editing entries. There's no UI for people to save and resume, or for admins to edit an entry's answers. The API supports both (FormEntry::DRAFT, EntryRecorder::update()).
The builder is page-based. Each section or field is edited on its own page, rather than in an all-in-one canvas. PUT forms/{form}/structure is there for a richer client later.
The print page is Blade in both stacks, as a standalone printable document.
Notifications on submission are left to apps, through EntrySubmitted or FillController::submitted().
For the user
Codecov. Uploads run but are rejected with "Token required". Make CODECOV_TOKEN available to this repo; the upload is set to fail_ci_if_error: false.
Dhivehi review. The lines in docs/advanced-usage/dhivehi-and-rtl.md#for-review need a Dhivehi speaker.
Release. Tag and submit to Packagist. Once stencil, forms, manifest, permissions and translatable v3 are released, drop the VCS repositories in composer.json. Once @javaabu/forms is on npm, change its npm dependency in Assets and package.json. pnpm is pinned to 10.28 for the Git-hosted @javaabu/forms build, as in the other packages.
Not checked in a real browser. The React pages and Blade scripts are tested with Vitest/jsdom, and the examples build (pnpm examples), but I haven't looked at the styling in a real browser.
Activity
n4ff4h commentedon Nov 5, 2026
Implementation plan
Laravel package
javaabu/form-builder, namespaceJavaabu\FormBuilder\, structured like javaabu/widgets: PHP + TypeScript inresources/jsshipped through javaabu/manifest, with a committeddist/for the Blade scripts.Architecture
Definitions (Form definition storage #2). These tables:
forms: key, name, description, settings, status,published_version_id, soft deletes, plustranslations/langcolumns for optional translatable.form_versions: number,published_at, author morph.form_sections: a version's sections, steps and groups, in order, with conditions.form_fields: key, type, label, help, settings, rules, conditions, position, andparent_idfor fields inside a repeating group.A form has at most one draft version, which the builder edits. Publishing freezes it, and published versions are immutable (the model refuses updates). Editing a published form copies it into a new draft.
Entries (Form entry storage #3). Each
form_entriesrow points at theform_version_idit was filled with. It holds:submitted_at, localedata(the full answers)form_entry_valueshas one row per answer (field key, path such asreferences.0.name, typedvalue/number/datecolumns), so entries are queryable:FormEntry::whereAnswer('age', '>=', 18). CSV export comes from the version's columns.Compiling to stencil.
EntryForm extends Javaabu\Stencil\Formgets the version throughwith(['version' => $version])and compiles it indefinition(). That gives us:visibleWhen/hiddenWhenconditions and live fields from stencilInput types (Extensible input types #9).
InputTypeclasses in a registry (FormBuilder::inputTypes(),config('form-builder.input_types')). Each one has:toStencil()(field → stencil component)Built-in types: text, textarea, email, number, phone, url, select, radio, checkboxes, checkbox, date, time, datetime, file, cascading select, and paragraph/heading (display only). Apps add types, or replace and extend built-ins (
FormBuilder::extendInputType('text', fn …)). Custom React inputs use @javaabu/forms' custom-field hooks and are registered withStencilProvider.Structure (Sections, repeating groups, wizards, cascading and conditional inputs #6).
Section. In wizard mode each section is aStep.in,not_in,checked,not_checked, combined with AND. They compile to stencil's declarative conditions, so they work in the browser and on the server.parent+ajaxUrl+filterField) against a package options endpoint, or any URL such as a javaabu/locations API. The server validates the pair.Javaabu\FormBuilder\Stencil\Repeater. Its state is a list of rows, and it handles its own per-row rules, attributes, conditions and dehydration. It renders a template row plus the existing rows, with a Blade view and a small script, and a React component. I'll open across-packageissue on stencil proposing to upstream it once it's settled.Rendering entries (Filling forms and read-only rendering #5). A read-only rendering compiles the version into stencil
TextEntrycomponents formatted by input type. The same code serves the Blade and React pages and a printable page with print CSS.Fluent definitions (Define forms fluently in seeders #7). Seeders define forms like this:
Section::make(),Field::text('name')->label()->required()->thaana(),Field::repeater('refs')->schema([...]), and so on.sync()is idempotent: it publishes a new version only when the structure changes. Forms can be locked (code-only), andform-builder.buildercan turn the builder UI off.Export/import (Export and import form structure #8). A versioned JSON format,
{"form_builder": 1, "form": {…}, "sections": [...]}, validated on import. Importing an existing key makes a new draft. Available as builder actions and as theform-builder:export/form-builder:importcommands.HTTP (Overridable controllers, policies and default permissions (RBAC, multi-auth) #15, Form builder UI #4, Filling forms and read-only rendering #5, Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #14).
Route::formBuilder([...])serves the admin side: forms CRUD, the builder (sections and fields: add, configure, reorder, delete), preview, publish, versions, export/import, entries list and CSV, entry view/print, and the AI assistant.Route::formBuilderFill([...])serves filling (show, submit, thank-you). It can be public or behind any guard.FormPolicy/FormEntryPolicyand the permissionsview_forms,edit_forms,delete_forms,publish_forms,view_form_entries,delete_form_entriesandexport_form_entriesare registered through javaabu/permissions.form-builder.viewschoosesbladeorinertia. In the Blade builder every action is a server round trip, with a script for keyboard-accessible reordering. The React builder uses the same endpoints.Translatable (Optional javaabu/translatable support #16). Optional, feature-detected in
Support\Translations. Form names/descriptions and field labels, help and options become locale maps, and the builder shows one input per active locale.AI (AI assistant to build forms (optional) #10). Optional, with laravel/ai. An agent with structured output returns the export format. It goes through the importer's validation and is saved as a draft, either a new form or an edit of an existing one. Tests use laravel/ai's fakes.
Public API sketch (for javaabu/workflows)
Order of work
#1 foundations, then #2 with the input-type registry and compiler (#9 core), #3, #6, #7, #8, #15, #5, #4, #14, #16, #13, #10, #11, #12.
Open questions (I'll decide and record as I go)
n4ff4h commentedon Nov 5, 2026
API for javaabu/workflows (definitions, entries, rendering)
On
main. Install with a VCS repository forJavaabu/form-builderand"javaabu/form-builder": "^1.0@dev". The full reference is docs/advanced-usage/php-api.md. Everything below is a public contract.Definitions and versions
A workflow step should store the
form_version_idit was configured with, or always use the form's currentpublishedVersion. Versions never change once published.Rendering a step's form
{{ $stencil }}.<StencilForm schema={form} components={formBuilderComponents} />(formBuilderComponentsfrom@javaabu/form-builder, needed for repeating groups and headings).Storing step data
Events
Events\EntrySubmittedandEvents\EntryUpdatedcarry$event->entry.Reading and showing it
Embedding fields in a bigger form
This returns stencil components whose field names are the field keys.
n4ff4h commentedon Nov 5, 2026
Final summary
javaabu/form-builder v1.0 is on
mainand CI is green:distfreshness checkThat's 112 PHPUnit tests and 31 Vitest tests. Every
next-genissue is closed except #17 (needs a human).What was built
Definitions. These are stored in
forms,form_versions,form_sectionsandform_fields.FormBuilder::define()->…->sync()) are idempotent and can be locked to code.form-builder.builderturns the builder off.17 input types in an extensible registry:
registerInputType(),extendInputType(),forgetInputType(). One test type has its own stencil field and a React component built with @javaabu/forms' custom-field hooks.Compiled to stencil (
EntryForm,DisplayForm):Repeaterstencil field with per-row validation and conditions, a Blade view and script, and a React component. Upstreaming is proposed in Proposal: a Repeater field (repeating groups), implemented in javaabu/form-builder stencil#18.Entries. These are stored in
form_entriesandform_entry_values.FormBuilder::submit()validates and stores, andEntryRecorderand its events are there for other packages.whereAnswer()queries indexed answers.Builder pages (
Route::formBuilder()):Fill pages come from
Route::formBuilderFill(). Every page is in Blade and in Inertia React, and every route also answers JSON.Controllers and permissions.
FormPolicyandFormEntryPolicycheck seven permissions through javaabu/permissions, for any guard or user model.JSON export and import (format 1), validated, from the builder or
form-builder:export/form-builder:import.Optional packages.
Dhivehi and RTL. Every string is translated into Dhivehi, Thaana inputs work, and the CSS uses logical properties only.
Docs and tooling.
llms.txt, the Boost guideline and skill, runnable examples, andAGENTS.md.For workflows. The API is in the comment above.
Intentionally out of scope for v1
options_url) can't be checked against a list on the server. The docs say so and show how to add a rule.FormEntry::DRAFT,EntryRecorder::update()).PUT forms/{form}/structureis there for a richer client later.EntrySubmittedorFillController::submitted().For the user
CODECOV_TOKENavailable to this repo; the upload is set tofail_ci_if_error: false.composer.json. Once @javaabu/forms is on npm, change its npm dependency inAssetsandpackage.json. pnpm is pinned to 10.28 for the Git-hosted @javaabu/forms build, as in the other packages.pnpm examples), but I haven't looked at the styling in a real browser.