@waysnx/ui-form-builder
v0.3.0
Published
Schema-driven form builder from WaysNX - renders forms from JSON Schema using ui-core components
Maintainers
Readme
@waysnx/ui-form-builder
Schema-driven form builder from WaysNX. Renders dynamic forms from JSON Schema using @waysnx/ui-core components.
Installation
npm install @waysnx/ui-form-builder @waysnx/ui-core react-datepickerNote: Don't forget to import CSS files:
import '@waysnx/ui-core/dist/index.css';
import '@waysnx/ui-form-builder/dist/index.css';
import 'react-datepicker/dist/react-datepicker.css';Choosing the Right Package
This package provides schema-driven forms. For simpler installation:
- Recommended: Install
@waysnx/ui-kit(includes ui-core + ui-form-builder) - Modular: Install
@waysnx/ui-core+@waysnx/ui-form-builder(this approach)
📖 Installation Guide for detailed comparison.
Features
- DynamicForm - Complete form from JSON schema + layout — zero manual code
- SchemaRenderer - Convert JSON Schema to form fields
- DynamicField - Resolve a single schema property to a ui-core component
- FormArray - Dynamic repeatable form sections (add/remove rows)
- Conditional Logic - Show/hide, enable/disable, and require fields based on conditions
- Secure by Design - No XSS, SSRF, or code injection vulnerabilities
Usage
DynamicForm — Full Form from JSON
The simplest way to render a form. Provide a JSON schema and a layout, and DynamicForm handles everything — field rendering, layout, state, validation, and buttons.
import { DynamicForm } from '@waysnx/ui-form-builder';
import '@waysnx/ui-core/dist/index.css';
import '@waysnx/ui-form-builder/dist/index.css';
const schema = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['firstName', 'email'],
};
const layout = {
rows: [
{
cells: [
{ settings: { fieldName: 'firstName', title: 'First Name', controlType: 'input', inputType: 'text', 'x-col-size': 6, required: true } },
{ settings: { fieldName: 'lastName', title: 'Last Name', controlType: 'input', inputType: 'text', 'x-col-size': 6 } },
{ settings: { fieldName: 'email', title: 'Email', controlType: 'input', inputType: 'email', 'x-col-size': 12, required: true } },
],
settings: { rowId: 'row-1' },
},
],
settings: {
fieldGroup: 'Contact Info',
buttonsPosition: 'bottom',
buttonsAlignment: 'text-right',
buttons: [
{ label: 'Cancel', name: 'cancel', type: 'button', appearance: 'accent' },
{ label: 'Save', name: 'save', type: 'submit', appearance: 'primary' },
],
},
};
function App() {
return (
<DynamicForm
schema={schema}
formLayout={layout}
onSubmit={(data) => console.log(data)}
onBtnClick={(name) => console.log('Button:', name)}
/>
);
}DynamicForm Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| schema | JSONSchema \| string | required | JSON Schema (object or JSON string) |
| formLayout | FormLayout \| string | required | Layout config (object or JSON string) |
| formData | Record<string, any> | - | Initial form values |
| formClass | string | '' | CSS class for the form element |
| isFormReadonly | boolean | false | Make all fields read-only |
| onSubmit | (data) => void | - | Called on form submit |
| onBtnClick | (buttonName) => void | - | Called on non-submit button click |
| onFieldChange | (name, value, formData) => void | - | Called on any field change |
| onFormReady | (form) => void | - | Exposes getData() and reset() |
Layout Cell Settings
Each cell in the layout can override schema properties:
| Setting | Type | Description |
|---------|------|-------------|
| fieldName | string | Maps to schema property name |
| title | string | Label override |
| controlType | string | Control type: input, select, textarea, hidden, toggle, checkbox, radio, slider, file-upload, autocomplete, html-editor, etc. |
| inputType | string | For input controls: text, number, email, tel, password |
| x-col-size | number | Column width (1–12 grid) |
| required | boolean | Override required state |
| readonly | boolean | Make field read-only |
| disabled | boolean | Disable field |
| pattern | string | Validation pattern |
| x-columns | number | Number of columns for Radio/Checkbox grid layout |
| x-searchable | boolean | Enable search box in Select dropdowns |
| x-rows | number | Number of rows for Textarea and SpeechToTextTextarea |
Layout Settings
| Setting | Type | Description |
|---------|------|-------------|
| fieldGroup | string | Wraps form in fieldset with legend |
| infoText | string | Info text shown above fields |
| buttonsPosition | 'top' \| 'bottom' | Button placement |
| buttonsAlignment | 'text-left' \| 'text-right' \| 'text-center' | Button alignment |
| buttons | LayoutButton[] | Array of button configs |
| path | string | API endpoint (metadata) |
| method | string | HTTP method (metadata) |
Schema to Form Fields (Manual Layout)
import { schemaToFormFields } from '@waysnx/ui-form-builder';
import { useState } from 'react';
const schema = {
type: 'object',
properties: {
name: { type: 'string', title: 'Full Name' },
email: { type: 'string', format: 'email', title: 'Email' },
},
required: ['name', 'email'],
};
const fields = schemaToFormFields(schema, formData, (name, value) => {
setFormData(prev => ({ ...prev, [name]: value }));
});
// Render fields
{fields.map((field) => (
<div key={field.name}>
{field.useFormFieldLabel && (
<label>
{field.label}
{field.required && <span>*</span>}
</label>
)}
{field.component}
</div>
))}FormArray - Dynamic Repeatable Sections
Add/remove multiple instances of a form group dynamically.
import { FormArray } from '@waysnx/ui-form-builder';
<FormArray
label="Bank Accounts"
itemSchema={{
type: 'object',
properties: {
bank_name: { type: 'string', title: 'Bank Name' },
account_no: { type: 'string', title: 'Account Number' },
ifsc_code: { type: 'string', title: 'IFSC Code' },
},
required: ['bank_name', 'account_no'],
}}
value={bankAccounts}
onChange={setBankAccounts}
addButtonTitle="Add Bank Account"
deleteButtonTitle="Remove"
minItems={1}
maxItems={5}
/>FormArray Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | - | Display label for the array section |
| itemSchema | JSONSchema | required | JSON Schema for each item |
| value | Record<string, any>[] | [] | Current array value |
| onChange | (value) => void | - | Called when array changes |
| canAdd | boolean | true | Show add button |
| canDelete | boolean | true | Show delete buttons |
| addButtonTitle | string | 'Add' | Custom add button text |
| deleteButtonTitle | string | 'Remove' | Custom delete button text |
| minItems | number | 0 | Minimum number of items |
| maxItems | number | - | Maximum number of items |
| disabled | boolean | false | Read-only mode |
FormArray with Conditional Logic
Fields within FormArray support conditional logic:
<FormArray
label="Employees"
itemSchema={{
type: 'object',
properties: {
employmentType: {
type: 'string',
title: 'Type',
enum: ['fulltime', 'contractor'],
},
ssn: {
type: 'string',
title: 'SSN',
'x-show-when': [{ name: 'employmentType', value: 'fulltime' }],
},
taxId: {
type: 'string',
title: 'Tax ID',
'x-show-when': [{ name: 'employmentType', value: 'contractor' }],
},
},
}}
value={employees}
onChange={setEmployees}
/>Nested FormArray (Child Forms in Parent Forms)
FormArray supports unlimited nesting depth - you can have arrays within arrays, creating parent-child form relationships. This is useful for complex data structures like companies with employees, or projects with tasks and subtasks.
Pattern: Use type: 'array' with items.type: 'object' to create nested child forms.
<FormArray
label="Companies"
itemSchema={{
type: 'object',
properties: {
company_name: { type: 'string', title: 'Company Name' },
// Nested array of employees (child form)
employees: {
type: 'array',
title: 'Employees',
items: {
type: 'object', // ← This creates a nested child form
properties: {
name: { type: 'string', title: 'Employee Name' },
email: { type: 'string', format: 'email', title: 'Email' },
position: { type: 'string', title: 'Position' },
},
},
'x-add-button-title': 'Add Employee',
'x-min-items': 1,
},
},
}}
value={companies}
onChange={setCompanies}
/>How it works:
- Parent form has a single Company Name field
- Each company has a nested Employees array (child form)
- Each employee can have their own nested arrays (e.g., addresses)
- Supports unlimited nesting depth
- Each nesting level gets its own add/remove buttons
- Conditional logic works at all nesting levels
Example data structure:
[
{
"company_name": "Acme Corp",
"employees": [
{
"name": "John Doe",
"email": "[email protected]",
"position": "Developer"
},
{
"name": "Jane Smith",
"email": "[email protected]",
"position": "Designer"
}
]
}
]Conditional Logic
Control field visibility, disabled state, and required validation based on other field values.
x-show-when - Conditional Visibility
Show/hide fields based on conditions:
const schema = {
type: 'object',
properties: {
accountType: {
type: 'string',
title: 'Account Type',
enum: ['personal', 'business'],
},
companyName: {
type: 'string',
title: 'Company Name',
'x-show-when': [{ name: 'accountType', value: 'business' }],
},
},
};x-disable-when - Conditional Disabled State
Enable/disable fields based on conditions:
const schema = {
type: 'object',
properties: {
hasShippingAddress: {
type: 'boolean',
title: 'Different shipping address',
},
shippingStreet: {
type: 'string',
title: 'Shipping Street',
'x-disable-when': [{ name: 'hasShippingAddress', value: false }],
},
},
};x-required-when - Conditional Required Validation
Make fields required based on conditions:
const schema = {
type: 'object',
properties: {
contactMethod: {
type: 'string',
title: 'Contact Method',
enum: ['email', 'phone', 'both'],
},
email: {
type: 'string',
format: 'email',
title: 'Email',
'x-required-when': [
{ name: 'contactMethod', value: 'email' },
{ name: 'contactMethod', value: 'both' },
],
},
phone: {
type: 'string',
title: 'Phone',
'x-required-when': [
{ name: 'contactMethod', value: 'phone' },
{ name: 'contactMethod', value: 'both' },
],
},
},
};Supported Operators
| Operator | Description | Example |
|----------|-------------|---------|
| == (default) | Equal to | { name: 'age', value: 18 } |
| != | Not equal to | { name: 'status', value: 'inactive', operator: '!=' } |
| > | Greater than | { name: 'age', value: 18, operator: '>' } |
| < | Less than | { name: 'age', value: 65, operator: '<' } |
| >= | Greater than or equal | { name: 'age', value: 18, operator: '>=' } |
| <= | Less than or equal | { name: 'age', value: 65, operator: '<=' } |
| notEmpty | Field has value | { name: 'comments', operator: 'notEmpty' } |
| isEmpty | Field is empty | { name: 'comments', operator: 'isEmpty' } |
Complex Conditions Example
const schema = {
type: 'object',
properties: {
age: {
type: 'integer',
title: 'Age',
},
parentConsent: {
type: 'boolean',
title: 'Parent Consent Required',
'x-show-when': [{ name: 'age', value: 18, operator: '<' }],
},
seniorDiscount: {
type: 'boolean',
title: 'Senior Discount',
'x-show-when': [{ name: 'age', value: 65, operator: '>=' }],
},
comments: {
type: 'string',
title: 'Comments',
},
reviewComments: {
type: 'string',
title: 'Review Comments',
'x-show-when': [{ name: 'comments', operator: 'notEmpty' }],
},
},
};Theming
All form components use CSS variables from @waysnx/ui-core for theming. Override these in your CSS:
:root {
--wx-color-primary: #f19924; /* Buttons, focus rings */
--wx-color-text: #1e293b; /* Labels, text */
--wx-color-text-muted: #64748b; /* Hints, descriptions */
--wx-color-error: #ef4444; /* Required asterisks, errors */
--wx-color-border: #e2e8f0; /* Fieldset borders */
--wx-color-surface-alt: #f8fafc; /* Fieldset backgrounds */
}These are the same variables used across all WaysNX libraries. See the Theming Guide for the full list.
Security
This library is designed with security as a top priority:
- ✅ No XSS vulnerabilities - All HTML is sanitized via DOMPurify in @waysnx/ui-core
- ✅ No SSRF vulnerabilities - Applications control all HTTP requests
- ✅ No Code Injection - Uses safe operator-based conditional logic (no
eval()ornew Function())
For detailed security information, contact [email protected]
TypeScript Support
Full TypeScript support with exported types:
import type {
JSONSchema,
JSONSchemaProperty,
FormFieldConfig,
ControlCondition,
FormArrayProps,
FormLayout,
GridRow,
GridCell,
CellSettings,
RowSettings,
LayoutSettings,
LayoutButton,
DynamicFormProps,
} from '@waysnx/ui-form-builder';Exported Functions
import {
DynamicForm,
schemaToFormFields,
FormArray,
resolveField,
evaluateCondition,
evaluateConditions,
shouldShowField,
shouldDisableField,
shouldRequireField,
} from '@waysnx/ui-form-builder';Peer Dependencies
react>= 18@waysnx/ui-core>= 0.1.0
Links
- npm Package: @waysnx/ui-form-builder
License
MIT
