FormRenderer
Render any FormId on one application screen. Validates answers and navigates form steps inside this Block. FileUpload fields use a consumer-supplied upload endpoint. The application owns storage, authorization and email/edit links.
Use the complete form workflow in your app
One shared workspace loads a form by ID, saves the complete definition as RawJson, submits answers with attachment references, then restores both on reload. Built for @blukavantui/web 0.9.0.
Upload a test file, save the response, then reload. Download your saved attachment below the form. The preview stores JSON in this browser and actual files privately on this website for two hours (1 MB per file). Your app supplies its own storage and retention policy.
1. Mount the shared workspace
Install the released SDK, copy form-workspace.ts and form-http-store.ts into the same application folder, and add <div id="forms"></div>. Your server must implement the routes in step 3 before the HTTP adapter can load or save.
npm install https://blukavantui.blutekmedia.com/downloads/blukavantui-web-0.9.0.tgzimport '@blukavantui/web/theme.css';
import { mountFormWorkspace } from './form-workspace';
import { createHttpFormStore } from './form-http-store';
const store = createHttpFormStore('/api/forms', () => ({
'x-csrf-token': document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')!.content,
}));
const workspace = mountFormWorkspace(document.getElementById('forms')!, {
formIds: ['service_request', 'event_feedback'], // Authorized IDs loaded by your app
mode: 'design', // or 'fill'
store,
});
// On view/framework unmount: workspace.destroy();The web API takes typed Definition, Values and UploadHeaders objects. Native DefinitionJson, ValuesJson and UploadHeadersJson are not web component options. Only the storage adapter serializes JSON.
2. Copy the complete implementation
The callbacks await real persistence and echo matching request IDs. Reload refreshes the saved definition, answers and upload headers. Failed saves retain drafts; pending operations block record changes.
form-workspace.ts · complete source
import { createFormBuilder } from '@blukavantui/web/form-builder';
import { createFormRenderer, type BluFormDefinition, type FormValues, type BluFormAttachment } from '@blukavantui/web/form-renderer';
import '@blukavantui/web/form-builder.css';
import '@blukavantui/web/form-renderer.css';
// Load @blukavantui/web/theme.css once in your application's browser entry.
// These interfaces belong to this application example, not to the component SDK.
export interface SavedForm {
FormId: string;
RawJson: string; // JSON.stringify(complete BluFormDefinition)
ValuesJson: string; // JSON.stringify(complete submitted FormValues)
Revision: number; // server-issued definition revision
ResponseRevision: number; // server-issued response revision (prevents lost edits)
Upload: { url: string; headers: Record<string, string>; withCredentials?: boolean };
}
export interface FormStore {
load(formId: string, signal: AbortSignal): Promise<SavedForm>;
saveDefinition(formId: string, rawJson: string, expectedRevision: number, signal: AbortSignal): Promise<{ Revision: number }>;
saveResponse(formId: string, valuesJson: string, definitionRevision: number, expectedResponseRevision: number, signal: AbortSignal): Promise<{ ResponseRevision: number }>;
download(fileId: string, signal: AbortSignal): Promise<Blob>;
}
export interface WorkspaceOptions {
formIds: string[];
store: FormStore;
mode?: 'design' | 'fill';
}
/** One mounted workspace loads any authorized FormId. No DOM tree per form. */
export function mountFormWorkspace(host: HTMLElement, options: WorkspaceOptions) {
const lifetime = new AbortController(), store = options.store;
const panel = document.createElement('section'), controls = document.createElement('div');
controls.style.cssText = 'display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-bottom:16px';
const select = document.createElement('select'); select.setAttribute('aria-label', 'Form to load');
for (const id of options.formIds) { const option = document.createElement('option'); option.value = id; option.textContent = id; select.append(option); }
const reload = document.createElement('button'); reload.type = 'button'; reload.textContent = 'Reload saved form (discard draft)';
const design = document.createElement('button'); design.type = 'button'; design.textContent = 'Design form';
const fill = document.createElement('button'); fill.type = 'button'; fill.textContent = 'Fill saved form';
for (const button of [reload, design, fill]) button.className = 'blu-forms__button';
const status = document.createElement('p'); status.setAttribute('role', 'status');
const surface = document.createElement('div'), downloads = document.createElement('div');
controls.append(select, reload, design, fill); panel.append(controls, status, surface, downloads); host.append(panel);
let record: SavedForm | undefined, activeId = options.formIds[0] || '', mode = options.mode || 'fill';
let builder: ReturnType<typeof createFormBuilder> | undefined, renderer: ReturnType<typeof createFormRenderer> | undefined;
let busy = false, generation = 0, reset = 0, disposed = false;
const current = (ticket: number) => !disposed && ticket === generation;
function message(error: unknown) { return error instanceof Error ? error.message : 'The operation failed. Please retry.'; }
function setBusy(value: boolean) {
busy = value;
for (const input of [select, reload, design, fill]) input.disabled = value;
builder?.update({ IsDisabled: value }); renderer?.update({ IsDisabled: value });
}
function fileLinks(definition: BluFormDefinition, values: FormValues) {
downloads.replaceChildren();
const fields = definition.Pages.flatMap(page => page.Sections.flatMap(section => section.Fields)).filter(field => field.Type === 'FileUpload');
for (const field of fields) {
const files = values[field.Key]; if (!Array.isArray(files)) continue;
for (const file of files as BluFormAttachment[]) {
if (!file || typeof file.Id !== 'string') continue;
const button = document.createElement('button'); button.type = 'button'; button.className = 'blu-forms__button';
button.textContent = 'Download ' + file.Name; button.style.margin = '8px';
button.onclick = async () => {
button.disabled = true; const ticket = generation;
try {
const blob = await store.download(file.Id, lifetime.signal); if (!current(ticket)) return;
const url = URL.createObjectURL(blob), anchor = document.createElement('a');
anchor.href = url; anchor.download = file.Name; anchor.hidden = true; panel.append(anchor); anchor.click(); anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (error) { if (current(ticket)) status.textContent = message(error); }
finally { button.disabled = false; }
};
downloads.append(button);
}
}
}
async function load(formId: string) {
if (busy || disposed) return;
let ticket = generation; setBusy(true); status.textContent = 'Loading saved form…';
try {
const loaded = await store.load(formId, lifetime.signal); if (!current(ticket)) return;
if (loaded.FormId !== formId) throw Error('The server returned a different form.');
const definition = JSON.parse(loaded.RawJson) as BluFormDefinition, values = JSON.parse(loaded.ValuesJson) as FormValues;
if (definition.Version !== 1 || !Array.isArray(definition.Pages) || !values || Array.isArray(values) || typeof values !== 'object') throw Error('Invalid saved form JSON.');
record = loaded; activeId = formId; select.value = formId;
ticket = ++generation;
// Replace the instances only on an explicit load/mode change, never on each keystroke.
builder?.destroy(); renderer?.destroy(); builder = undefined; renderer = undefined;
surface.replaceChildren(); downloads.replaceChildren();
const resetKey = String(++reset);
if (mode === 'design') {
builder = createFormBuilder(surface, { FormId: formId, Definition: definition, ResetKey: resetKey }, {
async SaveRequested(requestId, requestedFormId, requestedDefinition) {
if (busy || requestedFormId !== activeId || !current(ticket)) return;
setBusy(true);
try {
const result = await store.saveDefinition(requestedFormId, JSON.stringify(requestedDefinition), loaded.Revision, lifetime.signal);
if (!current(ticket)) return;
loaded.Revision = result.Revision; loaded.RawJson = JSON.stringify(requestedDefinition);
builder!.update({ SaveResultRequestId: requestId, SaveResultSuccess: true, SaveResultMessage: 'Form saved. Choose Fill saved form to use it.' });
status.textContent = 'Complete definition saved as RawJson.';
} catch (error) {
if (current(ticket)) { builder!.update({ SaveResultRequestId: requestId, SaveResultSuccess: false, SaveResultMessage: message(error) }); status.textContent = message(error); }
} finally { if (current(ticket)) setBusy(false); }
},
Error: text => { status.textContent = text; },
});
} else {
renderer = createFormRenderer(surface, { FormId: formId, Definition: definition, Values: values, ResetKey: resetKey,
UploadUrl: loaded.Upload.url, UploadHeaders: loaded.Upload.headers, UploadWithCredentials: loaded.Upload.withCredentials || false }, {
ValuesChanged: draft => { fileLinks(definition, draft); }, // Draft state only; uploads yield durable IDs here.
async SubmitRequested(requestId, requestedFormId, submittedValues) {
if (busy || requestedFormId !== activeId || !current(ticket)) return;
setBusy(true);
try {
const result = await store.saveResponse(requestedFormId, JSON.stringify(submittedValues), loaded.Revision, loaded.ResponseRevision, lifetime.signal);
if (!current(ticket)) return;
loaded.ResponseRevision = result.ResponseRevision; loaded.ValuesJson = JSON.stringify(submittedValues);
renderer!.update({ SubmitResultRequestId: requestId, SubmitResultSuccess: true, SubmitResultMessage: 'Answers and attachment references saved.' });
fileLinks(definition, submittedValues); status.textContent = 'Saved. Reload to restore the form, answers and attachments.';
} catch (error) {
if (current(ticket)) { renderer!.update({ SubmitResultRequestId: requestId, SubmitResultSuccess: false, SubmitResultMessage: message(error) }); status.textContent = message(error); }
} finally { if (current(ticket)) setBusy(false); }
},
Error: text => { status.textContent = text; },
});
fileLinks(definition, values);
}
design.setAttribute('aria-pressed', String(mode === 'design')); fill.setAttribute('aria-pressed', String(mode === 'fill'));
status.textContent = 'Loaded saved definition and answers. Files are downloaded through your application.';
} catch (error) { if (current(ticket)) { select.value = activeId; status.textContent = message(error); } }
finally { if (current(ticket)) setBusy(false); }
}
select.onchange = () => { void load(select.value); };
reload.onclick = () => { void load(activeId); };
design.onclick = () => { if (!busy) { mode = 'design'; void load(activeId); } };
fill.onclick = () => { if (!busy) { mode = 'fill'; void load(activeId); } };
void load(activeId);
return { reload() { return load(activeId); }, get record() { return record; }, destroy() { disposed = true; generation++; lifetime.abort(); builder?.destroy(); renderer?.destroy(); panel.remove(); } };
}
form-http-store.ts · complete source
import type { FormStore, SavedForm } from './form-workspace';
/** Connect the workspace to same-origin application routes documented in the guide.
* Implement these routes on YOUR server; they are not included in the SDK.
* The server derives the owner from its session, validates the stored definition,
* checks attachment ownership, and uses optimistic concurrency for both records.
*/
export function createHttpFormStore(baseUrl: string, headers: () => Record<string, string>): FormStore {
const base = baseUrl.replace(/\/$/, '');
async function request<T>(path: string, signal: AbortSignal, body?: unknown): Promise<T> {
const response = await fetch(base + path, { method: body === undefined ? 'GET' : 'PUT', signal,
credentials: 'same-origin', cache: 'no-store',
headers: { Accept: 'application/json', ...headers(), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw Error(result?.error || 'Request failed (' + response.status + '). Your draft is retained.');
if (!result) throw Error('The server returned an invalid JSON response.');
return result as T;
}
return {
load: (formId, signal) => request<SavedForm>('/' + encodeURIComponent(formId), signal),
saveDefinition: (formId, RawJson, ExpectedRevision, signal) => request('/' + encodeURIComponent(formId) + '/definition', signal, { RawJson, ExpectedRevision }),
saveResponse: (formId, ValuesJson, DefinitionRevision, ExpectedResponseRevision, signal) => request('/' + encodeURIComponent(formId) + '/response', signal, { ValuesJson, DefinitionRevision, ExpectedResponseRevision }),
async download(fileId, signal) {
const response = await fetch(base + '/attachments/' + encodeURIComponent(fileId), { signal, credentials: 'same-origin', cache: 'no-store', headers: headers() });
if (!response.ok) throw Error(response.status === 404 ? 'Attachment expired or unavailable. Remove it and upload it again.' : 'Download failed. Please retry.');
return response.blob();
},
};
}
Website preview adapter · browser JSON and temporary uploads
The published Code preview uses the same workspace with website-demo-store.ts: browser-local JSON/answers, actual session-private website uploads, 1 MiB each, 12 files / 8 MiB per visitor, automatic two-hour expiry. These are website demo limits, not SDK or production limits. Do not call blukavantui.blutekmedia.com/demo/forms/files from another app.
import { validateFormValues, type BluFormDefinition, type FormValues } from '@blukavantui/web/form-renderer';
import type { FormStore } from './form-workspace';
// WEBSITE DEMONSTRATION ONLY. Your own app uses createHttpFormStore and its own
// endpoints. /demo/forms/files is private to this site's session and is not a
// public upload service. Definitions/answers below are browser-local, NOT a DB.
const definition: BluFormDefinition = { Version: 1, Key: 'web_storage_example', Title: 'Save a form with attachments',
Description: 'Upload a test file, submit, reload this page and download it. Demo uploads expire after two hours.',
SubmitLabel: 'Save response & attachments', Pages: [{ Key: 'request', Title: 'Your request', Sections: [{ Key: 'details', Fields: [
{ Key: 'notes', Type: 'TextArea', Label: 'Notes', MaxLength: 500 },
{ Key: 'files', Type: 'FileUpload', Label: 'Supporting files', Required: true, Accept: '.txt,.pdf,.png,.jpg,.jpeg', MaxFiles: 3, MaxFileSizeMB: 1 },
] }] }] };
type BrowserRecord = { RawJson: string; ValuesJson: string; Revision: number; ResponseRevision: number };
export function createWebsiteDemoStore(scenario: string): FormStore {
const key = (id: string) => 'blukavantui-code-storage-v1:' + scenario + ':' + id;
const read = (id: string): BrowserRecord => JSON.parse(localStorage.getItem(key(id)) || 'null') || { RawJson: JSON.stringify(definition), ValuesJson: '{}', Revision: 1, ResponseRevision: 0 };
const save = (id: string, record: BrowserRecord) => localStorage.setItem(key(id), JSON.stringify(record));
async function files(signal: AbortSignal) {
const response = await fetch('/demo/forms/files', { signal, credentials: 'same-origin', cache: 'no-store' });
if (!response.ok) throw Error('Could not connect attachment storage. Reload and retry.');
return (await response.json()).files as { Id: string; formId: string; fieldKey: string; ExpiresAt: number }[];
}
return {
async load(FormId, signal) {
await files(signal);
return { FormId, ...read(FormId), Upload: { url: '/demo/forms/files', headers: { 'x-csrf-token': document.querySelector<HTMLMetaElement>('meta[name=csrf-token]')?.content || '' } } };
},
async saveDefinition(id, RawJson, expectedRevision) {
const old = read(id); if (old.Revision !== expectedRevision) throw Error('This definition changed in another tab. Reload before saving.');
JSON.parse(RawJson);
const next = { RawJson, Revision: old.Revision + 1, ValuesJson: '{}', ResponseRevision: old.ResponseRevision + 1 };
save(id, next); return { Revision: next.Revision };
},
async saveResponse(id, ValuesJson, definitionRevision, expectedResponseRevision, signal) {
const storedFiles = await files(signal), old = read(id), form: BluFormDefinition = JSON.parse(old.RawJson), values: FormValues = JSON.parse(ValuesJson);
if (old.Revision !== definitionRevision || old.ResponseRevision !== expectedResponseRevision) throw Error('This record changed in another tab. Reload before submitting.');
const errors = validateFormValues(form, values); if (errors.length) throw Error(errors[0].Message);
for (const field of form.Pages.flatMap(page => page.Sections.flatMap(section => section.Fields)).filter(field => field.Type === 'FileUpload')) {
const references = values[field.Key]; if (!Array.isArray(references)) continue;
for (const file of references) if (typeof file !== 'object' || !storedFiles.some(stored => stored.Id === file.Id && stored.formId === id && stored.fieldKey === field.Key && stored.ExpiresAt > Date.now())) throw Error('An attachment expired. Remove it and upload it again.');
}
save(id, { ...old, ValuesJson, ResponseRevision: old.ResponseRevision + 1 }); return { ResponseRevision: old.ResponseRevision + 1 };
},
async download(id, signal) {
const response = await fetch('/demo/forms/files/' + encodeURIComponent(id), { signal, credentials: 'same-origin', cache: 'no-store' });
if (!response.ok) throw Error('This attachment expired or is unavailable in this browser session.');
return response.blob();
},
};
}
3. Connect your backend
These are application-owned routes used by the HTTP adapter, not endpoints included in the SDK. Register fixed /attachments routes before /:formId in routers that use first-match ordering.
GET /api/forms/:formId
Authorize owner and form. Load the saved definition and compatible response. Supply fresh upload headers separately; do not persist Upload in RawJson.
Authenticated session; optional CSRF/auth headers{
"FormId": "service_request",
"RawJson": "JSON.stringify(completeDefinition)",
"ValuesJson": "JSON.stringify(savedAnswers)",
"Revision": 1,
"ResponseRevision": 0,
"Upload": {
"url": "/api/forms/attachments",
"headers": {
"x-csrf-token": "fresh session CSRF token"
},
"withCredentials": false
}
}PUT /api/forms/:formId/definition
Authorize design permission, validate definition, compare revision and commit atomically. Return 409 on conflict. Changing the schema needs a response migration or invalidation policy.
{
"RawJson": "complete definition JSON string",
"ExpectedRevision": 1
}{
"Revision": 2
}PUT /api/forms/:formId/response
Authorize the respondent. Validate against the stored definition: visible/required fields, types, choices, limits. Recheck every attachment owner/form/field/expiry and stored bytes. Compare both revisions and commit atomically. Return 409 on conflict; 422 for invalid data.
{
"ValuesJson": "complete answer JSON string",
"DefinitionRevision": 2,
"ExpectedResponseRevision": 0
}{
"ResponseRevision": 1
}POST /api/forms/attachments
Authenticate and authorize. Validate the stored FileUpload field, actual size/type, file counts and quota; store bytes before returning 2xx JSON. Id must identify actual durable storage. Do not return stored:false or invent an ID. Use clientKey for idempotent retries scoped to owner/form/field; reject reuse with different content.
multipart/form-data: file (binary), formId (text), fieldKey (text), clientKey (text). Browser sets Content-Type and boundary.{
"Id": "durable opaque storage ID",
"stored": true
}GET /api/forms/attachments/:id
Authorize by stored owner/form, never by caller-supplied owner ID. Return 404 for inaccessible/deleted/expired files. The workspace fetches a Blob with auth headers and downloads a short-lived object URL; it does not put credentials in URLs.
Authenticated session and required auth headersStored file bytes; Content-Disposition: attachment; Cache-Control: private, no-store; X-Content-Type-Options: nosniffUpload contract
Set UploadUrl and fresh UploadHeaders. The renderer sends file, formId, fieldKey and clientKey as multipart form data. Return {"Id":"stored-file-id","stored":true} only after storing the bytes. Do not set the multipart Content-Type yourself.
File IDs arrive through ValuesChanged(values, fieldKey); SubmitRequested(requestId, formId, values) saves the final answers. Downloads fetch authenticated bytes and use a temporary object URL. No credentials or bytes go into saved form JSON.
Storage, authorization and concurrency requirements
- Derive owner from authenticated server context; preserve existing role checks and CSRF protection. The list of FormIds in the browser is not authorization.
- Keep file bytes private, use random opaque IDs, validate content as well as extensions, enforce body/field/file limits and quotas. Apply scanning/quarantine and retention appropriate to your application.
- Use transactional compare-and-swap for both definition and response revisions. The backend validates JSON and attachments independently of client-side helpers.
- Link uploaded IDs only after successful response persistence. Define orphan cleanup and expiry; removing a reference from the renderer does not delete stored bytes automatically.
- Register fixed /attachments routes before /:formId where your router uses first-match routing. Upload/download share the same storage and owner checks.
- The provided HTTP adapter is for same-origin routes. Replace authentication and transport deliberately if your application uses another deployment layout.
4. Verify your integration
- Design and save a form containing FileUpload; reload and verify every field/condition/file limit is retained.
- Upload a small test file; verify values contain Id/Name/Size/Type and actual bytes exist server-side. Submit, fully reload the page and download; compare bytes.
- Reject a definition save and response save; verify the draft remains and matching failure acknowledgement is shown.
- Reject unauthorized downloads, invalid/oversized files, wrong form/field IDs, expired references and stale revisions. Test concurrent tabs.
- Switch FormId on the shared page and unmount during a pending request; late results must not update another record or removed view.
Download the complete recipe and source bundle as JSON ↓ · Web API reference →
For an AI assistant, use blukavantui_get_guide({"topic":"web-form-storage"}). MCP setup →
createFormRenderer
Pass Definition and Values as JavaScript objects. Handle SubmitRequested in the third argument. The submission helpers prepare output only; your application validates and stores or sends it.
import { createFormRenderer } from '@blukavantui/web/form-renderer';
import '@blukavantui/web/form-renderer.css';Load @blukavantui/web/theme.css once in your app shell.createFormRenderer(host: HTMLElement, options: FormRendererOptions, events?: FormRendererEvents)Reference for @blukavantui/web 0.9.0. Required and optional follow the shipped TypeScript declarations; optional does not imply a particular default. Pass typed arrays and objects directly where shown.
Options
14| Name | Type / requirement | Usage |
|---|---|---|
UploadUrl | stringOptional | Consumer-owned HTTPS multipart endpoint; POST file, formId, fieldKey, clientKey. Respond with JSON {Id:"durable-storage-id"} after storage succeeds. Never embed endpoints or credentials in a stored definition. |
UploadHeaders | Record<string, string>Optional | Per-request auth/CSRF headers. Default {}. Never persisted in the definition. |
UploadWithCredentials | booleanOptional | Enables credentials for cross-origin uploads. Defaults to false. |
Values | FormValuesOptional | Initial/external answers for editing an existing response. Omitted values use field defaults. |
IsReadOnly | booleanOptional | Displays the current value without allowing edits. |
SubmitResultRequestId | stringOptional | The submission request being acknowledged. |
SubmitResultSuccess | booleanOptional | Whether the matching submission succeeded. |
SubmitResultMessage | stringOptional | Feedback for the matching submission result. |
FormId | stringOptional | Owning application's record ID. Defaults to Definition.Key; never fetches by itself. |
Definition | BluFormDefinitionRequired | Data loaded by the owning app for the current FormId. |
IsDisabled | booleanOptional | Disables interaction with this component. |
ResetKey | stringOptional | Change to explicitly reload the definition and reset local state. |
AccessibleLabel | stringOptional | Accessible name for assistive technology. |
ExtendedClass | stringOptional | Additional CSS class names on the component root. |
Callbacks
5| Name | Type / requirement | Usage |
|---|---|---|
ValuesChanged | (values: FormValues, fieldKey: string) => void | Promise<void>Optional | Reports updated answers. |
PageChanged | (pageKey: string, pageNumber: number) => void | Promise<void>Optional | |
ValidationFailed | (errors: BluFormError[]) => void | Promise<void>Optional | |
SubmitRequested | (requestId: string, formId: string, values: FormValues) => void | Promise<void>Optional | Reports the prepared submission for your application to validate and persist. |
Error | (message: string) => void | Promise<void>Optional |
Instance methods & properties
3| Name | Signature / type | Usage |
|---|---|---|
element | HTMLElement | Root owned by this instance. Dispose before removing its host. |
update | (patch: Partial<{ Values: FormValues; UploadHeaders: Record<string, string>; UploadUrl?: string; UploadWithCredentials?: boolean; IsReadOnly?: boolean; SubmitResultRequestId?: string; SubmitResultSuccess?: boolean; SubmitResultMessage?: string; FormId?: string; Definition: BluFormDefinition; IsDisabled?: boolean; ResetKey?: string; AccessibleLabel?: string; ExtendedClass?: string; }>) => void | Apply a partial options update to this instance. |
destroy | () => void | Dispose the instance and its listeners when the view is removed. |
Exported helpers
validateFormValues
Client-side UX validation. The receiving server must also validate submissions.
validateFormValues(definition: BluFormDefinition, values: FormValues): BluFormError[]createFormSubmission
Returns JSON-ready output only; does not fetch, save or send email.
createFormSubmission(formId: string, definition: BluFormDefinition, values: FormValues): BluFormSubmissionformatFormResponseText
formatFormResponseText(definition: BluFormDefinition, values: FormValues): stringRelated data types
Expand a type for its fields and allowed values.
BluFormAttachment
Durable storage reference returned by the application's upload endpoint. File bytes stay outside JSON.
interface BluFormAttachment {
Id: string;
Name: string;
Size: number;
Type: string;
}BluFormCondition
interface BluFormCondition {
/** Key of an earlier input field. Forward references and cycles are rejected. */
FieldKey: string;
Operator: 'Equals' | 'NotEquals' | 'Contains' | 'IsEmpty' | 'IsNotEmpty';
/** Text representation; checkboxes use "true"/"false". Ignored for empty tests. */
Value?: string;
}BluFormDefinition
Pages are steps within one mounted component, never routes or OutSystems screens.
interface BluFormDefinition {
Version: 1;
Key: string;
Title: string;
Description?: string;
SubmitLabel?: string;
Pages: BluFormPage[];
}BluFormError
interface BluFormError {
FieldKey: string;
PageKey: string;
Message: string;
Label: string;
}BluFormField
interface BluFormField {
/** Stable answer key, unique within the definition. */
Key: string;
Type: FormFieldType;
Label: string;
HelpText?: string;
Placeholder?: string;
/** HTML input-purpose token (for example name, email, organization or postal-code).
* Set when collecting the user's own details. Omit for unrelated information.
* Supported on Text, TextArea, Email, Number, Date, DateTime and Select fields. */
Autocomplete?: string;
Required?: boolean;
DefaultValue?: FormValue;
/** Required for Select, Radio and MultiSelect. */
Options?: BluFormOption[];
/** Number fields only. Omit either bound to leave that side unlimited. */
Min?: number;
Max?: number;
MaxLength?: number;
/** FileUpload: comma-separated extensions or MIME types. Empty allows any type. */
Accept?: string;
/** FileUpload: 1–20; defaults to 1. */
MaxFiles?: number;
/** FileUpload: 0.1–100 MB per file; defaults to 10. Enforce limits on the server too. */
MaxFileSizeMB?: number;
Visibility?: BluFormCondition;
}BluFormOption
interface BluFormOption {
Value: string;
Label: string;
}BluFormPage
interface BluFormPage {
Key: string;
Title: string;
Description?: string;
Sections: BluFormSection[];
}BluFormSection
interface BluFormSection {
Key: string;
Title?: string;
Description?: string;
Columns?: 1 | 2;
Fields: BluFormField[];
}BluFormSubmission
interface BluFormSubmission {
FormId: string;
DefinitionKey: string;
SchemaVersion: 1;
Values: FormValues;
}FormBase
interface FormBase {
/** Owning application's record ID. Defaults to Definition.Key; never fetches by itself. */
FormId?: string;
/** Data loaded by the owning app for the current FormId. */
Definition: BluFormDefinition;
IsDisabled?: boolean;
/** Change to explicitly reload the definition and reset local state. */
ResetKey?: string;
AccessibleLabel?: string;
ExtendedClass?: string;
}FormFieldType
type FormFieldType = 'Text' | 'TextArea' | 'Email' | 'Number' | 'Date' | 'DateTime' | 'Checkbox' | 'Select' | 'Radio' | 'MultiSelect' | 'FileUpload' | 'Heading' | 'Paragraph';FormValue
type FormValue = string | number | boolean | string[] | BluFormAttachment[];FormValues
type FormValues = Record<string, FormValue>;Use these inputs, events, actions and structures in Service Studio. Names and data bindings are specific to the native Blocks.
Use one RawJson field for complete form definitions, one shared screen to load any FormId, and one shared upload endpoint for attachments. The published native demo saves definitions, answers and file bytes in OutSystems.
Save, reload & attachments: complete OutSystems setup →
Part of the full commercial BluKavantUI for OutSystems edition. View OutSystems editions →
@if(nativeUrl)@if(nativeUrl)Open OutSystems showcase ↗@endif@endif- Web runtime
- Supported
- TypeScript
- Supported
- OutSystems
- Supported
Statuses describe the component’s rendered code and interactions. Supply accessible content and follow the component guidance when integrating it.
Accessibility guidance →Purpose and use
Render any FormId on one application screen. Validates answers and navigates form steps inside this Block. FileUpload fields use a consumer-supplied upload endpoint. The application owns storage, authorization and email/edit links.
Keyboard design guidance
Use Tab to reach each labelled control. Fields can be reordered with buttons without dragging. Next validates the current step; validation links focus the affected field.
See the support status above for review coverage and any limitations.
In your application
Provide useful labels, preserve visible focus, and test the component with your actual content. Use text alongside status colors and check contrast in each state.
Events0No events yet.
Interact with an example to see its events.