extended-dynamic-forms
v0.6.2
Published
Extended React JSON Schema Form (RJSF) v6 with custom components, widgets, templates, layouts, and form events
Maintainers
Readme
Extended Dynamic Forms
Extended Dynamic Forms (EDF) extends React JSON Schema Form (RJSF) v6 with
Ant Design widgets, JSON-configured conditional logic, multi-step forms and
field events. Use it in a React application with JSON Schema for data constraints
and uiSchema for presentation.
Installation
npm install extended-dynamic-formsInstall compatible peer dependencies if your application does not already have them:
npm install 'react@^18.0.0' 'react-dom@^18.0.0' 'antd@^5.24.0' 'dayjs@^1.11.13'React and React DOM 19 are also supported by the peer ranges. Keep their major versions aligned. EDF bundles RJSF 6.11.0 and the AJV validator; you do not need a sibling RJSF checkout.
Use ESM imports and include the exported stylesheet. TypeScript declarations are provided; use an ESNext/Bundler-compatible module configuration.
Features
- Single-page forms and wizards with step visibility, navigation and validation.
- Declarative JsonLogic conditions and JSON Patch effects in JSON configuration.
- Ant Design text, choice, numeric, date/time, file and display widgets.
- Per-field options, custom widgets, fields, templates and grid layouts.
- Field focus, blur and change events, submission hooks and webhook configuration.
Basic usage
import { useState } from 'react';
import { ExtendedForm, type RJSFSchema, type UiSchema } from 'extended-dynamic-forms';
import 'extended-dynamic-forms/dist/style.css';
const schema: RJSFSchema = {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', title: 'Name', minLength: 1 },
email: { type: 'string', title: 'Email', format: 'email' },
},
};
const uiSchema: UiSchema = {
name: { 'ui:widget': 'text', 'ui:options': { allowClear: true } },
email: { 'ui:widget': 'email' },
};
export default function ContactForm() {
const [formData, setFormData] = useState({});
return (
<ExtendedForm
schema={schema}
uiSchema={uiSchema}
formData={formData}
onChange={({ formData }) => setFormData(formData)}
onSubmit={({ formData }) => console.log('Accepted answers', formData)}
/>
);
}EDF supplies its widgets and validator by default. Override the RJSF widgets,
fields or templates props when your application needs custom rendering.
customWidgets, validator and commonly used RJSF types are exported from
extended-dynamic-forms; no private subpath import is needed.
Both ExtendedForm and WizardForm accept an omitted formData prop. It uses a
stable empty initial value, and edited answers persist across host re-renders.
Pass formData when your host needs to supply answers explicitly.
Multi-step forms
WizardForm detects steps from root properties beginning with STEP_. Define at
least two step objects to show wizard navigation; one step renders as a
single-page form.
import { WizardForm } from 'extended-dynamic-forms';
import 'extended-dynamic-forms/dist/style.css';
const schema = {
type: 'object',
properties: {
STEP_PERSONAL: {
type: 'object',
title: 'Personal details',
required: ['name'],
properties: { name: { type: 'string', minLength: 1 } },
},
STEP_CONTACT: {
type: 'object',
title: 'Contact details',
properties: { email: { type: 'string', format: 'email' } },
},
},
};
export default function RegistrationForm() {
return (
<WizardForm
schema={schema}
uiSchema={{}}
onStepChange={(stepIndex, data, stepId) => {
console.log('Current step', stepIndex, stepId, data);
}}
onLiveChange={(flatData) => console.log('Current answers', flatData)}
onSubmit={({ formData, flatFormData }) => {
console.log('Accepted step answers', formData);
console.log('Accepted flat answers', flatFormData);
}}
/>
);
}Conditions use flat answer paths without the STEP_ prefix and can read answers
from all wizard steps. A rule on one step can use an answer from another step to
show, hide or require a field on its own step. If answer keys collide, the
evaluating step's own answers win; distinct keys avoid ambiguity in flat data.
One-step forms take flat initial answers only. With two or more configured steps,
initial answers may be flat or nested under step keys; explicit nested snapshots
take precedence.
Step visibility rules use x-edf:stepRules. Hidden steps are skipped in navigation
and excluded from structured submissions unless includeHiddenStepsInSubmit is
true; flatFormData retains the all-step context after inactive-null omission. onStepChange reports the
visible-step index; onStepChangeDetailed also includes the original index as
allStepsIndex. The last visible step can submit, and final submission validates
all visible steps, including an earlier step newly revealed by a later answer.
Next and Submit stay enabled when the current step has a missing required answer,
including alongside other errors. Pressing either reveals all step errors without
advancing or submitting invalid answers. Final Submit also reveals errors when it
returns to an earlier invalid step; non-required-only error gating is unchanged.
Required arrays below minItems also allow an attempt, including requirements
applied by a rule and nested object fields. The click reveals the step's feedback
without advancing or submitting. Steps with only optional-array minItems errors
keep Next/Submit disabled.
Array positions and blank array entries are preserved through editing and
navigation. Supplied strings with directly declared pattern, positive
minLength, or a valid explicit maxLength keep their characters for
validation; other nested/edit blank strings are subject to normalization.
JSON Schema required checks presence. A positive minLength rejects an empty
string; rejecting whitespace-only answers requires an appropriate non-whitespace
pattern. Malformed schema patterns block progression and show configuration
errors by default; hiding the error list does not bypass validation.
LayoutGridField is included for step layouts. Configure ui:field as
LayoutGridField and use ui:layoutGrid with Ant Design's 24-column spans and
gutters. Grouped objects display their own title and description once before
their members, using the registered templates. UI titles and descriptions
override schema metadata; empty UI values hide that heading, and
ui:options.label: false hides both. You can override custom fields and templates.
Form headings and step descriptions
Root uiSchema['ui:options'].label defaults to true. Set it to false to hide
the form title and description together on one-step and multi-step forms; field
labels are unaffected. An empty root ui:title or ui:description hides only
that heading.
The WizardForm prop showStepDescription defaults to true. Multi-step forms
show one active-step description inside the form, above the fields, even when
progress is hidden. Set it to false to hide that description. It is a component
prop, not a persisted uiSchema option, so hosts such as Formstudio must pass it
explicitly. One-step forms show no step description.
<WizardForm
schema={schema}
uiSchema={{ 'ui:options': { label: false } }}
showStepDescription={false}
/>Conditional logic
Put JsonLogic conditions and JSON Patch effects in x-edf:rules on the schema
or UI schema. Configuration is JSON-only; JavaScript source strings are not
executed.
For example, this schema rule adds a property when accountType is premium:
{
"type": "object",
"properties": {
"accountType": { "type": "string", "enum": ["basic", "premium"] }
},
"x-edf:rules": [
{
"name": "Premium details",
"condition": { "==": [{ "var": "accountType" }, "premium"] },
"effect": [
{ "op": "add", "path": "/properties/premiumFeatures", "value": { "type": "string" } }
]
}
]
}UI rules belong in uiSchema['x-edf:rules']; schema rules belong in
schema['x-edf:rules']. Array-item presentation can use ui:dynamicItems with
per-item JsonLogic. Conditional requiredness changes validation, not just a
visual required marker.
Rule order
Rules in each conditional engine run in ascending numeric order. Use a finite
number; negative values and zero are valid. Equal orders retain their array
order. Rules without order run after every explicitly ordered rule, keeping
their own array order. Evaluation does not reorder the caller's array.
Only matching rules apply effects. Effects run sequentially, so a later effect on the same property wins: hide followed by show reveals a field, and a later widget replacement replaces the earlier widget. Effects within one rule also retain their array order. A false condition does not apply an automatic inverse; authors that need an inverse must supply another rule.
This contract applies separately to the UI, schema, and validation engines.
Schema effects determine both rendered constraints and submission validation.
The code-only useConditionalValidation hook preserves the same rule order;
createCustomValidate then runs the selected validation callbacks in sequence.
Callbacks that add errors accumulate them; an earlier error is not automatically
removed unless a later callback explicitly changes it.
ExtendedForm applies embedded schema and UI rules with this ordering.
WizardForm applies this ordering to each step's local rules followed by inherited
root rules, including navigation and final validation. Root rules therefore follow
local rules when their orders tie or both omit order. Both local and inherited
rules evaluate conditions against all wizard answers, with the evaluating step's
own answers taking precedence on key collisions. Effect paths remain relative to
that step: effects that name a field on another step are dropped; other effect
paths apply to the evaluating step unchanged. Rules may still add fields to their
own step. Put a field's rule on its owning step or inherit it from the root.
For example, these UI rules reveal answer as a textarea even though the hide
rule appears last in the array:
{
"x-edf:rules": [
{ "name": "Show", "order": 20, "condition": true,
"effect": [{ "op": "replace", "path": "/answer/ui:widget", "value": "textarea" }] },
{ "name": "Hide", "order": 10, "condition": true,
"effect": [{ "op": "replace", "path": "/answer/ui:widget", "value": "hidden" }] }
]
}Widgets and options
Select a registered widget using ui:widget. The same registered key can also
be used as ui:field; an explicit field override controls its own rendering.
| Input | Registered keys |
| --- | --- |
| Text | text, textarea, password |
| Email, URL and phone | email, url, tel, telephone |
| Number | number, updown |
| Choices | choice with dropdown, radio or checkbox presentation |
| Date and time | date, time, datetime-local |
| Visual inputs | color, range, slider, rating, switch |
| Files | file, fileUpload |
| Display | title, heading, html, markdown, image, alert, spacer, divider |
Configure supported Ant Design props in ui:options. Container options belong
in ui:options.formItem; input styling and container styling are separate.
For the unified choice widget, use ui:options.choiceConfig for data sources,
presentation and presenter-specific Ant Design options.
{
"name": {
"ui:widget": "text",
"ui:options": {
"allowClear": true,
"autoComplete": "name",
"variant": "outlined",
"style": { "fontSize": 18 },
"formItem": { "style": { "marginBottom": 24 } }
}
},
"birthday": {
"ui:widget": "date",
"ui:options": { "format": "DD/MM/YYYY", "disableFutureDates": true }
},
"score": {
"ui:widget": "rating",
"ui:options": { "count": 5, "allowHalf": true }
}
}- Choices can use schema enums or configured static/remote data sources. Single choices emit scalars; multiple choices emit arrays. Remote paging is not implemented.
- Date options include
disablePastDates,disableFutureDates,disableWeekendsandrange: truefor a range picker. - Number options include
min,max,step,precision,groupSeparator,decimalSeparator,controls,keyboardandchangeOnWheel. Display grouping does not become part of numeric answers. Schema bounds validate independently of input clamping. - Exact decimals require
stringMode: truewith a string schema and suitable string/decimal-aware validation. Numeric schemas convert values to JavaScript numbers; numeric JSON Schema bounds do not validate decimal strings. - Telephone
maskuses9for a digit,afor an ASCII letter and*for any character. Mask literals affect display, while answers retain digits and an optional leading+.useCountryMasks: trueenables country-pattern fallback; an explicit field mask takes precedence. Letter slots are display-only. - URL answers accept HTTP/HTTPS by default.
allowedProtocolsreplaces those defaults;[]rejects every nonempty URL.javascript:,data:andvbscript:remain blocked even if listed. Link previews are restricted to HTTP/HTTPS. - File upload options include
accept,maxSize,multipleand Ant Upload presentation controls. Use a schema compatible with the selected value mode. Required data-URL uploads show one inline message: "Upload a file." or "Upload at least one file." for multiple files. HosttransformErrorsorcustomValidatefeedback takes precedence; empty uploads keep the normal label.
Choice presenter-specific Ant Design options use choiceConfig.antd, such as
choiceConfig.antd.select for dropdown props.
Values and validation
Empty answers
Clearing Text, Textarea, Email, URL or Telephone emits null by default.
Set an explicit own ui:options.emptyValue to override it, including "" or
undefined. Other widgets and custom fields have their own clear semantics.
Undefined-valued object properties are omitted from JSON, including inside
array items; callbacks may contain those properties.
In ExtendedForm and WizardForm, clear an optional field to leave it unanswered:
validation treats null object properties as absent, including nested properties
and properties inside array items. Put required fields in the parent schema's
required array; a required null fails even with a nullable schema. Custom
validation receives the actual null and may reject it. Null does not refill a
schema default. For primitive array slots, declare a nullable item schema such
as items: { type: ['string', 'null'] } to accept cleared text values.
For EDF text widgets in your own native RJSF Form, import
nullContractValidator from extended-dynamic-forms and pass
validator={nullContractValidator} for the same null acceptance policy.
The exported validator is plain AJV: use nullable schemas such as
type: ['string', 'null'] with it. A nullable type alone does not bypass enums,
dependencies or combinators, or treat required nulls as absent.
Visible, enabled null answers are included in submissions; distinguish them from missing keys. Clearing an array row does not delete it during editing. Hidden or disabled nulls are omitted at submit, which can compact arrays; see Submitted data for values and metadata.
ExtendedForm defaults experimental_defaultFormStateBehavior.requiredBooleanDefault
to 'skip': an unanswered required boolean stays absent. An explicit host value
(for example 'populateFalse') overrides that default. Authored boolean defaults
are preserved.
Stored/default answers are not rewritten on load. Turning off interaction feedback does not disable Submit or Wizard Next validation. Change callbacks can contain incomplete or invalid drafts; only accepted submissions should be stored as completed responses.
Conditional validation feedback
Set ui:options.validation.messages at the field's own UI schema path to supply
JSON-safe validation text in ExtendedForm and WizardForm:
{
"answer": {
"ui:options": {
"validation": {
"messages": {
"minLength": "Use at least three characters.",
"format": "Enter a valid email address."
}
}
}
}
}Keys are AJV error keywords (required, minLength, maxLength, pattern,
format, minimum, maximum, exclusiveMinimum, exclusiveMaximum,
multipleOf, minItems, maxItems, enum, const, or another error name).
Values must be non-empty strings; empty strings and non-string values leave
default messages unchanged. Put nested field options under their object path
and array-item options under items (or the corresponding tuple item).
Declarative ui:dynamicItems uses each item's effective UI options. Native
catch-all field and tuple-item UI use additionalProperties and
additionalItems, respectively.
EDF folds redundant AJV if wrappers for failed then/else branches when a
specific error exists at the same property path; a sole wrapper stays visible.
It then replaces matching messages and error-list stack text. Repeated
failures with the same configured text appear once per field in each surface.
Other keywords without a message keep their defaults, including Required when
only minLength is configured. Phone keeps its first-failure widget wording
when that check has no message, even when another keyword has one; unconfigured
AJV siblings are not added inline. URL and Email widget feedback uses format;
Phone pattern feedback uses pattern.
Phone's existing customErrorMessage
and Choice widgets' existing choiceConfig.validation.messages.required/invalid
options retain their meaning.
Use transformErrors(errors, uiSchema) on either component for host code to
customise validation feedback. The host runs after folding and keyword
replacement, so it can override the configured text. For forms using keyword
messages, widget constraints join the errors before this host callback.
Field feedback, callbacks, the error list and Wizard validation use the replaced
text without changing Next/Submit gating or error classification.
edf:configuration errors are never rewritten by keyword messages.
Configuration errors and email feedback
Configuration failures carry name: 'edf:configuration', including errors from
the exported nullContractValidator. This marker is the supported way for hosts
to recognise configuration failures. Host transformErrors callbacks should
preserve name when copying or replacing error objects so Wizard retains its
configuration-error feedback path.
Set ui:hideError on Email fields to hide schema and custom validation messages
in ExtendedForm and WizardForm. Widget-local blur feedback applies when
there are no schema or custom errors.
Date and time answers
| Widget/schema | Accepted answer |
| --- | --- |
| Single Date with format: "date" | Calendar day, YYYY-MM-DD |
| Time with format: "time" | Wall-clock time, HH:mm:ss, without timezone conversion |
| DateTime | UTC ISO instant with Z, displayed in the respondent's browser-local timezone |
| Calendar Date Range (date with range: true) | Pair of ISO calendar days, even with plain string item schemas |
Date/Time display formats do not change those schema-formatted answers. Native
Date/Time string schemas without their respective format retain configured-format
output. Explicit showTime / withTime ranges retain formatted time-bearing
pairs rather than the UTC DateTime contract.
DateTime rejects nonexistent daylight-saving times and requires an explicit
occurrence choice for repeated local times. Restored valid offset-bearing instants
and fractional-second precision are preserved until edited. Zone-less or invalid
saved DateTime strings require re-entry; no timezone is guessed. Pending invalid
edits block submission and wizard progression, even on optional fields, and can
emit the Invalid date-time draft marker. DateTime clear emits undefined
regardless of emptyValue.
Calendar ranges reject impossible, reversed and incomplete pairs. Clear emits
[]; optional schemas must allow zero items if clear should be valid. Required
ranges reject []. Explicit allowEmpty endpoint options also need schemas
that permit an empty string at that endpoint.
Date, calendar Date Range and DateTime accept ui:options.minDate / maxDate
as "YYYY-MM-DD" or ISO date-time strings. Bounds are inclusive calendar dates
as written, without time-zone shifts; unparseable strings are ignored.
Out-of-bounds typed or restored answers block submit and Wizard Next.
A function disabledDate takes precedence over convenience flags, while bounds
still apply; non-function values are ignored. Single dates also check restored
answers against these flags and callbacks before submission or progression.
Events and webhooks
ExtendedForm delivers onFieldFocus, onFieldBlur and onFieldChange events
with fieldId and fieldValue. WizardForm currently does not deliver those
field callback props; use onLiveChange to subscribe to wizard answer snapshots.
Use onSubmit for accepted form answers. Optional webhooks configure event
destinations, event names, debounce timing and retries. Keep endpoint authorization
and response-storage policy in your host application.
Custom widgets can import useFieldEventHandlers from the root package alongside
other event exports. Call the RJSF onChange to update the answer and the field
event handler to participate in EDF events.
Both components send webhooks through FormEventHub, using a single JSON envelope
with eventType, timestamp, source and formData. Subscriptions support
focus, blur, change, submit, validationError and wizard stepChange.
Field events also carry fieldId, fieldValue and fieldPath; step events carry
fromStep and toStep; validation events carry errors.
Submit webhook formData contains the RJSF event for ExtendedForm, flat answers
for one-step WizardForm, and step data for multi-step WizardForm.
Submitted data
ExtendedForm and WizardForm send valued hidden, disabled and read-only answers
as entered. Hidden and disabled fields are exempt from field validation and
Wizard navigation requirements, so their values may be invalid. Their null
values are omitted without clearing live answers; showing a field restores its
value. Array-level minItems, maxItems and uniqueItems validate the live array.
The submit callback and afterSubmit include optional meta alongside formData
when at least one flag list is non-empty. The exported SubmissionMeta type is:
{ hiddenFields: string[]; disabledFields: string[] }Both arrays are present when meta is emitted. Paths are RFC 6901 JSON Pointers
relative to delivered answers, such as /address/street or /items/0/note;
structured Wizard paths include the step key. Indices refer to arrays after
inactive-null omission. A hidden object and each delivered descendant are listed
once, in document order, including nested objects and array items; an inactive
root uses "" without descendant expansion.
hiddenFields flags delivered values hidden by visibility-capable conditional
rules (ui:widget, ui:options or whole-UI replacements), including
ui:dynamicItems and base-hidden Show targets. Whole-UI replacements also
address descendant fields; other visibility effects address their named target.
Static hidden fields outside a hidden object, such as IDs and prefills, are
sent without hidden flags unless addressed by these rules. disabledFields
includes static and rule-set disabled fields; a field can appear in both lists.
beforeSubmit receives answers after inactive-null omission. Its returned
answers are delivered without a second filter. Metadata reflects submit-time
field state and is restricted to returned paths, not item identity: reordering
an array in this hook can make a flag refer to a different item.
Submit webhooks place meta at the envelope root only when flags exist.
meta is reserved: body cannot supply or override it. formDataFields restricts
metadata to selected answers, using flat answer pointers for Wizard selections.
Other events carry unfiltered answer snapshots without metadata. Standalone
FormEventHub callers must supply their own field-state handling.
For whole-step exclusion, use structured Wizard answers with
includeHiddenStepsInSubmit={false}; hidden-step membership does not produce field flags.
Webhook options
import type { JsonValue, WebhookConfig } from 'extended-dynamic-forms';
const metadata: Record<string, JsonValue> = {
tenant: 'operations',
correlation: null,
label: '{{literal text}}',
};
const webhooks: WebhookConfig[] = [{
url: 'https://example.com/events?channel=forms#receiver',
events: ['change', 'submit'],
body: metadata,
query: [{ key: 'tag', value: 'customer care' }, { key: 'tag', value: '' }],
formDataFields: ['contact', 'consent'],
}];bodyadds fixed extra root properties once. Its recursiveJsonValuetype accepts JSON primitives, objects, arrays and null. There is no templating or script execution:{{...}}remains literal text. Reserved keys are always ignored, even when absent from the event:eventType,timestamp,source,formData,fieldId,fieldValue,fieldPath,fromStep,toStep,errorsandmeta.queryappends URL-encoded string pairs in order. Existing query values, duplicate keys and empty values are retained; the fragment remains last. An absent or empty list leaves the URL byte-for-byte unchanged.formDataFieldsselects literal own top-level answer keys for this webhook only. It does not interpret paths, wildcards or renames. An absent list sends all answers; an empty list sends{}. Missing keys stay absent. Selected JSON values retain their complete nested structure and nulls. Null or missing answer data stays as it is. Submit selection uses ExtendedForm's inner answers and WizardForm's accepted flat answer view, not their wrappers. Other events select from their answer data. Selection does not alter other webhooks or callback payloads.
Native FormEventHub callers may supply handleAfterSubmit(data, { formData: answers })
when their canonical submit data is wrapped. Omitting the second argument selects
from data; an explicit null or undefined snapshot stays null or missing.
The snapshot travels with that event through deferred delivery and retries.
These options apply to every subscribed event without changing methods, headers,
debouncing, retries, timeouts or subscriptions. They do not mutate the shared
event, another webhook's payload or host/lifecycle callback data.
Answer selection is not a redaction control: it filters only formData, not
fieldValue, errors, query values or authored body properties. Hosts must apply
their own disclosure policy before configuring a destination.
These EDF options do not interpret Formstudio's legacy request_body or
form_data_fields storage.
Native RJSF integration
For native hosts that register EDF's TelephoneWidget on their own RJSF 6.11
Form, wrap that Form in the exported RJSF611ErrorStateAdapter, inside
FormEventProvider. It preserves the native ref and real change notifications
while clearing only widget-owned stale feedback. ExtendedForm and Wizard steps
install this boundary internally.
See Telephone integration
for a native example.
Use the wrapper to clear stale Telephone feedback in native RJSF 6.11 forms. Non-live forms do not acquire schema-only errors from the adapter before Submit; post-submit feedback can clear when corrected.
Known limitations
- CommonJS
requireis unsupported; use ESM imports. NodeNext declaration compatibility is not established; use the ESNext/Bundler configuration above. - The separate vanilla JavaScript build lacks JsonLogic support. Use the React package for declarative conditional forms.
- Arrays do not support drag-and-drop.
- JSON Patch effects support
add,removeandreplaceonly, notmove,copyortest.
Links and license
MIT. See LICENSE.
