# 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

```bash
npm install extended-dynamic-forms
```

Install compatible peer dependencies if your application does not already have them:

```bash
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

```tsx
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.

```tsx
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.

```tsx
<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`:

```json
{
  "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:

```json
{
  "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.

```json
{
  "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`,
  `disableWeekends` and `range: true` for a range picker.
- Number options include `min`, `max`, `step`, `precision`, `groupSeparator`,
  `decimalSeparator`, `controls`, `keyboard` and `changeOnWheel`. Display grouping
  does not become part of numeric answers. Schema bounds validate independently
  of input clamping.
- Exact decimals require `stringMode: true` with 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 `mask` uses `9` for a digit, `a` for an ASCII letter and `*` for any
  character. Mask literals affect display, while answers retain digits and an
  optional leading `+`. `useCountryMasks: true` enables country-pattern fallback;
  an explicit field mask takes precedence. Letter slots are display-only.
- URL answers accept HTTP/HTTPS by default. `allowedProtocols` replaces those
  defaults; `[]` rejects every nonempty URL. `javascript:`, `data:` and `vbscript:`
  remain blocked even if listed. Link previews are restricted to HTTP/HTTPS.
- File upload options include `accept`, `maxSize`, `multiple` and 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. Host `transformErrors` or
  `customValidate` feedback 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](#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`:

```json
{
  "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:

```ts
{ 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

```tsx
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'],
}];
```

- `body` adds fixed extra root properties once. Its recursive `JsonValue` type
  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`, `errors` and `meta`.
- `query` appends 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.
- `formDataFields` selects 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](docs/custom-widgets-configuration.md#rjsf-611-native-error-state-compatibility-050)
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 `require` is 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`, `remove` and `replace` only, not `move`, `copy` or `test`.

## Links and license

- [npm package](https://www.npmjs.com/package/extended-dynamic-forms)
- [Version history](CHANGELOG.md)

MIT. See [LICENSE](LICENSE).
