@iress-oss/ids-mcp-server 6.0.0-alpha.1-canary-20251204032753-fe18cab → 6.0.0-alpha.1-canary-20251204040305-3639454
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/generated/docs/components_components-alert-docs.md +1054 -0
- package/generated/docs/components_components-autocomplete-docs.md +3304 -0
- package/generated/docs/components_components-autocomplete-recipes-docs.md +98 -0
- package/generated/docs/components_components-badge-docs.md +312 -0
- package/generated/docs/components_components-button-docs.md +2339 -0
- package/generated/docs/components_components-buttongroup-docs.md +980 -0
- package/generated/docs/components_components-card-docs.md +1970 -0
- package/generated/docs/components_components-checkbox-docs.md +1083 -0
- package/generated/docs/components_components-checkboxgroup-docs.md +1597 -0
- package/generated/docs/components_components-checkboxgroup-recipes-docs.md +209 -0
- package/generated/docs/components_components-col-docs.md +755 -0
- package/generated/docs/components_components-container-docs.md +172 -0
- package/generated/docs/components_components-divider-docs.md +235 -0
- package/generated/docs/components_components-expander-docs.md +428 -0
- package/generated/docs/components_components-field-docs.md +3345 -0
- package/generated/docs/components_components-filter-docs.md +4091 -0
- package/generated/docs/components_components-hide-docs.md +450 -0
- package/generated/docs/components_components-icon-docs.md +1017 -0
- package/generated/docs/components_components-image-docs.md +168 -0
- package/generated/docs/components_components-inline-docs.md +1962 -0
- package/generated/docs/components_components-input-docs.md +1388 -0
- package/generated/docs/components_components-input-recipes-docs.md +349 -0
- package/generated/docs/components_components-inputcurrency-docs.md +636 -0
- package/generated/docs/components_components-inputcurrency-recipes-docs.md +208 -0
- package/generated/docs/components_components-introduction-docs.md +448 -0
- package/generated/docs/components_components-label-docs.md +229 -0
- package/generated/docs/components_components-link-docs.md +454 -0
- package/generated/docs/components_components-menu-docs.md +2219 -0
- package/generated/docs/components_components-menu-menuitem-docs.md +1455 -0
- package/generated/docs/components_components-modal-docs.md +2002 -0
- package/generated/docs/components_components-panel-docs.md +342 -0
- package/generated/docs/components_components-placeholder-docs.md +98 -0
- package/generated/docs/components_components-popover-docs.md +1631 -0
- package/generated/docs/components_components-popover-recipes-docs.md +537 -0
- package/generated/docs/components_components-progress-docs.md +128 -0
- package/generated/docs/components_components-provider-docs.md +123 -0
- package/generated/docs/components_components-radio-docs.md +499 -0
- package/generated/docs/components_components-radiogroup-docs.md +1573 -0
- package/generated/docs/components_components-readonly-docs.md +277 -0
- package/generated/docs/components_components-richselect-docs.md +6101 -0
- package/generated/docs/components_components-row-docs.md +2172 -0
- package/generated/docs/components_components-select-docs.md +1110 -0
- package/generated/docs/components_components-skeleton-docs.md +467 -0
- package/generated/docs/components_components-skeleton-recipes-docs.md +110 -0
- package/generated/docs/components_components-skiplink-docs.md +220 -0
- package/generated/docs/components_components-slideout-docs.md +1910 -0
- package/generated/docs/components_components-slider-docs.md +1284 -0
- package/generated/docs/components_components-spinner-docs.md +90 -0
- package/generated/docs/components_components-stack-docs.md +730 -0
- package/generated/docs/components_components-table-docs.md +4038 -0
- package/generated/docs/components_components-tabset-docs.md +955 -0
- package/generated/docs/components_components-tabset-tab-docs.md +342 -0
- package/generated/docs/components_components-tag-docs.md +410 -0
- package/generated/docs/components_components-text-docs.md +593 -0
- package/generated/docs/components_components-toaster-docs.md +451 -0
- package/generated/docs/components_components-toggle-docs.md +513 -0
- package/generated/docs/components_components-tooltip-docs.md +564 -0
- package/generated/docs/components_components-validationmessage-docs.md +608 -0
- package/generated/docs/components_contact-us-docs.md +9 -0
- package/generated/docs/components_foundations-accessibility-docs.md +33 -0
- package/generated/docs/components_foundations-consistency-docs.md +44 -0
- package/generated/docs/components_foundations-content-docs.md +18 -0
- package/generated/docs/components_foundations-introduction-docs.md +17 -0
- package/generated/docs/components_foundations-principles-docs.md +60 -0
- package/generated/docs/components_foundations-responsive-layout-docs.md +2692 -0
- package/generated/docs/components_foundations-user-experience-docs.md +53 -0
- package/generated/docs/components_foundations-visual-design-docs.md +39 -0
- package/generated/docs/components_foundations-z-index-stacking-docs.md +288 -0
- package/generated/docs/components_frequently-asked-questions-docs.md +44 -0
- package/generated/docs/components_get-started-develop-docs.md +47 -0
- package/generated/docs/components_get-started-using-storybook-docs.md +55 -0
- package/generated/docs/components_introduction-docs.md +85 -0
- package/generated/docs/components_patterns-form-docs.md +2469 -0
- package/generated/docs/components_patterns-form-recipes-docs.md +1597 -0
- package/generated/docs/components_patterns-introduction-docs.md +31 -0
- package/generated/docs/components_patterns-loading-docs.md +1908 -0
- package/generated/docs/components_patterns-shadow-docs.md +195 -0
- package/generated/docs/components_resources-code-katas-docs.md +25 -0
- package/generated/docs/components_resources-introduction-docs.md +28 -0
- package/generated/docs/components_resources-mcp-server-docs.md +23 -0
- package/generated/docs/components_resources-migration-guides-from-v4-to-v5-docs.md +142 -0
- package/generated/docs/components_styling-props-colour-docs.md +91 -0
- package/generated/docs/components_styling-props-elevation-docs.md +69 -0
- package/generated/docs/components_styling-props-radius-docs.md +63 -0
- package/generated/docs/components_styling-props-reference-docs.md +160 -0
- package/generated/docs/components_styling-props-screen-readers-docs.md +66 -0
- package/generated/docs/components_styling-props-sizing-docs.md +217 -0
- package/generated/docs/components_styling-props-spacing-docs.md +545 -0
- package/generated/docs/components_styling-props-typography-docs.md +66 -0
- package/generated/docs/components_versions-docs.md +14 -0
- package/generated/docs/guidelines.md +3083 -0
- package/generated/docs/introduction-docs.md +37 -0
- package/generated/docs/tokens_colour-docs.md +479 -0
- package/generated/docs/tokens_elevation-docs.md +39 -0
- package/generated/docs/tokens_introduction-docs.md +150 -0
- package/generated/docs/tokens_radius-docs.md +67 -0
- package/generated/docs/tokens_spacing-docs.md +87 -0
- package/generated/docs/tokens_typography-docs.md +305 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1597 @@
|
|
|
1
|
+
Recipes
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
With readonly data
|
|
5
|
+
------------------
|
|
6
|
+
|
|
7
|
+
You can use `IressForm` with readonly data by setting the `readOnly` prop to `true` on controlled elements. This will disable those form controls, but will include the values in the form submission.
|
|
8
|
+
|
|
9
|
+
Please take note of the following when displaying read only data.
|
|
10
|
+
|
|
11
|
+
* It is best to keep readonly data in a separate section of the form, to further avoid confusion with editable fields.
|
|
12
|
+
|
|
13
|
+
[](./iframe.html?id=patterns-form-recipes--with-readonly-data)
|
|
14
|
+
|
|
15
|
+
User Details
|
|
16
|
+
------------
|
|
17
|
+
|
|
18
|
+
First Name
|
|
19
|
+
|
|
20
|
+
Leia
|
|
21
|
+
|
|
22
|
+
Last Name
|
|
23
|
+
|
|
24
|
+
Skywalker
|
|
25
|
+
|
|
26
|
+
* * *
|
|
27
|
+
|
|
28
|
+
Email
|
|
29
|
+
|
|
30
|
+
Submit
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
export const WithReadonlyDataForm \= () \=> {
|
|
35
|
+
const \[values, setValues\] \= useState<FieldValues\>({
|
|
36
|
+
firstName: 'Leia',
|
|
37
|
+
lastName: 'Skywalker',
|
|
38
|
+
email: 'leia.skywalker@iress.com',
|
|
39
|
+
});
|
|
40
|
+
const \[preview, setPreview\] \= useState(false);
|
|
41
|
+
return (
|
|
42
|
+
<\>
|
|
43
|
+
<IressForm
|
|
44
|
+
onSubmit\={(data) \=> {
|
|
45
|
+
setValues(data);
|
|
46
|
+
setPreview(true);
|
|
47
|
+
}}
|
|
48
|
+
values\={values}
|
|
49
|
+
\>
|
|
50
|
+
<IressContainer\>
|
|
51
|
+
<IressStack gap\="md"\>
|
|
52
|
+
<IressText element\="h2"\>User Details</IressText\>
|
|
53
|
+
<IressRow gutter\="md"\>
|
|
54
|
+
<IressCol\>
|
|
55
|
+
<IressFormField
|
|
56
|
+
name\="firstName"
|
|
57
|
+
label\="First Name"
|
|
58
|
+
render\={(controlledProps) \=> (
|
|
59
|
+
<IressInput {...controlledProps} readOnly />
|
|
60
|
+
)}
|
|
61
|
+
mb\="none"
|
|
62
|
+
/>
|
|
63
|
+
</IressCol\>
|
|
64
|
+
<IressCol\>
|
|
65
|
+
<IressFormField
|
|
66
|
+
name\="lastName"
|
|
67
|
+
label\="Last Name"
|
|
68
|
+
render\={(controlledProps) \=> (
|
|
69
|
+
<IressInput {...controlledProps} readOnly />
|
|
70
|
+
)}
|
|
71
|
+
mb\="none"
|
|
72
|
+
/>
|
|
73
|
+
</IressCol\>
|
|
74
|
+
</IressRow\>
|
|
75
|
+
<IressDivider />
|
|
76
|
+
<IressRow\>
|
|
77
|
+
<IressCol\>
|
|
78
|
+
<IressFormField
|
|
79
|
+
name\="email"
|
|
80
|
+
label\="Email"
|
|
81
|
+
render\={(controlledProps) \=> (
|
|
82
|
+
<IressInput {...controlledProps} type\="email" />
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
</IressCol\>
|
|
86
|
+
</IressRow\>
|
|
87
|
+
<IressButton type\="submit" mode\="primary"\>
|
|
88
|
+
Submit </IressButton\>
|
|
89
|
+
</IressStack\>
|
|
90
|
+
</IressContainer\>
|
|
91
|
+
</IressForm\>
|
|
92
|
+
<IressModal show\={!!preview} onShowChange\={(show) \=> setPreview(show)}\>
|
|
93
|
+
<IressTable
|
|
94
|
+
caption\="Submitted"
|
|
95
|
+
rows\={Object.entries(values).map((entry) \=> ({
|
|
96
|
+
name: entry\[0\],
|
|
97
|
+
value: JSON.stringify(entry\[1\], null, 2),
|
|
98
|
+
}))}
|
|
99
|
+
/>
|
|
100
|
+
</IressModal\>
|
|
101
|
+
</\>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Props
|
|
108
|
+
|
|
109
|
+
| Name | Description | Default | Control |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| actions |
|
|
112
|
+
object
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
| \- | Choose option...nonesubmitsave |
|
|
117
|
+
| alert |
|
|
118
|
+
|
|
119
|
+
\-
|
|
120
|
+
|
|
121
|
+
| \- | Choose option...nonebasicAlert |
|
|
122
|
+
| children |
|
|
123
|
+
|
|
124
|
+
array
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
129
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
130
|
+
|
|
131
|
+
Switching between readonly and edit modes
|
|
132
|
+
-----------------------------------------
|
|
133
|
+
|
|
134
|
+
It is recommended to use a button to toggle between read-only and editable input modes.
|
|
135
|
+
|
|
136
|
+
Please take note of the following when switching between modes:
|
|
137
|
+
|
|
138
|
+
* Switching is done on a per-section basis, not on a per-field basis.
|
|
139
|
+
* When the user saves the data, it should switch back to read-only mode to avoid any confusion about whether the changes have been saved.
|
|
140
|
+
|
|
141
|
+
[](./iframe.html?id=patterns-form-recipes--switch-edit-readonly)
|
|
142
|
+
|
|
143
|
+
User Details
|
|
144
|
+
------------
|
|
145
|
+
|
|
146
|
+
First Name
|
|
147
|
+
|
|
148
|
+
Leia
|
|
149
|
+
|
|
150
|
+
Last Name
|
|
151
|
+
|
|
152
|
+
Skywalker
|
|
153
|
+
|
|
154
|
+
Email
|
|
155
|
+
|
|
156
|
+
leia.skywalker@iress.com
|
|
157
|
+
|
|
158
|
+
Dependents
|
|
159
|
+
|
|
160
|
+
0
|
|
161
|
+
|
|
162
|
+
Edit
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
const Form \= () \=> {
|
|
167
|
+
const dependentOptions \= \[
|
|
168
|
+
{ value: 0, label: '0' },
|
|
169
|
+
{ value: 1, label: '1' },
|
|
170
|
+
{ value: 2, label: '2' },
|
|
171
|
+
{ value: 3, label: '3' },
|
|
172
|
+
{ value: 4, label: '4' },
|
|
173
|
+
{ value: 5, label: '5' },
|
|
174
|
+
{ value: 6, label: '6' },
|
|
175
|
+
{ value: 7, label: '7' },
|
|
176
|
+
{ value: 8, label: '8' },
|
|
177
|
+
{ value: 9, label: '9' },
|
|
178
|
+
{ value: 10, label: '10' },
|
|
179
|
+
\];
|
|
180
|
+
const \[values, setValues\] \= useState<FieldValues\>({
|
|
181
|
+
firstName: 'Leia',
|
|
182
|
+
lastName: 'Skywalker',
|
|
183
|
+
email: 'leia.skywalker@iress.com',
|
|
184
|
+
dependents: 0,
|
|
185
|
+
});
|
|
186
|
+
const \[editable, setEditable\] \= useState(false);
|
|
187
|
+
const { success } \= useToaster();
|
|
188
|
+
return (
|
|
189
|
+
<IressForm
|
|
190
|
+
onSubmit\={(data) \=> {
|
|
191
|
+
setValues(data);
|
|
192
|
+
setEditable(false);
|
|
193
|
+
success({
|
|
194
|
+
content: 'Saved successfully',
|
|
195
|
+
});
|
|
196
|
+
}}
|
|
197
|
+
values\={values}
|
|
198
|
+
\>
|
|
199
|
+
<IressContainer\>
|
|
200
|
+
<IressStack gap\="md"\>
|
|
201
|
+
<IressText element\="h2"\>User Details</IressText\>
|
|
202
|
+
<IressRow gutter\="md"\>
|
|
203
|
+
<IressCol\>
|
|
204
|
+
<IressFormField
|
|
205
|
+
name\="firstName"
|
|
206
|
+
label\="First Name"
|
|
207
|
+
render\={(controlledProps) \=> (
|
|
208
|
+
<IressInput {...controlledProps} readOnly\={!editable} />
|
|
209
|
+
)}
|
|
210
|
+
/>
|
|
211
|
+
</IressCol\>
|
|
212
|
+
<IressCol\>
|
|
213
|
+
<IressFormField
|
|
214
|
+
name\="lastName"
|
|
215
|
+
label\="Last Name"
|
|
216
|
+
render\={(controlledProps) \=> (
|
|
217
|
+
<IressInput {...controlledProps} readOnly\={!editable} />
|
|
218
|
+
)}
|
|
219
|
+
/>
|
|
220
|
+
</IressCol\>
|
|
221
|
+
</IressRow\>
|
|
222
|
+
<IressRow gutter\="md"\>
|
|
223
|
+
<IressCol\>
|
|
224
|
+
<IressFormField
|
|
225
|
+
name\="email"
|
|
226
|
+
label\="Email"
|
|
227
|
+
render\={(controlledProps) \=> (
|
|
228
|
+
<IressInput
|
|
229
|
+
{...controlledProps}
|
|
230
|
+
readOnly\={!editable}
|
|
231
|
+
type\="email"
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
/>
|
|
235
|
+
</IressCol\>
|
|
236
|
+
<IressCol\>
|
|
237
|
+
<IressFormField
|
|
238
|
+
name\="dependents"
|
|
239
|
+
label\="Dependents"
|
|
240
|
+
render\={(controlledProps) \=> (
|
|
241
|
+
<IressSelect
|
|
242
|
+
{...controlledProps}
|
|
243
|
+
readOnly\={!editable}
|
|
244
|
+
onChange\={(
|
|
245
|
+
\_e: React.ChangeEvent<HTMLSelectElement\>,
|
|
246
|
+
value?: FormControlValue,
|
|
247
|
+
) \=> controlledProps.onChange(value)}
|
|
248
|
+
\>
|
|
249
|
+
{dependentOptions.map((option) \=> (
|
|
250
|
+
<option key\={option.value} value\={option.value}\>
|
|
251
|
+
{option.label}
|
|
252
|
+
</option\>
|
|
253
|
+
))}
|
|
254
|
+
</IressSelect\>
|
|
255
|
+
)}
|
|
256
|
+
/>
|
|
257
|
+
</IressCol\>
|
|
258
|
+
</IressRow\>
|
|
259
|
+
{editable ? (
|
|
260
|
+
<IressInline gap\="sm"\>
|
|
261
|
+
<IressButton type\="submit" mode\="primary"\>
|
|
262
|
+
Save </IressButton\>
|
|
263
|
+
<IressButton onClick\={() \=> setEditable(false)}\>
|
|
264
|
+
Cancel </IressButton\>
|
|
265
|
+
</IressInline\>
|
|
266
|
+
) : (
|
|
267
|
+
<IressButton
|
|
268
|
+
onClick\={() \=> setEditable(true)}
|
|
269
|
+
prepend\={<IressIcon name\="pencil" />}
|
|
270
|
+
\>
|
|
271
|
+
Edit </IressButton\>
|
|
272
|
+
)}
|
|
273
|
+
</IressStack\>
|
|
274
|
+
</IressContainer\>
|
|
275
|
+
</IressForm\>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
278
|
+
export const SwitchEditReadonlyForm \= () \=> (
|
|
279
|
+
<IressToasterProvider\>
|
|
280
|
+
<Form />
|
|
281
|
+
</IressToasterProvider\>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Props
|
|
287
|
+
|
|
288
|
+
| Name | Description | Default | Control |
|
|
289
|
+
| --- | --- | --- | --- |
|
|
290
|
+
| actions |
|
|
291
|
+
object
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
| \- | Choose option...nonesubmitsave |
|
|
296
|
+
| alert |
|
|
297
|
+
|
|
298
|
+
\-
|
|
299
|
+
|
|
300
|
+
| \- | Choose option...nonebasicAlert |
|
|
301
|
+
| children |
|
|
302
|
+
|
|
303
|
+
array
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
308
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
309
|
+
|
|
310
|
+
Alternative form validation
|
|
311
|
+
---------------------------
|
|
312
|
+
|
|
313
|
+
`IressForm` is always recommended for all validation, as it is the cleanest way (least code) to provide the best form user experience for your users. Please visit the [`IressForm` documentation](/docs/components-form--docs) for different validation examples.
|
|
314
|
+
|
|
315
|
+
However, if you have more complex requirements and you find `IressForm` too opinionated for your needs, you can always bring your own form validation using a native `form` element and the other IDS components such as `IressField`.
|
|
316
|
+
|
|
317
|
+
Here is an example showcasing a form using the native form constraints API to achieve validation using IDS components. There are other libraries such as: [Yup](https://github.com/jquense/yup), [Joi](https://github.com/hapijs/joi) or [Zod](https://zod.dev/)) which can improve scalability, with the downside being you will have to maintain all validation yourself.
|
|
318
|
+
|
|
319
|
+
[](./iframe.html?id=patterns-form-recipes--native-validation)
|
|
320
|
+
|
|
321
|
+
**\*RequiredName**
|
|
322
|
+
|
|
323
|
+
**\*RequiredEmail address**
|
|
324
|
+
|
|
325
|
+
Sign up
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
export const NativeValidationForm \= () \=> {
|
|
330
|
+
const \[formData, setFormData\] \= useState({
|
|
331
|
+
name: '',
|
|
332
|
+
email: '',
|
|
333
|
+
});
|
|
334
|
+
const \[errors, setErrors\] \= useState<Record<string, boolean\>>({
|
|
335
|
+
name: false,
|
|
336
|
+
email: false,
|
|
337
|
+
});
|
|
338
|
+
const \[isSubmitted, setIsSubmitted\] \= useState(false);
|
|
339
|
+
const hasErrors \= Object.values(errors).some((error) \=> !!error);
|
|
340
|
+
const handleInputChange \= (e: React.ChangeEvent<InputBaseElement\>) \=> {
|
|
341
|
+
setFormData({ ...formData, \[e.target.name\]: e.target.value });
|
|
342
|
+
setErrors({
|
|
343
|
+
...errors,
|
|
344
|
+
\[e.target.name\]: !e.currentTarget.reportValidity(),
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
const handleSubmit \= (e: React.FormEvent<HTMLFormElement\>) \=> {
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
const form \= e.currentTarget;
|
|
350
|
+
setIsSubmitted(true);
|
|
351
|
+
if (!form.checkValidity()) {
|
|
352
|
+
const fieldData \= Object.fromEntries(new FormData(form).entries());
|
|
353
|
+
const fieldNames \= Object.keys(fieldData);
|
|
354
|
+
setErrors(
|
|
355
|
+
fieldNames.reduce(
|
|
356
|
+
(newErrors, fieldName) \=> {
|
|
357
|
+
newErrors\[fieldName\] \= !form
|
|
358
|
+
.querySelector<HTMLInputElement\>(\`\[name=${fieldName}\]\`)
|
|
359
|
+
?.checkValidity();
|
|
360
|
+
return newErrors;
|
|
361
|
+
},
|
|
362
|
+
{} as Record<string, boolean\>,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
console.log(formData);
|
|
367
|
+
};
|
|
368
|
+
return (
|
|
369
|
+
<form onSubmit\={handleSubmit} noValidate\>
|
|
370
|
+
{isSubmitted && hasErrors && (
|
|
371
|
+
<IressAlert status\="danger"\>
|
|
372
|
+
There's a problem with your submission. </IressAlert\>
|
|
373
|
+
)}
|
|
374
|
+
<IressField
|
|
375
|
+
label\="Name"
|
|
376
|
+
error\={errors.name && 'Name is required'}
|
|
377
|
+
required
|
|
378
|
+
\>
|
|
379
|
+
<IressInput name\="name" onChange\={handleInputChange} required />
|
|
380
|
+
</IressField\>
|
|
381
|
+
<IressField
|
|
382
|
+
label\="Email address"
|
|
383
|
+
error\={errors.email && 'Email is required'}
|
|
384
|
+
required
|
|
385
|
+
\>
|
|
386
|
+
<IressInput name\="email" onChange\={handleInputChange} required />
|
|
387
|
+
</IressField\>
|
|
388
|
+
<IressButton mode\="primary" type\="submit"\>
|
|
389
|
+
Sign up </IressButton\>
|
|
390
|
+
</form\>
|
|
391
|
+
);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### Props
|
|
397
|
+
|
|
398
|
+
| Name | Description | Default | Control |
|
|
399
|
+
| --- | --- | --- | --- |
|
|
400
|
+
| actions |
|
|
401
|
+
object
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
| \- | Choose option...nonesubmitsave |
|
|
406
|
+
| alert |
|
|
407
|
+
|
|
408
|
+
\-
|
|
409
|
+
|
|
410
|
+
| \- | Choose option...nonebasicAlert |
|
|
411
|
+
| children |
|
|
412
|
+
|
|
413
|
+
array
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
418
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
419
|
+
|
|
420
|
+
Nested forms
|
|
421
|
+
------------
|
|
422
|
+
|
|
423
|
+
Unfortunately, it is [forbidden to nest form elements as per the HTML specifications](https://developer.mozilla.org/en-US/docs/Learn/Forms/How_to_structure_a_web_form).
|
|
424
|
+
|
|
425
|
+
To achieve a similar effect, you can use multiple `IressForm` components, and trigger validation in multiple ways:
|
|
426
|
+
|
|
427
|
+
1. You can trigger specific forms using the `form` attribute of `IressButton`. The [`form` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#form) allows you to specify the form ID to submit when the button is clicked, which can be any form on the page, and will take precedence over the parent form of a button.
|
|
428
|
+
2. If you need to trigger multiple forms, you can use the [`requestSubmit` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit) on the form element to trigger the validation of multiple forms.
|
|
429
|
+
3. If you only want to trigger validation and not trigger submission even if the validation passes, you can use the `ref` attribute of `IressForm` and trigger validation manually using `ref.current?.api.trigger()`, which is based on the [React Hook Form API](https://react-hook-form.com/docs/useform/trigger).
|
|
430
|
+
|
|
431
|
+
The example here showcases triggering validation using the `form` attribute of `IressButton` and the `requestSubmit` method on the form element.
|
|
432
|
+
|
|
433
|
+
[](./iframe.html?id=patterns-form-recipes--nested-forms)
|
|
434
|
+
|
|
435
|
+
\*RequiredName
|
|
436
|
+
|
|
437
|
+
Add new dependant
|
|
438
|
+
|
|
439
|
+
\*RequiredName
|
|
440
|
+
|
|
441
|
+
Save
|
|
442
|
+
|
|
443
|
+
* * *
|
|
444
|
+
|
|
445
|
+
Submit main formSubmit all forms
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
interface FormData {
|
|
450
|
+
name: string;
|
|
451
|
+
}
|
|
452
|
+
const MainForm \= () \=> {
|
|
453
|
+
const \[details, setDetails\] \= useState<FormData | undefined\>();
|
|
454
|
+
return (
|
|
455
|
+
<\>
|
|
456
|
+
<IressForm<FormData> alert={
|
|
457
|
+
<IressFormValidationSummary heading\="Please fix the errors for the main form" />
|
|
458
|
+
}
|
|
459
|
+
id="mainForm" onSubmit={(data) \=> {
|
|
460
|
+
setDetails(data);
|
|
461
|
+
}}
|
|
462
|
+
> <IressFormField<FormData> name="name" label="Name" render={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
463
|
+
rules={{ required: true }}
|
|
464
|
+
/> </IressForm\>
|
|
465
|
+
<IressModal
|
|
466
|
+
show\={!!details}
|
|
467
|
+
onShowChange\={(show) \=> !show && setDetails(undefined)}
|
|
468
|
+
\>
|
|
469
|
+
{details && (
|
|
470
|
+
<IressTable
|
|
471
|
+
caption\="Submitted main form"
|
|
472
|
+
rows\={Object.entries(details).map((entry) \=> ({
|
|
473
|
+
name: entry\[0\],
|
|
474
|
+
value: JSON.stringify(entry\[1\], null, 2),
|
|
475
|
+
}))}
|
|
476
|
+
/>
|
|
477
|
+
)}
|
|
478
|
+
</IressModal\>
|
|
479
|
+
</\>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
const SubForm \= () \=> {
|
|
483
|
+
const \[details, setDetails\] \= useState<FormData | undefined\>();
|
|
484
|
+
return (
|
|
485
|
+
<IressPanel bg\="alt"\>
|
|
486
|
+
<IressStack gap\="md"\>
|
|
487
|
+
<IressForm<FormData> alert={
|
|
488
|
+
<IressFormValidationSummary heading\="Please fix the errors for the dependants" />
|
|
489
|
+
}
|
|
490
|
+
id="subForm" onSubmit={(data) \=> {
|
|
491
|
+
setDetails(data);
|
|
492
|
+
}}
|
|
493
|
+
> <IressFieldGroup label\="Add new dependant" inline mb\="none"\>
|
|
494
|
+
<IressFormField
|
|
495
|
+
name\="name"
|
|
496
|
+
label\="Name"
|
|
497
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
498
|
+
rules\={{ required: true }}
|
|
499
|
+
/>
|
|
500
|
+
<IressButton type\="submit"\>Save</IressButton\>
|
|
501
|
+
</IressFieldGroup\>
|
|
502
|
+
</IressForm\>
|
|
503
|
+
<IressTable
|
|
504
|
+
caption\="Dependants"
|
|
505
|
+
columns\={\[
|
|
506
|
+
{ key: 'name', label: 'Name' },
|
|
507
|
+
{ key: 'value', label: 'Value' },
|
|
508
|
+
\]}
|
|
509
|
+
rows\={Object.entries(details ?? {}).map((entry) \=> ({
|
|
510
|
+
name: entry\[0\],
|
|
511
|
+
value: JSON.stringify(entry\[1\], null, 2),
|
|
512
|
+
}))}
|
|
513
|
+
/>
|
|
514
|
+
</IressStack\>
|
|
515
|
+
</IressPanel\>
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
export const NestedFormsExample \= () \=> {
|
|
519
|
+
const submitAllForms \= () \=> {
|
|
520
|
+
document.querySelector<HTMLFormElement\>('\[id=mainForm\]')?.requestSubmit();
|
|
521
|
+
document.querySelector<HTMLFormElement\>('\[id=subForm\]')?.requestSubmit();
|
|
522
|
+
};
|
|
523
|
+
return (
|
|
524
|
+
<IressStack gap\="md"\>
|
|
525
|
+
<MainForm />
|
|
526
|
+
<SubForm />
|
|
527
|
+
<IressDivider />
|
|
528
|
+
<IressInline gap\="md"\>
|
|
529
|
+
<IressButton type\="submit" form\="mainForm"\>
|
|
530
|
+
Submit main form </IressButton\>
|
|
531
|
+
<IressButton onClick\={submitAllForms}\>Submit all forms</IressButton\>
|
|
532
|
+
</IressInline\>
|
|
533
|
+
</IressStack\>
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### Props
|
|
540
|
+
|
|
541
|
+
| Name | Description | Default | Control |
|
|
542
|
+
| --- | --- | --- | --- |
|
|
543
|
+
| actions |
|
|
544
|
+
object
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
| \- | Choose option...nonesubmitsave |
|
|
549
|
+
| alert |
|
|
550
|
+
|
|
551
|
+
\-
|
|
552
|
+
|
|
553
|
+
| \- | Choose option...nonebasicAlert |
|
|
554
|
+
| children |
|
|
555
|
+
|
|
556
|
+
array
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
561
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
562
|
+
|
|
563
|
+
Form Groups
|
|
564
|
+
-----------
|
|
565
|
+
|
|
566
|
+
Powered by [React Hook Form](https://react-hook-form.com/docs/usefieldarray)'s `useFieldArray`, this example allows you add/edit/delete multiple children sections within ONE form (not nested form).
|
|
567
|
+
|
|
568
|
+
[](./iframe.html?id=patterns-form-recipes--form-groups)
|
|
569
|
+
|
|
570
|
+
Form groups
|
|
571
|
+
===========
|
|
572
|
+
|
|
573
|
+
This is one form with child sections (not nested forms). Play around to add/edit/delete child form sections:
|
|
574
|
+
|
|
575
|
+
Client
|
|
576
|
+
------
|
|
577
|
+
|
|
578
|
+
\*RequiredName
|
|
579
|
+
|
|
580
|
+
\*RequiredSalary
|
|
581
|
+
|
|
582
|
+
\*RequiredGoal
|
|
583
|
+
|
|
584
|
+
Dependant 1
|
|
585
|
+
-----------
|
|
586
|
+
|
|
587
|
+
\*RequiredName
|
|
588
|
+
|
|
589
|
+
\*RequiredRelationship
|
|
590
|
+
|
|
591
|
+
\*RequiredAge
|
|
592
|
+
|
|
593
|
+
Save
|
|
594
|
+
|
|
595
|
+
Add Dependant
|
|
596
|
+
|
|
597
|
+
* * *
|
|
598
|
+
|
|
599
|
+
Submit All
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
interface Client {
|
|
604
|
+
name: string | undefined;
|
|
605
|
+
salary: number | undefined;
|
|
606
|
+
goal: string | undefined;
|
|
607
|
+
}
|
|
608
|
+
interface Dependant {
|
|
609
|
+
name: string | undefined;
|
|
610
|
+
relationship: string | undefined;
|
|
611
|
+
age: number | undefined;
|
|
612
|
+
}
|
|
613
|
+
interface FormValues {
|
|
614
|
+
client: Client;
|
|
615
|
+
dependants: Dependant\[\];
|
|
616
|
+
}
|
|
617
|
+
interface ClientProps {
|
|
618
|
+
control: Control<FormValues\> | undefined;
|
|
619
|
+
}
|
|
620
|
+
interface DependantProps {
|
|
621
|
+
index: number;
|
|
622
|
+
control: Control<FormValues\> | undefined;
|
|
623
|
+
update: (index: number, data: Dependant) \=> void;
|
|
624
|
+
remove: (index: number) \=> void;
|
|
625
|
+
getValues: UseFormGetValues<FormValues\>;
|
|
626
|
+
}
|
|
627
|
+
const defaultValues \= {
|
|
628
|
+
client: {
|
|
629
|
+
name: '',
|
|
630
|
+
salary: undefined,
|
|
631
|
+
goal: '',
|
|
632
|
+
},
|
|
633
|
+
dependants: \[
|
|
634
|
+
{
|
|
635
|
+
name: '',
|
|
636
|
+
relationship: '',
|
|
637
|
+
age: undefined,
|
|
638
|
+
},
|
|
639
|
+
\],
|
|
640
|
+
};
|
|
641
|
+
const ClientSection: React.FC<ClientProps\> \= ({ control }) \=> {
|
|
642
|
+
return (
|
|
643
|
+
<IressFieldGroup label\={<IressText element\="h2"\>Client</IressText\>} inline\>
|
|
644
|
+
<IressFormField
|
|
645
|
+
name\="client.name"
|
|
646
|
+
label\="Name"
|
|
647
|
+
control\={control}
|
|
648
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
649
|
+
rules\={{ required: true }}
|
|
650
|
+
/>
|
|
651
|
+
<IressFormField
|
|
652
|
+
name\="client.salary"
|
|
653
|
+
label\="Salary"
|
|
654
|
+
control\={control}
|
|
655
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
656
|
+
rules\={{ required: true }}
|
|
657
|
+
/>
|
|
658
|
+
<IressFormField
|
|
659
|
+
name\="client.goal"
|
|
660
|
+
label\="Goal"
|
|
661
|
+
control\={control}
|
|
662
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
663
|
+
rules\={{ required: true }}
|
|
664
|
+
/>
|
|
665
|
+
</IressFieldGroup\>
|
|
666
|
+
);
|
|
667
|
+
};
|
|
668
|
+
const DependantSection: React.FC<DependantProps\> \= ({
|
|
669
|
+
index,
|
|
670
|
+
update,
|
|
671
|
+
remove,
|
|
672
|
+
control,
|
|
673
|
+
getValues,
|
|
674
|
+
}: DependantProps) \=> {
|
|
675
|
+
return (
|
|
676
|
+
<IressPanel bg\="alt"\>
|
|
677
|
+
<IressInline horizontalAlign\="right"\>
|
|
678
|
+
<IressCloseButton
|
|
679
|
+
onClick\={() \=> remove(index)}
|
|
680
|
+
mb\="\-lg"
|
|
681
|
+
style\={{ zIndex: 1 }}
|
|
682
|
+
/>
|
|
683
|
+
</IressInline\>
|
|
684
|
+
<IressStack gap\="md"\>
|
|
685
|
+
<IressFieldGroup
|
|
686
|
+
label\={<IressText element\="h2"\>Dependant {index + 1}</IressText\>}
|
|
687
|
+
inline
|
|
688
|
+
\>
|
|
689
|
+
<IressFormField
|
|
690
|
+
name\={\`dependants.${index}.name\`}
|
|
691
|
+
label\="Name"
|
|
692
|
+
control\={control}
|
|
693
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
694
|
+
rules\={{ required: true }}
|
|
695
|
+
/>
|
|
696
|
+
<IressFormField
|
|
697
|
+
name\={\`dependants.${index}.relationship\`}
|
|
698
|
+
label\="Relationship"
|
|
699
|
+
control\={control}
|
|
700
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
701
|
+
rules\={{ required: true }}
|
|
702
|
+
/>
|
|
703
|
+
<IressFormField
|
|
704
|
+
name\={\`dependants.${index}.age\`}
|
|
705
|
+
label\="Age"
|
|
706
|
+
control\={control}
|
|
707
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
708
|
+
rules\={{ required: true }}
|
|
709
|
+
/>
|
|
710
|
+
<IressButton
|
|
711
|
+
type\="button"
|
|
712
|
+
prepend\={<IressIcon name\="check" />}
|
|
713
|
+
onClick\={() \=> {
|
|
714
|
+
const data \= getValues();
|
|
715
|
+
const value \= data?.dependants\[index\];
|
|
716
|
+
update(index, value);
|
|
717
|
+
}}
|
|
718
|
+
\>
|
|
719
|
+
Save </IressButton\>
|
|
720
|
+
</IressFieldGroup\>
|
|
721
|
+
</IressStack\>
|
|
722
|
+
</IressPanel\>
|
|
723
|
+
);
|
|
724
|
+
};
|
|
725
|
+
export const FormGroups \= () \=> {
|
|
726
|
+
const form \= IressHookForm.useForm<FormValues\>({
|
|
727
|
+
defaultValues: defaultValues,
|
|
728
|
+
mode: 'onBlur',
|
|
729
|
+
});
|
|
730
|
+
const { control, getValues } \= form;
|
|
731
|
+
const { fields, append, update, remove } \= IressHookForm.useFieldArray({
|
|
732
|
+
name: 'dependants',
|
|
733
|
+
control,
|
|
734
|
+
});
|
|
735
|
+
const onSubmit \= (data: FormValues) \=> console.log(data);
|
|
736
|
+
return (
|
|
737
|
+
<IressText\>
|
|
738
|
+
<h1\>Form groups</h1\>
|
|
739
|
+
<p\>
|
|
740
|
+
This is one form with child sections (not nested forms). Play around to add/edit/delete child form sections: </p\>
|
|
741
|
+
<IressHookForm<FormValues> id="mainForm" form={form} onSubmit={onSubmit}\>
|
|
742
|
+
<IressStack gap\="md"\>
|
|
743
|
+
<ClientSection control\={control} />
|
|
744
|
+
{fields.map((field, index) \=> (
|
|
745
|
+
<DependantSection
|
|
746
|
+
key\={field.id}
|
|
747
|
+
index\={index}
|
|
748
|
+
control\={control}
|
|
749
|
+
update\={update}
|
|
750
|
+
remove\={remove}
|
|
751
|
+
getValues\={getValues}
|
|
752
|
+
/>
|
|
753
|
+
))}
|
|
754
|
+
<IressButton
|
|
755
|
+
type\="button"
|
|
756
|
+
prepend\={<IressIcon name\="plus" />}
|
|
757
|
+
onClick\={() \=> {
|
|
758
|
+
append({ name: '', relationship: '', age: undefined });
|
|
759
|
+
}}
|
|
760
|
+
\>
|
|
761
|
+
Add Dependant </IressButton\>
|
|
762
|
+
</IressStack\>
|
|
763
|
+
<IressDivider my\="md" />
|
|
764
|
+
<IressButton type\="submit" mode\="primary"\>
|
|
765
|
+
Submit All </IressButton\>
|
|
766
|
+
</IressHookForm\>
|
|
767
|
+
</IressText\>
|
|
768
|
+
);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
#### Props
|
|
774
|
+
|
|
775
|
+
| Name | Description | Default | Control |
|
|
776
|
+
| --- | --- | --- | --- |
|
|
777
|
+
| actions |
|
|
778
|
+
object
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
| \- | Choose option...nonesubmitsave |
|
|
783
|
+
| alert |
|
|
784
|
+
|
|
785
|
+
\-
|
|
786
|
+
|
|
787
|
+
| \- | Choose option...nonebasicAlert |
|
|
788
|
+
| children |
|
|
789
|
+
|
|
790
|
+
array
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
795
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
796
|
+
|
|
797
|
+
Forms in expanders (lazy loading)
|
|
798
|
+
---------------------------------
|
|
799
|
+
|
|
800
|
+
If you are using forms in expanders, or in other scenarios when the loading of the form may be delayed, it is recommended to only load the form when it is required. This will improve performance of your application, and make it more predictable. It may also fix act warnings in tests if you are seeing some appearing due to conditionally loaded `IressForm` elements.
|
|
801
|
+
|
|
802
|
+
[](./iframe.html?id=patterns-form-recipes--forms-in-expanders)
|
|
803
|
+
|
|
804
|
+
Sender
|
|
805
|
+
|
|
806
|
+
Recipient
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
interface FieldValues {
|
|
811
|
+
name?: string;
|
|
812
|
+
email?: string;
|
|
813
|
+
}
|
|
814
|
+
const Form \= (args: IressFormProps<FieldValues\>) \=> (
|
|
815
|
+
<IressForm {...args}\>
|
|
816
|
+
<IressStack gap\="md"\>
|
|
817
|
+
<IressFormField
|
|
818
|
+
label\="Name"
|
|
819
|
+
name\="name"
|
|
820
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
821
|
+
rules\={{
|
|
822
|
+
required: 'Name is required',
|
|
823
|
+
}}
|
|
824
|
+
/>
|
|
825
|
+
<IressFormField
|
|
826
|
+
label\="Email address"
|
|
827
|
+
name\="email"
|
|
828
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
829
|
+
rules\={{
|
|
830
|
+
minLength: {
|
|
831
|
+
message: 'Use a longer email address',
|
|
832
|
+
value: 6,
|
|
833
|
+
},
|
|
834
|
+
required: 'Email is required',
|
|
835
|
+
}}
|
|
836
|
+
/>
|
|
837
|
+
</IressStack\>
|
|
838
|
+
</IressForm\>
|
|
839
|
+
);
|
|
840
|
+
export const FormExpanders \= (args: IressFormProps<FieldValues\>) \=> {
|
|
841
|
+
const \[expander, setExpander\] \= useState('');
|
|
842
|
+
const isOpen \= (name: string) \=> expander \=== name;
|
|
843
|
+
return (
|
|
844
|
+
<IressStack gap\="sm"\>
|
|
845
|
+
<IressExpander
|
|
846
|
+
activator\="Sender"
|
|
847
|
+
open\={isOpen('Sender')}
|
|
848
|
+
onChange\={(open) \=> open && setExpander('Sender')}
|
|
849
|
+
\>
|
|
850
|
+
{isOpen('Sender') && <Form {...args} />}
|
|
851
|
+
</IressExpander\>
|
|
852
|
+
<IressExpander
|
|
853
|
+
activator\="Recipient"
|
|
854
|
+
open\={isOpen('Recipient')}
|
|
855
|
+
onChange\={(open) \=> open && setExpander('Recipient')}
|
|
856
|
+
\>
|
|
857
|
+
{isOpen('Recipient') && <Form {...args} />}
|
|
858
|
+
</IressExpander\>
|
|
859
|
+
</IressStack\>
|
|
860
|
+
);
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
#### Props
|
|
866
|
+
|
|
867
|
+
| Name | Description | Default | Control |
|
|
868
|
+
| --- | --- | --- | --- |
|
|
869
|
+
| actions |
|
|
870
|
+
object
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
| \- | Choose option...nonesubmitsave |
|
|
875
|
+
| alert |
|
|
876
|
+
|
|
877
|
+
\-
|
|
878
|
+
|
|
879
|
+
| \- | Choose option...nonebasicAlert |
|
|
880
|
+
| children |
|
|
881
|
+
|
|
882
|
+
array
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
887
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
888
|
+
|
|
889
|
+
Conditional fields (useWatch)
|
|
890
|
+
-----------------------------
|
|
891
|
+
|
|
892
|
+
When you have fields that are conditionally shown, you can use the `IressForm.useWatch` hook to watch the value of another field and conditionally render the field.
|
|
893
|
+
|
|
894
|
+
**Notes:**
|
|
895
|
+
|
|
896
|
+
* You can use the `api.watch` method on the `IressForm`'s ref to watch the value of a field, but it is recommended to use the hook for better performance by isolating re-rendering at the component level.
|
|
897
|
+
|
|
898
|
+
[](./iframe.html?id=patterns-form-recipes--use-watch)
|
|
899
|
+
|
|
900
|
+
Conditional fields using `useWatch`
|
|
901
|
+
-----------------------------------
|
|
902
|
+
|
|
903
|
+
This example demonstrates how to use the `IressForm.useWatch()` hook to watch the value of a field and conditionally render other fields based on that value.
|
|
904
|
+
|
|
905
|
+
\*RequiredSelect fields to show
|
|
906
|
+
|
|
907
|
+
Name
|
|
908
|
+
|
|
909
|
+
Email
|
|
910
|
+
|
|
911
|
+
Submit
|
|
912
|
+
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
interface FieldValues {
|
|
916
|
+
show?: string\[\];
|
|
917
|
+
name?: string;
|
|
918
|
+
email?: string;
|
|
919
|
+
}
|
|
920
|
+
/\*\*
|
|
921
|
+
\* Conditional fields need to be rendered in a sub-component, to allow it to use the \`useWatch\` \* hook to watch the value of the field dictating the display of conditional fields. \*/
|
|
922
|
+
const FormSectionWithConditionalFields \= () \=> {
|
|
923
|
+
const show \= IressForm.useWatch<FieldValues\>({ name: 'show' });
|
|
924
|
+
return (
|
|
925
|
+
<IressText\>
|
|
926
|
+
<h2\>
|
|
927
|
+
Conditional fields using <code\>useWatch</code\>
|
|
928
|
+
</h2\>
|
|
929
|
+
<p\>
|
|
930
|
+
This example demonstrates how to use the{' '}
|
|
931
|
+
<code\>IressForm.useWatch()</code\> hook to watch the value of a field and conditionally render other fields based on that value. </p\>
|
|
932
|
+
<IressFormField
|
|
933
|
+
name\="show"
|
|
934
|
+
label\="Select fields to show"
|
|
935
|
+
rules\={{
|
|
936
|
+
required: 'Please select at least one field to show',
|
|
937
|
+
}}
|
|
938
|
+
render\={(controlledProps) \=> (
|
|
939
|
+
<IressCheckboxGroup {...controlledProps} layout\="inline"\>
|
|
940
|
+
<IressCheckbox value\="name"\>Name</IressCheckbox\>
|
|
941
|
+
<IressCheckbox value\="email"\>Email</IressCheckbox\>
|
|
942
|
+
</IressCheckboxGroup\>
|
|
943
|
+
)}
|
|
944
|
+
/>
|
|
945
|
+
{show?.includes('name') && (
|
|
946
|
+
<IressFormField
|
|
947
|
+
name\="name"
|
|
948
|
+
label\="Name"
|
|
949
|
+
rules\={{
|
|
950
|
+
required: 'Name is required',
|
|
951
|
+
}}
|
|
952
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
953
|
+
/>
|
|
954
|
+
)}
|
|
955
|
+
{show?.includes('email') && (
|
|
956
|
+
<IressFormField
|
|
957
|
+
name\="email"
|
|
958
|
+
label\="Email"
|
|
959
|
+
rules\={{
|
|
960
|
+
required: 'Email is required',
|
|
961
|
+
pattern: {
|
|
962
|
+
value:
|
|
963
|
+
/^\[a-zA-Z0-9.!#$%&'\*+/=?^\_\`{|}~-\]+@\[a-zA-Z0-9\](?:\[a-zA-Z0-9-\]{0,61}\[a-zA-Z0-9\])?(?:\\.\[a-zA-Z0-9\](?:\[a-zA-Z0-9-\]{0,61}\[a-zA-Z0-9\])?)\*$/,
|
|
964
|
+
message: 'Please enter a valid email address',
|
|
965
|
+
},
|
|
966
|
+
}}
|
|
967
|
+
render\={(controlledProps) \=> (
|
|
968
|
+
<IressInput {...controlledProps} type\="email" />
|
|
969
|
+
)}
|
|
970
|
+
/\>
|
|
971
|
+
)}
|
|
972
|
+
</IressText\>
|
|
973
|
+
);
|
|
974
|
+
};
|
|
975
|
+
export const UseWatchForm \= (args: IressFormProps<FieldValues\>) \=> (
|
|
976
|
+
<IressForm {...args}\>
|
|
977
|
+
<FormSectionWithConditionalFields />
|
|
978
|
+
</IressForm\>
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### Props
|
|
984
|
+
|
|
985
|
+
| Name | Description | Default | Control |
|
|
986
|
+
| --- | --- | --- | --- |
|
|
987
|
+
| actions |
|
|
988
|
+
object
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
| \- | Choose option...nonesubmitsave |
|
|
993
|
+
| alert |
|
|
994
|
+
|
|
995
|
+
\-
|
|
996
|
+
|
|
997
|
+
| \- | Choose option...nonebasicAlert |
|
|
998
|
+
| children |
|
|
999
|
+
|
|
1000
|
+
array
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
1005
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
1006
|
+
|
|
1007
|
+
Hidden inputs
|
|
1008
|
+
-------------
|
|
1009
|
+
|
|
1010
|
+
You can use hidden inputs to store data that you do not want to display to the user, but still need to include in the form submission. This is useful for storing metadata or other information that is not editable by the user.
|
|
1011
|
+
|
|
1012
|
+
Warning
|
|
1013
|
+
-------
|
|
1014
|
+
|
|
1015
|
+
This is not a recommended practice, as it can lead to security issues if sensitive data is stored in hidden inputs. It is better to use a variable to store your data, and include it in the form submission using the `onSubmit` handler.
|
|
1016
|
+
|
|
1017
|
+
[](./iframe.html?id=patterns-form-recipes--hidden-inputs)
|
|
1018
|
+
|
|
1019
|
+
Visible Input
|
|
1020
|
+
|
|
1021
|
+
Submit
|
|
1022
|
+
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
export const HiddenInputsForm \= () \=> {
|
|
1026
|
+
const form \= IressForm.useForm();
|
|
1027
|
+
const { register } \= form;
|
|
1028
|
+
// This is a hidden input field that the user cannot see or interact with.
|
|
1029
|
+
// This is the recommended way to handle hidden inputs in Iress forms.
|
|
1030
|
+
const hiddenInputStoredInVariable \= 'hiddenValue';
|
|
1031
|
+
return (
|
|
1032
|
+
<IressHookForm
|
|
1033
|
+
form\={form}
|
|
1034
|
+
onSubmit\={(data) \=> {
|
|
1035
|
+
console.log('Form submitted with data:', {
|
|
1036
|
+
...data,
|
|
1037
|
+
hiddenInputStoredInVariable,
|
|
1038
|
+
});
|
|
1039
|
+
}}
|
|
1040
|
+
\>
|
|
1041
|
+
<IressStack gap\="md"\>
|
|
1042
|
+
<IressFormField
|
|
1043
|
+
label\="Visible Input"
|
|
1044
|
+
name\="visibleInput"
|
|
1045
|
+
render\={(controlledProps) \=> <IressInput {...controlledProps} />}
|
|
1046
|
+
/>
|
|
1047
|
+
{/\* Hidden field - NOT RECOMMENDED \*/}
|
|
1048
|
+
<input
|
|
1049
|
+
type\="hidden"
|
|
1050
|
+
{...register('hiddenField')} // Manually register the hidden field with react-hook-form
|
|
1051
|
+
value\="hiddenValue"
|
|
1052
|
+
/>
|
|
1053
|
+
<IressButton type\="submit"\>Submit</IressButton\>
|
|
1054
|
+
</IressStack\>
|
|
1055
|
+
</IressHookForm\>
|
|
1056
|
+
);
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
#### Props
|
|
1062
|
+
|
|
1063
|
+
| Name | Description | Default | Control |
|
|
1064
|
+
| --- | --- | --- | --- |
|
|
1065
|
+
| actions |
|
|
1066
|
+
object
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
| \- | Choose option...nonesubmitsave |
|
|
1071
|
+
| alert |
|
|
1072
|
+
|
|
1073
|
+
\-
|
|
1074
|
+
|
|
1075
|
+
| \- | Choose option...nonebasicAlert |
|
|
1076
|
+
| children |
|
|
1077
|
+
|
|
1078
|
+
array
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
1083
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
1084
|
+
|
|
1085
|
+
Validation depend on other fields
|
|
1086
|
+
---------------------------------
|
|
1087
|
+
|
|
1088
|
+
This example shows how to validate one field based on another field's value.
|
|
1089
|
+
|
|
1090
|
+
The budget amount input validates against the selected budget range using the custom `validateBudgetInput` rules.
|
|
1091
|
+
|
|
1092
|
+
[](./iframe.html?id=patterns-form-recipes--validation-depend-on-other-fields)
|
|
1093
|
+
|
|
1094
|
+
Validation depend on other fields
|
|
1095
|
+
=================================
|
|
1096
|
+
|
|
1097
|
+
This form demonstrates how to validate a field based on the value of another field. The budget amount field is validated against the selected budget range.
|
|
1098
|
+
|
|
1099
|
+
\*RequiredMonthly investment budget
|
|
1100
|
+
|
|
1101
|
+
Select your budget rangeLess than $499Between $500 to $999More than $1000
|
|
1102
|
+
|
|
1103
|
+
\*RequiredEnter your budget amount ($)
|
|
1104
|
+
|
|
1105
|
+
AUD
|
|
1106
|
+
|
|
1107
|
+
Submit
|
|
1108
|
+
|
|
1109
|
+
* * *
|
|
1110
|
+
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
interface FormData {
|
|
1114
|
+
primaryField: string;
|
|
1115
|
+
dependentField: string;
|
|
1116
|
+
}
|
|
1117
|
+
const budgetOptions \= \[
|
|
1118
|
+
{ value: 'less-than-499', label: 'Less than $499' },
|
|
1119
|
+
{ value: 'between-500-999', label: 'Between $500 to $999' },
|
|
1120
|
+
{ value: 'more-than-1000', label: 'More than $1000' },
|
|
1121
|
+
\];
|
|
1122
|
+
const validateBudgetInput \= (
|
|
1123
|
+
value: string,
|
|
1124
|
+
selectedBudget: string,
|
|
1125
|
+
): string | true \=> {
|
|
1126
|
+
if (!selectedBudget) return 'Select budget range first';
|
|
1127
|
+
const numericValue \= parseFloat(value);
|
|
1128
|
+
if (isNaN(numericValue)) return 'Enter a valid number';
|
|
1129
|
+
switch (selectedBudget) {
|
|
1130
|
+
case 'less-than-499':
|
|
1131
|
+
return numericValue < 499 || 'Must be less than $499';
|
|
1132
|
+
case 'between-500-999':
|
|
1133
|
+
return (
|
|
1134
|
+
(numericValue \>= 500 && numericValue <= 999) ||
|
|
1135
|
+
'Must be between $500-$999'
|
|
1136
|
+
);
|
|
1137
|
+
case 'more-than-1000':
|
|
1138
|
+
return numericValue \> 1000 || 'Must be more than $1000';
|
|
1139
|
+
default:
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
export const ValidationDependOnOtherFields \= () \=> {
|
|
1144
|
+
const \[submitted, setSubmitted\] \= useState<FormData | undefined\>(undefined);
|
|
1145
|
+
const form \= IressForm.useForm<FormData\>({
|
|
1146
|
+
defaultValues: {
|
|
1147
|
+
primaryField: '',
|
|
1148
|
+
dependentField: '',
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
const onSubmit \= (data: FormData) \=> {
|
|
1152
|
+
console.log(data);
|
|
1153
|
+
setSubmitted(data);
|
|
1154
|
+
};
|
|
1155
|
+
const onError \= (errors: Record<string, unknown\>) \=> {
|
|
1156
|
+
console.log('Form validation errors:', errors);
|
|
1157
|
+
};
|
|
1158
|
+
return (
|
|
1159
|
+
<\>
|
|
1160
|
+
<IressText element\="h1"\>Validation depend on other fields</IressText\>
|
|
1161
|
+
<IressText element\="p"\>
|
|
1162
|
+
This form demonstrates how to validate a field based on the value of another field. The budget amount field is validated against the selected budget range. </IressText\>
|
|
1163
|
+
<IressHookForm form\={form} onSubmit\={onSubmit} onError\={onError}\>
|
|
1164
|
+
<IressStack gap\="md"\>
|
|
1165
|
+
<IressRow\>
|
|
1166
|
+
<IressCol\>
|
|
1167
|
+
<IressFormField
|
|
1168
|
+
name\="primaryField"
|
|
1169
|
+
label\="Monthly investment budget"
|
|
1170
|
+
rules\={{
|
|
1171
|
+
required: 'Budget range is required',
|
|
1172
|
+
}}
|
|
1173
|
+
render\={(field) \=> (
|
|
1174
|
+
<IressSelect
|
|
1175
|
+
{...field}
|
|
1176
|
+
placeholder\="Select your budget range"
|
|
1177
|
+
\>
|
|
1178
|
+
{budgetOptions.map((option) \=> (
|
|
1179
|
+
<option key\={option.value} value\={option.value}\>
|
|
1180
|
+
{option.label}
|
|
1181
|
+
</option\>
|
|
1182
|
+
))}
|
|
1183
|
+
</IressSelect\>
|
|
1184
|
+
)}
|
|
1185
|
+
/>
|
|
1186
|
+
</IressCol\>
|
|
1187
|
+
</IressRow\>
|
|
1188
|
+
<IressRow\>
|
|
1189
|
+
<IressCol\>
|
|
1190
|
+
<IressFormField
|
|
1191
|
+
name\="dependentField"
|
|
1192
|
+
label\="Enter your budget amount ($)"
|
|
1193
|
+
rules\={{
|
|
1194
|
+
required: 'Budget amount is required',
|
|
1195
|
+
validate: (value: string, formValues: FormData) \=>
|
|
1196
|
+
validateBudgetInput(value, formValues.primaryField),
|
|
1197
|
+
}}
|
|
1198
|
+
render\={(field) \=> (
|
|
1199
|
+
<IressInputCurrency {...field} type\="number" />
|
|
1200
|
+
)}
|
|
1201
|
+
/>
|
|
1202
|
+
</IressCol\>
|
|
1203
|
+
</IressRow\>
|
|
1204
|
+
<IressButton type\="submit"\>Submit</IressButton\>
|
|
1205
|
+
</IressStack\>
|
|
1206
|
+
</IressHookForm\>
|
|
1207
|
+
<IressDivider />
|
|
1208
|
+
{submitted && (
|
|
1209
|
+
<IressStack gap\="md"\>
|
|
1210
|
+
<IressText element\="h3"\>Submitted Values</IressText\>
|
|
1211
|
+
<IressText\>
|
|
1212
|
+
Budget Range:{' '}
|
|
1213
|
+
{budgetOptions.find(
|
|
1214
|
+
(option) \=> option.value \=== submitted.primaryField,
|
|
1215
|
+
)?.label ?? submitted.primaryField}
|
|
1216
|
+
</IressText\>
|
|
1217
|
+
<IressText\>Budget Amount: ${submitted.dependentField}</IressText\>
|
|
1218
|
+
</IressStack\>
|
|
1219
|
+
)}
|
|
1220
|
+
</\>
|
|
1221
|
+
);
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
#### Props
|
|
1227
|
+
|
|
1228
|
+
| Name | Description | Default | Control |
|
|
1229
|
+
| --- | --- | --- | --- |
|
|
1230
|
+
| actions |
|
|
1231
|
+
object
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
| \- | Choose option...nonesubmitsave |
|
|
1236
|
+
| alert |
|
|
1237
|
+
|
|
1238
|
+
\-
|
|
1239
|
+
|
|
1240
|
+
| \- | Choose option...nonebasicAlert |
|
|
1241
|
+
| children |
|
|
1242
|
+
|
|
1243
|
+
array
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
1248
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|
|
1249
|
+
|
|
1250
|
+
Custom form field components
|
|
1251
|
+
----------------------------
|
|
1252
|
+
|
|
1253
|
+
You can integrate custom components within `IressFormField` to create enhanced form experiences.
|
|
1254
|
+
|
|
1255
|
+
This demo showcases how to embed a custom `TranscriptTextBox` component into `IressFormField` while leveraging its built-in validation rules, error handling, and state management without additional implementation.
|
|
1256
|
+
|
|
1257
|
+
**Reminder:** When building custom form components, avoid managing error message state internally. This helps maintain the IressForm as the single source of truth and ensures consistent, predictable UI behavior.
|
|
1258
|
+
|
|
1259
|
+
Key features demonstrated:
|
|
1260
|
+
|
|
1261
|
+
* **Universal Integration Pattern**: Shows how any custom component can be embedded in IressFormField
|
|
1262
|
+
* **Built-in Validation**: Leverages IressFormField's validation rules with custom validation logic
|
|
1263
|
+
* **Multiple Error Messages**: Displays simultaneous validation errors (e.g., wrong file type AND too large)
|
|
1264
|
+
* **Drag & Drop**: Files can be dragged and dropped directly onto the textarea
|
|
1265
|
+
* **File Upload Button**: Traditional file selection via button click
|
|
1266
|
+
* **Visual Feedback**: UI changes during drag operations with border and background updates
|
|
1267
|
+
* **Form State Management**: Automatically integrates with form context using controlled props
|
|
1268
|
+
* **File Management**: Display uploaded files with remove functionality using `IressPanel`
|
|
1269
|
+
|
|
1270
|
+
[](./iframe.html?id=patterns-form-recipes--custom-form-field-components)
|
|
1271
|
+
|
|
1272
|
+
Custom FormField Components
|
|
1273
|
+
===========================
|
|
1274
|
+
|
|
1275
|
+
This demo showcases how to embed any custom component (TranscriptTextBox) into IressFormField while leveraging its form validation, error handling, and state management without additional implementation. When building custom form components, avoid managing error message state internally. This helps maintain the IressForm as the single source of truth and ensures consistent, predictable UI behavior.
|
|
1276
|
+
|
|
1277
|
+
\*RequiredTranscript
|
|
1278
|
+
|
|
1279
|
+
Upload or copy and paste transcript here
|
|
1280
|
+
|
|
1281
|
+
Upload
|
|
1282
|
+
|
|
1283
|
+
Submit
|
|
1284
|
+
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
import React, { useState } from 'react';
|
|
1288
|
+
|
|
1289
|
+
interface TranscriptFormValues {
|
|
1290
|
+
transcript: TranscriptData | string;
|
|
1291
|
+
}
|
|
1292
|
+
interface TranscriptData {
|
|
1293
|
+
content: string;
|
|
1294
|
+
size?: number;
|
|
1295
|
+
type: 'file' | 'text';
|
|
1296
|
+
extension?: string;
|
|
1297
|
+
fileName?: string;
|
|
1298
|
+
rejectedReasons?: REJECTION\_REASONS\[\];
|
|
1299
|
+
}
|
|
1300
|
+
interface TranscriptTextBoxProps {
|
|
1301
|
+
value: TranscriptData | string;
|
|
1302
|
+
onChange: (data: TranscriptData) \=> void;
|
|
1303
|
+
placeholder?: string;
|
|
1304
|
+
rows?: number;
|
|
1305
|
+
style?: React.CSSProperties;
|
|
1306
|
+
allowedExtensions?: string\[\];
|
|
1307
|
+
maxSizeInMB?: number;
|
|
1308
|
+
}
|
|
1309
|
+
interface SubmittedValuesDisplayProps {
|
|
1310
|
+
submittedValues: TranscriptFormValues | null;
|
|
1311
|
+
title?: string;
|
|
1312
|
+
}
|
|
1313
|
+
enum REJECTION\_REASONS {
|
|
1314
|
+
TYPE \= 'type',
|
|
1315
|
+
SIZE \= 'size',
|
|
1316
|
+
}
|
|
1317
|
+
const validateFile \=
|
|
1318
|
+
(allowedExtensions: string\[\], maxSizeInMB: number) \=>
|
|
1319
|
+
(data: TranscriptData | string) \=> {
|
|
1320
|
+
if (
|
|
1321
|
+
!!data &&
|
|
1322
|
+
typeof data \=== 'object' &&
|
|
1323
|
+
data.type \=== 'file' &&
|
|
1324
|
+
Array.isArray(data.rejectedReasons) &&
|
|
1325
|
+
data.rejectedReasons.length \> 0
|
|
1326
|
+
) {
|
|
1327
|
+
const errors: string\[\] \= \[\];
|
|
1328
|
+
if (data.rejectedReasons.includes(REJECTION\_REASONS.TYPE)) {
|
|
1329
|
+
errors.push(\`Only .${allowedExtensions.join(', ')} accepted\`);
|
|
1330
|
+
}
|
|
1331
|
+
if (data.rejectedReasons.includes(REJECTION\_REASONS.SIZE)) {
|
|
1332
|
+
errors.push(\`File size must be less than ${maxSizeInMB}MB\`);
|
|
1333
|
+
}
|
|
1334
|
+
if (errors.length \> 0) {
|
|
1335
|
+
return errors.join('. ');
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return true;
|
|
1339
|
+
};
|
|
1340
|
+
const TranscriptTextBox \= ({
|
|
1341
|
+
value,
|
|
1342
|
+
onChange,
|
|
1343
|
+
placeholder \= 'Copy and paste transcripts OR drag and drop / upload recordings, transcripts or documents here (.txt format).',
|
|
1344
|
+
rows \= 10,
|
|
1345
|
+
style,
|
|
1346
|
+
allowedExtensions \= \['txt'\],
|
|
1347
|
+
maxSizeInMB \= 10,
|
|
1348
|
+
}: TranscriptTextBoxProps) \=> {
|
|
1349
|
+
// Extract content and file info from value
|
|
1350
|
+
const currentData \=
|
|
1351
|
+
typeof value \=== 'string'
|
|
1352
|
+
? { content: value, type: 'text' as const }
|
|
1353
|
+
: value;
|
|
1354
|
+
const currentFile \=
|
|
1355
|
+
currentData?.type \=== 'file' &&
|
|
1356
|
+
(!currentData.rejectedReasons || currentData.rejectedReasons.length \=== 0)
|
|
1357
|
+
? {
|
|
1358
|
+
name: currentData.fileName ?? 'Unknown file',
|
|
1359
|
+
size: currentData.size,
|
|
1360
|
+
}
|
|
1361
|
+
: null;
|
|
1362
|
+
const createTranscriptData \= (
|
|
1363
|
+
content: string,
|
|
1364
|
+
type: 'file' | 'text',
|
|
1365
|
+
additionalData?: Partial<TranscriptData\>,
|
|
1366
|
+
): TranscriptData \=> ({
|
|
1367
|
+
content,
|
|
1368
|
+
type,
|
|
1369
|
+
...additionalData,
|
|
1370
|
+
});
|
|
1371
|
+
const handleFileRead \= (file: File) \=> {
|
|
1372
|
+
const reader \= new FileReader();
|
|
1373
|
+
reader.onload \= (e) \=> {
|
|
1374
|
+
const content \= e.target?.result as string;
|
|
1375
|
+
onChange(
|
|
1376
|
+
createTranscriptData(content, 'file', {
|
|
1377
|
+
size: file.size,
|
|
1378
|
+
extension: file.name.split('.').pop()?.toLowerCase(),
|
|
1379
|
+
fileName: file.name,
|
|
1380
|
+
}),
|
|
1381
|
+
);
|
|
1382
|
+
};
|
|
1383
|
+
reader.onerror \= () \=> {
|
|
1384
|
+
// Let parent handle errors through validation
|
|
1385
|
+
onChange(
|
|
1386
|
+
createTranscriptData('', 'file', {
|
|
1387
|
+
fileName: file.name,
|
|
1388
|
+
}),
|
|
1389
|
+
);
|
|
1390
|
+
};
|
|
1391
|
+
reader.readAsText(file);
|
|
1392
|
+
};
|
|
1393
|
+
const handleTextChange: IressInputProps<string, number\>\['onChange'\] \= (
|
|
1394
|
+
\_e,
|
|
1395
|
+
textContent \= '',
|
|
1396
|
+
) \=> {
|
|
1397
|
+
onChange(createTranscriptData(textContent, 'text'));
|
|
1398
|
+
};
|
|
1399
|
+
const onFileSelected \= (files: File\[\]) \=> {
|
|
1400
|
+
if (files.length \=== 0) return;
|
|
1401
|
+
const file \= files\[0\];
|
|
1402
|
+
handleFileRead(file);
|
|
1403
|
+
};
|
|
1404
|
+
const { getRootProps, getInputProps, open, isDragActive } \= useDropzone({
|
|
1405
|
+
multiple: false,
|
|
1406
|
+
noClick: true,
|
|
1407
|
+
maxSize: maxSizeInMB \* 1024 \* 1024,
|
|
1408
|
+
accept: allowedExtensions.reduce(
|
|
1409
|
+
(acc, ext) \=> {
|
|
1410
|
+
const mimeType \=
|
|
1411
|
+
ext \=== 'txt' ? 'text/plain' : 'application/octet-stream';
|
|
1412
|
+
acc\[mimeType\] \= acc\[mimeType\] || \[\];
|
|
1413
|
+
acc\[mimeType\].push(\`.${ext}\`);
|
|
1414
|
+
return acc;
|
|
1415
|
+
},
|
|
1416
|
+
{} as Record<string, string\[\]\>,
|
|
1417
|
+
),
|
|
1418
|
+
onDrop: (acceptedFiles, rejectedFiles) \=> {
|
|
1419
|
+
if (acceptedFiles.length \> 0) {
|
|
1420
|
+
onFileSelected(acceptedFiles);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (rejectedFiles.length \> 0) {
|
|
1424
|
+
const rejectedFile \= rejectedFiles\[0\];
|
|
1425
|
+
const { file, errors } \= rejectedFile;
|
|
1426
|
+
// Map error codes to rejection reasons
|
|
1427
|
+
const errorCodeMap \= {
|
|
1428
|
+
'file-invalid-type': REJECTION\_REASONS.TYPE,
|
|
1429
|
+
'file-too-large': REJECTION\_REASONS.SIZE,
|
|
1430
|
+
} as const;
|
|
1431
|
+
const rejectedReasons \= errors .map((error) \=> errorCodeMap\[error.code as keyof typeof errorCodeMap\])
|
|
1432
|
+
.filter((reason): reason is REJECTION\_REASONS \=> Boolean(reason));
|
|
1433
|
+
onChange(
|
|
1434
|
+
createTranscriptData('', 'file', {
|
|
1435
|
+
fileName: file.name,
|
|
1436
|
+
rejectedReasons,
|
|
1437
|
+
}),
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
const handleUploadClick \= () \=> {
|
|
1443
|
+
open();
|
|
1444
|
+
};
|
|
1445
|
+
const removeFile \= () \=> {
|
|
1446
|
+
onChange(createTranscriptData('', 'text'));
|
|
1447
|
+
};
|
|
1448
|
+
return (
|
|
1449
|
+
<IressStack gap\="sm"\>
|
|
1450
|
+
<div {...getRootProps()} style\={{ position: 'relative' }}\>
|
|
1451
|
+
<input {...getInputProps()} />
|
|
1452
|
+
<IressInput
|
|
1453
|
+
value\={currentData?.content || ''}
|
|
1454
|
+
onChange\={handleTextChange}
|
|
1455
|
+
rows\={rows}
|
|
1456
|
+
placeholder\={isDragActive ? 'Drop your file here...' : placeholder}
|
|
1457
|
+
style\={{
|
|
1458
|
+
boxSizing: 'border-box',
|
|
1459
|
+
border: isDragActive ? '1px dashed #007acc' : undefined,
|
|
1460
|
+
backgroundColor: isDragActive ? '#f0f8ff' : undefined,
|
|
1461
|
+
...style,
|
|
1462
|
+
}}
|
|
1463
|
+
/>
|
|
1464
|
+
</div\>
|
|
1465
|
+
{currentFile && (
|
|
1466
|
+
<IressPanel\>
|
|
1467
|
+
<IressInline horizontalAlign\="between" verticalAlign\="middle"\>
|
|
1468
|
+
<IressText\>📄 {currentFile.name}</IressText\>
|
|
1469
|
+
<IressButton mode\="secondary" onClick\={removeFile}\>
|
|
1470
|
+
Remove </IressButton\>
|
|
1471
|
+
</IressInline\>
|
|
1472
|
+
</IressPanel\>
|
|
1473
|
+
)}
|
|
1474
|
+
<IressButton
|
|
1475
|
+
mode\="secondary"
|
|
1476
|
+
onClick\={handleUploadClick}
|
|
1477
|
+
prepend\={<IressIcon name\="upload" />}
|
|
1478
|
+
\>
|
|
1479
|
+
Upload </IressButton\>
|
|
1480
|
+
</IressStack\>
|
|
1481
|
+
);
|
|
1482
|
+
};
|
|
1483
|
+
const SubmittedValuesDisplay: React.FC<SubmittedValuesDisplayProps\> \= ({
|
|
1484
|
+
submittedValues,
|
|
1485
|
+
title \= 'Submitted Values:',
|
|
1486
|
+
}) \=> {
|
|
1487
|
+
if (!submittedValues) {
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
return (
|
|
1491
|
+
<IressPanel\>
|
|
1492
|
+
<IressStack gap\="sm"\>
|
|
1493
|
+
<IressText textStyle\="typography.body.md.strong"\>{title}</IressText\>
|
|
1494
|
+
<IressText\>
|
|
1495
|
+
<strong\>Type:</strong\>
|
|
1496
|
+
{typeof submittedValues.transcript \=== 'string'
|
|
1497
|
+
? 'text'
|
|
1498
|
+
: submittedValues.transcript.type}
|
|
1499
|
+
</IressText\>
|
|
1500
|
+
<IressText\>
|
|
1501
|
+
<strong\>Content:</strong\>
|
|
1502
|
+
{typeof submittedValues.transcript \=== 'string'
|
|
1503
|
+
? submittedValues.transcript
|
|
1504
|
+
: submittedValues.transcript.content}
|
|
1505
|
+
</IressText\>
|
|
1506
|
+
{typeof submittedValues.transcript \=== 'object' &&
|
|
1507
|
+
submittedValues.transcript.fileName && (
|
|
1508
|
+
<IressText\>
|
|
1509
|
+
<strong\>File Name:</strong\> {submittedValues.transcript.fileName}
|
|
1510
|
+
</IressText\>
|
|
1511
|
+
)}
|
|
1512
|
+
{typeof submittedValues.transcript \=== 'object' &&
|
|
1513
|
+
submittedValues.transcript.size && (
|
|
1514
|
+
<IressText\>
|
|
1515
|
+
<strong\>File Size:</strong\>
|
|
1516
|
+
{(submittedValues.transcript.size / 1024).toFixed(2)} KB </IressText\>
|
|
1517
|
+
)}
|
|
1518
|
+
</IressStack\>
|
|
1519
|
+
</IressPanel\>
|
|
1520
|
+
);
|
|
1521
|
+
};
|
|
1522
|
+
const Heading \= () \=> {
|
|
1523
|
+
return (
|
|
1524
|
+
<\>
|
|
1525
|
+
<IressText element\="h1"\>Custom FormField Components</IressText\>
|
|
1526
|
+
<IressText element\="p"\>
|
|
1527
|
+
This demo showcases how to embed any custom component (TranscriptTextBox) into IressFormField while leveraging its form validation, error handling, and state management without additional implementation. When building custom form components, avoid managing error message state internally. This helps maintain the IressForm as the single source of truth and ensures consistent, predictable UI behavior. </IressText\>
|
|
1528
|
+
</\>
|
|
1529
|
+
);
|
|
1530
|
+
};
|
|
1531
|
+
export const CustomFormFieldComponents \= () \=> {
|
|
1532
|
+
const \[submittedValues, setSubmittedValues\] \=
|
|
1533
|
+
useState<TranscriptFormValues | null\>(null);
|
|
1534
|
+
const allowedExtensions \= \['txt'\];
|
|
1535
|
+
const maxSizeInMB \= 0.1;
|
|
1536
|
+
const handleSubmit \= (data: TranscriptFormValues) \=> {
|
|
1537
|
+
setSubmittedValues(data);
|
|
1538
|
+
console.log('Form submitted:', data);
|
|
1539
|
+
};
|
|
1540
|
+
return (
|
|
1541
|
+
<\>
|
|
1542
|
+
<Heading />
|
|
1543
|
+
<IressForm<TranscriptFormValues> mode="onChange" onSubmit={handleSubmit}
|
|
1544
|
+
defaultValues={{ transcript: { content: '', type: 'text' } }}
|
|
1545
|
+
> <IressStack gap\="md"\>
|
|
1546
|
+
<IressFormField
|
|
1547
|
+
label\="Transcript"
|
|
1548
|
+
name\="transcript"
|
|
1549
|
+
hint\="Upload or copy and paste transcript here"
|
|
1550
|
+
render\={(controlledProps) \=> (
|
|
1551
|
+
<TranscriptTextBox
|
|
1552
|
+
{...controlledProps}
|
|
1553
|
+
allowedExtensions\={allowedExtensions}
|
|
1554
|
+
maxSizeInMB\={maxSizeInMB}
|
|
1555
|
+
/>
|
|
1556
|
+
)}
|
|
1557
|
+
rules\={{
|
|
1558
|
+
required: 'Transcript is required',
|
|
1559
|
+
validate: {
|
|
1560
|
+
file: validateFile(allowedExtensions, maxSizeInMB),
|
|
1561
|
+
},
|
|
1562
|
+
}}
|
|
1563
|
+
/>
|
|
1564
|
+
<IressButton type\="submit" mode\="primary"\>
|
|
1565
|
+
Submit </IressButton\>
|
|
1566
|
+
<SubmittedValuesDisplay submittedValues\={submittedValues} />
|
|
1567
|
+
</IressStack\>
|
|
1568
|
+
</IressForm\>
|
|
1569
|
+
</\>
|
|
1570
|
+
);
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
#### Props
|
|
1576
|
+
|
|
1577
|
+
| Name | Description | Default | Control |
|
|
1578
|
+
| --- | --- | --- | --- |
|
|
1579
|
+
| actions |
|
|
1580
|
+
object
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
| \- | Choose option...nonesubmitsave |
|
|
1585
|
+
| alert |
|
|
1586
|
+
|
|
1587
|
+
\-
|
|
1588
|
+
|
|
1589
|
+
| \- | Choose option...nonebasicAlert |
|
|
1590
|
+
| children |
|
|
1591
|
+
|
|
1592
|
+
array
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
| \- | Choose option...nonesimplesupportedControlsreadOnly |
|
|
1597
|
+
| Show React Hook Forms itemsReact Hook Forms | Show React Hook Forms items |
|