FormBuilder
Build dynamic, multi-page forms as versioned JSON. Owns field editing, sections, conditions, preview, undo and correlated saving. No application storage or endpoint calls.
Live TypeScript example
Try the component here, then use the source below in your application.
- JavaScript
- 12.2 KiB
- Component CSS
- 3.1 KiB
- Shared theme CSS
- 3.8 KiBonce per application
How sizes are measured
The JavaScript figure includes this component’s required helpers. CSS includes its styling dependencies. Each entry is bundled separately for ES2022, minified, then compressed with gzip level 9. Your bundler may share helpers across components. Images, fonts, examples and application code are excluded.
Before compression: JavaScript 36.1 KiB; component CSS 15.3 KiB. These figures describe browser assets, not the complete package download.
View all component measurements →Add fields, edit properties, try Preview, then Save form. This example saves only in browser storage. Your application chooses its storage and endpoints.
import '@blukavantui/web/theme.css'
import { createFormBuilder } from '@blukavantui/web/form-builder';
import '@blukavantui/web/form-builder.css';
import type { BluFormDefinition } from '@blukavantui/web/forms';
const definitions: Record<string, BluFormDefinition> = {
"service_request": {
"Version": 1,
"Key": "service_request",
"Title": "Workplace service request",
"Description": "Tell us what you need. Your answers stay here as you move between steps.",
"SubmitLabel": "Submit request",
"Pages": [
{
"Key": "requester_page",
"Title": "About you",
"Description": "Start with your contact details.",
"Sections": [
{
"Key": "contact_section",
"Title": "Contact details",
"Columns": 2,
"Fields": [
{
"Key": "full_name",
"Type": "Text",
"Label": "Full name",
"Required": true,
"Placeholder": "Jordan Lee",
"MaxLength": 100
},
{
"Key": "email",
"Type": "Email",
"Label": "Work email",
"Required": true,
"Placeholder": "jordan@example.com"
},
{
"Key": "department",
"Type": "Select",
"Label": "Department",
"Required": true,
"Options": [
{
"Value": "operations",
"Label": "Operations"
},
{
"Value": "engineering",
"Label": "Engineering"
},
{
"Value": "people",
"Label": "People team"
}
]
}
]
}
]
},
{
"Key": "details_page",
"Title": "Request details",
"Sections": [
{
"Key": "request_section",
"Title": "What can we help with?",
"Columns": 1,
"Fields": [
{
"Key": "request_type",
"Type": "Radio",
"Label": "Request type",
"Required": true,
"Options": [
{
"Value": "equipment",
"Label": "Equipment"
},
{
"Value": "workspace",
"Label": "Workspace"
},
{
"Value": "other",
"Label": "Something else"
}
]
},
{
"Key": "budget",
"Type": "Number",
"Label": "Estimated budget",
"HelpText": "Shown only for equipment requests.",
"Min": 0,
"Max": 10000,
"Required": true,
"Visibility": {
"FieldKey": "request_type",
"Operator": "Equals",
"Value": "equipment"
}
},
{
"Key": "needed_by",
"Type": "Date",
"Label": "Needed by",
"Required": true
},
{
"Key": "description",
"Type": "TextArea",
"Label": "Describe your request",
"Required": true,
"MaxLength": 2000
}
]
}
]
},
{
"Key": "confirm_page",
"Title": "Preferences & confirmation",
"Sections": [
{
"Key": "preferences_section",
"Title": "Stay informed",
"Columns": 1,
"Fields": [
{
"Key": "updates",
"Type": "MultiSelect",
"Label": "Preferred updates",
"Options": [
{
"Value": "email",
"Label": "Email"
},
{
"Value": "portal",
"Label": "In-app notification"
}
],
"DefaultValue": [
"email"
]
},
{
"Key": "confirmed",
"Type": "Checkbox",
"Label": "I confirm these details are correct",
"Required": true
}
]
}
]
}
]
},
"event_feedback": {
"Version": 1,
"Key": "event_feedback",
"Title": "How was your experience?",
"Description": "A second form definition, loaded by ID on the same screen.",
"SubmitLabel": "Send feedback",
"Pages": [
{
"Key": "experience_page",
"Title": "Your experience",
"Sections": [
{
"Key": "experience_section",
"Columns": 1,
"Fields": [
{
"Key": "rating",
"Type": "Radio",
"Label": "Overall experience",
"Required": true,
"Options": [
{
"Value": "great",
"Label": "Great"
},
{
"Value": "okay",
"Label": "Okay"
},
{
"Value": "poor",
"Label": "Needs improvement"
}
]
},
{
"Key": "comments",
"Type": "TextArea",
"Label": "What could we improve?",
"MaxLength": 2000
}
]
}
]
},
{
"Key": "followup_page",
"Title": "Follow-up",
"Sections": [
{
"Key": "followup_section",
"Fields": [
{
"Key": "followup",
"Type": "Checkbox",
"Label": "I would like someone to follow up"
},
{
"Key": "contact_email",
"Type": "Email",
"Label": "Contact email",
"Required": true,
"Visibility": {
"FieldKey": "followup",
"Operator": "Equals",
"Value": "true"
}
}
]
}
]
}
]
}
};
export function mount(host: HTMLElement) {
const picker = document.createElement('select');
picker.setAttribute('aria-label', 'Load form by ID');
for (const [id, definition] of Object.entries(definitions)) {
const option = document.createElement('option'); option.value = id;
option.textContent = definition.Title; picker.append(option);
}
const surface = document.createElement('div'), output = document.createElement('pre');
output.style.whiteSpace = 'pre-wrap'; output.style.overflowWrap = 'anywhere';
const label = document.createElement('label'); label.textContent = 'Load form by ID '; label.append(picker);
host.append(label, surface, output);
let formId = 'service_request';
const key = (id: string) => 'bluui-ts-forms:' + id;
const load = (id: string): BluFormDefinition => {
try { return JSON.parse(localStorage.getItem(key(id)) || 'null') || definitions[id]; }
catch { return definitions[id]; }
};
const builder = createFormBuilder(surface, { FormId: formId, Definition: load(formId) }, {
SaveRequested(requestId, requestedFormId, definition) {
// Replace this sample-only storage with your application's persistence.
let success = true, message = 'Definition saved in this browser.';
try { localStorage.setItem(key(requestedFormId), JSON.stringify(definition)); }
catch { success = false; message = 'Save failed. Your draft is retained.'; }
output.textContent = JSON.stringify(definition, null, 2);
builder.update({ SaveResultRequestId: requestId, SaveResultSuccess: success, SaveResultMessage: message });
},
Error(message) { output.textContent = message; },
});
picker.onchange = () => {
formId = picker.value;
builder.update({ FormId: formId, Definition: load(formId), ResetKey: String(Date.now()) });
output.textContent = 'Loaded ' + formId + ' on the same page.';
};
return { destroy() { builder.destroy(); host.replaceChildren(); } };
}
const instance = mount(document.getElementById('example')!)
// When this page or framework component is removed:
// instance.destroy()
Install the library and add the host element
npm install https://blukavantui.blutekmedia.com/downloads/blukavantui-web-0.6.0.tgz<div id="example"></div>This example uses the downloadable @blukavantui/web package. Your application handles persistence and authentication through its typed callbacks. The starter ZIP includes this same example.
createFormBuilder
Pass a typed Definition object. Handle SaveRequested in the third argument, persist in your application, then echo the matching SaveResultRequestId and success or failure through update().
import { createFormBuilder } from '@blukavantui/web/form-builder';
import '@blukavantui/web/form-builder.css';Load @blukavantui/web/theme.css once in your app shell.createFormBuilder(host: HTMLElement, options: FormBuilderOptions, events?: FormBuilderEvents)Reference for @blukavantui/web 0.6.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
9| Name | Type / requirement | Usage |
|---|---|---|
SaveResultRequestId | stringOptional | The request ID being acknowledged. |
SaveResultSuccess | booleanOptional | Whether the matching save succeeded. |
SaveResultMessage | stringOptional | Feedback for the matching save 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
4| Name | Type / requirement | Usage |
|---|---|---|
DefinitionChanged | (definition: BluFormDefinition) => void | Promise<void>Optional | Reports the edited definition to the owning application. |
DirtyChanged | (isDirty: boolean) => void | Promise<void>Optional | |
SaveRequested | (requestId: string, formId: string, definition: BluFormDefinition) => void | Promise<void>Optional | Requests persistence. Acknowledge the matching request only after your save completes. |
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<FormBuilderOptions>) => void | Apply a partial options update to this instance. |
destroy | () => void | Dispose the instance and its listeners when the view is removed. |
Related data types
Expand a type for its fields and allowed values.
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[];
}BluFormField
interface BluFormField {
/** Stable answer key, unique within the definition. */
Key: string;
Type: FormFieldType;
Label: string;
HelpText?: string;
Placeholder?: 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;
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[];
}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' | 'Heading' | 'Paragraph';FormValue
type FormValue = string | number | boolean | string[];Use these inputs, events, actions and structures in Service Studio. Names and data bindings are specific to the native Blocks.
Part of the full commercial BluKavantUI for OutSystems edition. View OutSystems editions →
Open OutSystems showcase ↗- Web runtime
- Partially supported
- TypeScript
- Partially supported
- OutSystems
- Partially 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
Build dynamic, multi-page forms as versioned JSON. Owns field editing, sections, conditions, preview, undo and correlated saving. No application storage or endpoint calls.
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.