extended-dynamic-forms
Version:
Extended React JSON Schema Form (RJSF) v6 with custom components, widgets, templates, layouts, and form events
742 lines (629 loc) • 18.4 kB
Markdown
# 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` (builds `disabledDate` internally)
- Range support: set `ui:options.range: true` to render AntD `RangePicker` (see examples below)
## Installation
**Note:** This library currently requires local development setup and cannot be installed via npm due to local file dependencies.
```bash
# 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 dev
```
**Important:** 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):
```json
{
"x-edf:rules": [
{
"name": "Show license number when has license",
"condition": { "==": [{ "var": "hasDriversLicense" }, true] },
"effect": [{ "op": "remove", "path": "/licenseNumber/ui:widget" }]
}
]
}
```
Example (Schema rules):
```json
{
"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:
```json
{
"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:
```typescript
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:
```typescript
// 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`.
```tsx
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 `LayoutGridField` out of the box. Just set `ui: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 `fields` prop if needed.
- LayoutGridField now writes nested updates immutably using path metadata. This is enabled by default; set `ui:options.pathAware: false` on a step to fall back to the previous shallow merge behaviour.
## Event System & Webhooks
Central event orchestration for all form interactions:
```typescript
<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 focus
- `blur` - Field blur
- `change` - Value changes
- `submit` - Form submission
- `stepChange` - Wizard navigation
- `validationError` - Validation failures
## Custom Widgets
All widgets must integrate with the event system:
```typescript
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:widget` or `ui:field` with the same key.
Examples:
```json
{
"schema": { "type": "string", "title": "First Name" },
"uiSchema": { "ui:widget": "text" }
}
```
```json
{
"schema": { "type": "string", "title": "First Name" },
"uiSchema": { "ui:field": "text" }
}
```
## JsonLogic Migration Examples
Common patterns for migrating from function-based conditions:
```typescript
// 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:widget` is "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
```json
{
"schema": {
"type": "string",
"title": "Full Name"
},
"uiSchema": {
"ui:widget": "text",
"ui:placeholder": "Enter your full name"
}
}
```
#### Text Input with Variant and Auto-complete
```json
{
"schema": { "type": "string", "title": "Username" },
"uiSchema": {
"ui:widget": "text",
"ui:options": {
"variant": "outlined",
"autoFocus": true,
"autoComplete": "username",
"allowClear": true
}
}
}
```
#### Date Range (RangePicker)
```json
{
"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:
```json
{
"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:
```json
{
"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
```json
{
"schema": {
"type": "number",
"title": "Age",
"minimum": 0,
"maximum": 120
},
"uiSchema": {
"ui:widget": "number"
}
}
```
#### Rating Widget
```json
{
"schema": {
"type": "number",
"title": "Rate your experience",
"minimum": 0,
"maximum": 5
},
"uiSchema": {
"ui:widget": "rating",
"ui:options": {
"count": 5,
"allowHalf": true
}
}
}
```
#### Choice Widget (Dropdown)
```json
{
"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)
```json
{
"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)
```json
{
"schema": {
"type": "string",
"title": "Gender",
"enum": ["male", "female", "other"]
},
"uiSchema": {
"ui:widget": "choice",
"ui:options": {
"presenter": "radio"
}
}
}
```
#### Choice Widget (Checkbox Group - Multi-select)
```json
{
"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
```json
{
"schema": {
"type": "string",
"title": "Date of Birth",
"format": "date"
},
"uiSchema": {
"ui:widget": "date",
"ui:options": {
"format": "DD/MM/YYYY",
"picker": "date"
}
}
}
```
#### File Upload
```json
{
"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)
```json
{
"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
```json
{
"schema": {
"type": "string",
"title": "Favorite Color",
"format": "color"
},
"uiSchema": {
"ui:widget": "color"
}
}
```
#### Range/Slider Widget
```json
{
"schema": {
"type": "number",
"title": "Volume",
"minimum": 0,
"maximum": 100
},
"uiSchema": {
"ui:widget": "range",
"ui:options": {
"marks": {
"0": "Mute",
"50": "50%",
"100": "Max"
}
}
}
}
```
#### Hidden Field
```json
{
"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:
```typescript
import { ExtendedForm, customWidgets } from 'extended-dynamic-forms';
// Override or register specific widgets
<ExtendedForm
widgets={{
text: customWidgets.text,
date: customWidgets.date,
choice: customWidgets.choice
}}
/>
```
## Development
```bash
# 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 # Prettier
```
## Project 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/ # Utilities
```
## Examples
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
1. **Dependencies**: Requires RJSF source in sibling directory
2. **Installation**: Cannot install from npm
3. **Vanilla JS**: JsonLogic not supported in standalone builds
4. **Arrays**: No drag-and-drop functionality
5. **JSON Patch**: Only supports add/remove/replace operations
6. **Test Coverage**: Limited for UI components and events
## TypeScript
Full TypeScript support with exported types:
```typescript
import { ExtendedForm, WizardForm } from 'extended-dynamic-forms';
// Config is JSON-only; rules are embedded via x-edf:rules
```
## Contributing
1. Fork the repository
2. Create your feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## License
MIT License - see LICENSE file for details.