# Dynamic forms — one screen, any FormId

[Read the formatted website guide](https://blukavantui.blutekmedia.com/guides/form-builder-storage) · Updated 2026-10-14.

The published native demo now saves definitions, answers and attachments in
OutSystems. See **Published native database example** below for the complete
one-Entity, shared-screen and shared-endpoint setup.

`BluKavantUi_Forms` owns **FormBuilder**, **FormRenderer**, and the public Client Action
**Form_PrepareSubmission**. Definitions, pages, sections and fields are JSON data.
Adding a form does not generate an OutSystems Screen, Block or Entity.

**Store the complete `OnSaveRequested.DefinitionJson` string in one `RawJson`
Text attribute. Load that same string into `DefinitionJson` to reconstruct the
whole form.** Field types, labels, pages, sections, order, choices, conditions,
validation and upload limits are all part of that JSON. No per-form widget tree
or per-form OutSystems Structure is required.

A form's **pages are steps inside FormRenderer**. Next/Back keeps the consumer on
the same OutSystems screen. The published showcase uses one `FormBuilderDemo`
screen for both designing and filling different form IDs.

Drag a field type from the toolbox into a section. The insertion line shows its
position; you can drop before or after an existing field, or into an empty section.
Existing fields can also be dragged between sections. Each change supports Undo
and Redo and uses the same definition/save events. On touch screens or with a
keyboard, click a field type to add it, then use Move up/down and Section.

The **File upload** toolbox item creates a `FileUpload` field. When someone fills
the form, it offers a drop zone and the native Browse/file picker, including on
mobile. A consumer-owned endpoint stores the bytes; answer JSON contains durable
file identifiers and metadata. See **File storage** below for the exact contract.

For fields collecting the user's own details, choose **Input purpose** in the
field settings. This stores an optional `Autocomplete` token in the definition,
such as `name`, `email`, `organization`, or `postal-code`. FormRenderer emits the
matching HTML `autocomplete` attribute on web and OutSystems. Leave it unset for
information about something else. Labels and `Type: "Email"` alone do not identify
whose information is being collected. See [WCAG input-purpose guidance](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html).

The schema remains Version 1. Existing definitions still load; add the metadata
to saved personal-information fields through the builder or your stored JSON.

## 1. Reference the library

In your consuming Reactive application, add these dependencies from `BluKavantUi_Forms`:

| Element | Use |
| --- | --- |
| FormBuilder | Optional authoring interface for users who can edit definitions. |
| FormRenderer | Displays and validates the loaded definition. |
| Form_PrepareSubmission | Prepares validated JSON and plain text for your application. |

Inherit the existing `BluUITheme` for Light/Dark/System appearance. Each Block
owns its CSS and selective runtime. A renderer-only screen does not load the
builder runtime.

## 2. Create one consumer Screen

Create **Form** with these Screen Inputs:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| FormId | Text | `""` | Your form record's identifier. |
| ResponseId | Text | `""` | Optional saved response to edit. The application authorizes access. |

Create these Screen Local Variables:

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| DefinitionJson | Text | `""` | Loaded definition for FormId. |
| ValuesJson | Text | `"{}"` | Saved or draft answers, keyed by field Key. |
| ResetKey | Text | `""` | Change when explicitly loading a different record. |
| SubmitResultRequestId | Text | `""` | Request ID being acknowledged. |
| SubmitResultSuccess | Boolean | `False` | Result of the application's submission operation. |
| SubmitResultMessage | Text | `""` | Feedback for that operation. |

Use one **Data Action** to load and authorize the form identified by `FormId` and,
if supplied, its saved response. Return DefinitionJson and ValuesJson. Keep
FormRenderer behind your loading/readiness If until that Data Action finishes;
show a useful error when the record is missing or inaccessible.

The library does not prescribe your database schema. You can keep definitions in
one table or retrieve them from a service. Do not create a screen or table per form.

### Store definitions as rows

A minimal application can use one reusable `FormDefinition` Entity with `Id` and
`RawJson` attributes, plus optional title/revision metadata. `RawJson` is Text;
configure sufficient capacity for the whole definition and reject truncation.
Use one generic load/save Server Action for every record. New forms are rows in
this Entity, rather than new Entities or Screens. Next/Back steps are also JSON
data within the same component. This avoids a screen/entity/API for each form;
the application's shared platform objects still count under its licence.

The save handler must persist the event's **RequestedDefinitionJson**, together
with its **RequestedFormId**, after checking edit permission. Acknowledge only
after the database commit. On load, authorize FormId and assign that row's
**RawJson** directly to the Block's **DefinitionJson**. Increment ResetKey when
explicitly reloading or switching records; do not increment it on every edit.

Definitions describe the form. `ValuesJson` contains a response, keyed by field
Key. A single shared response Entity can store responses for all FormIds. If your
storage contract needs both in one RawJson column, use a JSON envelope such as
`{"Definition":{...},"Values":{...},"Revision":3}`. Unpack its Definition and Values
as JSON strings before passing them to the Blocks; do not pass the envelope as
DefinitionJson. Keep the definition revision with saved responses so future edits
do not silently reinterpret old answers. Uploaded file bytes remain outside JSON.

## 3. Place one FormRenderer

In the readiness If's **True** branch, place **FormRenderer**:

| Input | Value |
| --- | --- |
| FormId | `FormId` |
| DefinitionJson | `DefinitionJson` |
| ValuesJson | `ValuesJson` |
| ResetKey | `ResetKey` |
| IsReadOnly | `False` |
| IsDisabled | `False` |
| SubmitResultRequestId | `SubmitResultRequestId` |
| SubmitResultSuccess | `SubmitResultSuccess` |
| SubmitResultMessage | `SubmitResultMessage` |
| AccessibleLabel | `"Application form"` |
| ExtendedClass | `""` |
| UploadUrl | Your shared application upload endpoint; `""` when unused |
| UploadFormat | `"Multipart"` by default; `"Binary"` for the current native demo’s raw-file REST body |
| UploadHeadersJson | Auth/CSRF headers as a JSON object; default `"{}"` |
| UploadWithCredentials | `False` unless the authorized cross-origin endpoint needs credentials |

Connect **OnValuesChanged** to a Screen Client Action with Text input
**RequestedValuesJson**, mapped from the event's **ValuesJson**. Its flow is:
**Start → Assign `ValuesJson = RequestedValuesJson` → End**.
The optional FieldKey event value identifies the changed field.

OnPageChanged and OnValidationFailed are optional reporting events. The reusable
component already handles step navigation, validation messages and error focus.

## 4. Handle one submission

Connect **OnSubmitRequested** to a Screen Client Action **SubmitForm**:

| Handler input | Type | Map from event |
| --- | --- | --- |
| RequestedRequestId | Text | RequestId |
| RequestedFormId | Text | FormId |
| RequestedValuesJson | Text | ValuesJson |

Build the flow in this order:

1. Call **Form_PrepareSubmission** with FormId = RequestedFormId,
   DefinitionJson = DefinitionJson, ValuesJson = RequestedValuesJson.
2. If **IsValid = False**, assign SubmitResultRequestId = RequestedRequestId,
   SubmitResultSuccess = False and a useful SubmitResultMessage, then End.
   ErrorsJson contains field errors; ErrorMessage contains a definition/parsing error.
3. Call **your Server Action** to save or deliver the response. It must authorize
   the record and validate the received values against the trusted definition.
4. Assign **SubmitResultRequestId = RequestedRequestId**,
   **SubmitResultSuccess = True**, **SubmitResultMessage = "Response saved."**.
5. In the action's exception handler, acknowledge the same RequestId with
   **SubmitResultSuccess = False** and a useful failure message.

A failed acknowledgement leaves answers available for retry. Unmatched or stale
request IDs cannot acknowledge another submission. Use a server-side idempotency
key when an endpoint operation must not run twice.

The helper's outputs are:

| Output | Type | Purpose |
| --- | --- | --- |
| IsValid | Boolean | Whether the supplied definition and visible answers validate. |
| PayloadJson | Text | FormId, DefinitionKey, SchemaVersion and visible Values. |
| ResponseText | Text | Plain-text labels and answers for a ticket or email body. |
| ErrorsJson | Text | FieldKey, PageKey, Label and Message for each validation failure. |
| ErrorMessage | Text | Definition/parsing error, otherwise empty. |

Use **PayloadJson** with any endpoint your application owns. Use **ResponseText**
with the application's email action. The helper does not send anything.
SchemaVersion identifies the JSON format; maintain your own record revision if
forms or responses require optimistic concurrency checks.

For an emailed edit link, the application creates an authorized link to this same
Form screen. It validates its response identifier/token and loads saved ValuesJson.
The reusable library neither creates access tokens nor emails links; those are application responsibilities.

## 5. Add authoring where needed

Place **FormBuilder** on an authorized authoring screen, or alongside the renderer
in a mode-controlled area of the same screen. Supply FormId and DefinitionJson.

| Event | Screen action behavior |
| --- | --- |
| OnDefinitionChanged | Assign its DefinitionJson back to the bound definition variable. |
| OnDirtyChanged | Optional: use IsDirty for application-level navigation protection. |
| OnSaveRequested | Persist DefinitionJson using the supplied FormId. After completion, return RequestId through SaveResultRequestId, plus SaveResultSuccess and SaveResultMessage. |
| OnError | Show or log the configuration error. |

Save success establishes the new saved baseline. Failure preserves edits. The
Block owns Preview, Undo/Redo, Discard, field placement, sections and form pages.
Use ResetKey for an explicit reload. Loading another FormId resets the component's
local state; the owning app decides whether to confirm abandoning unsaved work.

## Definition and answer contracts

The API tab documents every JSON shape and default. The canonical TypeScript
contracts are `forms-types.ts`. JSON is deliberately used instead of generating
native Structures for every user-authored field.

Supported fields: Text, TextArea, Email, Number, Date, DateTime, Checkbox, Select,
Radio, MultiSelect, Heading and Paragraph. Dates are `YYYY-MM-DD`; DateTime is
local `YYYY-MM-DDTHH:mm`, without an implied timezone.

`FileUpload` is also supported. Its definition is ordinary JSON:

```json
{
  "Key": "evidence",
  "Type": "FileUpload",
  "Label": "Inspection evidence",
  "Required": true,
  "Accept": ".pdf,.png,.jpg",
  "MaxFiles": 3,
  "MaxFileSizeMB": 10
}
```

Defaults are any file type, one file, and 10 MB per file. Limits are 1–20 files
and 0.1–100 MB per file. Files cannot be supplied as definition defaults.

## File storage

Configure **FormRenderer.UploadUrl** once on the shared screen. The component
defaults to a real HTTPS multipart POST for each selected/dropped file, with fields
`file` (binary), `formId`, `fieldKey`, and `clientKey`. Two transfers can run at a
time. `UploadHeadersJson` supports consumer-supplied authentication/CSRF headers;
the browser sets the multipart Content-Type. Endpoints and credentials are
runtime configuration and are never saved in the form definition.

The endpoint must authorize the current user and FormId, enforce the trusted
definition's file limits, inspect content, store the bytes, and return a successful
HTTP response with JSON such as `{"Id":"stored-file-123"}`. Return the durable
identifier only after storage succeeds. A missing Id, failed HTTP response, or
`stored:false` response is a failure, not a completed attachment. One generic
endpoint can serve every form and file field. Use an existing file service or an
application-owned upload adapter; BluKavantUI does not provision a storage service.

After success, **OnValuesChanged.ValuesJson** includes:

```json
{
  "evidence": [
    {"Id":"stored-file-123","Name":"inspection.pdf","Size":12840,"Type":"application/pdf"}
  ]
}
```

Persist the complete ValuesJson. Loading it again displays the saved attachments
without re-uploading them. The file input itself stays empty, as required by
browsers. Attachment metadata is untrusted input: the submission server must check
that each Id exists, belongs to an authorized upload, and is permitted for this
form. Downloads should resolve the Id through an authorized application action;
the form does not expose arbitrary file URLs.

Pending or failed files block Next/Submit until uploaded or removed. Retry and
Cancel are available, and a missing UploadUrl produces a configuration message.
Read-only/disabled forms cannot add or remove files. Reloading, changing form
definitions, hiding an uploading field, or destroying the renderer cancels its
outstanding requests and ignores late results. Hidden draft references are
excluded from submitted JSON and plain text.

Remove detaches a reference from the response; it does not delete a server file.
The application owns orphan cleanup, retention, malware checks and deletion.
The builder's Preview deliberately has no storage endpoint: use the application
renderer for real uploads. The published native database demo now supplies its
shared BluFormDemoFiles upload endpoint; it returns success only after storing bytes.

Visibility rules reference an earlier input. Hidden answers remain in the local
draft so toggling a condition restores them; final submission and plain-text
output include visible input fields only. Conditions and input validation are
client-side user experience, not a replacement for server authorization.

## TypeScript

Use `@blukavantui/web/form-builder` with `@blukavantui/web/form-builder.css` for authoring.
Use `@blukavantui/web/form-renderer` with `@blukavantui/web/form-renderer.css` for filling.
Import `@blukavantui/web/theme.css` once in the application shell.

The renderer exports **validateFormValues**, **createFormSubmission**, and
**formatFormResponseText**. Typed callbacks use objects rather than JSON strings.
The downloadable examples demonstrate switching FormId on one page, local
saving, and output preparation without calling an endpoint or sending email.

Web uses typed `Definition` and `Values` objects: `JSON.stringify(definition)` on
SaveRequested and `JSON.parse(row.RawJson)` on load provide the same storage flow.
For files, supply `UploadUrl`, `UploadHeaders` (a typed object), and optional
`UploadWithCredentials` to `createFormRenderer`. The upload endpoint contract and
stored `BluFormAttachment[]` values are identical to OutSystems.

## Published native database example


**Deployment status: published and verified on 2026-10-14.** Complete definitions,
answers and attachment references survived a full page reload; the downloaded
attachment matched the original byte for byte.
[Open the native demo](https://personal-6kr71auc.outsystemscloud.com/BluGrid_Sample/FormBuilderDemo).
An existing BluGridDemoEditor role assignment is required.

`BluGrid_Sample.FormBuilderDemo` keeps the existing shared Screen and
`BluGridDemoEditor` role. It adds one private Entity, **BluFormDemoStore**, and a
private Server Action, **FormDemo_Store**, used by both sample FormIds. Forms are
data rows; pages, sections and fields remain inside the JSON definition. There is
no separate native Screen or Entity per form.
The demo allowlists `service_request` and `event_feedback`. A production consumer
can resolve authorized form-record IDs through the same shared screen; additional
form definitions are data rows rather than separately designed native screens.

| Row Kind | Stored data |
| --- | --- |
| Definition | Complete definition in RawJson, owner, FormKey and revision |
| Response | Complete ValuesJson in RawJson with its definition revision |
| Attachment | BinaryContent plus owner, FormKey, FieldKey, client key and metadata |

This uses a single RawJson attribute for whole documents, not one attribute per
question. Attachment bytes deliberately stay out of JSON. The demo stores the
latest response per editor/FormId; production apps needing multiple respondents
should add response identifiers as data and authorize them on the same screen.
The demo SQL targets SQL Server. Adapt the queries for another database engine.

**LoadForm**, called on screen ready or Reload saved form, calls FormDemo_Store
with Operation `Load`. The server obtains the owner from GetUserId(), checks the
editor role and seeds a sample definition only when that editor has no saved row.
It returns the complete definition and saved answers. The screen sets
DefinitionJson, ValuesJson and a new ResetKey. Answers from an older definition
revision are retained in storage but are not silently applied to the new schema.
Within FormDemo_Store, LoadOrSave sends Load to its own LoadDocument query and
LoadedResult assignment. Save operations use StoreDocument. Preserve this
separation: the earlier combined query failed during initial database load.

**OnSaveRequested** calls Operation `SaveDefinition` with the complete
RequestedDefinitionJson and expected revision. A successful database commit
acknowledges the matching SaveResultRequestId. **OnSubmitRequested** first calls
Form_PrepareSubmission, then Operation `SaveResponse`. The server independently
checks required/visible fields, types, options, bounds and attachment ownership
against the stored definition. Only a successful database operation acknowledges
SubmitResultSuccess. Failed saves retain the current draft. Concurrent definition
edits require a reload instead of silently overwriting another tab.

**BluFormDemoFiles** is one shared exposed REST API with POST `/upload` and
GET `/download`. FormRenderer binds UploadUrl to
`/BluGrid_Sample/rest/BluFormDemoFiles/upload`, UploadFormat to `"Binary"`, and
UploadHeadersJson to a short-lived `X-Blu-Upload-Token` header. Authorized Load
issues a form/owner-scoped 45-minute capability; the API also rechecks that owner's
editor role. Tokens never appear in URLs, definitions, answers or event history.
The native input is marked sensitive. Responses disable caching.

The upload method checks the saved FileUpload field, file type/content signature,
size and quota before storing bytes and returning Id/Name/Size/Type. Repeating the
same client key returns the existing Id only when bytes and metadata match.
Submitting a reference to another editor's file, another form or another field
fails. Downloads require the same authorized form scope. The sample's Saved
attachment selector and Download saved attachment button retrieve actual bytes.

The demo accepts nonempty TXT, PDF, PNG and JPEG files up to 5 MB each, also
respecting the saved field's stricter Accept and size limits. It retains up to
100 files/50 MB per editor and 20 uploads per field. Removing a reference does not
delete stored bytes. Use synthetic files for this test demo; content signature
checks are not a malware scanner. Production storage owns retention and scanning.
The reusable component's generic upload limits and default transport are unchanged.

### Try the complete round trip

1. Sign in with an existing BluGridDemoEditor assignment. Choose Workplace service
   request, edit a label or add a FileUpload field, and save the form.
2. Choose Fill a form. Complete its steps, choose or drop a small supported file,
   and wait for the upload to finish. Submit the response.
3. Reload saved form or reopen the same screen and FormId. Confirm the edited
   definition, answers and attachment names return from OutSystems.
4. Select the saved attachment and download it. Compare the bytes with the file
   you uploaded. Switch to the second FormId and back to confirm isolation.
5. Enable Simulate rejected save / submit to check that failed saves keep the draft. For a
   concurrency check, load the same form in two tabs, save a definition in one,
   and confirm a stale save in the other requests a reload.

Reset playground and Reset all reset controls and reload stored data; they do not
purge the Entity. Existing browser-saved definitions are not imported or deleted.
The demo does not send email, add accounts, or grant roles.

### Native REST wiring

The current native FormRenderer adds optional UploadFormat. With `"Binary"`, it
POSTs the raw file body as `application/octet-stream`; `formId`, `fieldKey`,
`clientKey`, `fileName` and `fileType` are URL parameters. Configure the upload
REST method's Content input as Binary Data in the body, and Token as a sensitive
Text header named `X-Blu-Upload-Token`. Keep the token out of URL parameters.

The default remains `"Multipart"` with fields `file`, `formId`, `fieldKey` and
`clientKey`; existing consumers retain that contract. Binary describes the current
native library, not a new release of the web package.

Only after successful storage, return JSON such as:

```json
{"Id":"123","Name":"evidence.pdf","Size":12840,"Type":"application/pdf"}
```

Save the resulting reference in ValuesJson. Download uses the same API's GET
`/BluGrid_Sample/rest/BluFormDemoFiles/download`, with `formId` and `fileId` URL
parameters and the same authorization header. Resolve the bytes from the verified
file ID. Do not accept an arbitrary path or download URL from saved answer JSON.

The demo Entity uses one RawJson Text attribute (capacity 1,048,576), Kind,
OwnerUserId, FormKey and Revision for whole documents. Attachment rows use
BinaryContent, FieldKey, ClientKey, FileName, MimeType and FileSize. UploadToken
and TokenExpiresAt support the short-lived capability; CreatedAt and UpdatedAt
track row changes. No attributes or native Structures are generated per question.
