@ilife-tech/react-application-flow-renderer 1.3.74 → 1.3.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -1855
- package/dist/example.js +19 -19
- package/dist/example.js.map +1 -1
- package/dist/index.js +19 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,1970 +1,460 @@
|
|
|
1
1
|
# React Dynamic Form Renderer
|
|
2
2
|
|
|
3
|
-
A powerful
|
|
4
|
-
conditional logic,
|
|
3
|
+
A powerful, schema-driven React renderer for dynamic forms and multi-page application flows — with
|
|
4
|
+
conditional logic (rules), rich field types, client + server validation, theming, and first-class
|
|
5
|
+
third-party/proxy API integration.
|
|
5
6
|
|
|
6
7
|
[](https://www.npmjs.com/package/@ilife-tech/react-application-flow-renderer)
|
|
7
|
-
[](
|
|
8
|
+
[](#license)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
> **Package name:** `@ilife-tech/react-application-flow-renderer` • **Default export:**
|
|
11
|
+
> `DynamicForm`
|
|
12
|
+
>
|
|
13
|
+
> You give `DynamicForm` a JSON `questionGroups` schema (plus optional rules, action buttons, and a
|
|
14
|
+
> theme); it renders a fully interactive, validated form and calls your `onSubmit`/`onApiTrigger`
|
|
15
|
+
> handlers. There is no `DynamicFormRenderer` component — the component is `DynamicForm`.
|
|
16
|
+
|
|
17
|
+
## Table of contents
|
|
10
18
|
|
|
11
|
-
- [Folder structure](#folder-structure)
|
|
12
19
|
- [Features](#features)
|
|
13
20
|
- [Prerequisites](#prerequisites)
|
|
14
21
|
- [Installation](#installation)
|
|
15
|
-
- [Quick
|
|
16
|
-
- [Form
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
+
- [Quick start](#quick-start)
|
|
23
|
+
- [Form schema structure](#form-schema-structure)
|
|
24
|
+
- [Field types](#field-types)
|
|
25
|
+
- [`DynamicForm` props](#dynamicform-props)
|
|
26
|
+
- [Conditional logic (rules)](#conditional-logic-rules)
|
|
27
|
+
- [Validation](#validation)
|
|
28
|
+
- [API integration](#api-integration)
|
|
29
|
+
- [Upload field](#upload-field)
|
|
30
|
+
- [Theming](#theming)
|
|
31
|
+
- [Toast notifications](#toast-notifications)
|
|
32
|
+
- [Imperative API (`formApiRef`)](#imperative-api-formapiref)
|
|
33
|
+
- [Performance & engine options](#performance--engine-options)
|
|
34
|
+
- [Test IDs](#test-ids)
|
|
22
35
|
- [Troubleshooting](#troubleshooting)
|
|
23
|
-
- [
|
|
24
|
-
|
|
25
|
-
## Folder structure
|
|
26
|
-
|
|
27
|
-
This package uses a **layer-only** structure (components, context, hooks, utils, theme, etc.). For
|
|
28
|
-
the full layout and where to place new modules, see
|
|
29
|
-
[docs/coding-standards/folder-structure.md](docs/coding-standards/folder-structure.md).
|
|
36
|
+
- [Documentation](#documentation)
|
|
37
|
+
- [License](#license)
|
|
30
38
|
|
|
31
39
|
## Features
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- **
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
- **
|
|
41
|
+
- **Schema-driven** — build complex forms from a JSON `questionGroups` schema; no per-field JSX.
|
|
42
|
+
- **Conditional logic** — show/hide, enable/disable, require, set values, and mutate option lists
|
|
43
|
+
via question-, group-, option-, and button-level rules evaluated by a built-in rules engine.
|
|
44
|
+
- **Rich field catalog** — text, numeric/masked, currency/percentage, date/time, select/combobox,
|
|
45
|
+
radio/checkbox groups, address (Google Places), identification, file/PDF upload, repeating
|
|
46
|
+
card-lists, review, and display/link fields.
|
|
47
|
+
- **Validation** — client-side (schema-driven) and server-side (`validationErrors`) with per-field
|
|
48
|
+
and nested/card-list error handling.
|
|
49
|
+
- **Third-party API integration** — a "universal" `onApiTrigger` contract lets fields drive external
|
|
50
|
+
API calls and apply the response back into form state.
|
|
51
|
+
- **Theming** — a plain-object custom theme, shallow-merged over sensible defaults.
|
|
52
|
+
- **Accessibility & test hooks** — ARIA on interactive fields and a deterministic `data-testid`
|
|
53
|
+
system.
|
|
42
54
|
|
|
43
55
|
## Prerequisites
|
|
44
56
|
|
|
45
|
-
- React 18
|
|
46
|
-
- Node.js 20
|
|
47
|
-
-
|
|
48
|
-
- Browser support: Latest versions of Chrome, Firefox, Safari, and Edge
|
|
57
|
+
- **React 17, 18, or 19** (peer dependency).
|
|
58
|
+
- **Node.js `>= 20`** and **npm `>= 10`** for local development of this package.
|
|
59
|
+
- Modern browsers (latest Chrome, Firefox, Safari, Edge).
|
|
49
60
|
|
|
50
61
|
## Installation
|
|
51
62
|
|
|
52
63
|
```bash
|
|
53
|
-
# Using npm
|
|
54
64
|
npm install @ilife-tech/react-application-flow-renderer
|
|
55
|
-
|
|
56
|
-
# Using yarn
|
|
65
|
+
# or
|
|
57
66
|
yarn add @ilife-tech/react-application-flow-renderer
|
|
58
67
|
```
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### Basic Implementation
|
|
69
|
+
`react` and `react-dom` are peer dependencies — install them in your app if you haven't already:
|
|
63
70
|
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
import DynamicForm from '@ilife-tech/react-application-flow-renderer';
|
|
67
|
-
|
|
68
|
-
const App = () => {
|
|
69
|
-
// Form schema definition
|
|
70
|
-
const questionGroups = [
|
|
71
|
-
{
|
|
72
|
-
groupId: 'personalInfo',
|
|
73
|
-
groupName: 'Personal Information',
|
|
74
|
-
questions: [
|
|
75
|
-
{
|
|
76
|
-
questionId: 'fullName',
|
|
77
|
-
questionType: 'text',
|
|
78
|
-
label: 'Full Name',
|
|
79
|
-
validation: { required: true },
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
questionId: 'email',
|
|
83
|
-
questionType: 'email',
|
|
84
|
-
label: 'Email Address',
|
|
85
|
-
validation: { required: true, email: true },
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
// Action buttons configuration
|
|
92
|
-
const actionSchema = [
|
|
93
|
-
{ text: 'Submit', action: 'submit' },
|
|
94
|
-
{ text: 'Cancel', action: 'cancel' },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
// Form submission handler
|
|
98
|
-
const handleSubmit = (formState, action) => {
|
|
99
|
-
console.log('Form submitted:', formState.values);
|
|
100
|
-
console.log('Action:', action);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<DynamicForm
|
|
105
|
-
questionGroups={questionGroups}
|
|
106
|
-
actionSchema={actionSchema}
|
|
107
|
-
onSubmit={handleSubmit}
|
|
108
|
-
/>
|
|
109
|
-
);
|
|
110
|
-
};
|
|
71
|
+
```bash
|
|
72
|
+
npm install react react-dom
|
|
111
73
|
```
|
|
112
74
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Below is a comprehensive real-world implementation showcasing advanced features like API
|
|
116
|
-
integration, state management, and theming:
|
|
75
|
+
## Quick start
|
|
117
76
|
|
|
118
77
|
```jsx
|
|
119
|
-
import
|
|
120
|
-
import SectionListRenderer from '@ilife-tech/react-section-renderer';
|
|
121
|
-
import { DynamicForm } from '@ilife-tech/react-application-flow-renderer';
|
|
122
|
-
|
|
123
|
-
// Constants and configuration
|
|
124
|
-
const API_CONFIG = {
|
|
125
|
-
BASE_URL: process.env.REACT_APP_API_BASE_URL || '<API_BASE_URL>',
|
|
126
|
-
ENDPOINTS: {
|
|
127
|
-
STORE_VENDOR: '<API_ENDPOINT>',
|
|
128
|
-
},
|
|
129
|
-
MAX_API_ATTEMPTS: 8,
|
|
130
|
-
};
|
|
78
|
+
import DynamicForm from '@ilife-tech/react-application-flow-renderer';
|
|
131
79
|
|
|
132
|
-
|
|
80
|
+
// A minimal schema: one group with three questions.
|
|
81
|
+
const FORM_SCHEMA = [
|
|
133
82
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const apiConfig = {
|
|
144
|
-
googlePlaces: {
|
|
145
|
-
apiKey: process.env.REACT_APP_GOOGLE_PLACES_API_KEY,
|
|
146
|
-
options: {
|
|
147
|
-
minSearchLength: 3,
|
|
148
|
-
debounceTime: 300,
|
|
149
|
-
componentRestrictions: {
|
|
150
|
-
country: process.env.REACT_APP_GOOGLE_PLACES_REGION || 'US',
|
|
83
|
+
groupId: 1,
|
|
84
|
+
groupName: 'Personal Information',
|
|
85
|
+
questions: [
|
|
86
|
+
{
|
|
87
|
+
questionId: 101,
|
|
88
|
+
name: 'firstName',
|
|
89
|
+
questionType: 'text',
|
|
90
|
+
label: 'First Name',
|
|
91
|
+
required: true,
|
|
151
92
|
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
'Carrier-Auth': process.env.REACT_APP_CARRIER_AUTH,
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// Theme configuration
|
|
170
|
-
const THEME = {
|
|
171
|
-
breakpoints: {
|
|
172
|
-
mobile: '@media (max-width: 480px)',
|
|
173
|
-
tablet: '@media (min-width: 481px) and (max-width: 768px)',
|
|
174
|
-
desktop: '@media (min-width: 769px)',
|
|
175
|
-
},
|
|
176
|
-
body: {
|
|
177
|
-
backgroundColor: '#FFFFFF',
|
|
178
|
-
color: '#333333',
|
|
179
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
180
|
-
fontSize: '16px',
|
|
181
|
-
lineHeight: '1.5',
|
|
182
|
-
},
|
|
183
|
-
form: {
|
|
184
|
-
backgroundColor: '#FFFFFF',
|
|
185
|
-
padding: '20px',
|
|
186
|
-
maxWidth: '800px',
|
|
187
|
-
margin: '0 auto',
|
|
188
|
-
fieldSpacing: '1.5rem',
|
|
189
|
-
groupSpacing: '2rem',
|
|
190
|
-
},
|
|
191
|
-
colors: {
|
|
192
|
-
primary: '#1D4ED8',
|
|
193
|
-
secondary: '#6B7280',
|
|
194
|
-
success: '#059669',
|
|
195
|
-
danger: '#DC2626',
|
|
196
|
-
warning: '#D97706',
|
|
197
|
-
info: '#2563EB',
|
|
198
|
-
light: '#F3F4F6',
|
|
199
|
-
dark: '#1F2937',
|
|
200
|
-
error: '#DC2626',
|
|
201
|
-
border: '#D1D5DB',
|
|
202
|
-
inputBackground: '#FFFFFF',
|
|
203
|
-
headerBackground: '#EFF6FF',
|
|
204
|
-
},
|
|
205
|
-
field: {
|
|
206
|
-
marginBottom: '1rem',
|
|
207
|
-
width: '100%',
|
|
208
|
-
},
|
|
209
|
-
label: {
|
|
210
|
-
display: 'block',
|
|
211
|
-
marginBottom: '0.5rem',
|
|
212
|
-
fontSize: '0.875rem',
|
|
213
|
-
fontWeight: '500',
|
|
214
|
-
color: '#374151',
|
|
215
|
-
},
|
|
216
|
-
input: {
|
|
217
|
-
width: '100%',
|
|
218
|
-
padding: '0.5rem',
|
|
219
|
-
fontSize: '1rem',
|
|
220
|
-
borderRadius: '0.375rem',
|
|
221
|
-
border: '1px solid #D1D5DB',
|
|
222
|
-
backgroundColor: '#FFFFFF',
|
|
223
|
-
'&:focus': {
|
|
224
|
-
outline: 'none',
|
|
225
|
-
borderColor: '#2563EB',
|
|
226
|
-
boxShadow: '0 0 0 2px rgba(37, 99, 235, 0.2)',
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
error: {
|
|
230
|
-
color: '#DC2626',
|
|
231
|
-
fontSize: '0.875rem',
|
|
232
|
-
marginTop: '0.25rem',
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
function App() {
|
|
237
|
-
// State Management
|
|
238
|
-
const [formState, setFormState] = useState({
|
|
239
|
-
storeVenderData: null,
|
|
240
|
-
questionGroups: [],
|
|
241
|
-
pageRules: [],
|
|
242
|
-
pageRuleGroup: [],
|
|
243
|
-
validationSchema: [],
|
|
244
|
-
actionSchema: [],
|
|
245
|
-
sectionSchema: [],
|
|
246
|
-
caseId: null,
|
|
247
|
-
pageId: null,
|
|
248
|
-
apiCallCounter: 0,
|
|
249
|
-
selectedAction: '',
|
|
250
|
-
error: null,
|
|
251
|
-
isLoading: false,
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Memoized API headers
|
|
255
|
-
const headers = useMemo(
|
|
256
|
-
() => ({
|
|
257
|
-
'Content-Type': 'application/json',
|
|
258
|
-
Authorization: process.env.REACT_APP_AUTH_TOKEN || '<AUTH_TOKEN>',
|
|
259
|
-
CarrierAuthorization: process.env.REACT_APP_CARRIER_AUTH || '<CARRIER_AUTH_TOKEN>',
|
|
260
|
-
}),
|
|
261
|
-
[]
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
// Transform form data with error handling
|
|
265
|
-
const transformFormData = useCallback((formData, schema) => {
|
|
266
|
-
try {
|
|
267
|
-
// Handle empty array or non-object formData
|
|
268
|
-
const formDataObj = Array.isArray(formData) || typeof formData !== 'object' ? {} : formData;
|
|
269
|
-
const { values = {}, visibility = {}, disabled = {} } = formDataObj;
|
|
270
|
-
const transformedData = [];
|
|
271
|
-
|
|
272
|
-
// Process form data based on schema
|
|
273
|
-
// ... (transform logic omitted for brevity)
|
|
274
|
-
|
|
275
|
-
return transformedData;
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error('Error transforming form data:', error);
|
|
278
|
-
throw new Error('Failed to transform form data');
|
|
279
|
-
}
|
|
280
|
-
}, []);
|
|
281
|
-
|
|
282
|
-
// API call with error handling
|
|
283
|
-
const fetchStoreQuestionVendor = useCallback(
|
|
284
|
-
async (questions, action = '', nextPageId = '') => {
|
|
285
|
-
try {
|
|
286
|
-
const { caseId, pageId } = formState;
|
|
287
|
-
setFormState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
288
|
-
|
|
289
|
-
const requestPayload = {
|
|
290
|
-
caseId,
|
|
291
|
-
pageId,
|
|
292
|
-
questions,
|
|
293
|
-
user: '<USER_TYPE>',
|
|
294
|
-
agentId: '<AGENT_ID>',
|
|
295
|
-
templateName: '<PRODUCT_NAME>',
|
|
296
|
-
...(action && { action }),
|
|
297
|
-
...(nextPageId && { nextPageId }),
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.STORE_VENDOR}`, {
|
|
301
|
-
method: 'POST',
|
|
302
|
-
headers,
|
|
303
|
-
body: JSON.stringify(requestPayload),
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
if (!response.ok) {
|
|
307
|
-
throw new Error(`API Error: ${response.status}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const { data: result } = await response.json();
|
|
311
|
-
const { pageDefinition, sectionList, caseId: newCaseId, pageId: newPageId } = result || {};
|
|
312
|
-
|
|
313
|
-
const {
|
|
314
|
-
questionGroups = [],
|
|
315
|
-
pageRules: rawPageRules = null,
|
|
316
|
-
pageRuleGroup: rawPageRuleGroup = null,
|
|
317
|
-
validationErrors = [],
|
|
318
|
-
actionButtons = [],
|
|
319
|
-
} = pageDefinition || {};
|
|
320
|
-
|
|
321
|
-
// Process and update state with API response
|
|
322
|
-
setFormState((prev) => ({
|
|
323
|
-
...prev,
|
|
324
|
-
storeVenderData: result,
|
|
325
|
-
questionGroups,
|
|
326
|
-
pageRules: Array.isArray(rawPageRules) ? rawPageRules : [],
|
|
327
|
-
pageRuleGroup: Array.isArray(rawPageRuleGroup) ? rawPageRuleGroup : [],
|
|
328
|
-
validationSchema: validationErrors,
|
|
329
|
-
actionSchema: actionButtons,
|
|
330
|
-
sectionSchema: sectionList || [],
|
|
331
|
-
caseId: newCaseId || '',
|
|
332
|
-
pageId: newPageId || '',
|
|
333
|
-
selectedAction: action,
|
|
334
|
-
isLoading: false,
|
|
335
|
-
}));
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.error('API Error:', error);
|
|
338
|
-
setFormState((prev) => ({
|
|
339
|
-
...prev,
|
|
340
|
-
error: error.message,
|
|
341
|
-
isLoading: false,
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
[formState.caseId, formState.pageId, headers]
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
// Form submission handler
|
|
349
|
-
const handleSubmit = useCallback(
|
|
350
|
-
(newFormState, action) => {
|
|
351
|
-
const { questionGroups } = formState;
|
|
352
|
-
const transformedData = transformFormData(newFormState, questionGroups);
|
|
353
|
-
fetchStoreQuestionVendor(transformedData, action);
|
|
354
|
-
},
|
|
355
|
-
[fetchStoreQuestionVendor, formState.questionGroups, transformFormData]
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
// Section change handler
|
|
359
|
-
const handleSectionChange = useCallback(
|
|
360
|
-
(pageId) => {
|
|
361
|
-
const { questionGroups } = formState;
|
|
362
|
-
const transformedData = transformFormData([], questionGroups);
|
|
363
|
-
fetchStoreQuestionVendor(transformedData, '', pageId);
|
|
364
|
-
},
|
|
365
|
-
[fetchStoreQuestionVendor, formState.questionGroups, transformFormData]
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
// Initial data fetch
|
|
369
|
-
useEffect(() => {
|
|
370
|
-
fetchStoreQuestionVendor(INITIAL_QUESTIONS);
|
|
371
|
-
}, []);
|
|
372
|
-
|
|
373
|
-
// Extract commonly used state values
|
|
374
|
-
const {
|
|
375
|
-
error,
|
|
376
|
-
isLoading,
|
|
377
|
-
storeVenderData,
|
|
378
|
-
sectionSchema,
|
|
379
|
-
questionGroups,
|
|
380
|
-
pageRules,
|
|
381
|
-
pageRuleGroup,
|
|
382
|
-
validationSchema,
|
|
383
|
-
actionSchema,
|
|
384
|
-
pageId,
|
|
385
|
-
} = formState;
|
|
386
|
-
|
|
387
|
-
// Handle API triggers from form fields
|
|
388
|
-
const handleApiTrigger = (apiRequest) => {
|
|
389
|
-
// Implementation for API trigger handling
|
|
390
|
-
console.log('API request:', apiRequest);
|
|
391
|
-
// Return promise with response structure
|
|
392
|
-
return Promise.resolve({
|
|
393
|
-
success: true,
|
|
394
|
-
data: {
|
|
395
|
-
// Example: update options for a dropdown
|
|
93
|
+
{
|
|
94
|
+
questionId: 102,
|
|
95
|
+
name: 'dob',
|
|
96
|
+
questionType: 'date',
|
|
97
|
+
label: 'Date of Birth',
|
|
98
|
+
dateFormat: 'MM/dd/yyyy',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
questionId: 103,
|
|
102
|
+
name: 'state',
|
|
103
|
+
questionType: 'dropdown',
|
|
104
|
+
label: 'State',
|
|
396
105
|
options: [
|
|
397
|
-
{
|
|
398
|
-
{
|
|
106
|
+
{ optionId: 'AL', text: 'Alabama' },
|
|
107
|
+
{ optionId: 'CA', text: 'California' },
|
|
399
108
|
],
|
|
400
109
|
},
|
|
401
|
-
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
return (
|
|
405
|
-
<div className="app-container">
|
|
406
|
-
<header className="app-header">
|
|
407
|
-
<h1>Dynamic Form Example</h1>
|
|
408
|
-
</header>
|
|
409
|
-
|
|
410
|
-
<div className="app-main">
|
|
411
|
-
{error && (
|
|
412
|
-
<div className="error-message" role="alert">
|
|
413
|
-
{error}
|
|
414
|
-
</div>
|
|
415
|
-
)}
|
|
416
|
-
|
|
417
|
-
{isLoading && (
|
|
418
|
-
<div className="loading-indicator" role="status">
|
|
419
|
-
Loading...
|
|
420
|
-
</div>
|
|
421
|
-
)}
|
|
422
|
-
|
|
423
|
-
{storeVenderData && (
|
|
424
|
-
<>
|
|
425
|
-
<aside className="app-sidebar">
|
|
426
|
-
{!isLoading && sectionSchema?.length > 0 && (
|
|
427
|
-
<SectionListRenderer
|
|
428
|
-
sectionLists={sectionSchema}
|
|
429
|
-
onSectionClick={handleSectionChange}
|
|
430
|
-
customStyles={{ backgroundColor: '#f5f5f5' }}
|
|
431
|
-
pageId={pageId || sectionSchema[0]?.pageId}
|
|
432
|
-
/>
|
|
433
|
-
)}
|
|
434
|
-
</aside>
|
|
435
|
-
|
|
436
|
-
<main className="app-content">
|
|
437
|
-
{questionGroups?.length > 0 && (
|
|
438
|
-
<DynamicForm
|
|
439
|
-
questionGroups={questionGroups}
|
|
440
|
-
pageRules={pageRules}
|
|
441
|
-
pageRuleGroup={pageRuleGroup}
|
|
442
|
-
validationErrors={validationSchema}
|
|
443
|
-
actionButtons={actionSchema}
|
|
444
|
-
theme={THEME}
|
|
445
|
-
onSubmit={handleSubmit}
|
|
446
|
-
onApiTrigger={handleApiTrigger}
|
|
447
|
-
apiConfig={apiConfig}
|
|
448
|
-
/>
|
|
449
|
-
)}
|
|
450
|
-
</main>
|
|
451
|
-
</>
|
|
452
|
-
)}
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export default App;
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
## Form Schema Structure
|
|
462
|
-
|
|
463
|
-
The form structure is defined by a JSON schema that specifies question groups and their fields. Each
|
|
464
|
-
form consists of one or more question groups, each containing multiple field definitions.
|
|
465
|
-
|
|
466
|
-
### Question Group Structure
|
|
467
|
-
|
|
468
|
-
```javascript
|
|
469
|
-
{
|
|
470
|
-
groupId: string, // Unique identifier for the group
|
|
471
|
-
groupName: string, // Display name for the group (optional)
|
|
472
|
-
description: string, // Group description (optional)
|
|
473
|
-
columnCount: number, // Number of columns for layout (default: 1)
|
|
474
|
-
visible: boolean, // Group visibility (default: true)
|
|
475
|
-
questions: Array<Question> // Array of question objects
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Question (Field) Structure
|
|
480
|
-
|
|
481
|
-
```javascript
|
|
482
|
-
{
|
|
483
|
-
questionId: string, // Unique identifier for the question/field
|
|
484
|
-
questionType: string, // Field type (text, dropdown, checkbox, etc.)
|
|
485
|
-
label: string, // Field label
|
|
486
|
-
placeholder: string, // Field placeholder (optional)
|
|
487
|
-
helpText: string, // Help text below the field (optional)
|
|
488
|
-
defaultValue: any, // Default field value (optional)
|
|
489
|
-
validation: Object, // Validation rules (optional)
|
|
490
|
-
visible: boolean, // Field visibility (default: true)
|
|
491
|
-
disabled: boolean, // Field disabled state (default: false)
|
|
492
|
-
required: boolean, // Whether field is required (default: false)
|
|
493
|
-
options: Array<Option>, // Options for select/dropdown/radio fields
|
|
494
|
-
layout: Object, // Layout configuration (optional)
|
|
495
|
-
style: Object, // Custom styling (optional)
|
|
496
|
-
apiTrigger: Object // API integration configuration (optional)
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Action Schema Structure
|
|
501
|
-
|
|
502
|
-
The action schema defines form buttons and their behaviors:
|
|
503
|
-
|
|
504
|
-
```javascript
|
|
505
|
-
[
|
|
506
|
-
{
|
|
507
|
-
text: string, // Button text
|
|
508
|
-
action: string, // Action identifier (e.g., 'submit', 'back', 'next')
|
|
509
|
-
position: string, // Button position (optional, default: 'right')
|
|
110
|
+
],
|
|
510
111
|
},
|
|
511
112
|
];
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## API Integration
|
|
515
|
-
|
|
516
|
-
The package provides robust third-party API integration capabilities, allowing forms to fetch data,
|
|
517
|
-
populate options, and submit data to external services.
|
|
518
113
|
|
|
519
|
-
|
|
114
|
+
const ACTION_SCHEMA = [{ actionName: 'submit', label: 'Submit', type: 'submit' }];
|
|
520
115
|
|
|
521
|
-
|
|
522
|
-
form
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
import DynamicForm from '@ilife-tech/react-application-flow-renderer';
|
|
527
|
-
import { handleApiTrigger } from './services/apiService';
|
|
528
|
-
|
|
529
|
-
const MyForm = () => {
|
|
530
|
-
// Form schema with API trigger configuration
|
|
531
|
-
const questionGroups = [
|
|
532
|
-
{
|
|
533
|
-
groupId: 'locationInfo',
|
|
534
|
-
questions: [
|
|
535
|
-
{
|
|
536
|
-
questionId: 'country',
|
|
537
|
-
questionType: 'dropdown',
|
|
538
|
-
label: 'Country',
|
|
539
|
-
options: [
|
|
540
|
-
{ name: 'United States', value: 'US' },
|
|
541
|
-
{ name: 'Canada', value: 'CA' },
|
|
542
|
-
],
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
questionId: 'state',
|
|
546
|
-
questionType: 'dropdown',
|
|
547
|
-
label: 'State/Province',
|
|
548
|
-
placeholder: 'Select a state',
|
|
549
|
-
// API trigger configuration
|
|
550
|
-
apiTrigger: {
|
|
551
|
-
type: 'dropdown',
|
|
552
|
-
dependsOn: ['country'], // Fields this API call depends on
|
|
553
|
-
apiUrl: 'getStatesByCountry', // API identifier
|
|
554
|
-
triggerEvents: ['onChange', 'onLoad'], // When to trigger API
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
},
|
|
559
|
-
];
|
|
560
|
-
|
|
561
|
-
// API configuration
|
|
562
|
-
const apiConfig = {
|
|
563
|
-
baseUrl: process.env.REACT_APP_API_BASE_URL || 'https://api.example.com',
|
|
564
|
-
headers: {
|
|
565
|
-
'Content-Type': 'application/json',
|
|
566
|
-
Authorization: `Bearer ${process.env.REACT_APP_API_TOKEN}`,
|
|
567
|
-
},
|
|
568
|
-
timeout: 5000,
|
|
569
|
-
// Note: API helpers in this repo default to `credentials: 'same-origin'`.
|
|
570
|
-
// If your backend relies on cookies for auth and you need cross-origin cookies, opt-in to:
|
|
571
|
-
// { credentials: 'include' } and ensure you also have proper CSRF protections.
|
|
116
|
+
export default function App() {
|
|
117
|
+
// onSubmit receives the current form state and the action that triggered submission.
|
|
118
|
+
const handleSubmit = (formState, action) => {
|
|
119
|
+
console.log('action:', action);
|
|
120
|
+
console.log('values:', formState?.values ?? formState);
|
|
572
121
|
};
|
|
573
122
|
|
|
574
123
|
return (
|
|
575
124
|
<DynamicForm
|
|
576
|
-
questionGroups={
|
|
577
|
-
|
|
578
|
-
|
|
125
|
+
questionGroups={FORM_SCHEMA}
|
|
126
|
+
actionButtons={ACTION_SCHEMA}
|
|
127
|
+
onSubmit={handleSubmit}
|
|
128
|
+
theme={{ colors: { primary: '#1D4ED8' } }}
|
|
579
129
|
/>
|
|
580
130
|
);
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
export default MyForm;
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### Implementing the API Handler
|
|
587
|
-
|
|
588
|
-
The API handler function processes API triggers and returns updates to the form. Here's a practical
|
|
589
|
-
example based on the package's example implementation:
|
|
590
|
-
|
|
591
|
-
```javascript
|
|
592
|
-
// services/apiService.js
|
|
593
|
-
import { createApiResponse } from '../utils/apiUtils';
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Handle API trigger events from form fields
|
|
597
|
-
* @param {Object} triggerData - Data from the form's onApiTrigger event
|
|
598
|
-
* @returns {Promise<Object>} - Response with form updates
|
|
599
|
-
*/
|
|
600
|
-
export const handleApiTrigger = async (triggerData) => {
|
|
601
|
-
const { question, value, triggerType, formState } = triggerData;
|
|
602
|
-
|
|
603
|
-
// Extract API configuration
|
|
604
|
-
const { apiTrigger, questionId } = question;
|
|
605
|
-
|
|
606
|
-
// Skip if no API trigger is configured
|
|
607
|
-
if (!apiTrigger || !apiTrigger.apiUrl) {
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
// For dropdown fields that depend on other fields
|
|
613
|
-
if (question.questionType === 'dropdown' && triggerType === 'onChange') {
|
|
614
|
-
// Example: Fetch states based on selected country
|
|
615
|
-
if (apiTrigger.apiUrl === 'getStatesByCountry') {
|
|
616
|
-
const countryValue = formState.values[apiTrigger.dependsOn[0]];
|
|
617
|
-
|
|
618
|
-
// Don't proceed if no country is selected
|
|
619
|
-
if (!countryValue) {
|
|
620
|
-
return createApiResponse({
|
|
621
|
-
options: { [questionId]: [] },
|
|
622
|
-
values: { [questionId]: '' },
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Make API request
|
|
627
|
-
const response = await fetch(`${apiConfig.baseUrl}/states?country=${countryValue}`, {
|
|
628
|
-
headers: apiConfig.headers,
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
const data = await response.json();
|
|
632
|
-
|
|
633
|
-
// Map API response to dropdown options
|
|
634
|
-
const stateOptions = data.map((state) => ({
|
|
635
|
-
name: state.name,
|
|
636
|
-
value: state.code,
|
|
637
|
-
}));
|
|
638
|
-
|
|
639
|
-
// Return updates for the form
|
|
640
|
-
return createApiResponse({
|
|
641
|
-
options: { [questionId]: stateOptions },
|
|
642
|
-
visibility: { [questionId]: true },
|
|
643
|
-
disabled: { [questionId]: false },
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return null;
|
|
649
|
-
} catch (error) {
|
|
650
|
-
console.error('API error:', error);
|
|
651
|
-
|
|
652
|
-
// Return error response
|
|
653
|
-
return createApiResponse({
|
|
654
|
-
apiErrors: { [questionId]: 'Failed to fetch data. Please try again.' },
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### API Response Structure
|
|
661
|
-
|
|
662
|
-
API responses should be formatted as an object with field updates:
|
|
663
|
-
|
|
664
|
-
```javascript
|
|
665
|
-
// utils/apiUtils.js
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Create formatted API response for form updates
|
|
669
|
-
* @param {Object} updates - Updates to apply to the form
|
|
670
|
-
* @returns {Object} - Formatted response
|
|
671
|
-
*/
|
|
672
|
-
export const createApiResponse = (updates = {}) => {
|
|
673
|
-
return {
|
|
674
|
-
// Field values to update
|
|
675
|
-
values: updates.values || {},
|
|
676
|
-
|
|
677
|
-
// Dropdown options to update
|
|
678
|
-
options: updates.options || {},
|
|
679
|
-
|
|
680
|
-
// Field visibility updates
|
|
681
|
-
visibility: updates.visibility || {},
|
|
682
|
-
|
|
683
|
-
// Field disabled state updates
|
|
684
|
-
disabled: updates.disabled || {},
|
|
685
|
-
|
|
686
|
-
// Field required state updates
|
|
687
|
-
required: updates.required || {},
|
|
688
|
-
|
|
689
|
-
// Error messages
|
|
690
|
-
apiErrors: updates.apiErrors || {},
|
|
691
|
-
|
|
692
|
-
// Loading states
|
|
693
|
-
apiLoading: updates.apiLoading || {},
|
|
694
|
-
};
|
|
695
|
-
};
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
### Advanced API Integration Example
|
|
699
|
-
|
|
700
|
-
Here's a more comprehensive example showing how to handle complex API integration with request
|
|
701
|
-
queuing and response mapping:
|
|
702
|
-
|
|
703
|
-
```javascript
|
|
704
|
-
// services/api/handlers/dropdownApiHandler.js
|
|
705
|
-
|
|
706
|
-
import { createApiResponse } from '../../../utils/apiUtils';
|
|
707
|
-
|
|
708
|
-
// Request queue for managing concurrent API calls
|
|
709
|
-
class ApiRequestQueue {
|
|
710
|
-
constructor() {
|
|
711
|
-
this.queue = new Map();
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
async enqueue(questionId, apiCall) {
|
|
715
|
-
if (this.queue.has(questionId)) {
|
|
716
|
-
return this.queue.get(questionId);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const promise = apiCall().finally(() => {
|
|
720
|
-
this.queue.delete(questionId);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
this.queue.set(questionId, promise);
|
|
724
|
-
return promise;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const requestQueue = new ApiRequestQueue();
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Handle API calls for dropdown fields
|
|
732
|
-
* @param {Object} triggerData - API trigger data
|
|
733
|
-
* @returns {Promise<Object>} Response with options and updates
|
|
734
|
-
*/
|
|
735
|
-
export const handleDropdownApi = async (triggerData) => {
|
|
736
|
-
const { question, value, triggerType, formState } = triggerData;
|
|
737
|
-
const { questionId, apiTrigger } = question;
|
|
738
|
-
|
|
739
|
-
// Example: Handle different API endpoints based on apiUrl
|
|
740
|
-
switch (apiTrigger?.apiUrl) {
|
|
741
|
-
case 'getHouseholdMembers':
|
|
742
|
-
return requestQueue.enqueue(questionId, () =>
|
|
743
|
-
getHouseholdMembers(apiTrigger.apiUrl, formState)
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
case 'getStatesByCountry':
|
|
747
|
-
// Get dependent field value
|
|
748
|
-
const country = formState.values[apiTrigger.dependsOn[0]];
|
|
749
|
-
if (!country) {
|
|
750
|
-
return createApiResponse({
|
|
751
|
-
options: { [questionId]: [] },
|
|
752
|
-
disabled: { [questionId]: true },
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
return requestQueue.enqueue(questionId, () => getStatesByCountry(apiTrigger.apiUrl, country));
|
|
757
|
-
|
|
758
|
-
default:
|
|
759
|
-
return null;
|
|
760
|
-
}
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Example API implementation for fetching states by country
|
|
765
|
-
*/
|
|
766
|
-
async function getStatesByCountry(apiUrl, country) {
|
|
767
|
-
try {
|
|
768
|
-
// Here we'd normally implement our API call
|
|
769
|
-
const response = await fetch(`https://api.example.com/states?country=${country}`);
|
|
770
|
-
const data = await response.json();
|
|
771
|
-
|
|
772
|
-
// Map response to options format
|
|
773
|
-
const options = data.map((state) => ({
|
|
774
|
-
name: state.stateName || state.name,
|
|
775
|
-
value: state.stateCode || state.code,
|
|
776
|
-
}));
|
|
777
|
-
|
|
778
|
-
return createApiResponse({
|
|
779
|
-
options: { state: options },
|
|
780
|
-
disabled: { state: false },
|
|
781
|
-
visibility: { state: true },
|
|
782
|
-
});
|
|
783
|
-
} catch (error) {
|
|
784
|
-
console.error('API error:', error);
|
|
785
|
-
return createApiResponse({
|
|
786
|
-
apiErrors: { state: 'Failed to fetch states. Please try again.' },
|
|
787
|
-
options: { state: [] },
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
131
|
}
|
|
791
132
|
```
|
|
792
133
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
The `DynamicForm` component accepts the following props:
|
|
796
|
-
|
|
797
|
-
| Prop | Type | Required | Default | Description |
|
|
798
|
-
| ------------------ | ---------- | -------- | ------- | ------------------------------------------------- |
|
|
799
|
-
| `questionGroups` | `Array` | Yes | `[]` | Array of question groups defining form structure |
|
|
800
|
-
| `actionSchema` | `Array` | No | `[]` | Array of action buttons configuration |
|
|
801
|
-
| `onSubmit` | `Function` | No | - | Form submission handler |
|
|
802
|
-
| `onApiTrigger` | `Function` | No | - | API trigger event handler |
|
|
803
|
-
| `apiConfig` | `Object` | No | `{}` | API configuration options |
|
|
804
|
-
| `theme` | `Object` | No | `{}` | Theme customization |
|
|
805
|
-
| `validationSchema` | `Array` | No | `[]` | Additional validation rules |
|
|
806
|
-
| `pageRules` | `Array` | No | `[]` | Page rules for conditional logic |
|
|
807
|
-
| `pageRuleGroup` | `Array` | No | `[]` | Page rule groups with complex boolean expressions |
|
|
808
|
-
| `debug` | `Boolean` | No | `false` | Enable debug mode |
|
|
809
|
-
| `formApiRef` | `Object` | No | - | Ref object to access form API methods |
|
|
810
|
-
| `toastConfig` | `Object` | No | `{}` | Toast configuration |
|
|
134
|
+
A complete, runnable example (rules, API integration, `formApiRef`, theming) lives in
|
|
135
|
+
[`example/`](example) — see [`example/index.js`](example/index.js) for the canonical invocation.
|
|
811
136
|
|
|
812
|
-
|
|
137
|
+
## Form schema structure
|
|
813
138
|
|
|
814
|
-
|
|
139
|
+
A form is an array of **groups**, each containing an array of **questions**:
|
|
815
140
|
|
|
816
|
-
|
|
817
|
-
(fields) to render.
|
|
818
|
-
|
|
819
|
-
```javascript
|
|
141
|
+
```js
|
|
820
142
|
const questionGroups = [
|
|
821
143
|
{
|
|
822
|
-
groupId:
|
|
823
|
-
groupName: '
|
|
824
|
-
questions: [
|
|
825
|
-
/* question objects */
|
|
826
|
-
],
|
|
144
|
+
groupId: 1, // string | number
|
|
145
|
+
groupName: 'Group Title',
|
|
146
|
+
questions: [/* question objects */],
|
|
827
147
|
},
|
|
828
148
|
];
|
|
829
149
|
```
|
|
830
150
|
|
|
831
|
-
|
|
151
|
+
Common question fields:
|
|
152
|
+
|
|
153
|
+
| Field | Type | Description |
|
|
154
|
+
| ------------------------ | ---------------- | -------------------------------------------------------------------------------------- |
|
|
155
|
+
| `questionId` | string \| number | Unique identifier used by state, rules, and validation. |
|
|
156
|
+
| `name` | string | Field name (used in the emitted value map). |
|
|
157
|
+
| `questionType` | string | One of the [field types](#field-types) below (case-insensitive). |
|
|
158
|
+
| `label` | string | Visible label (hidden when `showQuestionLabels={false}`). |
|
|
159
|
+
| `required` | boolean | Marks the field required for validation. |
|
|
160
|
+
| `visible` / `disabled` | boolean | Initial visibility / disabled state (rules can change these). |
|
|
161
|
+
| `value` / `defaultValue` | any | Initial value. |
|
|
162
|
+
| `options` | array | For choice fields — array of `{ optionId, text }` (or `{ value, label }`, or strings). |
|
|
163
|
+
| `validation` | object | Field-level validation config (min/max length, format, etc.). |
|
|
164
|
+
| `helperText` / `tooltip` | string | Supplementary text / info tooltip. |
|
|
165
|
+
|
|
166
|
+
For the full schema reference (every question type's fields and options), see
|
|
167
|
+
[docs/reference/form-schema.md](docs/reference/form-schema.md).
|
|
168
|
+
|
|
169
|
+
## Field types
|
|
170
|
+
|
|
171
|
+
Set `questionType` to one of the following (matched case-insensitively):
|
|
172
|
+
|
|
173
|
+
| Category | `questionType` values | Renders |
|
|
174
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
175
|
+
| Text | `text`, `textbox`, `email`, `url`, `password` | Single-line inputs. `text` uses the masking-capable input; `textbox` is the plain text field. |
|
|
176
|
+
| Numeric | `number`, `currency`, `percentage`, `ssn`, `itin`, `tin`, `tel` / `phone` / `phonenumber` / `mobile` | Formatted/masked numeric inputs. |
|
|
177
|
+
| Multi-line | `textarea` | Text area. |
|
|
178
|
+
| Choice | `select` / `dropdown`, `dynamicdropdown`, `combobox`\*, `radio` / `radiobutton` / `radioswitch`, `radiotabbuttons`, `checkbox`, `checkboxgroup` | Selection controls. |
|
|
179
|
+
| Date/time | `date`, `datetime`, `month`, `time`, `week` | Date pickers. |
|
|
180
|
+
| Composite | `address` (Google Places), `identification`, `cardlist` / `grid` / `beneficiary` (repeating sub-forms), `file` / `upload`, `pdf`, `pdfpackage` | Complex fields. |
|
|
181
|
+
| Display/action | `displaytext`, `hyperlink` / `link`, `button`, `spinner` | Read-only / action fields. |
|
|
182
|
+
|
|
183
|
+
Unknown `questionType` values fall back to a text input (with a console warning). The authoritative
|
|
184
|
+
type→component mapping is `src/components/core/FormFieldRegistry.jsx`; see
|
|
185
|
+
[docs/architecture/field-components.md](docs/architecture/field-components.md) for the full catalog
|
|
186
|
+
and `BaseField` contract.
|
|
187
|
+
|
|
188
|
+
> \*Note: a few field files exist in the source but are not wired into the registry (e.g.
|
|
189
|
+
> `combobox`). See the [documentation audit](docs/architecture/documentation-audit.md) for the
|
|
190
|
+
> current list.
|
|
191
|
+
|
|
192
|
+
## `DynamicForm` props
|
|
193
|
+
|
|
194
|
+
| Prop | Type | Default | Description |
|
|
195
|
+
| --------------------------------------- | ------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------- |
|
|
196
|
+
| `questionGroups` | `Array<{ groupName, questions[] }>` | — | **The form schema.** Array of groups. |
|
|
197
|
+
| `actionButtons` | `array` | `[]` | Action buttons (submit/next/back/save…) rendered below the form. |
|
|
198
|
+
| `onSubmit` | `(formState, action) => void` | — | Called when an action button submits; receives form state and the action name. |
|
|
199
|
+
| `questionRuleDetails` / `questionRules` | `array` | `[]` | Question-level conditional logic (conditions + target actions). |
|
|
200
|
+
| `groupRuleDetails` / `groupRules` | `array` | `[]` | Group/page-level conditional logic. |
|
|
201
|
+
| `optionRuleDetails` / `optionRules` | `array` | `[]` | Option-level rules (show/hide dropdown/radio options). |
|
|
202
|
+
| `buttonRules` / `buttonRuleDetails` | `array` | `[]` | Action-button rules (enable/disable/visibility). |
|
|
203
|
+
| `validationErrors` | `Array<{ questionid, message, groupId? }>` | `[]` | Server-side validation messages to display. |
|
|
204
|
+
| `theme` | `object` | `{}` | Custom theme object, shallow-merged over the default. See [Theming](#theming). |
|
|
205
|
+
| `apiConfig` | `object` | — | Configuration for API-driven fields (base URL, upload endpoints, Google Places). |
|
|
206
|
+
| `onApiTrigger` | `(payload) => Promise<response>` | — | Handler that resolves field-driven API calls. See [API integration](#api-integration). |
|
|
207
|
+
| `formApiRef` | `React.Ref` | — | Ref populated with imperative form methods. See [Imperative API](#imperative-api-formapiref). |
|
|
208
|
+
| `toastConfig` | `{ disableToasts?, useHostToastify? }` | `{}` | Toast notification behavior. |
|
|
209
|
+
| `performanceConfig` | `{ debounceDelayMs? }` | — | Input-processing debounce. |
|
|
210
|
+
| `engineOptions` | `{ enablePerActionGating?, maxEvaluationDepth? }` | — | Rules-engine tuning. |
|
|
211
|
+
| `rerunPageRulesOnNext` | `boolean` | `true` | Re-run page-load rules when a `next` action fires. |
|
|
212
|
+
| `showQuestionLabels` | `boolean` | `true` | Show labels, tooltips, and required asterisks. |
|
|
213
|
+
| `isCardlistModal` | `boolean` | `false` | Render card-list fields as a table with modal editing. |
|
|
214
|
+
| `pageRenderers` / `pageType` | `object` / `string\|number` | `{}` | Custom page/group renderer overrides (advanced). |
|
|
215
|
+
| `testIdPrefix` | `string` | `'agp'` | Prefix for all `data-testid` attributes. |
|
|
216
|
+
| `testIdsEnabled` | `boolean` | `true` | When `false`, emits no test IDs. |
|
|
217
|
+
| `debug` | `boolean` | `false` | Verbose console logging (dev only). |
|
|
218
|
+
|
|
219
|
+
> Additional advanced props (`isNested`, `skipFormProvider`, `parentContext`,
|
|
220
|
+
> `handleIncompleteChange`, `isSanitizeAddress`) are used for nested forms and host integrations.
|
|
221
|
+
> `DynamicForm` is exported both as the default and as a named export (`import { DynamicForm }`),
|
|
222
|
+
> along with `FormProvider`, `useFormContext`, and `useForm` for advanced composition.
|
|
223
|
+
|
|
224
|
+
## Conditional logic (rules)
|
|
225
|
+
|
|
226
|
+
Behavior is driven by four independent rule systems, each a pair of _details_ (conditions) and
|
|
227
|
+
_rules_ (target actions):
|
|
228
|
+
|
|
229
|
+
- **Question rules** (`questionRuleDetails` + `questionRules`) — show/hide, enable/disable, require,
|
|
230
|
+
set values, transform answers on individual questions.
|
|
231
|
+
- **Group rules** (`groupRuleDetails` + `groupRules`) — the same, at group/page granularity.
|
|
232
|
+
- **Option rules** (`optionRuleDetails` + `optionRules`) — show/hide individual options in
|
|
233
|
+
dropdown/radio/checkbox fields.
|
|
234
|
+
- **Button rules** (`buttonRules` + `buttonRuleDetails`) — enable/disable/visibility of action
|
|
235
|
+
buttons, handled by a separate `ButtonRulesEngine`.
|
|
236
|
+
|
|
237
|
+
See [docs/reference/page-rules.md](docs/reference/page-rules.md) for the rule vocabulary (comparison
|
|
238
|
+
operators, actions, execution types),
|
|
239
|
+
[docs/reference/page-rule-groups.md](docs/reference/page-rule-groups.md) for AND/OR expression
|
|
240
|
+
groups, and [docs/reference/button-rules.md](docs/reference/button-rules.md) for button rules. For
|
|
241
|
+
how the two engines work internally and relate to each other, see
|
|
242
|
+
[docs/architecture/rules-engine.md](docs/architecture/rules-engine.md).
|
|
243
|
+
|
|
244
|
+
## Validation
|
|
245
|
+
|
|
246
|
+
- **Server-side / initial:** pass `validationErrors` as an array of
|
|
247
|
+
`{ questionid, message, groupId? }`. These are displayed immediately (and mark the form submitted
|
|
248
|
+
so messages show).
|
|
249
|
+
- **Client-side:** driven by each question's schema (`required`, min/max length, min/max value,
|
|
250
|
+
`format`, masks, date format, upload limits). Validation runs on blur (debounced) and on submit;
|
|
251
|
+
on change it only _clears_ errors, never adds them.
|
|
252
|
+
- **Card-list / nested:** each repeating item validates its own sub-questions, with an aggregated
|
|
253
|
+
parent-level message when any item is incomplete.
|
|
254
|
+
|
|
255
|
+
Internals and the full validator list:
|
|
256
|
+
[docs/architecture/validation.md](docs/architecture/validation.md).
|
|
257
|
+
|
|
258
|
+
## API integration
|
|
259
|
+
|
|
260
|
+
`DynamicForm` never calls your network directly for form logic. Instead, API-capable fields emit a
|
|
261
|
+
payload and your `onApiTrigger` handler resolves it (typically by calling your backend/proxy) and
|
|
262
|
+
returns a response the form applies to its state:
|
|
832
263
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
Callback function called when a form action is triggered. Receives the form state and action
|
|
845
|
-
identifier.
|
|
846
|
-
|
|
847
|
-
```javascript
|
|
848
|
-
const handleSubmit = (formState, action) => {
|
|
849
|
-
// formState contains values, errors, etc.
|
|
850
|
-
// action is the identifier of the action button clicked
|
|
851
|
-
if (action === 'submit') {
|
|
852
|
-
// Handle form submission
|
|
853
|
-
console.log('Form values:', formState.values);
|
|
854
|
-
}
|
|
264
|
+
```jsx
|
|
265
|
+
const handleApiTrigger = async (payload) => {
|
|
266
|
+
const data = await callYourBackend(payload);
|
|
267
|
+
return {
|
|
268
|
+
updates: {/* questionId: value */}, // set field values
|
|
269
|
+
options: {/* questionId: [ { name, value } ] */}, // replace option lists
|
|
270
|
+
visibility: {/* questionId: boolean */}, // show/hide fields
|
|
271
|
+
disabled: {/* questionId: boolean */}, // enable/disable fields
|
|
272
|
+
errors: {/* questionId: message */}, // set field errors
|
|
273
|
+
};
|
|
855
274
|
};
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
#### `onApiTrigger`
|
|
859
|
-
|
|
860
|
-
Callback function for handling API trigger events from form fields. Receives trigger data object.
|
|
861
275
|
|
|
862
|
-
|
|
863
|
-
const handleApiTrigger = async (triggerData) => {
|
|
864
|
-
// triggerData contains question, value, triggerType, and formState
|
|
865
|
-
// Return formatted API response with updates to the form
|
|
866
|
-
};
|
|
276
|
+
<DynamicForm questionGroups={FORM_SCHEMA} apiConfig={apiConfig} onApiTrigger={handleApiTrigger} />;
|
|
867
277
|
```
|
|
868
278
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
Object containing API configuration options passed to the `onApiTrigger` handler.
|
|
279
|
+
`apiConfig` supplies base configuration for API-driven fields and integrations, e.g.:
|
|
872
280
|
|
|
873
|
-
```
|
|
281
|
+
```js
|
|
874
282
|
const apiConfig = {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
'
|
|
878
|
-
|
|
283
|
+
googlePlaces: {
|
|
284
|
+
apiKey: process.env.GOOGLE_PLACES_API_KEY,
|
|
285
|
+
options: { componentRestrictions: { country: 'US' }, types: ['address'] },
|
|
286
|
+
},
|
|
287
|
+
customApi: {
|
|
288
|
+
baseUrl: 'https://api.example.com',
|
|
289
|
+
uploadUrl: '/document/upload',
|
|
290
|
+
deleteUrl: '/document/delete',
|
|
291
|
+
convertToPdfUrl: '/document/convert-to-pdf',
|
|
292
|
+
s3BucketUrl: 'https://cdn.example.com',
|
|
293
|
+
headers: { Authorization: '<token>' },
|
|
879
294
|
},
|
|
880
|
-
timeout: 5000,
|
|
881
295
|
};
|
|
882
296
|
```
|
|
883
297
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
### Basic Field Types
|
|
889
|
-
|
|
890
|
-
| Field Type | Component | Description |
|
|
891
|
-
| ---------- | --------------- | ------------------------------------- |
|
|
892
|
-
| `text` | `TextField` | Standard text input field |
|
|
893
|
-
| `textarea` | `TextAreaField` | Multi-line text input |
|
|
894
|
-
| `email` | `EmailField` | Email input with validation |
|
|
895
|
-
| `password` | `PasswordField` | Password input with visibility toggle |
|
|
896
|
-
| `number` | `NumberField` | Numeric input with constraints |
|
|
897
|
-
| `phone` | `PhoneField` | Phone number input with formatting |
|
|
898
|
-
| `currency` | `CurrencyField` | Monetary value input with formatting |
|
|
899
|
-
| `date` | `DateField` | Date picker with calendar |
|
|
900
|
-
| `datetime` | `DateTimeField` | Combined date and time picker |
|
|
901
|
-
|
|
902
|
-
### Selection Field Types
|
|
903
|
-
|
|
904
|
-
| Field Type | Component | Description |
|
|
905
|
-
| ----------------- | ---------------------- | ------------------------------------------- |
|
|
906
|
-
| `dropdown` | `SelectField` | Dropdown selection menu |
|
|
907
|
-
| `combobox` | `ComboboxField` | Searchable dropdown with multiple selection |
|
|
908
|
-
| `radio` | `RadioField` | Single-selection radio buttons |
|
|
909
|
-
| `radioGroup` | `RadioGroupField` | Group of radio buttons with custom layout |
|
|
910
|
-
| `radioSwitch` | `RadioSwitchField` | Toggle-style radio selection |
|
|
911
|
-
| `radioTabButtons` | `RadioTabButtonsField` | Tab-style radio selection |
|
|
912
|
-
| `checkbox` | `CheckboxField` | Single checkbox |
|
|
913
|
-
| `checkboxGroup` | `CheckboxGroupField` | Group of checkboxes with multiple selection |
|
|
298
|
+
For the complete integration contract (trigger flow, response format, caching, error handling), see
|
|
299
|
+
[docs/reference/api-integration.md](docs/reference/api-integration.md), and for internal wiring
|
|
300
|
+
[docs/architecture/api-and-file-handling.md](docs/architecture/api-and-file-handling.md).
|
|
914
301
|
|
|
915
|
-
|
|
302
|
+
## Upload field
|
|
916
303
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
| `address` | `AddressField` | Address input with validation and auto-complete |
|
|
920
|
-
| `beneficiary` | `BeneficiaryField` | Complex beneficiary information with percentage validation |
|
|
921
|
-
| `identification` | `IdentificationField` | SSN/ITIN input with masking and validation |
|
|
922
|
-
| `file` | `FileField` | File upload with preview |
|
|
923
|
-
| `pdf` | `PDFField` | PDF file upload and display |
|
|
924
|
-
| `cardList` | `CardListField` | Dynamic array of nested form fields |
|
|
925
|
-
| `display` | `DisplayTextField` | Read-only text display |
|
|
926
|
-
| `hyperlink` | `HyperlinkField` | Clickable hyperlink field |
|
|
927
|
-
| `button` | `ButtonField` | Action button field |
|
|
928
|
-
| `spinner` | `SpinnerField` | Loading indicator field |
|
|
304
|
+
The `upload` field (`questionType: "upload"`) provides a document-upload UX with server-backed
|
|
305
|
+
convert → upload → delete and optional per-row dropdowns.
|
|
929
306
|
|
|
930
|
-
|
|
307
|
+
**Schema keys:**
|
|
931
308
|
|
|
932
|
-
|
|
933
|
-
|
|
309
|
+
| Key | Description |
|
|
310
|
+
| ---------------------------------- | ------------------------------------------------------------------------------ |
|
|
311
|
+
| `questionType` | `"upload"` |
|
|
312
|
+
| `allowedFileType` | Comma-separated extensions, e.g. `"pdf,tiff,tif,jpeg,jpg"`. |
|
|
313
|
+
| `fileSizeLimit` | Max size in bytes, e.g. `30000000`. |
|
|
314
|
+
| `numberOfFiles` | Max number of rows to keep. |
|
|
315
|
+
| `includeExtraDropDowns` | When `true`, each row must select an Attachment Type. |
|
|
316
|
+
| `uploadQuestionAdditionalDropDown` | `[{ title, options: string[] }]` (first element used). |
|
|
317
|
+
| `showFileType`, `showFileSize` | Show those columns. |
|
|
318
|
+
| `value` | Optional bracketed S3-keys string or array. |
|
|
319
|
+
| `fileUploads` | Optional array of server-provided file objects (preferred for initial render). |
|
|
934
320
|
|
|
935
|
-
|
|
321
|
+
**`apiConfig.customApi`** provides the endpoints the field calls:
|
|
936
322
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
1. **Input Masking**: Real-time formatting during user input (e.g., `123-45-6789`)
|
|
940
|
-
2. **Security Masking**: Hide sensitive characters for privacy (e.g., `***-**-6789`)
|
|
941
|
-
|
|
942
|
-
### Supported Field Types
|
|
943
|
-
|
|
944
|
-
The following field types support dual masking:
|
|
945
|
-
|
|
946
|
-
| Field Type | Input Formatting | Security Masking | Eye Icon Toggle |
|
|
947
|
-
| ------------- | ---------------- | ---------------- | --------------- |
|
|
948
|
-
| `ssn` | ✅ | ✅ | ✅ |
|
|
949
|
-
| `itin` | ✅ | ✅ | ✅ |
|
|
950
|
-
| `phonenumber` | ✅ | ✅ | ✅ |
|
|
951
|
-
| `phone` | ✅ | ✅ | ✅ |
|
|
952
|
-
| `currency` | ✅ | ❌ | ❌ |
|
|
953
|
-
| `number` | ✅ | ❌ | ❌ |
|
|
954
|
-
| `percentage` | ✅ | ❌ | ❌ |
|
|
955
|
-
|
|
956
|
-
### Configuration Options
|
|
957
|
-
|
|
958
|
-
#### Input Masking
|
|
959
|
-
|
|
960
|
-
```javascript
|
|
323
|
+
```json
|
|
961
324
|
{
|
|
962
|
-
"
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
#### Security Masking
|
|
971
|
-
|
|
972
|
-
```javascript
|
|
973
|
-
{
|
|
974
|
-
"questionId": "ssn_field",
|
|
975
|
-
"label": "Social Security Number",
|
|
976
|
-
"questionType": "ssn",
|
|
977
|
-
"maskInput": "###-##-####", // Input formatting
|
|
978
|
-
"maskingType": "prefix", // "prefix" or "suffix"
|
|
979
|
-
"maskingLength": 6, // Number of characters to mask
|
|
980
|
-
"required": true
|
|
981
|
-
}
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
### Masking Patterns
|
|
985
|
-
|
|
986
|
-
#### Common Patterns
|
|
987
|
-
|
|
988
|
-
| Field Type | Pattern | Example Input | Formatted | Security Masked |
|
|
989
|
-
| ---------- | ---------------- | ------------- | -------------- | ------------------ |
|
|
990
|
-
| SSN | `###-##-####` | 123456789 | 123-45-6789 | **\*-**-6789 |
|
|
991
|
-
| Phone | `(###) ###-####` | 1234567890 | (123) 456-7890 | (123) 456-\*\*\*\* |
|
|
992
|
-
| ITIN | `###-##-####` | 912456789 | 912-45-6789 | **\*-**-6789 |
|
|
993
|
-
|
|
994
|
-
#### Custom Patterns
|
|
995
|
-
|
|
996
|
-
You can define custom masking patterns using:
|
|
997
|
-
|
|
998
|
-
- `#` for digits (0-9)
|
|
999
|
-
- `*` for any character
|
|
1000
|
-
- Special characters (hyphens, parentheses, etc.) are preserved
|
|
1001
|
-
|
|
1002
|
-
### Eye Icon Toggle
|
|
1003
|
-
|
|
1004
|
-
Fields with security masking automatically display an eye icon (👁️/🙈) that allows users to toggle
|
|
1005
|
-
between:
|
|
1006
|
-
|
|
1007
|
-
- **Masked View**: Shows security masked value (e.g., `***-**-6789`)
|
|
1008
|
-
- **Original View**: Shows formatted value (e.g., `123-45-6789`)
|
|
1009
|
-
|
|
1010
|
-
#### Accessibility Features
|
|
1011
|
-
|
|
1012
|
-
- **Keyboard Navigation**: Eye icon supports Tab navigation
|
|
1013
|
-
- **Keyboard Activation**: Toggle with Enter or Space keys
|
|
1014
|
-
- **Screen Reader Support**: Proper ARIA labels and descriptions
|
|
1015
|
-
- **Focus Management**: Clear focus indicators
|
|
1016
|
-
|
|
1017
|
-
### Advanced Configuration
|
|
1018
|
-
|
|
1019
|
-
#### Prefix vs Suffix Masking
|
|
1020
|
-
|
|
1021
|
-
```javascript
|
|
1022
|
-
// Prefix masking (mask first N characters)
|
|
1023
|
-
{
|
|
1024
|
-
"maskingType": "prefix",
|
|
1025
|
-
"maskingLength": 6,
|
|
1026
|
-
// "123-45-6789" → "***-**-6789"
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Suffix masking (mask last N characters)
|
|
1030
|
-
{
|
|
1031
|
-
"maskingType": "suffix",
|
|
1032
|
-
"maskingLength": 4,
|
|
1033
|
-
// "123-45-6789" → "123-45-****"
|
|
1034
|
-
}
|
|
1035
|
-
```
|
|
1036
|
-
|
|
1037
|
-
#### Validation with Masking
|
|
1038
|
-
|
|
1039
|
-
The masking system integrates seamlessly with validation:
|
|
1040
|
-
|
|
1041
|
-
```javascript
|
|
1042
|
-
{
|
|
1043
|
-
"questionId": "ssn_field",
|
|
1044
|
-
"maskInput": "###-##-####",
|
|
1045
|
-
"maskingType": "prefix",
|
|
1046
|
-
"maskingLength": 6,
|
|
1047
|
-
"minLength": 9, // Validates unmasked length
|
|
1048
|
-
"maxLength": 9, // Validates unmasked length
|
|
1049
|
-
"required": true,
|
|
1050
|
-
"validation": {
|
|
1051
|
-
"pattern": "^\\d{9}$", // Validates digits only
|
|
1052
|
-
"message": "Please enter a valid 9-digit SSN"
|
|
325
|
+
"customApi": {
|
|
326
|
+
"baseUrl": "<API_BASE_URL>",
|
|
327
|
+
"convertToPdfUrl": "/document/convert-to-pdf",
|
|
328
|
+
"uploadUrl": "/document/upload",
|
|
329
|
+
"deleteUrl": "/document/delete",
|
|
330
|
+
"s3BucketUrl": "https://s3-bucket-host",
|
|
331
|
+
"headers": { "Authorization": "<token>" }
|
|
1053
332
|
}
|
|
1054
333
|
}
|
|
1055
334
|
```
|
|
1056
335
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
-
|
|
1062
|
-
|
|
1063
|
-
- **Mobile Support**: Touch-friendly eye icon and responsive design
|
|
336
|
+
**Behavior:** a selected/dropped file is converted to PDF (`convertToPdfUrl`), uploaded
|
|
337
|
+
(`uploadUrl`), and listed in a table with Attachment Type, File Type/Size, and Preview/Delete
|
|
338
|
+
actions. When `includeExtraDropDowns` is `true`, each row requires a non-empty selection before
|
|
339
|
+
submit; deleting a row with an `id` calls `deleteUrl` before removing it locally. See
|
|
340
|
+
[docs/architecture/api-and-file-handling.md](docs/architecture/api-and-file-handling.md) for the
|
|
341
|
+
internals.
|
|
1064
342
|
|
|
1065
|
-
|
|
343
|
+
## Theming
|
|
1066
344
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
2. **Eye Icon Missing**: Verify both `maskingType` and `maskingLength` are set
|
|
1071
|
-
3. **Validation Errors**: Check that validation patterns match unmasked values
|
|
1072
|
-
4. **Performance Issues**: Avoid overly complex masking patterns in large forms
|
|
1073
|
-
|
|
1074
|
-
#### Debug Tips
|
|
1075
|
-
|
|
1076
|
-
```javascript
|
|
1077
|
-
// Enable console logging for masking operations
|
|
1078
|
-
console.log('Masked Value:', maskedValue);
|
|
1079
|
-
console.log('Display Value:', displayValue);
|
|
1080
|
-
console.log('Mask Config:', maskConfig);
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
## Conditional Logic
|
|
1084
|
-
|
|
1085
|
-
The Dynamic Form Renderer provides powerful conditional logic capabilities through the `pageRules`
|
|
1086
|
-
and `pageRuleGroup` props.
|
|
1087
|
-
|
|
1088
|
-
### PageRules
|
|
1089
|
-
|
|
1090
|
-
Page rules define individual conditions and actions for dynamic form behavior:
|
|
1091
|
-
|
|
1092
|
-
```javascript
|
|
1093
|
-
const pageRules = [
|
|
1094
|
-
{
|
|
1095
|
-
ruleId: 'rule1',
|
|
1096
|
-
triggerQuestionIds: ['101'],
|
|
1097
|
-
comparison: 'equals',
|
|
1098
|
-
compareValue: 'yes',
|
|
1099
|
-
action: 'showquestion',
|
|
1100
|
-
targetQuestionIds: ['201'],
|
|
1101
|
-
},
|
|
1102
|
-
// more rules...
|
|
1103
|
-
];
|
|
1104
|
-
```
|
|
1105
|
-
|
|
1106
|
-
### Enhanced PageRuleGroup
|
|
1107
|
-
|
|
1108
|
-
The enhanced `pageRuleGroup` feature provides sophisticated boolean expressions for complex
|
|
1109
|
-
conditional logic:
|
|
1110
|
-
|
|
1111
|
-
```javascript
|
|
1112
|
-
// New array-based format with complex boolean expressions
|
|
1113
|
-
const pageRuleGroup = [
|
|
1114
|
-
{
|
|
1115
|
-
questionId: '40374',
|
|
1116
|
-
evaluationExpression: 'rule1 AND rule2',
|
|
1117
|
-
},
|
|
1118
|
-
{
|
|
1119
|
-
questionId: '40555',
|
|
1120
|
-
evaluationExpression: '(rule1 AND rule2) OR rule3',
|
|
1121
|
-
},
|
|
1122
|
-
];
|
|
1123
|
-
```
|
|
1124
|
-
|
|
1125
|
-
#### Key Features
|
|
1126
|
-
|
|
1127
|
-
- **Complex Boolean Logic**: Combine multiple rules with AND/OR operations
|
|
1128
|
-
- **Parentheses Support**: Control operator precedence for complex evaluations
|
|
1129
|
-
- **Backward Compatibility**: Supports legacy object-based format
|
|
1130
|
-
|
|
1131
|
-
#### Expression Syntax
|
|
1132
|
-
|
|
1133
|
-
- **Operators**: `AND`, `OR` (case-insensitive)
|
|
1134
|
-
- **Rule References**: Use `ruleId` values from the pageRules array
|
|
1135
|
-
- **Parentheses**: Group expressions to control evaluation order
|
|
1136
|
-
|
|
1137
|
-
#### Examples
|
|
1138
|
-
|
|
1139
|
-
```javascript
|
|
1140
|
-
// Simple AND condition
|
|
1141
|
-
{
|
|
1142
|
-
questionId: "40374",
|
|
1143
|
-
evaluationExpression: "rule1 AND rule2"
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Complex nested conditions
|
|
1147
|
-
{
|
|
1148
|
-
questionId: "40890",
|
|
1149
|
-
evaluationExpression: "(rule1 AND rule2) OR (rule3 AND rule4)"
|
|
1150
|
-
}
|
|
1151
|
-
```
|
|
1152
|
-
|
|
1153
|
-
For detailed documentation on the enhanced pageRuleGroup feature, see the
|
|
1154
|
-
[page-rule-group-enhancement.md](docs/page-rule-group-implementation/page-rule-group-enhancement.md)
|
|
1155
|
-
file.
|
|
1156
|
-
|
|
1157
|
-
## Advanced Configuration
|
|
1158
|
-
|
|
1159
|
-
## Performance & Engine Options
|
|
1160
|
-
|
|
1161
|
-
When using the provider-level API, you can tune performance and engine behavior.
|
|
1162
|
-
|
|
1163
|
-
### FormProvider props
|
|
1164
|
-
|
|
1165
|
-
- `performanceConfig`
|
|
1166
|
-
- `debounceDelayMs: number` (default: 120) – Debounce for textual input processing
|
|
1167
|
-
|
|
1168
|
-
- `engineOptions`
|
|
1169
|
-
- `enablePerActionGating: boolean` (default: true) – Toggle per-action gating via questionRules
|
|
1170
|
-
- `maxEvaluationDepth: number` (default: 20) – Reflexive evaluation depth guard
|
|
1171
|
-
|
|
1172
|
-
### Example
|
|
1173
|
-
|
|
1174
|
-
```jsx
|
|
1175
|
-
import { FormProvider } from '@ilife-tech/react-application-flow-renderer/context/FormContext';
|
|
1176
|
-
import DynamicFormInner from '@ilife-tech/react-application-flow-renderer/src/components/core/components/DynamicFormInner';
|
|
1177
|
-
|
|
1178
|
-
<FormProvider
|
|
1179
|
-
formSchema={questionGroups}
|
|
1180
|
-
questionRuleDetails={questionRuleDetails}
|
|
1181
|
-
questionRules={questionRules}
|
|
1182
|
-
performanceConfig={{ debounceDelayMs: 150 }}
|
|
1183
|
-
engineOptions={{ enablePerActionGating: true, maxEvaluationDepth: 20 }}
|
|
1184
|
-
>
|
|
1185
|
-
<DynamicFormInner schema={questionGroups} actionSchema={actionSchema} onSubmit={handleSubmit} />
|
|
1186
|
-
</FormProvider>;
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
Note: The top-level `DynamicForm` abstraction may not expose these knobs directly. Use
|
|
1190
|
-
`FormProvider` for fine-grained control.
|
|
1191
|
-
|
|
1192
|
-
### Toast Notifications
|
|
1193
|
-
|
|
1194
|
-
The Dynamic Form Renderer uses react-toastify for displaying notifications such as error messages,
|
|
1195
|
-
warnings, and success confirmations.
|
|
1196
|
-
|
|
1197
|
-
#### Toast Configuration
|
|
1198
|
-
|
|
1199
|
-
The form renderer integrates with your application's react-toastify setup. **Important: Your host
|
|
1200
|
-
application MUST include a `<ToastContainer />` component** for notifications to appear.
|
|
1201
|
-
|
|
1202
|
-
```jsx
|
|
1203
|
-
// Required: Import react-toastify in your host application
|
|
1204
|
-
import { ToastContainer } from 'react-toastify';
|
|
1205
|
-
import 'react-toastify/dist/ReactToastify.css';
|
|
1206
|
-
|
|
1207
|
-
const App = () => {
|
|
1208
|
-
return (
|
|
1209
|
-
<>
|
|
1210
|
-
{/* Required: Add ToastContainer at the root level */}
|
|
1211
|
-
<ToastContainer />
|
|
1212
|
-
|
|
1213
|
-
<DynamicForm
|
|
1214
|
-
questionGroups={questionGroups}
|
|
1215
|
-
toastConfig={{
|
|
1216
|
-
disableToasts: false, // Disable toast notifications completely (default: false)
|
|
1217
|
-
useHostToastify: true, // Whether to use host app's react-toastify (default: true)
|
|
1218
|
-
}}
|
|
1219
|
-
onSubmit={handleSubmit}
|
|
1220
|
-
onApiTrigger={handleApiTrigger}
|
|
1221
|
-
/>
|
|
1222
|
-
</>
|
|
1223
|
-
);
|
|
1224
|
-
};
|
|
1225
|
-
```
|
|
1226
|
-
|
|
1227
|
-
#### API Response Format for Toast Notifications
|
|
1228
|
-
|
|
1229
|
-
When implementing the `onApiTrigger` handler, you can return success and error messages that will be
|
|
1230
|
-
displayed as toast notifications:
|
|
1231
|
-
|
|
1232
|
-
```javascript
|
|
1233
|
-
// Example of API trigger handler with toast notifications
|
|
1234
|
-
const handleApiTrigger = async (triggerData) => {
|
|
1235
|
-
try {
|
|
1236
|
-
// Your API logic here
|
|
1237
|
-
const response = await myApiCall(triggerData);
|
|
1238
|
-
|
|
1239
|
-
// Return success message (will display as green toast)
|
|
1240
|
-
return {
|
|
1241
|
-
success: {
|
|
1242
|
-
message: 'Operation completed successfully',
|
|
1243
|
-
// You can also include field-specific success messages
|
|
1244
|
-
// fieldName: 'Field updated successfully'
|
|
1245
|
-
},
|
|
1246
|
-
// Other response properties like updates, options, visibility...
|
|
1247
|
-
};
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
// Return error message (will display as red toast)
|
|
1250
|
-
return {
|
|
1251
|
-
errors: {
|
|
1252
|
-
error: 'An error occurred during the operation',
|
|
1253
|
-
// You can also include field-specific error messages
|
|
1254
|
-
// fieldName: 'Invalid value for this field'
|
|
1255
|
-
},
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
};
|
|
1259
|
-
```
|
|
1260
|
-
|
|
1261
|
-
#### Integration with Host Applications
|
|
1262
|
-
|
|
1263
|
-
If your application already uses react-toastify, the renderer will:
|
|
1264
|
-
|
|
1265
|
-
1. Use your application's react-toastify instance
|
|
1266
|
-
2. Respect your toast styling and configuration
|
|
1267
|
-
|
|
1268
|
-
**Important:** Only one `<ToastContainer />` should be rendered in your application tree. The
|
|
1269
|
-
package will not render its own ToastContainer - this must be provided by the host application.
|
|
1270
|
-
|
|
1271
|
-
### Toast Configuration
|
|
1272
|
-
|
|
1273
|
-
The Dynamic Form Renderer uses react-toastify for displaying notifications. You can configure toast
|
|
1274
|
-
behavior using the `toastConfig` prop:
|
|
345
|
+
The `theme` prop is a **plain custom object** (not a Material-UI `createTheme()` object). It is
|
|
346
|
+
shallow-merged over the package default (`{ ...defaultTheme, ...theme }`), so a top-level key you
|
|
347
|
+
provide **replaces** that whole slice. Override at the level you need:
|
|
1275
348
|
|
|
1276
349
|
```jsx
|
|
1277
350
|
<DynamicForm
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
351
|
+
questionGroups={FORM_SCHEMA}
|
|
352
|
+
theme={{
|
|
353
|
+
colors: {
|
|
354
|
+
primary: '#1D4ED8',
|
|
355
|
+
border: '#D1D5DB',
|
|
356
|
+
error: '#DC2626',
|
|
357
|
+
inputBackground: '#FFFFFF',
|
|
358
|
+
},
|
|
1283
359
|
}}
|
|
1284
360
|
/>
|
|
1285
361
|
```
|
|
1286
362
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
1. Import ToastContainer and CSS in your host application:
|
|
1292
|
-
|
|
1293
|
-
```jsx
|
|
1294
|
-
import { ToastContainer } from 'react-toastify';
|
|
1295
|
-
import 'react-toastify/dist/ReactToastify.css';
|
|
1296
|
-
```
|
|
1297
|
-
|
|
1298
|
-
2. Add the ToastContainer to your app's root component:
|
|
1299
|
-
```jsx
|
|
1300
|
-
function App() {
|
|
1301
|
-
return (
|
|
1302
|
-
<>
|
|
1303
|
-
<YourAppComponents />
|
|
1304
|
-
<ToastContainer position="top-right" autoClose={5000} />
|
|
1305
|
-
</>
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
```
|
|
1309
|
-
|
|
1310
|
-
#### API Response Format for Toast Messages
|
|
1311
|
-
|
|
1312
|
-
When implementing custom API triggers with `onApiTrigger`, your handler should return a promise that
|
|
1313
|
-
resolves to an object with the following structure to properly display toast notifications:
|
|
1314
|
-
|
|
1315
|
-
```javascript
|
|
1316
|
-
const handleApiTrigger = async (triggerData) => {
|
|
1317
|
-
// Your API call implementation
|
|
1318
|
-
try {
|
|
1319
|
-
// Process API call
|
|
1320
|
-
return {
|
|
1321
|
-
// Success messages that will trigger toast notifications
|
|
1322
|
-
success: {
|
|
1323
|
-
message: 'General success message', // General success toast
|
|
1324
|
-
fieldId: 'Field-specific success', // Field-specific success toast
|
|
1325
|
-
},
|
|
1326
|
-
// Optional field updates
|
|
1327
|
-
updates: {
|
|
1328
|
-
fieldId1: 'new value', // Update field values
|
|
1329
|
-
fieldId2: [{ name: 'Option 1', value: 'opt1' }], // Update field options
|
|
1330
|
-
},
|
|
1331
|
-
};
|
|
1332
|
-
} catch (error) {
|
|
1333
|
-
return {
|
|
1334
|
-
// Error messages that will trigger toast notifications
|
|
1335
|
-
errors: {
|
|
1336
|
-
error: 'General error message', // General error toast
|
|
1337
|
-
fieldId: 'Field-specific error', // Field-specific error toast
|
|
1338
|
-
},
|
|
1339
|
-
};
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
```
|
|
1343
|
-
|
|
1344
|
-
#### Compatibility Notes
|
|
1345
|
-
|
|
1346
|
-
- The Dynamic Form Renderer is compatible with react-toastify v9.1.x for React 17 applications
|
|
1347
|
-
- For React 18+ applications, you can use react-toastify v9.x or higher
|
|
1348
|
-
- Toast styles will follow your application's theme if available
|
|
1349
|
-
- Error and success messages from API responses and form validation will use appropriate toast types
|
|
1350
|
-
|
|
1351
|
-
#### Troubleshooting
|
|
1352
|
-
|
|
1353
|
-
- **No toasts appear**: Ensure your application has a `<ToastContainer />` component and has
|
|
1354
|
-
imported the CSS
|
|
1355
|
-
- **\_useSyncExternalStore error**: This usually indicates a version compatibility issue between
|
|
1356
|
-
React and react-toastify. For React 17, use react-toastify v9.1.x
|
|
1357
|
-
- **Multiple toasts appear**: Ensure you only have one `<ToastContainer />` in your application
|
|
1358
|
-
|
|
1359
|
-
### Theme Customization
|
|
1360
|
-
|
|
1361
|
-
The DynamicForm component supports deep theme customization through the `theme` prop:
|
|
1362
|
-
|
|
1363
|
-
```javascript
|
|
1364
|
-
const customTheme = {
|
|
1365
|
-
typography: {
|
|
1366
|
-
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
|
1367
|
-
fontSize: '16px',
|
|
1368
|
-
lineHeight: 1.5,
|
|
1369
|
-
headings: {
|
|
1370
|
-
h1: { fontSize: '2rem', fontWeight: 700 },
|
|
1371
|
-
h2: { fontSize: '1.5rem', fontWeight: 600 },
|
|
1372
|
-
h3: { fontSize: '1.25rem', fontWeight: 600 },
|
|
1373
|
-
},
|
|
1374
|
-
},
|
|
1375
|
-
palette: {
|
|
1376
|
-
primary: {
|
|
1377
|
-
main: '#0066cc',
|
|
1378
|
-
light: '#3383d6',
|
|
1379
|
-
dark: '#004c99',
|
|
1380
|
-
contrastText: '#ffffff',
|
|
1381
|
-
},
|
|
1382
|
-
error: {
|
|
1383
|
-
main: '#d32f2f',
|
|
1384
|
-
light: '#ef5350',
|
|
1385
|
-
dark: '#c62828',
|
|
1386
|
-
contrastText: '#ffffff',
|
|
1387
|
-
},
|
|
1388
|
-
success: {
|
|
1389
|
-
main: '#2e7d32',
|
|
1390
|
-
light: '#4caf50',
|
|
1391
|
-
dark: '#1b5e20',
|
|
1392
|
-
contrastText: '#ffffff',
|
|
1393
|
-
},
|
|
1394
|
-
},
|
|
1395
|
-
spacing: {
|
|
1396
|
-
unit: '8px',
|
|
1397
|
-
form: '24px',
|
|
1398
|
-
field: '16px',
|
|
1399
|
-
},
|
|
1400
|
-
shape: {
|
|
1401
|
-
borderRadius: '4px',
|
|
1402
|
-
},
|
|
1403
|
-
breakpoints: {
|
|
1404
|
-
xs: 0,
|
|
1405
|
-
sm: 600,
|
|
1406
|
-
md: 960,
|
|
1407
|
-
lg: 1280,
|
|
1408
|
-
xl: 1920,
|
|
1409
|
-
},
|
|
1410
|
-
fields: {
|
|
1411
|
-
input: {
|
|
1412
|
-
height: '40px',
|
|
1413
|
-
padding: '8px 12px',
|
|
1414
|
-
fontSize: '16px',
|
|
1415
|
-
borderRadius: '4px',
|
|
1416
|
-
borderColor: '#cccccc',
|
|
1417
|
-
focusBorderColor: '#0066cc',
|
|
1418
|
-
errorBorderColor: '#d32f2f',
|
|
1419
|
-
background: '#ffffff',
|
|
1420
|
-
},
|
|
1421
|
-
label: {
|
|
1422
|
-
fontSize: '14px',
|
|
1423
|
-
fontWeight: 600,
|
|
1424
|
-
marginBottom: '4px',
|
|
1425
|
-
color: '#333333',
|
|
1426
|
-
},
|
|
1427
|
-
helperText: {
|
|
1428
|
-
fontSize: '12px',
|
|
1429
|
-
color: '#666666',
|
|
1430
|
-
marginTop: '4px',
|
|
1431
|
-
},
|
|
1432
|
-
error: {
|
|
1433
|
-
fontSize: '12px',
|
|
1434
|
-
color: '#d32f2f',
|
|
1435
|
-
marginTop: '4px',
|
|
1436
|
-
},
|
|
1437
|
-
},
|
|
1438
|
-
buttons: {
|
|
1439
|
-
primary: {
|
|
1440
|
-
background: '#0066cc',
|
|
1441
|
-
color: '#ffffff',
|
|
1442
|
-
hoverBackground: '#004c99',
|
|
1443
|
-
fontSize: '16px',
|
|
1444
|
-
padding: '8px 24px',
|
|
1445
|
-
borderRadius: '4px',
|
|
1446
|
-
},
|
|
1447
|
-
secondary: {
|
|
1448
|
-
background: '#f5f5f5',
|
|
1449
|
-
color: '#333333',
|
|
1450
|
-
hoverBackground: '#e0e0e0',
|
|
1451
|
-
fontSize: '16px',
|
|
1452
|
-
padding: '8px 24px',
|
|
1453
|
-
borderRadius: '4px',
|
|
1454
|
-
},
|
|
1455
|
-
},
|
|
1456
|
-
};
|
|
1457
|
-
|
|
1458
|
-
// Usage
|
|
1459
|
-
<DynamicForm
|
|
1460
|
-
questionGroups={questionGroups}
|
|
1461
|
-
theme={customTheme}
|
|
1462
|
-
// Other props
|
|
1463
|
-
/>;
|
|
1464
|
-
```
|
|
1465
|
-
|
|
1466
|
-
### Validation Configuration
|
|
363
|
+
Beyond `colors`, the theme is composed of form-level settings and per-field-type slices. For the
|
|
364
|
+
full, authoritative theme shape and how to override a single field slice without discarding the
|
|
365
|
+
rest, see
|
|
366
|
+
[docs/architecture/theming-styling-accessibility.md](docs/architecture/theming-styling-accessibility.md).
|
|
1467
367
|
|
|
1468
|
-
|
|
368
|
+
> Material-UI and Emotion are used internally by some fields, but the package's public theming API
|
|
369
|
+
> is this custom object — not an MUI theme.
|
|
1469
370
|
|
|
1470
|
-
|
|
371
|
+
## Toast notifications
|
|
1471
372
|
|
|
1472
|
-
|
|
1473
|
-
// In form schema
|
|
1474
|
-
const questionGroups = [
|
|
1475
|
-
{
|
|
1476
|
-
groupId: 'personalInfo',
|
|
1477
|
-
questions: [
|
|
1478
|
-
{
|
|
1479
|
-
questionId: 'email',
|
|
1480
|
-
questionType: 'email',
|
|
1481
|
-
label: 'Email Address',
|
|
1482
|
-
validation: {
|
|
1483
|
-
required: true,
|
|
1484
|
-
email: true,
|
|
1485
|
-
requiredMessage: 'Email is required',
|
|
1486
|
-
errorMessage: 'Please enter a valid email address',
|
|
1487
|
-
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
1488
|
-
patternMessage: 'Email format is invalid',
|
|
1489
|
-
},
|
|
1490
|
-
},
|
|
1491
|
-
{
|
|
1492
|
-
questionId: 'password',
|
|
1493
|
-
questionType: 'password',
|
|
1494
|
-
label: 'Password',
|
|
1495
|
-
validation: {
|
|
1496
|
-
required: true,
|
|
1497
|
-
minLength: 8,
|
|
1498
|
-
minLengthMessage: 'Password must be at least 8 characters',
|
|
1499
|
-
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
|
|
1500
|
-
patternMessage:
|
|
1501
|
-
'Password must include uppercase, lowercase, number and special character',
|
|
1502
|
-
},
|
|
1503
|
-
},
|
|
1504
|
-
],
|
|
1505
|
-
},
|
|
1506
|
-
];
|
|
1507
|
-
|
|
1508
|
-
// Separate validation schema
|
|
1509
|
-
const validationSchema = [
|
|
1510
|
-
{
|
|
1511
|
-
field: 'confirmPassword',
|
|
1512
|
-
rules: [
|
|
1513
|
-
'required',
|
|
1514
|
-
{
|
|
1515
|
-
name: 'matchesField',
|
|
1516
|
-
params: ['password'],
|
|
1517
|
-
message: 'Passwords must match',
|
|
1518
|
-
},
|
|
1519
|
-
],
|
|
1520
|
-
},
|
|
1521
|
-
];
|
|
1522
|
-
```
|
|
1523
|
-
|
|
1524
|
-
#### Available Validation Message Types
|
|
1525
|
-
|
|
1526
|
-
| Message Type | Description |
|
|
1527
|
-
| ------------------ | --------------------------------------------------- |
|
|
1528
|
-
| `requiredMessage` | Displayed when a required field is empty |
|
|
1529
|
-
| `errorMessage` | General error message for field validation |
|
|
1530
|
-
| `lengthMessage` | Error for overall length validation |
|
|
1531
|
-
| `minLengthMessage` | Error for minimum length validation |
|
|
1532
|
-
| `maxLengthMessage` | Error for maximum length validation |
|
|
1533
|
-
| `patternMessage` | Error for regex pattern validation |
|
|
1534
|
-
| `typeMessage` | Error for type validation (email, number, etc.) |
|
|
1535
|
-
| `formatMessage` | Error for format validation (phone, currency, etc.) |
|
|
1536
|
-
|
|
1537
|
-
### CardList Configuration
|
|
1538
|
-
|
|
1539
|
-
The `CardListField` component enables dynamic form arrays with complete validation and state
|
|
1540
|
-
management:
|
|
1541
|
-
|
|
1542
|
-
```javascript
|
|
1543
|
-
const cardListQuestion = {
|
|
1544
|
-
questionId: 'beneficiaries',
|
|
1545
|
-
questionType: 'cardList',
|
|
1546
|
-
label: 'Beneficiaries',
|
|
1547
|
-
addCardLabel: 'Add Beneficiary',
|
|
1548
|
-
cardSchema: [
|
|
1549
|
-
{
|
|
1550
|
-
groupId: 'beneficiaryInfo',
|
|
1551
|
-
questions: [
|
|
1552
|
-
{
|
|
1553
|
-
questionId: 'name',
|
|
1554
|
-
questionType: 'text',
|
|
1555
|
-
label: 'Full Name',
|
|
1556
|
-
validation: { required: true },
|
|
1557
|
-
},
|
|
1558
|
-
{
|
|
1559
|
-
questionId: 'relationship',
|
|
1560
|
-
questionType: 'dropdown',
|
|
1561
|
-
label: 'Relationship',
|
|
1562
|
-
options: [
|
|
1563
|
-
{ name: 'Spouse', value: 'spouse' },
|
|
1564
|
-
{ name: 'Child', value: 'child' },
|
|
1565
|
-
{ name: 'Parent', value: 'parent' },
|
|
1566
|
-
{ name: 'Other', value: 'other' },
|
|
1567
|
-
],
|
|
1568
|
-
validation: { required: true },
|
|
1569
|
-
},
|
|
1570
|
-
{
|
|
1571
|
-
questionId: 'percentage',
|
|
1572
|
-
questionType: 'currency',
|
|
1573
|
-
label: 'Percentage',
|
|
1574
|
-
validation: {
|
|
1575
|
-
required: true,
|
|
1576
|
-
min: 0,
|
|
1577
|
-
max: 100,
|
|
1578
|
-
minMessage: 'Percentage must be positive',
|
|
1579
|
-
maxMessage: 'Percentage cannot exceed 100%',
|
|
1580
|
-
},
|
|
1581
|
-
},
|
|
1582
|
-
],
|
|
1583
|
-
},
|
|
1584
|
-
],
|
|
1585
|
-
validation: {
|
|
1586
|
-
minCards: 1,
|
|
1587
|
-
maxCards: 5,
|
|
1588
|
-
minCardsMessage: 'At least one beneficiary is required',
|
|
1589
|
-
maxCardsMessage: 'Maximum of 5 beneficiaries allowed',
|
|
1590
|
-
totalValidator: (cards) => {
|
|
1591
|
-
// Calculate total percentage across all cards
|
|
1592
|
-
const total = cards.reduce((sum, card) => {
|
|
1593
|
-
const percentage = parseFloat(card.values.percentage) || 0;
|
|
1594
|
-
return sum + percentage;
|
|
1595
|
-
}, 0);
|
|
1596
|
-
|
|
1597
|
-
// Validate total is 100%
|
|
1598
|
-
if (Math.abs(total - 100) > 0.01) {
|
|
1599
|
-
return 'Total percentage must equal 100%';
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
return null; // No error
|
|
1603
|
-
},
|
|
1604
|
-
},
|
|
1605
|
-
};
|
|
1606
|
-
```
|
|
1607
|
-
|
|
1608
|
-
#### CardList State Management
|
|
1609
|
-
|
|
1610
|
-
Each card in a CardList manages its own state, including:
|
|
1611
|
-
|
|
1612
|
-
- Form values
|
|
1613
|
-
- Validation errors
|
|
1614
|
-
- Visibility rules
|
|
1615
|
-
- Disabled states
|
|
1616
|
-
|
|
1617
|
-
You can access the CardList state through the form context:
|
|
1618
|
-
|
|
1619
|
-
```javascript
|
|
1620
|
-
const MyForm = () => {
|
|
1621
|
-
const formApiRef = useRef(null);
|
|
1622
|
-
|
|
1623
|
-
const handleSubmit = (formState) => {
|
|
1624
|
-
// Access cardList data as an array of card states
|
|
1625
|
-
const beneficiaries = formState.values.beneficiaries;
|
|
1626
|
-
console.log('Beneficiary cards:', beneficiaries);
|
|
1627
|
-
};
|
|
1628
|
-
|
|
1629
|
-
return (
|
|
1630
|
-
<DynamicForm questionGroups={questionGroups} onSubmit={handleSubmit} formApiRef={formApiRef} />
|
|
1631
|
-
);
|
|
1632
|
-
};
|
|
1633
|
-
```
|
|
1634
|
-
|
|
1635
|
-
## Troubleshooting
|
|
1636
|
-
|
|
1637
|
-
### Common Issues
|
|
1638
|
-
|
|
1639
|
-
#### API Integration Issues
|
|
1640
|
-
|
|
1641
|
-
| Issue | Solution |
|
|
1642
|
-
| ------------------------------- | ---------------------------------------------------------------------------------- |
|
|
1643
|
-
| API calls not triggering | Ensure `apiTrigger` configuration in field schema includes correct `triggerEvents` |
|
|
1644
|
-
| API responses not updating form | Verify that the `onApiTrigger` handler returns the correct response structure |
|
|
1645
|
-
| Multiple concurrent API calls | Use request queuing as shown in the advanced API integration example |
|
|
1646
|
-
| CORS errors | Configure proper CORS headers in your API or use a proxy service |
|
|
1647
|
-
|
|
1648
|
-
#### Validation Issues
|
|
1649
|
-
|
|
1650
|
-
| Issue | Solution |
|
|
1651
|
-
| ------------------------------ | -------------------------------------------------------------------------------- |
|
|
1652
|
-
| ValidationSchema not applying | Ensure field names in validation schema match questionIds in form schema |
|
|
1653
|
-
| Custom validation not working | Check that validation functions return error message strings, not boolean values |
|
|
1654
|
-
| Form submitting with errors | Verify that onSubmit handler checks formState.errors before proceeding |
|
|
1655
|
-
| Cross-field validation failing | Use the validationSchema approach for dependencies between fields |
|
|
1656
|
-
|
|
1657
|
-
#### Rendering Issues
|
|
1658
|
-
|
|
1659
|
-
| Issue | Solution |
|
|
1660
|
-
| ------------------------------------ | -------------------------------------------------------------- |
|
|
1661
|
-
| Fields not respecting column layout | Check columnCount setting in group configuration |
|
|
1662
|
-
| Theme customizations not applying | Ensure theme prop structure matches expected format |
|
|
1663
|
-
| Responsive layout issues | Verify breakpoints in theme configuration |
|
|
1664
|
-
| Fields rendering in unexpected order | Check if columns are properly balanced in group configurations |
|
|
1665
|
-
|
|
1666
|
-
### Debugging Tips
|
|
1667
|
-
|
|
1668
|
-
#### Using Debug Mode
|
|
1669
|
-
|
|
1670
|
-
Enable debug mode to get verbose console output of form state changes:
|
|
373
|
+
The package shows toasts (via `react-toastify`) for certain validation/API events:
|
|
1671
374
|
|
|
1672
375
|
```jsx
|
|
1673
376
|
<DynamicForm
|
|
1674
|
-
questionGroups={
|
|
1675
|
-
|
|
1676
|
-
|
|
377
|
+
questionGroups={FORM_SCHEMA}
|
|
378
|
+
toastConfig={{
|
|
379
|
+
disableToasts: false, // true to suppress all package toasts
|
|
380
|
+
useHostToastify: false, // true if your app already renders a <ToastContainer/>
|
|
381
|
+
}}
|
|
1677
382
|
/>
|
|
1678
383
|
```
|
|
1679
384
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
Use the formApiRef prop to access form methods imperatively:
|
|
1683
|
-
|
|
1684
|
-
```jsx
|
|
1685
|
-
const MyForm = () => {
|
|
1686
|
-
const formApiRef = useRef(null);
|
|
385
|
+
When `disableToasts` or `useHostToastify` is `true`, the package does not render its own
|
|
386
|
+
`<ToastContainer/>`.
|
|
1687
387
|
|
|
1688
|
-
|
|
1689
|
-
// Access form API methods
|
|
1690
|
-
const api = formApiRef.current;
|
|
388
|
+
## Imperative API (`formApiRef`)
|
|
1691
389
|
|
|
1692
|
-
|
|
1693
|
-
const state = api.getState();
|
|
1694
|
-
console.log('Form values:', state.values);
|
|
390
|
+
Pass a ref to access form methods imperatively:
|
|
1695
391
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
// Validate specific field
|
|
1700
|
-
api.validateField('email');
|
|
392
|
+
```jsx
|
|
393
|
+
const formApiRef = useRef(null);
|
|
1701
394
|
|
|
1702
|
-
|
|
1703
|
-
api.validateForm().then((errors) => {
|
|
1704
|
-
console.log('Validation errors:', errors);
|
|
1705
|
-
});
|
|
1706
|
-
};
|
|
395
|
+
<DynamicForm questionGroups={FORM_SCHEMA} formApiRef={formApiRef} />;
|
|
1707
396
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
<button onClick={handleClick}>Debug Form</button>
|
|
1712
|
-
</>
|
|
1713
|
-
);
|
|
1714
|
-
};
|
|
397
|
+
// later:
|
|
398
|
+
formApiRef.current.batchUpdateFields({ firstName: 'Ada', state: 'CA' });
|
|
399
|
+
formApiRef.current.setVisibility('middleName', true);
|
|
1715
400
|
```
|
|
1716
401
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
- Split form into multiple pages or sections
|
|
1724
|
-
- Use the `visibleGroups` prop to only render visible question groups
|
|
1725
|
-
- Implement form sections with conditional rendering
|
|
1726
|
-
- Consider lazy loading for complex field components
|
|
1727
|
-
|
|
1728
|
-
#### API Efficiency
|
|
402
|
+
`formApiRef.current` exposes methods such as `setValue`, `batchUpdateFields`, `updateFieldOptions`,
|
|
403
|
+
`setVisibility`, `setDisabled`, `setRequired`,
|
|
404
|
+
`getFieldValue`/`getFieldVisibility`/`getFieldDisabled`/ `getFieldRequired`, `updateCardList`,
|
|
405
|
+
`validateField`, `validateAllFields`, `getState`, and `goToPrevIncomplete`/`goToNextIncomplete`. The
|
|
406
|
+
exact surface (and how it differs from the `useFormContext()` action set) is documented in
|
|
407
|
+
[docs/architecture/state-management.md](docs/architecture/state-management.md).
|
|
1729
408
|
|
|
1730
|
-
|
|
409
|
+
## Performance & engine options
|
|
1731
410
|
|
|
1732
|
-
-
|
|
1733
|
-
-
|
|
1734
|
-
|
|
1735
|
-
- Implement request cancellation for outdated requests
|
|
411
|
+
- `performanceConfig={{ debounceDelayMs: 150 }}` — debounce input processing on rapid typing.
|
|
412
|
+
- `engineOptions={{ enablePerActionGating: true, maxEvaluationDepth: 25 }}` — tune rules-engine
|
|
413
|
+
evaluation.
|
|
1736
414
|
|
|
1737
|
-
|
|
415
|
+
The rules engine is memoized behind a content fingerprint so referentially-unstable props (fresh
|
|
416
|
+
arrays each render) don't trigger a full re-evaluation. Details:
|
|
417
|
+
[docs/architecture/rules-engine.md](docs/architecture/rules-engine.md) and
|
|
418
|
+
[docs/architecture/state-management.md](docs/architecture/state-management.md).
|
|
1738
419
|
|
|
1739
|
-
|
|
420
|
+
## Test IDs
|
|
1740
421
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
Implement a form wizard with progress tracking:
|
|
422
|
+
Every rendered element carries a deterministic `data-testid`. Pass your app namespace once:
|
|
1744
423
|
|
|
1745
424
|
```jsx
|
|
1746
|
-
|
|
1747
|
-
import DynamicForm from '@ilife-tech/react-application-flow-renderer';
|
|
1748
|
-
|
|
1749
|
-
const MultiStepForm = () => {
|
|
1750
|
-
const [step, setStep] = useState(0);
|
|
1751
|
-
const [formData, setFormData] = useState({});
|
|
1752
|
-
|
|
1753
|
-
// Define steps with their schemas
|
|
1754
|
-
const steps = [
|
|
1755
|
-
{
|
|
1756
|
-
title: 'Personal Information',
|
|
1757
|
-
schema: [
|
|
1758
|
-
{
|
|
1759
|
-
groupId: 'personal',
|
|
1760
|
-
questions: [
|
|
1761
|
-
/* Personal info questions */
|
|
1762
|
-
],
|
|
1763
|
-
},
|
|
1764
|
-
],
|
|
1765
|
-
actions: [{ text: 'Next', action: 'next' }],
|
|
1766
|
-
},
|
|
1767
|
-
{
|
|
1768
|
-
title: 'Contact Details',
|
|
1769
|
-
schema: [
|
|
1770
|
-
{
|
|
1771
|
-
groupId: 'contact',
|
|
1772
|
-
questions: [
|
|
1773
|
-
/* Contact info questions */
|
|
1774
|
-
],
|
|
1775
|
-
},
|
|
1776
|
-
],
|
|
1777
|
-
actions: [
|
|
1778
|
-
{ text: 'Back', action: 'back' },
|
|
1779
|
-
{ text: 'Next', action: 'next' },
|
|
1780
|
-
],
|
|
1781
|
-
},
|
|
1782
|
-
{
|
|
1783
|
-
title: 'Review & Submit',
|
|
1784
|
-
schema: [
|
|
1785
|
-
{
|
|
1786
|
-
groupId: 'review',
|
|
1787
|
-
questions: [
|
|
1788
|
-
/* Review fields */
|
|
1789
|
-
],
|
|
1790
|
-
},
|
|
1791
|
-
],
|
|
1792
|
-
actions: [
|
|
1793
|
-
{ text: 'Back', action: 'back' },
|
|
1794
|
-
{ text: 'Submit', action: 'submit' },
|
|
1795
|
-
],
|
|
1796
|
-
},
|
|
1797
|
-
];
|
|
1798
|
-
|
|
1799
|
-
const currentStep = steps[step];
|
|
1800
|
-
|
|
1801
|
-
const handleAction = (state, action) => {
|
|
1802
|
-
if (action === 'next' && step < steps.length - 1) {
|
|
1803
|
-
// Store current step data
|
|
1804
|
-
setFormData((prev) => ({ ...prev, ...state.values }));
|
|
1805
|
-
setStep(step + 1);
|
|
1806
|
-
} else if (action === 'back' && step > 0) {
|
|
1807
|
-
setStep(step - 1);
|
|
1808
|
-
} else if (action === 'submit') {
|
|
1809
|
-
// Combine all step data and submit
|
|
1810
|
-
const finalData = { ...formData, ...state.values };
|
|
1811
|
-
console.log('Submitting form:', finalData);
|
|
1812
|
-
// Submit to server
|
|
1813
|
-
}
|
|
1814
|
-
};
|
|
1815
|
-
|
|
1816
|
-
return (
|
|
1817
|
-
<div className="multi-step-form">
|
|
1818
|
-
<div className="progress-bar">
|
|
1819
|
-
{steps.map((s, i) => (
|
|
1820
|
-
<div key={i} className={`step ${i <= step ? 'active' : ''}`}>
|
|
1821
|
-
{s.title}
|
|
1822
|
-
</div>
|
|
1823
|
-
))}
|
|
1824
|
-
</div>
|
|
1825
|
-
|
|
1826
|
-
<DynamicForm
|
|
1827
|
-
questionGroups={currentStep.schema}
|
|
1828
|
-
actionSchema={currentStep.actions}
|
|
1829
|
-
initialValues={formData}
|
|
1830
|
-
onSubmit={handleAction}
|
|
1831
|
-
/>
|
|
1832
|
-
</div>
|
|
1833
|
-
);
|
|
1834
|
-
};
|
|
425
|
+
<DynamicForm questionGroups={FORM_SCHEMA} testIdPrefix="myapp" testIdsEnabled={true} />
|
|
1835
426
|
```
|
|
1836
427
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
Create a form that dynamically loads its schema from an API:
|
|
428
|
+
The prefix flows to every nested component. Set `testIdsEnabled={false}` to strip test IDs from
|
|
429
|
+
production builds. Full convention and per-field-type ID tables: [TESTING_IDS.md](TESTING_IDS.md).
|
|
1840
430
|
|
|
1841
|
-
|
|
1842
|
-
import React, { useState, useEffect } from 'react';
|
|
1843
|
-
import DynamicForm from '@ilife-tech/react-application-flow-renderer';
|
|
1844
|
-
import { handleApiTrigger } from './services/apiService';
|
|
1845
|
-
|
|
1846
|
-
const DynamicApiForm = ({ formId }) => {
|
|
1847
|
-
const [loading, setLoading] = useState(true);
|
|
1848
|
-
const [error, setError] = useState(null);
|
|
1849
|
-
const [formSchema, setFormSchema] = useState(null);
|
|
1850
|
-
|
|
1851
|
-
useEffect(() => {
|
|
1852
|
-
async function fetchFormSchema() {
|
|
1853
|
-
try {
|
|
1854
|
-
setLoading(true);
|
|
1855
|
-
|
|
1856
|
-
// Fetch form schema from API
|
|
1857
|
-
const response = await fetch(`https://api.example.com/forms/${formId}`);
|
|
1858
|
-
|
|
1859
|
-
if (!response.ok) {
|
|
1860
|
-
throw new Error(`Failed to load form: ${response.statusText}`);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
const data = await response.json();
|
|
1864
|
-
|
|
1865
|
-
setFormSchema({
|
|
1866
|
-
questionGroups: data.groups,
|
|
1867
|
-
actionSchema: data.actions,
|
|
1868
|
-
initialValues: data.initialValues || {},
|
|
1869
|
-
});
|
|
1870
|
-
} catch (err) {
|
|
1871
|
-
setError(err.message);
|
|
1872
|
-
} finally {
|
|
1873
|
-
setLoading(false);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
fetchFormSchema();
|
|
1878
|
-
}, [formId]);
|
|
1879
|
-
|
|
1880
|
-
const handleSubmit = async (formState, action) => {
|
|
1881
|
-
if (action === 'submit') {
|
|
1882
|
-
try {
|
|
1883
|
-
// Submit form data to API
|
|
1884
|
-
const response = await fetch('https://api.example.com/submissions', {
|
|
1885
|
-
method: 'POST',
|
|
1886
|
-
headers: {
|
|
1887
|
-
'Content-Type': 'application/json',
|
|
1888
|
-
},
|
|
1889
|
-
body: JSON.stringify({
|
|
1890
|
-
formId,
|
|
1891
|
-
values: formState.values,
|
|
1892
|
-
}),
|
|
1893
|
-
});
|
|
1894
|
-
|
|
1895
|
-
const result = await response.json();
|
|
1896
|
-
console.log('Submission result:', result);
|
|
1897
|
-
} catch (err) {
|
|
1898
|
-
console.error('Submission error:', err);
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
|
|
1903
|
-
if (loading) {
|
|
1904
|
-
return <div className="loading">Loading form...</div>;
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
if (error) {
|
|
1908
|
-
return <div className="error">{error}</div>;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
return (
|
|
1912
|
-
<DynamicForm
|
|
1913
|
-
questionGroups={formSchema.questionGroups}
|
|
1914
|
-
actionSchema={formSchema.actionSchema}
|
|
1915
|
-
initialValues={formSchema.initialValues}
|
|
1916
|
-
onSubmit={handleSubmit}
|
|
1917
|
-
onApiTrigger={handleApiTrigger}
|
|
1918
|
-
/>
|
|
1919
|
-
);
|
|
1920
|
-
};
|
|
1921
|
-
```
|
|
1922
|
-
|
|
1923
|
-
### Security Best Practices
|
|
1924
|
-
|
|
1925
|
-
1. **API Key Management**:
|
|
1926
|
-
- Never hardcode API keys in your JavaScript code
|
|
1927
|
-
- Use environment variables for all sensitive credentials
|
|
1928
|
-
- Consider using token exchange services for third-party APIs
|
|
1929
|
-
|
|
1930
|
-
2. **Sensitive Data Handling**:
|
|
1931
|
-
- Use the built-in masking features for fields like SSN and credit card numbers
|
|
1932
|
-
- Avoid storing sensitive data in localStorage or sessionStorage
|
|
1933
|
-
- Clear sensitive form data after submission
|
|
1934
|
-
|
|
1935
|
-
3. **Input Validation**:
|
|
1936
|
-
- Always validate both on client and server side
|
|
1937
|
-
- Use strong validation rules for sensitive fields
|
|
1938
|
-
- Implement proper error handling for failed validations
|
|
1939
|
-
|
|
1940
|
-
### Accessibility Guidelines
|
|
1941
|
-
|
|
1942
|
-
1. **Screen Reader Support**:
|
|
1943
|
-
- All form fields include proper ARIA attributes
|
|
1944
|
-
- Error messages are announced to screen readers
|
|
1945
|
-
- Focus management follows a logical flow
|
|
1946
|
-
|
|
1947
|
-
2. **Keyboard Navigation**:
|
|
1948
|
-
- All interactive elements are focusable
|
|
1949
|
-
- Tab order follows a logical sequence
|
|
1950
|
-
- Custom components support keyboard interactions
|
|
1951
|
-
|
|
1952
|
-
3. **Visual Considerations**:
|
|
1953
|
-
- Color is not the only means of conveying information
|
|
1954
|
-
- Sufficient color contrast for text and UI elements
|
|
1955
|
-
- Form supports browser zoom and text resizing
|
|
1956
|
-
|
|
1957
|
-
## Conclusion
|
|
1958
|
-
|
|
1959
|
-
The React Dynamic Form Renderer package provides a comprehensive solution for creating dynamic,
|
|
1960
|
-
interactive forms with advanced validation, conditional logic, and third-party API integration. By
|
|
1961
|
-
leveraging the JSON schema approach, you can rapidly develop complex forms without sacrificing
|
|
1962
|
-
flexibility or control.
|
|
1963
|
-
|
|
1964
|
-
For more detailed documentation, refer to the [developer user guide](./docs/developer-user-guide.md)
|
|
1965
|
-
or check out the [example implementations](./example) included with the package. Contributors: see
|
|
1966
|
-
[contributing](./docs/contributing.md), [running](./docs/running.md), and
|
|
1967
|
-
[folder structure](./docs/coding-standards/folder-structure.md) for setup and development workflow.
|
|
431
|
+
## Troubleshooting
|
|
1968
432
|
|
|
1969
|
-
|
|
1970
|
-
[
|
|
433
|
+
- **A field renders as a plain text input** — the `questionType` isn't recognized; check it against
|
|
434
|
+
the [field types](#field-types) table (matching is case-insensitive). Unknown types fall back to
|
|
435
|
+
text with a console warning.
|
|
436
|
+
- **Rules don't fire** — verify `questionRuleDetails`/`questionRules` (and the group/option/button
|
|
437
|
+
variants) are passed and shaped per [docs/reference/page-rules.md](docs/reference/page-rules.md).
|
|
438
|
+
- **Toasts appear twice** — set `toastConfig.useHostToastify: true` if your app already mounts a
|
|
439
|
+
`<ToastContainer/>`.
|
|
440
|
+
- **Theme override wiped out other styles** — the merge is shallow; spread the default slice you're
|
|
441
|
+
overriding (see [Theming](#theming)).
|
|
442
|
+
- **API response ignored** — return the `{ updates, options, visibility, disabled, errors }` shape
|
|
443
|
+
from `onApiTrigger` (note the key is `errors`, not `validation`).
|
|
444
|
+
|
|
445
|
+
## Documentation
|
|
446
|
+
|
|
447
|
+
- **[Documentation index](docs/README.md)** — the map to every doc in this repo.
|
|
448
|
+
- **[Developer & user guide](docs/guides/developer-user-guide.md)** — long-form usage walkthrough.
|
|
449
|
+
- **[Form schema reference](docs/reference/form-schema.md)** — every question type and field.
|
|
450
|
+
- **[Architecture docs](docs/architecture/README.md)** — internals: component hierarchy, state,
|
|
451
|
+
rules engine, data flow, field components, hooks, validation, API/file handling, theming,
|
|
452
|
+
build/tooling.
|
|
453
|
+
- **[Running & building](docs/contributing/running.md)** — local dev, build, and release commands.
|
|
454
|
+
- **[TESTING_IDS.md](TESTING_IDS.md)** — the `data-testid` contract.
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
`package.json` declares the **ISC** license. (Note: a `LICENSE` file is referenced by `package.json`
|
|
459
|
+
but is not currently present in the repository; see
|
|
460
|
+
[docs/architecture/documentation-audit.md](docs/architecture/documentation-audit.md).)
|