@iress-oss/ids-mcp-server 6.0.0-alpha.0 → 6.0.0-alpha.1-canary-20251204014525-3f0dce4

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