UNPKG

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
# 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.