@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.
Files changed (99) hide show
  1. package/generated/docs/components_components-alert-docs.md +1054 -0
  2. package/generated/docs/components_components-autocomplete-docs.md +3304 -0
  3. package/generated/docs/components_components-autocomplete-recipes-docs.md +98 -0
  4. package/generated/docs/components_components-badge-docs.md +312 -0
  5. package/generated/docs/components_components-button-docs.md +2339 -0
  6. package/generated/docs/components_components-buttongroup-docs.md +980 -0
  7. package/generated/docs/components_components-card-docs.md +1970 -0
  8. package/generated/docs/components_components-checkbox-docs.md +1083 -0
  9. package/generated/docs/components_components-checkboxgroup-docs.md +1597 -0
  10. package/generated/docs/components_components-checkboxgroup-recipes-docs.md +209 -0
  11. package/generated/docs/components_components-col-docs.md +755 -0
  12. package/generated/docs/components_components-container-docs.md +172 -0
  13. package/generated/docs/components_components-divider-docs.md +235 -0
  14. package/generated/docs/components_components-expander-docs.md +428 -0
  15. package/generated/docs/components_components-field-docs.md +3345 -0
  16. package/generated/docs/components_components-filter-docs.md +4091 -0
  17. package/generated/docs/components_components-hide-docs.md +450 -0
  18. package/generated/docs/components_components-icon-docs.md +1017 -0
  19. package/generated/docs/components_components-image-docs.md +168 -0
  20. package/generated/docs/components_components-inline-docs.md +1962 -0
  21. package/generated/docs/components_components-input-docs.md +1388 -0
  22. package/generated/docs/components_components-input-recipes-docs.md +349 -0
  23. package/generated/docs/components_components-inputcurrency-docs.md +636 -0
  24. package/generated/docs/components_components-inputcurrency-recipes-docs.md +208 -0
  25. package/generated/docs/components_components-introduction-docs.md +448 -0
  26. package/generated/docs/components_components-label-docs.md +229 -0
  27. package/generated/docs/components_components-link-docs.md +454 -0
  28. package/generated/docs/components_components-menu-docs.md +2219 -0
  29. package/generated/docs/components_components-menu-menuitem-docs.md +1455 -0
  30. package/generated/docs/components_components-modal-docs.md +2002 -0
  31. package/generated/docs/components_components-panel-docs.md +342 -0
  32. package/generated/docs/components_components-placeholder-docs.md +98 -0
  33. package/generated/docs/components_components-popover-docs.md +1631 -0
  34. package/generated/docs/components_components-popover-recipes-docs.md +537 -0
  35. package/generated/docs/components_components-progress-docs.md +128 -0
  36. package/generated/docs/components_components-provider-docs.md +123 -0
  37. package/generated/docs/components_components-radio-docs.md +499 -0
  38. package/generated/docs/components_components-radiogroup-docs.md +1573 -0
  39. package/generated/docs/components_components-readonly-docs.md +277 -0
  40. package/generated/docs/components_components-richselect-docs.md +6101 -0
  41. package/generated/docs/components_components-row-docs.md +2172 -0
  42. package/generated/docs/components_components-select-docs.md +1110 -0
  43. package/generated/docs/components_components-skeleton-docs.md +467 -0
  44. package/generated/docs/components_components-skeleton-recipes-docs.md +110 -0
  45. package/generated/docs/components_components-skiplink-docs.md +220 -0
  46. package/generated/docs/components_components-slideout-docs.md +1910 -0
  47. package/generated/docs/components_components-slider-docs.md +1284 -0
  48. package/generated/docs/components_components-spinner-docs.md +90 -0
  49. package/generated/docs/components_components-stack-docs.md +730 -0
  50. package/generated/docs/components_components-table-docs.md +4038 -0
  51. package/generated/docs/components_components-tabset-docs.md +955 -0
  52. package/generated/docs/components_components-tabset-tab-docs.md +342 -0
  53. package/generated/docs/components_components-tag-docs.md +410 -0
  54. package/generated/docs/components_components-text-docs.md +593 -0
  55. package/generated/docs/components_components-toaster-docs.md +451 -0
  56. package/generated/docs/components_components-toggle-docs.md +513 -0
  57. package/generated/docs/components_components-tooltip-docs.md +564 -0
  58. package/generated/docs/components_components-validationmessage-docs.md +608 -0
  59. package/generated/docs/components_contact-us-docs.md +9 -0
  60. package/generated/docs/components_foundations-accessibility-docs.md +33 -0
  61. package/generated/docs/components_foundations-consistency-docs.md +44 -0
  62. package/generated/docs/components_foundations-content-docs.md +18 -0
  63. package/generated/docs/components_foundations-introduction-docs.md +17 -0
  64. package/generated/docs/components_foundations-principles-docs.md +60 -0
  65. package/generated/docs/components_foundations-responsive-layout-docs.md +2692 -0
  66. package/generated/docs/components_foundations-user-experience-docs.md +53 -0
  67. package/generated/docs/components_foundations-visual-design-docs.md +39 -0
  68. package/generated/docs/components_foundations-z-index-stacking-docs.md +288 -0
  69. package/generated/docs/components_frequently-asked-questions-docs.md +44 -0
  70. package/generated/docs/components_get-started-develop-docs.md +47 -0
  71. package/generated/docs/components_get-started-using-storybook-docs.md +55 -0
  72. package/generated/docs/components_introduction-docs.md +85 -0
  73. package/generated/docs/components_patterns-form-docs.md +2469 -0
  74. package/generated/docs/components_patterns-form-recipes-docs.md +1597 -0
  75. package/generated/docs/components_patterns-introduction-docs.md +31 -0
  76. package/generated/docs/components_patterns-loading-docs.md +1908 -0
  77. package/generated/docs/components_patterns-shadow-docs.md +195 -0
  78. package/generated/docs/components_resources-code-katas-docs.md +25 -0
  79. package/generated/docs/components_resources-introduction-docs.md +28 -0
  80. package/generated/docs/components_resources-mcp-server-docs.md +23 -0
  81. package/generated/docs/components_resources-migration-guides-from-v4-to-v5-docs.md +142 -0
  82. package/generated/docs/components_styling-props-colour-docs.md +91 -0
  83. package/generated/docs/components_styling-props-elevation-docs.md +69 -0
  84. package/generated/docs/components_styling-props-radius-docs.md +63 -0
  85. package/generated/docs/components_styling-props-reference-docs.md +160 -0
  86. package/generated/docs/components_styling-props-screen-readers-docs.md +66 -0
  87. package/generated/docs/components_styling-props-sizing-docs.md +217 -0
  88. package/generated/docs/components_styling-props-spacing-docs.md +545 -0
  89. package/generated/docs/components_styling-props-typography-docs.md +66 -0
  90. package/generated/docs/components_versions-docs.md +14 -0
  91. package/generated/docs/guidelines.md +3083 -0
  92. package/generated/docs/introduction-docs.md +37 -0
  93. package/generated/docs/tokens_colour-docs.md +479 -0
  94. package/generated/docs/tokens_elevation-docs.md +39 -0
  95. package/generated/docs/tokens_introduction-docs.md +150 -0
  96. package/generated/docs/tokens_radius-docs.md +67 -0
  97. package/generated/docs/tokens_spacing-docs.md +87 -0
  98. package/generated/docs/tokens_typography-docs.md +305 -0
  99. 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 |