# Overview FormForge consists of two complementary packages: - **FormForge (backend):** deterministic form schema, revisions, server-side validation, endpoint security, and automation hooks for Laravel. - **FormForge Client (frontend):** typed Nuxt 4 runtime client, renderer components, composables, and admin helpers. ## Choose one mode first ### Backend modes 1. `Facade only` for pure code-first usage. 2. `Built-in HTTP API` for immediate API exposure. 3. `Scoped HTTP routes` for owner-context URLs (`/users/{user}`, `/teams/{team}`). 4. `Custom controllers` only for advanced behavior that cannot be configured. ### Client modes 1. `Renderer mode` with ``. 2. `Controlled mode` with composables and custom UI. 3. `Admin mode` for management/categories/responses. ## Wiki map ::card-group :::card --- icon: i-lucide-download title: Backend installation to: https://formforge.schleret.ch/docs/getting-started/installation/backend --- Install FormForge and submit your first form in Laravel. ::: :::card --- icon: i-lucide-monitor title: Client installation to: https://formforge.schleret.ch/docs/getting-started/installation/client --- Install the Nuxt module and render a form in minutes. ::: :::card --- icon: i-lucide-shield-check title: Backend wiki to: https://formforge.schleret.ch/docs/backend/overview --- Deep guides for API design, security, uploads, and automations. ::: :::card --- icon: i-lucide-layout-template title: Client wiki to: https://formforge.schleret.ch/docs/client/overview --- Rendering, submission flow, scoped calls, and admin workflows. ::: :::card --- icon: i-lucide-layers-3 title: Concepts to: https://formforge.schleret.ch/docs/concepts --- Understand how the backend and client fit together. ::: :::card --- icon: i-lucide-circle-help title: How do I to: https://formforge.schleret.ch/docs/how-do-i --- Jump straight to task-oriented guides. ::: :::card --- icon: i-lucide-bot title: MCP and AI wiki to: https://formforge.schleret.ch/docs/mcp-ai --- Connect this wiki as an MCP server and expose docs to AI assistants. ::: :: ::tip Start with one backend mode and one client mode. Add advanced features only when required. :: # Backend Installation ## Requirements - PHP `>=8.2` - Laravel `12.x` or `13.x` - For Laravel `13.x`, PHP `>=8.3` is required by Laravel itself. ## Install and migrate ```bash [Terminal] composer require evanschleret/formforge php artisan formforge:install php artisan migrate ``` For safe upgrades of existing installations: ```bash [Terminal] php artisan formforge:install:merge --dry-run php artisan formforge:install:merge php artisan migrate ``` ## Define your first form ```php title('Contact') ->version('1') ->category('contact') ->text('name')->required()->max(120) ->text('email')->required()->max(255) ->textarea('message')->required(); Form::sync(); ``` ## Submit from PHP ```php submit([ 'name' => 'Evan', 'email' => 'evan@example.com', 'message' => 'Hello' ]); ``` At this point, the package is fully usable without exposing any FormForge HTTP route. # Client Installation ## Requirements - Nuxt `4.x` - `@nuxt/ui` `4.x` - Node.js `>=20` or Bun `>=1.3` ## Install and register module ```bash [Terminal] bun add @evanschleret/formforgeclient ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/ui', '@evanschleret/formforgeclient'], formforgeClient: { baseURL: '/api/formforge/v1', credentials: 'include', uploadMode: 'staged', datetimeMode: 'offset', locale: 'en', autoImports: true } }) ``` ::tip If you need external save / publish orchestration or a playground-style preview, continue with [Client standalone usage](https://formforge.schleret.ch/docs/client/standalone). :: ## Render a form (renderer mode) ```vue ``` ## Controlled mode quick sample ```vue ``` # Concepts FormForge has one core rule: the backend owns the schema and the client consumes it. That split keeps behavior deterministic. The backend decides what a form is, how it evolves, and what the server accepts. The client decides how the form looks, how users move through it, and how editors orchestrate it in custom screens. ## What belongs where - **Backend** owns schema definition, validation, publication lifecycle, ownership, public links, response storage, and automation. - **Client** owns rendering, builder interactions, progress, navigation, standalone orchestration, and custom UX. - **Shared schema** connects both packages through the same contract. ## Start here ::card-group :::card --- icon: i-lucide-refresh-cw title: Lifecycle to: https://formforge.schleret.ch/docs/concepts/lifecycle --- Follow the full form lifecycle from definition to submission. ::: :::card --- icon: i-lucide-split-square-vertical title: Backend vs client to: https://formforge.schleret.ch/docs/concepts/backend-vs-client --- See which package owns each responsibility. ::: :::card --- icon: i-lucide-table-2 title: Decision matrix to: https://formforge.schleret.ch/docs/concepts/decision-matrix --- Pick the right path without reading every reference page first. ::: :::card --- icon: i-lucide-circle-help title: How do I guides to: https://formforge.schleret.ch/docs/how-do-i --- Jump directly to task-oriented guides. ::: :: # Lifecycle ## The lifecycle ::steps ### Define the form Write the form schema in Laravel or edit it through the builder. ### Save a draft Store the current definition without exposing it publicly yet. ### Publish the form Make the current revision available to the renderer and the HTTP API. ### Render the form Load the published schema in the Nuxt client and present it to end users. ### Validate the submission Run server-side validation before persisting the response. ### Store the response Keep the submission, files, and metadata in the backend. ### Trigger automations Dispatch post-submit handlers, exports, or follow-up workflows. :: ## Package split | Stage | Backend | Client | | ------- | ----------------------------------- | --------------------------------- | | Define | DSL, revisions, lifecycle settings | Builder UI | | Publish | Stored revision and public metadata | Save/publish orchestration | | Render | Schema source of truth | Renderer, progress, navigation | | Submit | Validation and persistence | Form state and submit UI | | Operate | Exports, automation, retention | Standalone workflows and admin UI | ::tip If you need a single screen to do everything, use the builder or renderer in standalone mode and keep orchestration outside the component tree. :: # Backend vs client ## Backend responsibility The backend is the source of truth for: - form schema and revisions - validation rules - publication windows and lifecycle state - ownership and scoped routes - response storage and export - `public_url` resolution - submission protection and business rules ## Client responsibility The client is responsible for: - rendering the current form or block - collecting user input - showing progress, previous, next, and submit controls - builder editing UX - standalone save/publish orchestration - admin screens built on composables ## Shared contract Both packages work on the same schema shape. The client must not invent backend rules on its own, and the backend must not depend on client-specific UI state. | Question | Owns it | Notes | | -------------------------------------------- | ------- | ----------------------------------------------- | | Can this response be accepted? | Backend | Validation always happens on the server. | | How should the form be displayed? | Client | Renderer and builder are presentation concerns. | | Is the form publishable? | Backend | Publication is part of the form lifecycle. | | Should settings be visible in a system form? | Client | `hideSettings` is a UI concern. | | What is the public link? | Backend | `public_url` is resolved server-side. | ::important If a rule affects data correctness, the backend must enforce it even if the client also mirrors the same rule for UX. :: # Decision matrix ## Quick decisions | Need | Recommended path | Why | Read next | | ---------------------------------------------- | ------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Define a form in Laravel | Backend DSL | Small, deterministic, and easy to version | [Forms Engine](https://formforge.schleret.ch/docs/backend/forms) | | Render a form fast in Nuxt | `` | Minimal setup and built-in navigation | [Renderer](https://formforge.schleret.ch/docs/client/components/formforge-renderer) | | Build a custom editor screen | `useFormForgeBuilder()` | Full control over save, publish, and selection state | [Standalone builder](https://formforge.schleret.ch/docs/client/standalone/builder) | | Keep the builder UI but move orchestration out | `hideSettings` + exposed ref methods | Clean standalone editing for system forms | [Standalone builder](https://formforge.schleret.ch/docs/client/standalone/builder) | | Scope forms to owners or teams | Scoped routes | Tenant-aware paths and public links | [Scoped routes](https://formforge.schleret.ch/docs/backend/http-api/scoped-routes-and-ownership) | | Protect the form with a PIN | Submission code settings | Server-enforced access control | [Publication lifecycle](https://formforge.schleret.ch/docs/backend/forms/categories-and-publication-lifecycle) | | Preview one block while editing | Standalone renderer | Keeps navigation visible while you simulate the flow | [Standalone renderer](https://formforge.schleret.ch/docs/client/standalone/renderer) | | Use custom validation feedback | Backend validation | The server owns real validation messages | [Validation](https://formforge.schleret.ch/docs/backend/validation) | ::tip Use this page when you are unsure where a feature belongs. If the feature changes data or security, it is probably backend-owned. If it changes the experience, it is probably client-owned. :: # Form DSL Reference ## Main builder - `Form::define('key')` - `->title('...')` - `->version('1')` - `->category('...')` - `->published(true|false)` - `->unpublished(true|false)` ## Field methods - `text`, `number` - `radio`, `checkboxGroup` - `consent` - `temporal` - `date`, `time` as convenience aliases for `temporal(...)` - `file` - `address` ::note Prefer `temporal(...)` in new code. The `date(...)` and `time(...)` helpers remain as convenience aliases for existing codebases. :: ## Common field options - `required(bool = true)` - `default(mixed)` - `label(string)` - `placeholder(string)` - `helpText(string)` - `rules(string|array ...$rules)` - `replaceRules(string|array ...$rules)` - `meta(array)` - `min(...)`, `max(...)`, `step(...)` - `options(array)` - `multiple(bool = true)` - `disabled(bool = true)` ## Publication settings These values are stored on the form definition and are consumed by the runtime and HTTP API: - `publish_at` - `pause_at` - `response_limit` - `submission_code_required` - `submission_code` `public_url` is read-only and is resolved at response time by the backend public-link resolver. ## File field options - `accept(string|array)` - `maxSize(int)` - `maxFiles(int)` - `maxTotalSize(int)` in bytes for all selected files combined - `storageDisk(string)` - `storageDirectory(string)` - `visibility('public'|'private')` `maxSize` limits each file individually. `maxFiles` limits the number of files when `multiple(true)` is enabled. `accept` accepts extensions (for example `.pdf`) and MIME types (for example `image/*`). # Schema, Pages, and Conditions ## Normalized schema shape - `key` - `version` - `title` - `fields` - `pages` - `conditions` - `drafts.enabled` - optional `api` endpoint overrides - optional `category` - optional `is_published` ## Condition model - `target_type`: `page|field` - `action`: `show|hide|skip|require|disable` - `match`: `all|any` - operators: - `eq`, `neq`, `in`, `not_in` - `gt`, `gte`, `lt`, `lte` - `contains`, `not_contains` - `is_empty`, `not_empty` At submit time, FormForge resolves an effective schema from payload + conditions, then validates against that effective shape. # Categories and Publication Lifecycle ## Categories - category `key` is UUID - category `slug` is stable and generated or maintained - categories can be `is_system` - form category assignment accepts UUID key or slug ## Forbidden category names Use `formforge.categories.forbidden_names` to block reserved category names in management operations. Name matching is normalized before validation: comparison is case-insensitive and ignores leading/trailing spaces. The rule is enforced on both category creation and category update. ```php 'categories' => [ 'forbidden_names' => ['Internal', 'System'], ], ``` If a forbidden name is sent to `POST /api/formforge/v1/categories` or `PATCH /api/formforge/v1/categories/{categoryKey}`, the API returns `422 Unprocessable Entity`. Validation includes an error on `category`. ## Revision lifecycle - `create` creates revision 1 - `patch` creates a new draft revision - `publish` creates a new published revision - `unpublish` creates a new draft revision - `delete` soft-deletes revisions ## Publishability A form can be published only when: - title is non-empty - at least one page exists - at least one field exists ## Publication settings Form schemas can carry publication-related settings that the runtime and client builder both understand: - `publish_at` - `pause_at` - `response_limit` - `submission_code_required` - `submission_code` These settings control when a form opens, when it stops accepting responses, and whether a PIN is required before submission. - `publish_at` opens the form at a specific date and time - `pause_at` stops the form from accepting new responses at a specific date and time - `response_limit` closes the form after the configured number of submissions - `submission_code_required` and `submission_code` protect the form with a PIN ::note The backend also resolves `public_url` through the configured public-link resolver, but that value is read-only and is not stored as a user-editable setting. :: # API Overview Default prefix: `/api/formforge/v1` Endpoint groups: - `schema` - `submission` - `upload` - `resolve` - `draft` - `management` You can toggle groups in `formforge.http.endpoints`. `resolve` now includes targeted field validation endpoints to validate one input without submitting a full payload. # Schema, Submission, Upload, and Resolve ## Schema - `GET /forms/{key}` - `GET /forms/{key}/versions` - `GET /forms/{key}/versions/{version}` ## Submission - `POST /forms/{key}/submit` - `POST /forms/{key}/versions/{version}/submit` Payload input keys are accepted by canonical field `name` and by `field_key` (default mode: `both`). Examples: ```json { "payload": { "name": "Evan", "email": "evan@example.com" } } ``` ```json { "payload": { "fk_abc123name": "Evan", "fk_def456mail": "evan@example.com" } } ``` If both `name` and `field_key` are sent for the same field, `name` wins. ## Upload (staged) - `POST /forms/{key}/uploads/stage` - `POST /forms/{key}/versions/{version}/uploads/stage` ## Resolve - `POST /forms/{key}/resolve` - `POST /forms/{key}/versions/{version}/resolve` - `POST /forms/{key}/validate-field` - `POST /forms/{key}/versions/{version}/validate-field` `validate-field` checks a single field value in isolation. Unlike `submit`, it does not validate or persist a full form payload. Request body: ```json { "field": "email", "value": "john@example.com" } ``` Success response: ```json { "valid": true, "errors": [], "validated": { "email": "john@example.com" } } ``` Failure response: ```json { "valid": false, "errors": [ "The email field is required." ], "validated": [] } ``` If `field` is missing from the request body, validation fails with a 422 response from the request validator. ## Drafts - `POST /forms/{key}/drafts` - `GET /forms/{key}/drafts/current` - `DELETE /forms/{key}/drafts/current` # Management, Responses, and GDPR ## Form management - `GET /forms` - `POST /forms` - `PATCH /forms/{key}` - `POST /forms/{key}/publish` - `POST /forms/{key}/unpublish` - `DELETE /forms/{key}` - `GET /forms/{key}/revisions` - `GET /forms/{key}/diff/{fromVersion}/{toVersion}` ### Auto-publish on create and patch Management create and patch support an optional boolean query parameter: - `auto_publish` - alias: `autoPublish` Supported endpoints: - `POST /api/formforge/v1/forms` - `PATCH /api/formforge/v1/forms/{key}` Behavior: - if `auto_publish=true`, FormForge publishes immediately after create or patch - the HTTP response returns the published revision Create example: ```json { "title": "Contact", "fields": [ { "type": "text", "name": "full_name", "required": true }, { "type": "text", "name": "email", "required": true } ] } ``` Request: - `POST /api/formforge/v1/forms?auto_publish=true` Patch example: ```json { "title": "Contact v2", "conditions": [] } ``` Request: - `PATCH /api/formforge/v1/forms/{key}?auto_publish=true` Without this parameter (or with `false`), behavior remains unchanged and returns a draft/non-published revision. ## Categories - `GET /categories` - `GET /category-routes/{routeKey}` - `GET /categories/{categoryKey}` - `POST /categories` - `PATCH /categories/{categoryKey}` - `DELETE /categories/{categoryKey}` ## Query routes - `GET /form-routes/{routeKey}` - `GET /category-routes/{routeKey}` ## Responses - `GET /forms/{key}/responses` - `GET /forms/{key}/responses/export` - `GET /forms/{key}/responses/{submissionUuid}` - `DELETE /forms/{key}/responses/{submissionUuid}` ## GDPR - `PUT /forms/{key}/gdpr-policy` - `POST /forms/{key}/responses/{submissionUuid}/gdpr/anonymize` - `POST /forms/{key}/responses/{submissionUuid}/gdpr/delete` - `POST /gdpr/run` # Security, Guards, Middleware, and Abilities ## Security layers 1. global route middleware (`formforge.http.middleware`) 2. endpoint guard config (`auth`, `guard`, `ability`, `abilities`) 3. endpoint middleware (`formforge.http..middleware`) 4. optional ownership resolution and authorization 5. optional scoped route authorization (`gate` or `policy`) ## Supported auth modes - `public` - `optional` - `required` ## Authorization action keys Examples: - `schema.latest`, `schema.versions`, `schema.show` - `submission.submit_latest`, `submission.submit_version` - `upload.stage_latest`, `upload.stage_version` - `resolve.resolve_latest`, `resolve.resolve_version` - `resolve.validate_field_latest`, `resolve.validate_field_version` - `draft.save`, `draft.current`, `draft.delete` - `management.index`, `management.create`, `management.update`, `management.publish` - `management.responses`, `management.responses_export`, `management.response_delete` - `management.gdpr_policy`, `management.response_gdpr_anonymize`, `management.gdpr_run` Policy method names follow snake\_case action naming. For field-validation actions, policy methods are: - `resolve_validate_field_latest` - `resolve_validate_field_version` # Scoped Routes and Ownership ## Ownership Optional polymorphic owner fields: - `owner_type` - `owner_id` Ownership can be required and fail-closed for selected endpoints. ## Scoped routes `formforge.http.scoped_routes[]` supports: - `name`, `enabled`, `prefix`, `middleware` - `endpoints` map - owner config (`route_param`, `model`, `route_key`, `type`, `required`) - `authorization.mode`: `none|gate|policy` - `authorization.policy` - `authorization.abilities` Scoped routes duplicate package endpoints under scoped prefixes without rewriting package business logic. This includes `resolve` and field-validation endpoints (`/validate-field`) when those endpoints are enabled in scoped route config. # Resources, Models, and Controller Overrides ## Resource customization Config keys: - `formforge.http.resources.form_definition` - `formforge.http.resources.submission` - `formforge.http.resources.submitter` - `formforge.http.resources.file_urls.*` File URL options: - `enabled` - `temporary` - `ttl_seconds` - `key` ## Public link resolution Form resources expose `public_url` when a public-link resolver is configured. Config keys: - `formforge.http.public_link.resolver` - `formforge.http.public_link.base_url` The resolver receives the form context and the current request. The context can include keys such as `key`, `owner_type`, `owner_id`, `schema`, `meta`, and `version`, which lets you build tenant-aware links, subdomain-specific URLs, or owner-derived public domains. ```php route('owner')?->domain ?? $request->getSchemeAndHttpHost(); $prefix = trim(config('formforge.http.prefix', 'api/formforge/v1'), '/'); return rtrim($ownerDomain, '/') . '/' . trim($prefix . '/forms/' . $key, '/'); } } ``` ::note `public_url` is resolved dynamically from the current request. If you need a fixed base URL, set `formforge.http.public_link.base_url` instead of deriving it from the request host. :: ## Model overrides Overridable keys under `formforge.models.*`: - `form_definition` - `form_category` - `form_submission` - `submission_file` - `staged_upload` - `idempotency_key` - `form_draft` - `submission_automation_run` - `submission_privacy_policy` - `submission_privacy_override` Each custom model must extend the corresponding package base model. ## Controller overrides Config keys: - `formforge.http.controllers.schema` - `formforge.http.controllers.submission` - `formforge.http.controllers.upload` - `formforge.http.controllers.resolve` - `formforge.http.controllers.draft` - `formforge.http.controllers.management` Rules: - extend the package controller - keep method signatures compatible - call package services instead of duplicating business logic # Query Routes `query_routes` lets you define reusable, named listing routes for forms and categories. ## Config ```php 'http' => [ 'query_routes' => [ 'forms' => [ 'legacy_or_busy' => [ 'where' => [ 'any' => [ ['field' => 'created_at', 'op' => 'lt', 'value' => '2026-01-01T00:00:00Z'], ['field' => 'responses_count', 'op' => 'gt', 'value' => 100], ], ], ], ], 'categories' => [ 'active_non_system' => [ 'where' => [ 'all' => [ ['field' => 'is_active', 'op' => 'eq', 'value' => true], ['field' => 'is_system', 'op' => 'eq', 'value' => false], ], ], ], ], ], ], ``` ## Endpoints - `GET /form-routes/{routeKey}` - `GET /category-routes/{routeKey}` Both endpoints are available on non-scoped and scoped route groups. ## Predicate DSL - `all`: AND group - `any`: OR group - nesting is supported Condition format: ```json { "field": "created_at", "op": "lt", "value": "2026-01-01T00:00:00Z" } ``` ## Operators - `eq`, `neq` - `gt`, `gte`, `lt`, `lte` - `in`, `not_in` - `contains`, `starts_with`, `ends_with` - `is_null`, `not_null` - `between` ## Aggregate fields - forms: `responses_count` - categories: `forms_count` Aggregate fields support numeric operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`. ## Form field note For forms: - `category` targets the stored category key/reference on form revisions - `category_slug` targets the linked category slug # Upload Modes and Drafts ## Upload modes Config: `formforge.uploads.mode` Values: - `managed` - `direct` - `staged` Staged flow: 1. stage file via upload endpoint 2. submit JSON payload with upload token reference ## Antivirus scanning Managed and staged uploads can be scanned through the HTTP API provided by `ajilaag/clamav-rest`. ```dotenv FORMFORGE_CLAMAV_ENABLED=true FORMFORGE_CLAMAV_ENDPOINT=https://clamav.example.com/v2/scan FORMFORGE_CLAMAV_USERNAME=clamav-client FORMFORGE_CLAMAV_PASSWORD=secret FORMFORGE_CLAMAV_TIMEOUT=30 ``` The endpoint must accept a multipart field named `file`. A clean response is HTTP 200; an infected response is HTTP 406. When scanning is enabled, scanner errors reject the upload. Basic authentication is optional. The scanner is disabled by default. Direct uploads reference files already stored elsewhere and are not scanned by FormForge. ## Drafts Drafts are owner-bound: - one draft per `(form_key, owner_type, owner_id)` - optional expiration via `drafts.ttl_days` - draft endpoints require authenticated owner # Idempotency and Automations ## Idempotency Management mutations support `Idempotency-Key`. - same key + same payload => replay previous response - same key + different payload => `409 Conflict` Config: - `formforge.http.idempotency.ttl_minutes` ## Submission automations Automations execute after submission persistence. Registration APIs: - `Form::automation('form-key')->sync()->handler(...)` - `Form::automation('form-key')->queue(...)->handler(...)` - `Form::automationForResolver(ResolverClass::class)->...` Contracts: - `SubmissionAutomation` - `SubmissionAutomationResolver` Runs are tracked in `formforge_submission_automation_runs` and execution is idempotent per submission plus automation key. ## Add metadata from an automation `EvanSchleret\FormForge\Models\FormSubmission` provides a helper to update `submission.meta` and persist immediately: ```php meta(string|array $key, mixed $value = null): self ``` Supported usages: ```php $submission->meta('team_request_id', (int) $id); ``` ```php $submission->meta([ 'team_request_id' => (int) $id, 'source' => 'automation', ]); ``` ### Complete automation example ```php create([ 'email' => (string) ($submission->payload['email'] ?? ''), 'team_name' => (string) ($submission->payload['team_name'] ?? ''), 'status' => 'pending', ]); $submission->meta([ 'team_request_id' => (int) $request->getKey(), 'source' => 'automation', ]); } } ``` ### Expose `team_request_id` in submit HTTP response Use a custom submission resource and map the meta value explicitly: ```php (string) $this->uuid, 'form_key' => (string) $this->form_key, 'payload' => $this->payload, 'team_request_id' => $this->meta['team_request_id'] ?? null, 'created_at' => $this->created_at?->toISOString(), ]; } } ``` ```php 'http' => [ 'resources' => [ 'submission' => \App\Http\Resources\FormForge\SubmissionResource::class, ], ], ``` ### Important note on queue vs sync If the automation runs in queue mode, `meta` updates may not be available immediately in the submit HTTP response. Use `->sync()` when the value must be present right away. ## Best practices - Use stable meta keys (for example `team_request_id`). - Prefer scalar or small array values for predictable serialization. - Do not store sensitive data in `meta`. # Submission Export (CSV and JSONL) Formats: - `csv` - `jsonl` (one JSON object per line) Entry points: - HTTP: `GET /forms/{key}/responses/export` - Facade: `Form::exportSubmissions(...)`, `Form::exportSubmissionsToPath(...)` - scoped facade: `Form::for(...)->exportSubmissions(...)` - CLI: `formforge:submissions:export` Schema helpers for CSV/import workflows: - `Form::get($key, $version)->exportableFields()` - `Form::get($key, $version)->flattenExportableFields()` - `Form::get($key, $version)->validateExportableHeaders(array $headers)` - `Form::get($key, $version)->mapExportableRow(array $row, bool $strict = true)` - `Form::latestByUuid(string $formUuid)` - `Form::for($owner)->latestByUuid(string $formUuid)` Supported filters: - `version` (CLI `--form-version`) - `is_test` - `submitted_by_type` - `submitted_by_id` - `has_files` - `from` / `to` - `created_from` / `created_to` CSV fields: - `id`, `form_key`, `form_version`, `is_test` - `submitted_by_type`, `submitted_by_id` - `ip_address`, `user_agent` - `created_at`, `updated_at` - `payload_json`, `files_json`, `meta_json` # GDPR Retention and Anonymization ## Policy levels 1. response override 2. form policy 3. global policy Priority: `response override > form policy > global policy/default` ## Actions - `none` - `anonymize` - `delete` ## Policy options - `after_days` - `anonymize_fields` - `delete_files` - `redact_submitter` - `redact_network` - `enabled` ## Execution notes - eligibility uses submission `created_at` - empty `anonymize_fields` means full payload anonymization - delete action removes submission and may remove files - runner supports dry-run and chunked execution ## APIs Facade: - `Form::setGdprGlobalPolicy(...)` - `Form::setGdprFormPolicy(...)` - `Form::scheduleGdprResponseAction(...)` - `Form::runGdpr(...)` CLI: - `formforge:gdpr:policy` - `formforge:gdpr:response` - `formforge:gdpr:run` HTTP: - `PUT /forms/{key}/gdpr-policy` - `POST /forms/{key}/responses/{submissionUuid}/gdpr/anonymize` - `POST /forms/{key}/responses/{submissionUuid}/gdpr/delete` - `POST /gdpr/run` # Model Overrides and Data Model ## Overridable models Config key: `formforge.models.*` - `form_definition` - `form_category` - `form_submission` - `submission_file` - `staged_upload` - `idempotency_key` - `form_draft` - `submission_automation_run` - `submission_privacy_policy` - `submission_privacy_override` Rule: custom model classes must extend package base models. ## Conceptual data model Main tables: - `formforge_forms` - `formforge_categories` - `formforge_submissions` - `formforge_submission_files` - `formforge_staged_uploads` - `formforge_drafts` - `formforge_idempotency_keys` - `formforge_submission_automation_runs` - `formforge_privacy_policies` - `formforge_submission_privacy_overrides` # Artisan Commands ## Core - `formforge:install` - `formforge:install:merge` - `formforge:sync` - `formforge:list` - `formforge:describe` ## HTTP tooling - `formforge:http:options` - `formforge:http:routes` - `formforge:http:resolve` ## Scaffolding - `formforge:make:automation` - `formforge:make:automation-resolver` - `formforge:make:http-controller` - `formforge:make:policy` ## Maintenance - `formforge:uploads:cleanup` - `formforge:drafts:cleanup` ## Exports and GDPR - `formforge:submissions:export` - `formforge:gdpr:policy` - `formforge:gdpr:response` - `formforge:gdpr:run` # Troubleshooting and Operational Notes Common issues to validate first: - ownership context required errors - scoped route parameter collisions or route ordering - optional auth endpoints and null submitter behavior - gate vs policy mismatch in scoped mode - missing management abilities despite valid auth - category key vs slug confusion - export format or filter mistakes - GDPR dry-run vs real execution confusion - missing guard or middleware wiring - model override and morph relation pitfalls # Overview FormForge is a deterministic dynamic forms engine for Laravel. Core capabilities: - code-first form definitions - immutable revision lifecycle (draft and publish) - scheduled publication windows, pause windows, response limits, and PIN protection - condition-aware schema resolution - strict server-side validation - built-in HTTP API - optional polymorphic ownership - scoped multi-context routes - public link resolution for tenant-aware form URLs - submission file workflows (`managed`, `direct`, `staged`) - submission automations - submission export (`csv`, `jsonl`) - GDPR retention and anonymization (global, form, response levels) Use this backend section as implementation reference for Laravel APIs. Installation lives in [Getting Started: Backend Installation](https://formforge.schleret.ch/docs/getting-started/installation/backend). If you want the shortest route to a concrete task, start with [Concepts](https://formforge.schleret.ch/docs/concepts) or [How do I](https://formforge.schleret.ch/docs/how-do-i). # Quickstart After Setup Install and migration are documented in [Getting Started: Backend Installation](https://formforge.schleret.ch/docs/getting-started/installation/backend). ## Quick code-first form ```php title('Contact') ->version('1') ->text('name')->required() ->text('email')->required()->max(255) ->textarea('message')->required(); Form::sync(); $submission = Form::get('contact')->submit([ 'name' => 'Evan', 'email' => 'evan@example.com', 'message' => 'Hello' ]); ``` # Integration Modes ## Mode A: Facade only - define forms in code (`Form::define(...)`) - resolve schema and submit in PHP - no package HTTP route exposure ## Mode B: Built-in HTTP API - use package routes under `formforge.http.prefix` - configure auth, guard, middleware, and abilities per endpoint group ## Mode C: Built-in HTTP API + scoped routes - expose package endpoints under contextual prefixes (`users/{user}`, `teams/{team}`) - resolve owner from route params - keep package business logic ## Mode D: HTTP API + controller override - override selected package controllers only when needed - keep method signatures compatible with package controllers - still reuse package services Scaffold override controllers: ```bash [Terminal] php artisan formforge:make:http-controller --controller=management --controller=schema ``` # Validation Overview This section covers FormForge validation flows: - full-form validation during submission - single-field validation - schema-driven partial subset validation - translated validation/error messages (EN/FR + custom locales) # Full-Form Validation ## Validation strategy - deterministic rules generated by field type - `rules(...)` appends custom rules - `replaceRules(...)` replaces generated rules - effective schema is resolved before payload validation Nullish behavior depends on `required`: - `required: false` treats null or empty string as absent - `required: true` rejects null or empty string ## Unknown fields `validation.reject_unknown_fields` controls unknown key handling in full submit flow: - `true`: unknown keys are rejected - `false`: unknown keys are ignored before persistence ## Submission lifecycle 1. fetch latest or requested version 2. resolve condition-aware effective schema 3. validate payload server-side 4. persist submission 5. run automations (sync or queued, if registered) ::note Server validation messages are user-facing and localized. Frontends should render the returned message directly instead of mapping it back to technical rule names. :: # Single-Field Validation Use this flow for real-time checks (blur/change) when you need to validate one input without submitting the whole form. API signatures: - `FormInstance::validateField(string $field, mixed $value, ?string $locale = null): array` - `FormManager::validateField(string $formKey, string $field, mixed $value, ?string $version = null, ?string $locale = null): array` - `ScopedFormManager::validateField(string $formKey, string $field, mixed $value, ?string $version = null, ?string $locale = null): array` Field identifier resolution: - `$field` accepts aliases: `name`, `field_key`, `key`, `id` - validation runs against the canonical field key `name` - if no alias matches a field, an `UnknownFieldsException` is thrown # Partial Field Validation ## Describe and resolve fields API signatures: - `FormInstance::describeFields(): array` - `FormInstance::resolveField(string $identifier): ?array` - `FormInstance::exportableFields(): array` - `FormInstance::flattenExportableFields(): array` - `FormInstance::validateExportableHeaders(array $headers): array` - `FormInstance::mapExportableRow(array $row, bool $strict = true): array` - `FormManager::describeFields(string $formKey, ?string $version = null): array` - `FormManager::resolveField(string $formKey, string $identifier, ?string $version = null): ?array` - `FormManager::exportableFields(string $formKey, ?string $version = null): array` - `FormManager::flattenExportableFields(string $formKey, ?string $version = null): array` - `FormManager::validateExportableHeaders(string $formKey, array $headers, ?string $version = null): array` - `FormManager::mapExportableRow(string $formKey, array $row, ?string $version = null, bool $strict = true): array` - `ScopedFormManager::describeFields(string $formKey, ?string $version = null): array` - `ScopedFormManager::resolveField(string $formKey, string $identifier, ?string $version = null): ?array` - `ScopedFormManager::exportableFields(string $formKey, ?string $version = null): array` - `ScopedFormManager::flattenExportableFields(string $formKey, ?string $version = null): array` - `ScopedFormManager::validateExportableHeaders(string $formKey, array $headers, ?string $version = null): array` - `ScopedFormManager::mapExportableRow(string $formKey, array $row, ?string $version = null, bool $strict = true): array` Descriptor shape: - `name` (canonical key) - `field_key`, `key`, `id`, `label` (nullable) - `type` - `required` (`bool`) - `rules` (`array`) - `options` (`array`, empty when not relevant) - `default` (mixed) - `lookup_keys` (`array`, ordered unique aliases used by resolver) Exportable field shape: - `id` (stable identifier for export/import mappings) - `path` (logical path, including composite subfield paths like `address.line1`) - `label` (human-readable column label) - `type` (field type) - `field_key`, `field_name`, `page_key` - `required`, `visible`, `disabled` - `composite` (`bool`) - `parent` and `subfield` metadata for composite fields - `rules` and `meta` for import and validation consumers ## Validate partial field subsets API signatures: - `FormInstance::validateFields(array $payload, array $onlyFields = [], ?string $locale = null): array` - `FormManager::validateFields(string $formKey, array $payload, array $onlyFields = [], ?string $version = null, ?string $locale = null): array` - `ScopedFormManager::validateFields(string $formKey, array $payload, array $onlyFields = [], ?string $version = null, ?string $locale = null): array` Behavior: - `onlyFields = []`: validate only keys present in payload that resolve to known fields - `onlyFields != []`: validate strictly the requested subset - identifiers in `onlyFields` may be aliases (`name`, `field_key`, `key`, `id`) - errors are returned by canonical `name` - unknown payload keys are ignored in this partial flow - unresolved identifiers in `onlyFields` return explicit per-identifier errors # Validation Localization FormForge translates messages while keeping technical keys stable in English. - only messages are translated - error keys remain unchanged - field and rule failures keep the same structure across locales Rule-specific validation messages are translated server-side, including required checks, length limits, date bounds, boolean checks, and option-based failures. Clients should render the returned message directly instead of trying to recompose it from technical rule names. Configuration: - `validation.locale` - `validation.fallback_locale` - `validation.supported_locales` - `validation.allow_request_locale` - `validation.locale_query_param` (default: `formforge_locale`) - `validation.locale_header` (default: `X-FormForge-Locale`) Runtime locale priority: 1. explicit locale argument in method call 2. request query/header (if enabled) 3. FormForge config locale 4. app locale 5. FormForge fallback locale ## Add your own language 1. `php artisan vendor:publish --tag=formforge-lang` 2. copy `lang/vendor/formforge/en` to your locale folder 3. translate message strings only 4. keep technical keys unchanged 5. add the locale to `validation.supported_locales` If a locale/key is missing, FormForge falls back to `validation.fallback_locale`. # FormForgeRenderer `FormForgeRenderer` renders and can submit a FormForge form. The component resolves two explicit external paths internally: - external model mode (`modelValue` provided) - external schema mode (`schema` provided) ## Internal mode Pass only a form key and let the component manage load, validation, navigation, and submit. ```vue ``` ## Standalone hybrid mode (`form-key` + `v-model`) You can combine `form-key` with `v-model` (`modelValue`) to read form data in real time without final internal submission. ```vue ``` When `modelValue` is provided: - the renderer emits `update:modelValue` on every field change - form submit does not trigger backend submission from the renderer - submit buttons are hidden by default - internal submit success and error UI states are hidden This mode is designed for external state management and custom submission flows. `showSubmit` uses tri-state behavior: - omit `show-submit` to hide submit buttons automatically when using `v-model` - use `:show-submit="true"` to show the submit button explicitly - use `:show-submit="false"` to keep it hidden explicitly ## Controlled mode Use composables and pass model and schema manually. ```vue ``` ## Progress and navigation `FormForgeRenderer` can show the current block progress and the built-in navigation buttons. - `pagination="auto"` keeps the default paginated navigation between visible blocks - `pagination="none"` renders all visible fields from all blocks in one view - `showProgress` renders a `UProgress` bar when more than one block is visible - `showProgress` is ignored when `pagination="none"` - `showSubmit` controls the submit button; it defaults to hidden with `v-model` and visible otherwise - `Previous` and `Next` appear when the renderer can navigate between blocks - `simulation` keeps navigation visible in preview-style usage - `previewPageKey` locks the renderer to a single block, which is useful for playground previews ```vue ``` Use `pagination="none"` when all fields should appear on a single screen: ```vue ``` In this mode, navigation controls and the progress indicator are hidden. Validation still covers the full form, the submitted payload remains unchanged, and conditional rules continue to work across blocks. ## Validation API (standalone and external model) `FormForgeRenderer` exposes validation helpers through the component ref: - `validate(options?)` - `validateField(name)` - `clearErrors(path?)` - `getErrors(path?)` ```vue ``` Validation props: - `validateOn` - `validateOnBlur` In external `v-model` integrations, blur-based validation is handled explicitly per field (`focusout` path) for more predictable standalone behavior. ## Integrating in a custom parent form When FormForge data is embedded in a parent form, block parent submit until renderer validation passes. ```vue ``` Use an object schema for embedded FormForge payloads (`z.record(...)`), not `z.string()`. # FormForgeBuilder `FormForgeBuilder` provides a form-definition editing UI. ```vue ``` ## Builder model shape ```ts { uuid: string | null, key: string | null, schema_version: number, title: string, publish_at?: string | null, pause_at?: string | null, response_limit?: number | null, submission_code_required?: boolean, submission_code?: string | null, public_url?: string | null, category: string | null, pages: FormForgePageSchema[], conditions: FormForgeCondition[], drafts: { enabled: boolean }, api: Record } ``` ## Current field palette The builder UI currently exposes these field families: - `text` for free text - `number` - `radio` for single choice - `checkbox_group` for multiple choice - `consent` - `address` - `temporal` for date and time fields - `file` ::note The builder UI labels schema pages as blocks. The underlying schema still stores them in `pages`. :: ## Standalone builder Use `standalone` when the builder lives inside a dedicated admin or playground screen. Combine it with `hideSettings` when you want the schema editor but not the settings tab. ```vue ``` ## UI control props Use these props to simplify the builder when you embed it in constrained admin screens. - `disableTitleInput` (`false`) - `disableCategoryControl` (`false`) - `disablePublishAction` (`false`) - `hideSettings` (`false`) - `disableSettingsTab` (`false`, deprecated alias for `hideSettings`) - `autoPublishOnSave` (`false`) - `defaultPublished` (`false`) - `standalone` (`false`) - `formRouteKey` (`undefined`) loads the first form from a configured `form-routes/{key}` endpoint when `loadFormKey` is not provided - `categoryRouteKey` (`undefined`) loads categories from `category-routes/{key}` ## Publication and save behavior - The settings tab covers opening / closing dates, response limits, and PIN protection - `autoPublishOnSave` publishes every successful save, including autosave-driven saves - `defaultPublished` only affects the initial publish state in the UI - `hideSettings` is runtime-only and is not stored on the form record - `public_url` is read-only and comes from the backend resolver when one is configured ```vue ``` ::warning If a workflow must always remain published, prefer `autoPublishOnSave` with `hideSettings`. Do not rely on `defaultPublished` alone. :: ## Exposed methods The component ref exposes these methods: - `save()` - `publish()` - `unpublish()` - `togglePublishState()` ```vue ``` ## Typical usage - create a new form definition - patch an existing form definition - publish and unpublish revisions - keep a system form published on every save # FormForgeResponse `FormForgeResponse` displays a read-only submission. ```vue ``` Main props: - `response-uuid` (required) - `form-key` (optional if inferred) - `layout` (`line` or `column`) # FormForgeCategoryCreateModal Use `FormForgeCategoryCreateModal` when admins need to create categories inline. Props: - `endpoint?` - `locale?` - `categoryRouteKey?` to align refresh/use with backend `category-routes/{key}` ```vue ``` Recommended flow: - open modal from category toolbar - create category - refresh category options in parent screen # Form and Submission Composables ## `useFormForgeSchema` Purpose: load a schema and optionally load versions. ```ts const schemaLoader = useFormForgeSchema({ key: 'contact', version: '3', immediate: true, loadVersions: true }) ``` Main API: - `fetchSchema(options?)` - `fetchVersions(options?)` - `refresh(options?)` State: - `schema` - `versions` - `loading` - `error` ## `useFormForgeGetForm` Purpose: imperative one-shot schema fetch. ```ts const getter = useFormForgeGetForm() const form = await getter.getForm({ key: 'contact' }) ``` ## `useFormForgeForm` Purpose: schema + reactive state + zod schema. ```ts const form = useFormForgeForm({ key: 'contact', immediate: true }) ``` Main API: - `fetchSchema(options?)` - `resetState()` - `setFieldValue(name, value)` - `replaceState(payload)` State: - `schema` - `state` - `loading` - `initialized` - `error` - `zodSchema` ## `useFormForgeSubmit` Purpose: submit payloads with upload strategy selection. ```ts const submitter = useFormForgeSubmit({ key: 'contact', schema: () => form.schema.value, state: () => form.state.value }) await submitter.submit({ mode: 'staged', test: true, meta: { source: 'landing-page' } }) ``` Submit options: - `mode`: `staged`, `managed`, `direct` - `version` - `meta` - `test` - `endpoint` - `scope` - `validateLocal` State: - `submitting` - `fieldErrors` - `error` - `response` ## `useFormForgeSubmission` Compatibility alias around `useFormForgeSubmit`. # Management, Categories, and Responses ## `useFormForgeManagement` ```ts const management = useFormForgeManagement({ scope: 'user' }) const formsList = await management.listForms(false, { filters: { category: 'survey', search: 'contact' } }) await formsList.refresh() ``` Methods: - `listForms(includeDeleted?, options?)` - `listFormRoute(routeKey, options?)` - `refreshForms()` / `refresh()` (global fallback list refresh) - `createForm(input, options?)` - `patchForm(key, input, options?)` - `publishForm(key, options?)` - `unpublishForm(key, options?)` - `deleteForm(key, options?)` - `getRevisions(key, includeDeleted?, options?)` - `getDiff(key, fromVersion, toVersion, options?)` State: - `forms` - `loading` - `error` - `clientError` - `fieldErrors` - `businessErrorCode` - `hasCategoryValidationError` `listForms()` and `listFormRoute()` return: - `data`: `FormForgeManagementForm[]` - `refresh()`: re-run the same list request context ### Auto-publish payload support `createForm` and `patchForm` accept both: - `auto_publish` - `autoPublish` Outgoing payloads are normalized to `auto_publish`. Create example (`POST /forms` with `auto_publish: true`): ```ts await management.createForm({ title: 'Contact', fields: [ { type: 'text', name: 'full_name', required: true } ], autoPublish: true }) ``` Patch example (`PATCH /forms/{key}` with `auto_publish: true`): ```ts await management.patchForm('form-key', { title: 'Contact v2', auto_publish: true }) ``` ## `useFormForgeCategory` ```ts const categories = useFormForgeCategory({ immediate: true, initialQuery: { per_page: 20, is_active: true } }) ``` Methods: - `listCategories(query?, options?)` - `listCategoryRoute(routeKey, query?, options?)` - `getCategory(categoryKey, options?)` - `createCategory(input, options?)` - `patchCategory(categoryKey, input, options?)` - `deleteCategory(categoryKey, options?)` - `refresh(options?)` ## `useFormForgeCategoryOptions` Transforms categories into sorted select options: - `label` - `value` - `disabled` ## `useFormForgeResponses` ```ts const responses = useFormForgeResponses({ key: 'contact', immediate: true, querySync: { enabled: true, pageKey: 'page', perPageKey: 'per_page', extraKeys: ['search', 'sort'] } }) ``` Methods: - `listResponses(query?, options?)` - `getResponse(submissionId, options?)` - `deleteResponse(submissionId, options?)` - `refresh(options?)` State: - `list` - `current` - `lastMeta` - `loading` - `error` # Drafts, Uploads, Resolver, Wizard, and i18n ## `useFormForgeDrafts` ```ts const drafts = useFormForgeDrafts({ key: 'contact' }) await drafts.saveDraft(payload) await drafts.fetchCurrentDraft() await drafts.deleteCurrentDraft() ``` State: - `draft` - `loading` - `error` ## `useFormForgeUploads` ```ts const uploads = useFormForgeUploads({ key: 'contact' }) await uploads.stageUpload({ field: 'resume', file }) ``` State: - `uploading` - `error` - `lastUpload` ## `useFormForgeResolver` ```ts const resolver = useFormForgeResolver({ key: 'contact', payload: () => form.state.value, watchPayload: true, delay: 250, immediate: false }) ``` Methods: - `resolve(payload?, options?)` - `resolveNow(payload?, options?)` - `clearScheduledResolve()` ## `useFormForgeWizard` Main API: - `nextPage()` - `previousPage()` - `goToPage(pageKey)` - `setPageIndex(index)` - `resetWizard()` ## `useFormForgeI18n` Package internal labels and translation keys. Supported locales include `en` and `fr`. # Overview `@evanschleret/formforgeclient` is the Nuxt frontend package for FormForge backend APIs. Installation lives in [Getting Started: Client Installation](https://formforge.schleret.ch/docs/getting-started/installation/client). If you want the shortest route to a concrete task, start with [Concepts](https://formforge.schleret.ch/docs/concepts) or [How do I](https://formforge.schleret.ch/docs/how-do-i). It provides three layers: - typed runtime API client capabilities - composables for schema, submission, and admin workflows - UI components for rendering, building, and reviewing responses ## Integration modes ### Renderer mode Use `` to ship fast with minimal custom code. ### Standalone orchestration Use the standalone guides when you want to keep FormForge UI components but control save, publish, preview, and page selection from your own screen. ### Controlled mode Use composables (`useFormForgeForm`, `useFormForgeSubmit`) to manage schema, local state, and submit flow yourself. ### Admin mode Use admin composables for forms, categories, responses, revisions, and diff. ## Client docs map ::card-group :::card --- icon: i-lucide-route title: Nuxt module and scoped routes to: https://formforge.schleret.ch/docs/client/overview/nuxt-module-and-scoped-routes --- Configure module options, named scopes, and per-request scope overrides. ::: :::card --- icon: i-lucide-cable title: Runtime client to: https://formforge.schleret.ch/docs/client/overview/runtime-client --- Use direct client methods when you want an imperative API layer. ::: :::card --- icon: i-lucide-function-square title: Composables to: https://formforge.schleret.ch/docs/client/composables --- Core, workflow, and admin composables with segmented guides. ::: :::card --- icon: i-lucide-panels-top-left title: Components to: https://formforge.schleret.ch/docs/client/components --- Dedicated page for each public UI component. ::: :::card --- icon: i-lucide-layers-3 title: Standalone usage to: https://formforge.schleret.ch/docs/client/standalone --- External save, publish, preview, and playground patterns for builder and renderer. ::: :: # Nuxt Module and Scoped Routes ## Module registration ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/ui', '@evanschleret/formforgeclient'], formforgeClient: { baseURL: '/api/formforge/v1', credentials: 'include', uploadMode: 'staged', datetimeMode: 'offset', locale: 'en', autoImports: true } }) ``` ## Main `formforgeClient` options - `baseURL` - `baseURLParams` - `scopedRoutes` - `defaultScope` - `scopeParams` - `credentials` - `headers` - `uploadMode` - `datetimeMode` - `locale` - `autoImports` ## Scoped routes Non-scoped endpoints: - `/api/formforge/v1/...` Scoped endpoints: - `/api/formforge/v1/users/{user}/...` - `/api/formforge/v1/teams/{team}/...` Named scope config: ```ts [nuxt.config.ts] export default defineNuxtConfig({ formforgeClient: { baseURL: '/api/formforge/v1', scopedRoutes: { user: { prefix: 'users/{user:uuid}', paramsFromRoute: { user: 'user' } }, team: { prefix: 'teams/{team}', paramsFromRoute: { team: 'team' } } }, defaultScope: 'user' } }) ``` Per-request override has priority over `defaultScope`: ```ts const management = useFormForgeManagement() await management.listForms(false, { scope: 'team' }) ``` ## Scope resolution order 1. per-request `scope` 2. global `defaultScope` 3. no scope # Runtime Client Use the runtime client directly when you want to call endpoints without composable state wrappers. ## Main method groups ### Schema and resolve - `getForm(key, options?)` - `getFormVersions(key, options?)` - `getFormVersion(key, version, options?)` - `resolveForm(key, input?, options?)` ### Submission - `submitForm(key, input, options?)` - `submitFormMultipart(key, formData, options?)` - `stageUpload(key, input, options?)` ### Drafts - `saveDraft(key, input, options?)` - `getCurrentDraft(key, options?)` - `deleteCurrentDraft(key, options?)` ### Responses - `listResponses(key, query?, options?)` - `getResponse(key, submissionId, options?)` - `deleteResponse(key, submissionId, options?)` ### Management - `listForms(includeDeleted?, options?)` - `createForm(input, options?)` - `patchForm(key, input, options?)` - `publishForm(key, options?)` - `unpublishForm(key, options?)` - `deleteForm(key, options?)` - `getRevisions(key, includeDeleted?, options?)` - `getDiff(key, fromVersion, toVersion, options?)` ### Categories - `listCategories(query?, options?)` - `getCategory(categoryKey, options?)` - `createCategory(input, options?)` - `patchCategory(categoryKey, input, options?)` - `deleteCategory(categoryKey, options?)` # Auth and Request Hooks Use the Nuxt hook `formforge:beforeRequest` to inject auth and contextual headers. ```ts [plugins/formforge-auth.client.ts] export default defineNuxtPlugin((nuxtApp) => { nuxtApp.hook('formforge:beforeRequest', ({ headers }) => { const token = useCookie('auth_token') if (typeof token.value === 'string' && token.value !== '') { headers.Authorization = `Bearer ${token.value}` } }) }) ``` With Sanctum or cookie-based auth, keep: - `credentials: 'include'` # Upload and DateTime Modes ## Upload mode - `staged` (default): stage files first, then submit upload tokens - `managed`: send multipart payload directly at submit time - `direct`: send JSON file references File fields support the following schema limits: - `accept`: accepted extensions or MIME types - `max_files`: maximum number of files when multiple selection is enabled - `max_size`: maximum size per file in bytes - `max_total_size`: maximum combined size in bytes The client validates these limits before submission. The backend remains authoritative and applies the same limits to managed and staged payloads. Per-call override: ```ts await submitter.submit({ mode: 'managed' }) ``` ## DateTime mode - `offset`: keep local timezone offset - `utc`: convert to UTC ISO ::note The current form UI treats `temporal` as the canonical field shape. Legacy temporal variants remain supported for existing schemas, but new integrations should prefer the unified `temporal` schema on the backend and `date` / `time` modes in the client renderer. :: # Error Handling The client normalizes backend and network failures into a stable shape. ## Common fields - `status` - `code` - `message` - `fieldErrors` - `businessCode` ## Typical handling ```ts try { await management.createForm(input) } catch { console.error(management.clientError.value) } ``` Example business code: - `CATEGORY_IN_USE` # Testing and FAQ ## Development commands ```bash [Terminal] bun install bun run lint bun run typecheck bun run test bun run build ``` ## Suggested coverage - named scopes (`scopedRoutes`, `defaultScope`, `scopeParams`) - scoped and non-scoped route compatibility - management filters and query serialization - submission upload mode parity - error normalization consistency ## FAQ ### Do I need scoped routes? No. Non-scoped endpoints are supported. ### Do I need to pass `scope` on every call? No. Use `defaultScope` and only override per request when needed. ### Can I filter forms by category server-side? Yes, for example: ```ts await management.listForms(false, { filters: { category: 'survey' } }) ``` ### Is owner resolved from headers in scoped mode? In scoped mode, owner resolution should happen from route params on backend side. # Standalone Usage Standalone usage keeps the FormForge UI but moves orchestration out of the component tree. Use it when you want to: - save or publish forms from your own toolbar - hide the settings tab for system-owned forms - auto-publish every save for a specific workflow - keep a renderer preview aligned with the currently selected builder block - generate tenant-aware public links from the backend ## What standalone means - the builder can expose save and publish methods through a component ref - the builder can hide its settings tab without persisting that choice in the backend - the renderer can show progress, previous, next, and submit controls in simulation mode - the backend can resolve `public_url` at response time through a request-aware resolver ::note Use standalone mode for orchestration, not for a different schema format. The same FormForge schema still powers the builder, the renderer, and the backend APIs. :: ## Guides ::card-group :::card --- icon: i-lucide-hammer title: Standalone builder to: https://formforge.schleret.ch/docs/client/standalone/builder --- Expose save and publish actions outside the builder component. ::: :::card --- icon: i-lucide-monitor title: Standalone renderer to: https://formforge.schleret.ch/docs/client/standalone/renderer --- Keep progress and navigation visible while you preview one block at a time. ::: :::card --- icon: i-lucide-link-2 title: Public links to: https://formforge.schleret.ch/docs/backend/http-api/resources-models-controllers --- Configure a resolver that generates tenant-aware `public_url` values. ::: :: # Standalone Builder `FormForgeBuilder` can run as a standalone editor when you want to keep the visual builder but move orchestration outside the component. If you do not want to use a component ref, use `useFormForgeBuilder()` directly. It exposes the same draft state plus `save()`, `publish()`, `unpublish()`, and `togglePublishState()`. ## Core props - `standalone` - `hideSettings` - `autoPublishOnSave` - `defaultPublished` ## Deprecated compatibility prop - `disableSettingsTab` Use `hideSettings` instead. The deprecated prop still works as an alias, but new integrations should not rely on it. ## Example ```vue ``` ## Exposed methods The component ref exposes: - `save()` - `publish()` - `unpublish()` - `togglePublishState()` ## Behavior notes - `autoPublishOnSave` publishes every successful save, including autosave-driven saves - `defaultPublished` affects the current publication state, but it does not persist a backend setting - `hideSettings` keeps the settings tab out of the UI without storing that choice in the form schema - `public_url` is read-only and comes from the backend resolver when one is configured ::warning When you want a system form that must always be published, use `autoPublishOnSave` together with `hideSettings`. That keeps the workflow deterministic and avoids exposing publication controls to editors. :: # Standalone Renderer `FormForgeRenderer` supports standalone and preview-style workflows where the renderer stays interactive but the parent screen owns the overall flow. ## Useful props - `showProgress` - `pagination` - `showSubmit` - `simulation` - `previewPageKey` - `validateOn` - `validateOnBlur` ## Example ```vue ``` ## Behavior notes - `pagination="auto"` is the default and keeps the renderer's block navigation - `pagination="none"` renders all visible fields on one screen and hides navigation controls - `showProgress` renders a `UProgress` bar when the form has more than one visible block - `showProgress` is ignored when `pagination="none"` - navigation buttons appear when more than one block is visible - `Previous`, `Next`, and `Submit` stay visible in simulation mode so a playground can mimic a real form run - `previewPageKey` locks the renderer to a single block, which is useful when you preview the block currently selected in the builder - the renderer still validates the current block before moving forward ::note Preview mode is not read-only by default. When `simulation` is enabled, the renderer keeps the controls visible so you can test the whole navigation flow. ::