extended-dynamic-forms
v0.3.13
Published
Extended React JSON Schema Form (RJSF) v6 with custom components, widgets, templates, layouts, and form events
Downloads
151
Maintainers
Readme
Extended Dynamic Forms
Declarative Configuration (JSON-only)
Conditional logic is configured with pure JSON via x-edf:rules in your schema and uiSchema. Function-based conditions and external hooks are not required and should not be used in app code.
Project Status
Important Notes:
- Requires RJSF source in sibling directory for development
- Cannot be installed from npm due to local file dependencies
- JSON logic conditions don't work with vanilla JavaScript builds
- Array drag-and-drop is not implemented
- Automated coverage is provided by logic and snapshot suites; component/UI tests are intentionally omitted
- JSON Patch implementation supports add/remove/replace only (not move/copy/test)
Overview
Extended Dynamic Forms is an extension library for React JSON Schema Form (RJSF) v6 that adds:
- Declarative conditionals with O(N) performance (rules + dynamic arrays)
- Multi-step wizard forms with automatic step detection
- Central event orchestration with webhook support
- 25+ Ant Design components for professional UI
- TypeScript-first development with full type safety
Ant Design Prop Pass-through
All custom widgets built on Ant Design components forward their props via ui:options. For example, Rate, Slider, DatePicker, Input, InputNumber, ColorPicker, and TimePicker can be configured using the same prop names from AntD. In addition, you can customize the containing Form.Item using ui:options.formItem. See docs/custom-widgets-configuration.md for details and examples.
- DatePicker notes:
- JSON-only flags:
disablePastDates,disableFutureDates,disableWeekends(buildsdisabledDateinternally) - Range support: set
ui:options.range: trueto render AntDRangePicker(see examples below)
- JSON-only flags:
Installation
Note: This library currently requires local development setup and cannot be installed via npm due to local file dependencies.
# Clone RJSF as sibling directory (required)
cd ..
git clone https://github.com/rjsf-team/react-jsonschema-form.git
# Setup Extended Dynamic Forms
cd extended-dynamic-forms
npm install
npm run devImportant: The library is currently designed for internal use or as part of a monorepo setup. Standalone npm package distribution is not yet supported.
Testing
The project uses Vitest with Node environment to cover pure logic and serialized outputs. Component or DOM-level tests are intentionally excluded.
- Run the full suite:
npm test - Logic-only focus:
npm test -- tests/logic - Snapshot updates:
npm test -- tests/snapshots -- -u
After pulling dependencies or updating package.json, run npm install (or
yarn install) so the lockfile reflects removed packages such as
@testing-library/* and jsdom.
Core Architecture
Declarative Conditionals (JSON-only)
Embed rules directly inside your configs. EDF evaluates JsonLogic condition and applies JSON Patch effect internally. No hooks, functions, or classes in user code.
- UI rules →
uiSchema['x-edf:rules'] - Schema rules →
schema['x-edf:rules']
Example (UI rules):
{
"x-edf:rules": [
{
"name": "Show license number when has license",
"condition": { "==": [{ "var": "hasDriversLicense" }, true] },
"effect": [{ "op": "remove", "path": "/licenseNumber/ui:widget" }]
}
]
}Example (Schema rules):
{
"x-edf:rules": [
{
"name": "Add premium fields",
"condition": { "==": [{ "var": "accountType" }, "premium"] },
"effect": [
{ "op": "add", "path": "/properties/premiumFeatures", "value": { "type": "object" } }
]
}
]
}Dynamic Arrays with O(N) Performance
Use declarative ui:dynamicItems with per-item JsonLogic. No functions:
{
"directors": {
"items": {
"ui:dynamicItems": {
"base": { "shareholdingPercentage": { "ui:widget": "hidden" } },
"rules": [
{
"condition": { "===": [{ "var": "hasShareholding" }, true] },
"ui": { "shareholdingPercentage": { "ui:widget": "updown" } },
"otherwise": { "shareholdingPercentage": { "ui:widget": "hidden" } }
}
]
}
}
}
}Multi-Step Wizard Forms
Create wizard forms with automatic step detection:
import { WizardForm } from 'extended-dynamic-forms';
<WizardForm
schema={wizardSchema}
uiSchema={uiSchema}
formData={formData}
onStepChange={(fromStep, toStep) => {
console.log(`Step ${fromStep} → ${toStep}`);
}}
/>Important: Wizard forms flatten nested step data. Use flat paths in conditions:
// Schema uses STEP_ prefixes
const schema = {
properties: {
STEP_PERSONAL: {
properties: {
hasDriversLicense: { type: 'boolean' }
}
}
}
};
// Conditions refer to flattened form data (no STEP_ prefix)
// Example rule (in uiSchema.x-edf:rules): { "==": [{ "var": "hasDriversLicense" }, true] }Note on wizard detection:
- Wizard mode activates only when the root schema contains two or more properties whose keys start with
STEP_. - With a single
STEP_…property, the form renders as a single-page form (no progress/navigation UI). - To display steps UI, define at least two step objects (e.g.,
STEP_PERSONAL,STEP_REVIEW).
Step Layouts with LayoutGridField
Wizard steps can define their own grid layout using a custom object field (e.g., LayoutGridField). Place the layout at the step key in uiSchema and pass your custom field via fields to WizardForm.
import { WizardForm } from 'extended-dynamic-forms';
const schema = {
type: 'object',
properties: {
STEP_LAYOUT: {
type: 'object',
title: 'Personal Info',
properties: {
firstName: { type: 'string', title: 'First Name' },
lastName: { type: 'string', title: 'Last Name' },
age: { type: 'number', title: 'Age' },
gender: { type: 'string', enum: ['Male','Female','Other'] },
favoriteBook: { type: 'string' },
}
}
}
};
const uiSchema = {
STEP_LAYOUT: {
'ui:field': 'LayoutGridField',
'ui:layoutGrid': {
'ui:row': [
{ 'ui:row': { gutter: [16,16], children: [
{ 'ui:col': { span: 12, children: ['firstName'] } },
{ 'ui:col': { span: 12, children: ['lastName'] } }
]}},
{ 'ui:row': { gutter: [16,16], children: [
{ 'ui:col': { span: 24, children: ['age'] } }
]}},
{ 'ui:row': { gutter: [16,16], children: [
{ 'ui:col': { span: 24, children: ['gender'] } }
]}},
{ 'ui:row': { gutter: [16,16], children: [
{ 'ui:col': { span: 24, children: ['favoriteBook'] } }
]}}
]
}
}
};
// LayoutGridField is included by default in EDF; no extra wiring needed
<WizardForm schema={schema} uiSchema={uiSchema} />Notes:
- EDF includes
LayoutGridFieldout of the box. Just setui:field: 'LayoutGridField'on the step object. - EDF variant uses
'ui:layoutGrid'with AntD gutter and 24-grid spans. The RJSF canonical layout ('ui:layout'with 12-grid widths) is also parsed. - You can still override or extend via the
fieldsprop if needed. - LayoutGridField now writes nested updates immutably using path metadata. This is enabled by default; set
ui:options.pathAware: falseon a step to fall back to the previous shallow merge behaviour.
Event System & Webhooks
Central event orchestration for all form interactions:
<ExtendedForm
schema={schema}
uiSchema={uiSchema}
webhooks={[{
url: 'https://api.example.com/events',
events: ['change', 'blur'],
debounceMs: 500,
retries: 3
}]}
onFieldChange={(event) => console.log(event.fieldId, event.fieldValue)}
/>Event Types:
focus- Field focusblur- Field blurchange- Value changessubmit- Form submissionstepChange- Wizard navigationvalidationError- Validation failures
Custom Widgets
All widgets must integrate with the event system:
import { WidgetProps } from '@rjsf/utils';
import { useFieldEventHandlers } from 'extended-dynamic-forms/events';
export const CustomWidget: FC<WidgetProps> = ({ id, value, onChange, schema }) => {
const fieldEventHandlers = useFieldEventHandlers(id, schema);
const handleChange = async (newValue) => {
onChange(newValue); // RJSF update
await fieldEventHandlers.onChange(newValue); // Event system
};
return <Input value={value} onChange={(e) => handleChange(e.target.value)} />;
};Auto-registered by Default
- All EDF custom widgets are included by default; no wiring is required.
- Every widget is also exposed as a field, so you can use either
ui:widgetorui:fieldwith the same key.
Examples:
{
"schema": { "type": "string", "title": "First Name" },
"uiSchema": { "ui:widget": "text" }
}{
"schema": { "type": "string", "title": "First Name" },
"uiSchema": { "ui:field": "text" }
}JsonLogic Migration Examples
Common patterns for migrating from function-based conditions:
// Equality
// Before: (formData) => formData.field === "value"
// After: { "==": [{ "var": "field" }, "value"] }
// Boolean check
// Before: (formData) => formData.isEnabled
// After: { "var": "isEnabled" }
// AND condition
// Before: (formData) => formData.a && formData.b
// After: { "and": [{ "var": "a" }, { "var": "b" }] }
// Array contains
// Before: (formData) => formData.roles?.includes("admin")
// After: { "in": ["admin", { "var": "roles" }] }Available Widgets
Complete Widget List
Choice Widget (Unified Selection)
- ChoiceWidget - Intelligent widget that automatically adapts based on schema:
- Renders as dropdown/select for regular choices
- Renders as radio group when
ui:widgetis "radio" - Renders as checkbox group for multi-select arrays
- Supports static and dynamic data sources (API, WebSocket)
Text Input Widgets
- TextWidget - Standard text input with Ant Design styling
- TextareaWidget - Multi-line text input
- PasswordWidget - Password input with visibility toggle
Specialized Input Widgets
- ColorWidget - Color picker using HTML5 color input
- EmailWidget - Email input with validation
- URLWidget - URL input with validation
- TelephoneWidget - Phone number input
- NumberWidget - Numeric input with increment/decrement controls
Visual Input Widgets
- RangeWidget / SliderWidget - Slider for numeric ranges
- RatingWidget - Star rating component
Date & Time Widgets
- DateWidget / EnhancedDateWidget - Date picker with Ant Design DatePicker
- DateTimeWidget - Combined date and time picker
File Upload Widgets
- FileWidget - Basic file input
- FileUploadWidget - Enhanced upload with progress, preview, and base64 support
Widget Configuration Examples
Basic Text Input
{
"schema": {
"type": "string",
"title": "Full Name"
},
"uiSchema": {
"ui:widget": "text",
"ui:placeholder": "Enter your full name"
}
}Text Input with Variant and Auto-complete
{
"schema": { "type": "string", "title": "Username" },
"uiSchema": {
"ui:widget": "text",
"ui:options": {
"variant": "outlined",
"autoFocus": true,
"autoComplete": "username",
"allowClear": true
}
}
}Date Range (RangePicker)
{
"schema": {
"type": "array",
"title": "Date Range",
"items": { "type": "string" },
"minItems": 2,
"maxItems": 2
},
"uiSchema": {
"ui:widget": "date",
"ui:options": {
"range": true,
"format": "YYYY-MM-DD",
"placeholder": ["Start date", "End date"],
"picker": "date"
}
}
}With time selection:
{
"schema": {
"type": "array",
"title": "Date Range With Time",
"items": { "type": "string" },
"minItems": 2,
"maxItems": 2
},
"uiSchema": {
"ui:widget": "date",
"ui:options": {
"range": true,
"showTime": true,
"format": "YYYY-MM-DD HH:mm",
"placeholder": ["Start date/time", "End date/time"]
}
}
}Month range:
{
"schema": {
"type": "array",
"title": "Month Range",
"items": { "type": "string" },
"minItems": 2,
"maxItems": 2
},
"uiSchema": {
"ui:widget": "date",
"ui:options": {
"range": true,
"picker": "month",
"format": "YYYY-MM",
"placeholder": ["Start month", "End month"]
}
}
}Number Input with Range
{
"schema": {
"type": "number",
"title": "Age",
"minimum": 0,
"maximum": 120
},
"uiSchema": {
"ui:widget": "number"
}
}Rating Widget
{
"schema": {
"type": "number",
"title": "Rate your experience",
"minimum": 0,
"maximum": 5
},
"uiSchema": {
"ui:widget": "rating",
"ui:options": {
"count": 5,
"allowHalf": true
}
}
}Choice Widget (Dropdown)
{
"schema": {
"type": "string",
"title": "Country",
"enum": ["us", "uk", "au", "ca"],
"enumNames": ["United States", "United Kingdom", "Australia", "Canada"]
},
"uiSchema": {
"ui:widget": "choice"
}
}Choice Widget with AntD props (Dropdown)
{
"schema": { "type": "string", "title": "Country" },
"uiSchema": {
"ui:widget": "choice",
"ui:options": {
"choiceConfig": {
"presentationMode": "dropdown",
"dataSource": { "type": "static", "options": [
{ "label": "USA", "value": "us" }, { "label": "UK", "value": "uk" }
]},
"antd": { "select": { "dropdownMatchSelectWidth": 280, "placement": "bottomLeft" } }
}
}
}
}Choice Widget (Radio Group)
{
"schema": {
"type": "string",
"title": "Gender",
"enum": ["male", "female", "other"]
},
"uiSchema": {
"ui:widget": "choice",
"ui:options": {
"presenter": "radio"
}
}
}Choice Widget (Checkbox Group - Multi-select)
{
"schema": {
"type": "array",
"title": "Interests",
"items": {
"type": "string",
"enum": ["sports", "music", "reading", "travel"]
},
"uniqueItems": true
},
"uiSchema": {
"ui:widget": "choice",
"ui:options": {
"presenter": "checkbox"
}
}
}Date Picker
{
"schema": {
"type": "string",
"title": "Date of Birth",
"format": "date"
},
"uiSchema": {
"ui:widget": "date",
"ui:options": {
"format": "DD/MM/YYYY",
"picker": "date"
}
}
}File Upload
{
"schema": {
"type": "string",
"title": "Resume",
"format": "data-url"
},
"uiSchema": {
"ui:widget": "fileUpload",
"ui:options": {
"accept": ".pdf,.doc,.docx",
"maxSize": 5242880,
"multiple": false
}
}
}File Upload (Ant Upload options)
{
"schema": { "type": "string", "title": "Profile Picture" },
"uiSchema": {
"ui:widget": "fileUpload",
"ui:options": {
"useAntUpload": true,
"listType": "picture-card",
"showUploadList": { "showPreviewIcon": true, "showRemoveIcon": true },
"maxCount": 1,
"accept": "image/*"
}
}
}Color Picker
{
"schema": {
"type": "string",
"title": "Favorite Color",
"format": "color"
},
"uiSchema": {
"ui:widget": "color"
}
}Range/Slider Widget
{
"schema": {
"type": "number",
"title": "Volume",
"minimum": 0,
"maximum": 100
},
"uiSchema": {
"ui:widget": "range",
"ui:options": {
"marks": {
"0": "Mute",
"50": "50%",
"100": "Max"
}
}
}
}Hidden Field
{
"schema": {
"type": "string",
"title": "User ID"
},
"uiSchema": {
"ui:widget": "hidden"
}
}Widget Registration (optional)
Widgets are auto-registered. You can still override or pass a subset if needed:
import { ExtendedForm, customWidgets } from 'extended-dynamic-forms';
// Override or register specific widgets
<ExtendedForm
widgets={{
text: customWidgets.text,
date: customWidgets.date,
choice: customWidgets.choice
}}
/>Development
# Development
npm run dev # Start playground
npm run test # Run tests
npm run test:ui # Test with UI
# Building
npm run build # Build library
npm run build:vanilla # Build standalone (currently broken - JsonLogic incompatible)
npm run preview # Preview build
# Code Quality
npm run lint # ESLint
npm run format # PrettierProject Structure
src/
├── components/ # Core form components
├── conditionals/ # Declarative conditional engines (internal)
│ └── v2/ # Current implementation
├── events/ # Event orchestration
├── layouts/ # Layout systems
│ └── wizard/ # Wizard form components
├── widgets/ # Input widgets
└── utils/ # UtilitiesExamples
Interactive examples available at /playground/src/demos/:
- Wizard Forms: Australian Company Onboarding, Employee Onboarding
- Conditionals: Dynamic visibility, validation, array handling
- Events: Webhook integration, field tracking
- Widgets: All available components
Run npm run dev to explore examples.
Known Limitations
- Dependencies: Requires RJSF source in sibling directory
- Installation: Cannot install from npm
- Vanilla JS: JsonLogic not supported in standalone builds
- Arrays: No drag-and-drop functionality
- JSON Patch: Only supports add/remove/replace operations
- Test Coverage: Limited for UI components and events
TypeScript
Full TypeScript support with exported types:
import { ExtendedForm, WizardForm } from 'extended-dynamic-forms';
// Config is JSON-only; rules are embedded via x-edf:rulesContributing
- Fork the repository
- Create your feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
MIT License - see LICENSE file for details.
