@springmicro/forms 0.6.4 → 0.7.0

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 (78) hide show
  1. package/.eslintrc.cjs +22 -22
  2. package/README.md +11 -11
  3. package/dist/index.d.ts +0 -0
  4. package/dist/index.js +0 -0
  5. package/dist/index.umd.cjs +0 -0
  6. package/package.json +3 -3
  7. package/src/builder/bottom-drawer.tsx +429 -429
  8. package/src/builder/form-builder.tsx +256 -256
  9. package/src/builder/modal.tsx +39 -39
  10. package/src/builder/nodes/node-base.tsx +94 -94
  11. package/src/builder/nodes/node-child-helpers.tsx +273 -273
  12. package/src/builder/nodes/node-parent.tsx +187 -187
  13. package/src/builder/nodes/node-types/array-node.tsx +134 -134
  14. package/src/builder/nodes/node-types/date-node.tsx +60 -60
  15. package/src/builder/nodes/node-types/file-node.tsx +67 -67
  16. package/src/builder/nodes/node-types/integer-node.tsx +60 -60
  17. package/src/builder/nodes/node-types/object-node.tsx +67 -67
  18. package/src/builder/nodes/node-types/text-node.tsx +66 -66
  19. package/src/fields/ArrayField.tsx +875 -875
  20. package/src/fields/BooleanField.tsx +110 -110
  21. package/src/fields/MultiSchemaField.tsx +236 -236
  22. package/src/fields/NullField.tsx +22 -22
  23. package/src/fields/NumberField.tsx +87 -87
  24. package/src/fields/ObjectField.tsx +338 -338
  25. package/src/fields/SchemaField.tsx +402 -402
  26. package/src/fields/StringField.tsx +67 -67
  27. package/src/fields/index.ts +24 -24
  28. package/src/index.tsx +26 -26
  29. package/src/interfaces/MessagesProps.interface.ts +5 -5
  30. package/src/interfaces/Option.interface.ts +4 -4
  31. package/src/styles/select.styles.ts +28 -28
  32. package/src/templates/ArrayFieldDescriptionTemplate.tsx +42 -42
  33. package/src/templates/ArrayFieldItemTemplate.tsx +78 -78
  34. package/src/templates/ArrayFieldTemplate.tsx +90 -90
  35. package/src/templates/ArrayFieldTitleTemplate.tsx +44 -44
  36. package/src/templates/BaseInputTemplate.tsx +94 -94
  37. package/src/templates/ButtonTemplates/AddButton.tsx +29 -29
  38. package/src/templates/ButtonTemplates/IconButton.tsx +49 -49
  39. package/src/templates/ButtonTemplates/SubmitButton.tsx +29 -29
  40. package/src/templates/ButtonTemplates/index.ts +16 -16
  41. package/src/templates/DescriptionField.tsx +29 -29
  42. package/src/templates/ErrorList.tsx +25 -25
  43. package/src/templates/FieldTemplate/FieldTemplate.tsx +39 -39
  44. package/src/templates/FieldTemplate/Label.tsx +29 -29
  45. package/src/templates/FieldTemplate/WrapIfAdditional.tsx +85 -85
  46. package/src/templates/FieldTemplate/index.ts +3 -3
  47. package/src/templates/ObjectFieldTemplate.tsx +79 -79
  48. package/src/templates/TitleField.tsx +20 -20
  49. package/src/templates/UnsupportedField.tsx +29 -29
  50. package/src/templates/index.ts +32 -32
  51. package/src/types/Message.type.ts +6 -6
  52. package/src/types/RawMessage.type.ts +15 -15
  53. package/src/types/form-builder.ts +135 -135
  54. package/src/types/utils.type.ts +1 -1
  55. package/src/utils/form-builder.ts +424 -424
  56. package/src/utils/processSelectValue.ts +50 -50
  57. package/src/widgets/AltDateTimeWidget.tsx +17 -17
  58. package/src/widgets/AltDateWidget.tsx +216 -216
  59. package/src/widgets/CheckboxWidget.tsx +80 -80
  60. package/src/widgets/CheckboxesWidget.tsx +74 -74
  61. package/src/widgets/ColorWidget.tsx +26 -26
  62. package/src/widgets/DateTimeWidget.tsx +28 -28
  63. package/src/widgets/DateWidget.tsx +36 -36
  64. package/src/widgets/EmailWidget.tsx +19 -19
  65. package/src/widgets/FileWidget.tsx +144 -144
  66. package/src/widgets/HiddenWidget.tsx +22 -22
  67. package/src/widgets/PasswordWidget.tsx +20 -20
  68. package/src/widgets/RadioWidget.tsx +87 -87
  69. package/src/widgets/RangeWidget.tsx +24 -24
  70. package/src/widgets/SelectWidget.tsx +99 -99
  71. package/src/widgets/TextWidget.tsx +19 -19
  72. package/src/widgets/TextareaWidget.tsx +64 -64
  73. package/src/widgets/URLWidget.tsx +19 -19
  74. package/src/widgets/UpDownWidget.tsx +20 -20
  75. package/src/widgets/index.ts +43 -43
  76. package/tsconfig.json +24 -24
  77. package/tsconfig.node.json +10 -10
  78. package/vite.config.ts +25 -25
@@ -1,875 +1,875 @@
1
- import React, { Component } from "react";
2
- import {
3
- getTemplate,
4
- getWidget,
5
- getUiOptions,
6
- isFixedItems,
7
- allowAdditionalItems,
8
- isCustomWidget,
9
- optionsList,
10
- ITEMS_KEY,
11
- } from "@rjsf/utils";
12
- import type {
13
- ArrayFieldTemplateProps,
14
- ErrorSchema,
15
- Field,
16
- FieldProps,
17
- IdSchema,
18
- UiSchema,
19
- GenericObjectType,
20
- ArrayFieldTemplateItemType,
21
- } from "@rjsf/utils";
22
- import get from "lodash/get";
23
- import isObject from "lodash/isObject";
24
- import set from "lodash/set";
25
- import { nanoid } from "nanoid";
26
-
27
- /** Type used to represent the keyed form data used in the state */
28
- type KeyedFormDataType<T> = { key: string; item: T };
29
-
30
- /** Type used for the state of the `ArrayField` component */
31
- type ArrayFieldState<T> = {
32
- /** The keyed form data elements */
33
- keyedFormData: KeyedFormDataType<T>[];
34
- /** Flag indicating whether any of the keyed form data has been updated */
35
- updatedKeyedFormData: boolean;
36
- };
37
-
38
- /** Used to generate a unique ID for an element in a row */
39
- function generateRowId() {
40
- return nanoid();
41
- }
42
-
43
- /** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
44
- *
45
- * @param formData - The data for the form
46
- * @returns - The `formData` converted into a `KeyedFormDataType` element
47
- */
48
- function generateKeyedFormData<T>(formData: T[]): KeyedFormDataType<T>[] {
49
- return !Array.isArray(formData)
50
- ? []
51
- : formData.map((item) => {
52
- return {
53
- key: generateRowId(),
54
- item,
55
- };
56
- });
57
- }
58
-
59
- /** Converts `KeyedFormDataType` data into the inner `formData`
60
- *
61
- * @param keyedFormData - The `KeyedFormDataType` to be converted
62
- * @returns - The inner `formData` item(s) in the `keyedFormData`
63
- */
64
- function keyedToPlainFormData<T>(
65
- keyedFormData: KeyedFormDataType<T> | KeyedFormDataType<T>[]
66
- ): T[] {
67
- if (Array.isArray(keyedFormData)) {
68
- return keyedFormData.map((keyedItem) => keyedItem.item);
69
- }
70
- return [];
71
- }
72
-
73
- /** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal
74
- * and fixed array, allowing user to add and remove elements from the array data.
75
- */
76
- class ArrayField<T = any, F extends GenericObjectType = any> extends Component<
77
- FieldProps<T[], F>,
78
- ArrayFieldState<T>
79
- > {
80
- /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData`
81
- *
82
- * @param props - The `FieldProps` for this template
83
- */
84
- constructor(props: FieldProps<T[], F>) {
85
- super(props);
86
- const { formData = [] } = props;
87
- const keyedFormData = generateKeyedFormData<T>(formData);
88
- this.state = {
89
- keyedFormData,
90
- updatedKeyedFormData: false,
91
- };
92
- }
93
-
94
- /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It
95
- * regenerates the keyed form data and returns it
96
- *
97
- * @param nextProps - The next set of props data
98
- * @param prevState - The previous set of state data
99
- */
100
- static getDerivedStateFromProps<T = any, F extends GenericObjectType = any>(
101
- nextProps: Readonly<FieldProps<T[], F>>,
102
- prevState: Readonly<ArrayFieldState<T>>
103
- ) {
104
- // Don't call getDerivedStateFromProps if keyed formdata was just updated.
105
- if (prevState.updatedKeyedFormData) {
106
- return {
107
- updatedKeyedFormData: false,
108
- };
109
- }
110
- const nextFormData = Array.isArray(nextProps.formData)
111
- ? nextProps.formData
112
- : [];
113
- const previousKeyedFormData = prevState.keyedFormData || [];
114
- const newKeyedFormData =
115
- nextFormData.length === previousKeyedFormData.length
116
- ? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
117
- return {
118
- key: previousKeyedFormDatum.key,
119
- item: nextFormData[index],
120
- };
121
- })
122
- : generateKeyedFormData<T>(nextFormData);
123
- return {
124
- keyedFormData: newKeyedFormData,
125
- };
126
- }
127
-
128
- /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to
129
- * the description from the schema.items, and finally the string "Item"
130
- */
131
- get itemTitle() {
132
- const { schema } = this.props;
133
- return get(
134
- schema,
135
- [ITEMS_KEY, "title"],
136
- get(schema, [ITEMS_KEY, "description"], "Item")
137
- );
138
- }
139
-
140
- /** Determines whether the item described in the schema is always required, which is determined by whether any item
141
- * may be null.
142
- *
143
- * @param itemSchema - The schema for the item
144
- * @return - True if the item schema type does not contain the "null" type
145
- */
146
- isItemRequired(itemSchema: F) {
147
- if (Array.isArray(itemSchema.type)) {
148
- // While we don't yet support composite/nullable jsonschema types, it's
149
- // future-proof to check for requirement against these.
150
- return !itemSchema.type.includes("null");
151
- }
152
- // All non-null array item types are inherently required by design
153
- return itemSchema.type !== "null";
154
- }
155
-
156
- /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding
157
- * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the
158
- * `formData` matches that value, then false is returned, otherwise true is returned.
159
- *
160
- * @param formItems - The list of items in the form
161
- * @returns - True if the item is addable otherwise false
162
- */
163
- canAddItem(formItems: any[]) {
164
- const { schema, uiSchema } = this.props;
165
- let { addable } = getUiOptions<T[], F>(uiSchema);
166
- if (addable !== false) {
167
- // if ui:options.addable was not explicitly set to false, we can add
168
- // another item if we have not exceeded maxItems yet
169
- if (schema.maxItems !== undefined) {
170
- addable = formItems.length < schema.maxItems;
171
- } else {
172
- addable = true;
173
- }
174
- }
175
- return addable;
176
- }
177
-
178
- /** Returns the default form information for an item based on the schema for that item. Deals with the possibility
179
- * that the schema is fixed and allows additional items.
180
- */
181
- _getNewFormDataRow = (): T => {
182
- const { schema, registry } = this.props;
183
- const { schemaUtils } = registry;
184
- let itemSchema = schema.items as F;
185
- if (isFixedItems(schema) && allowAdditionalItems(schema)) {
186
- itemSchema = schema.additionalItems as F;
187
- }
188
- // Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], F> call on the class
189
- return schemaUtils.getDefaultFormState(itemSchema as F) as unknown as T;
190
- };
191
-
192
- /** Callback handler for when the user clicks on the add button. Creates a new row of keyed form data at the end of
193
- * the list, adding it into the state, and then returning `onChange()` with the plain form data converted from the
194
- * keyed data
195
- *
196
- * @param event - The event for the click
197
- */
198
- onAddClick = (event: MouseEvent) => {
199
- if (event) {
200
- event.preventDefault();
201
- }
202
-
203
- const { onChange } = this.props;
204
- const { keyedFormData } = this.state;
205
- const newKeyedFormDataRow: KeyedFormDataType<T> = {
206
- key: generateRowId(),
207
- item: this._getNewFormDataRow(),
208
- };
209
- const newKeyedFormData = [...keyedFormData, newKeyedFormDataRow];
210
- this.setState(
211
- {
212
- keyedFormData: newKeyedFormData,
213
- updatedKeyedFormData: true,
214
- },
215
- () => onChange(keyedToPlainFormData(newKeyedFormData))
216
- );
217
- };
218
-
219
- /** Callback handler for when the user clicks on the add button on an existing array element. Creates a new row of
220
- * keyed form data inserted at the `index`, adding it into the state, and then returning `onChange()` with the plain
221
- * form data converted from the keyed data
222
- *
223
- * @param index - The index at which the add button is clicked
224
- */
225
- onAddIndexClick = (index: number) => {
226
- return (event: MouseEvent) => {
227
- if (event) {
228
- event.preventDefault();
229
- }
230
- const { onChange } = this.props;
231
- const { keyedFormData } = this.state;
232
- const newKeyedFormDataRow: KeyedFormDataType<T> = {
233
- key: generateRowId(),
234
- item: this._getNewFormDataRow(),
235
- };
236
- const newKeyedFormData = [...keyedFormData];
237
- newKeyedFormData.splice(index, 0, newKeyedFormDataRow);
238
-
239
- this.setState(
240
- {
241
- keyedFormData: newKeyedFormData,
242
- updatedKeyedFormData: true,
243
- },
244
- () => onChange(keyedToPlainFormData(newKeyedFormData))
245
- );
246
- };
247
- };
248
-
249
- /** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of
250
- * keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted
251
- * from the keyed data
252
- *
253
- * @param index - The index at which the remove button is clicked
254
- */
255
- onDropIndexClick = (index: number) => {
256
- return (event: MouseEvent) => {
257
- if (event) {
258
- event.preventDefault();
259
- }
260
- const { onChange, errorSchema } = this.props;
261
- const { keyedFormData } = this.state;
262
- // refs #195: revalidate to ensure properly reindexing errors
263
- let newErrorSchema: ErrorSchema<T>;
264
- if (errorSchema) {
265
- newErrorSchema = {};
266
- for (const idx in errorSchema) {
267
- const i = parseInt(idx);
268
- if (i < index) {
269
- set(newErrorSchema, [i], errorSchema[idx]);
270
- } else if (i > index) {
271
- set(newErrorSchema, [i - 1], errorSchema[idx]);
272
- }
273
- }
274
- }
275
- const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
276
- this.setState(
277
- {
278
- keyedFormData: newKeyedFormData,
279
- updatedKeyedFormData: true,
280
- },
281
- () =>
282
- onChange(
283
- keyedToPlainFormData(newKeyedFormData),
284
- newErrorSchema as ErrorSchema<T[]>
285
- )
286
- );
287
- };
288
- };
289
-
290
- /** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the
291
- * row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the
292
- * plain form data converted from the keyed data
293
- *
294
- * @param index - The index of the item to move
295
- * @param newIndex - The index to where the item is to be moved
296
- */
297
- onReorderClick = (index: number, newIndex: number) => {
298
- return (event: React.MouseEvent<HTMLButtonElement>) => {
299
- if (event) {
300
- event.preventDefault();
301
- event.currentTarget.blur();
302
- }
303
- const { onChange, errorSchema } = this.props;
304
- let newErrorSchema: ErrorSchema<T>;
305
- if (this.props.errorSchema) {
306
- newErrorSchema = {};
307
- for (const idx in errorSchema) {
308
- const i = parseInt(idx);
309
- if (i == index) {
310
- set(newErrorSchema, [newIndex], errorSchema[index]);
311
- } else if (i == newIndex) {
312
- set(newErrorSchema, [index], errorSchema[newIndex]);
313
- } else {
314
- set(newErrorSchema, [idx], errorSchema[i]);
315
- }
316
- }
317
- }
318
-
319
- const { keyedFormData } = this.state;
320
- function reOrderArray() {
321
- // Copy item
322
- const _newKeyedFormData = keyedFormData.slice();
323
-
324
- // Moves item from index to newIndex
325
- _newKeyedFormData.splice(index, 1);
326
- _newKeyedFormData.splice(newIndex, 0, keyedFormData[index]);
327
-
328
- return _newKeyedFormData;
329
- }
330
- const newKeyedFormData = reOrderArray();
331
- this.setState(
332
- {
333
- keyedFormData: newKeyedFormData,
334
- },
335
- () =>
336
- onChange(
337
- keyedToPlainFormData(newKeyedFormData),
338
- newErrorSchema as ErrorSchema<T[]>
339
- )
340
- );
341
- };
342
- };
343
-
344
- /** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the
345
- * `onChange` callback with the updated form data
346
- *
347
- * @param index - The index of the item being changed
348
- */
349
- onChangeForIndex = (index: number) => {
350
- return (value: any, newErrorSchema?: ErrorSchema<T>) => {
351
- const { formData, onChange, errorSchema } = this.props;
352
- const arrayData = Array.isArray(formData) ? formData : [];
353
- const newFormData = arrayData.map((item: T, i: number) => {
354
- // We need to treat undefined items as nulls to have validation.
355
- // See https://github.com/tdegrunt/jsonschema/issues/206
356
- const jsonValue = typeof value === "undefined" ? null : value;
357
- return index === i ? jsonValue : item;
358
- });
359
- onChange(
360
- newFormData,
361
- errorSchema &&
362
- errorSchema && {
363
- ...errorSchema,
364
- [index]: newErrorSchema,
365
- }
366
- );
367
- };
368
- };
369
-
370
- /** Callback handler used to change the value for a checkbox */
371
- onSelectChange = (value: any) => {
372
- const { onChange } = this.props;
373
- onChange(value);
374
- };
375
-
376
- /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements
377
- */
378
- render() {
379
- const { schema, uiSchema, idSchema, registry } = this.props;
380
- const { schemaUtils } = registry;
381
- if (!(ITEMS_KEY in schema)) {
382
- const uiOptions = getUiOptions<T[], F>(uiSchema);
383
- const UnsupportedFieldTemplate = getTemplate<
384
- "UnsupportedFieldTemplate",
385
- T[],
386
- F
387
- >("UnsupportedFieldTemplate", registry, uiOptions);
388
-
389
- return (
390
- <UnsupportedFieldTemplate
391
- schema={schema}
392
- idSchema={idSchema}
393
- reason="Missing items definition"
394
- registry={registry}
395
- />
396
- );
397
- }
398
- if (schemaUtils.isMultiSelect(schema)) {
399
- // If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
400
- return this.renderMultiSelect();
401
- }
402
- if (isCustomWidget<T[], F>(uiSchema)) {
403
- return this.renderCustomWidget();
404
- }
405
- if (isFixedItems(schema)) {
406
- return this.renderFixedArray();
407
- }
408
- if (schemaUtils.isFilesArray(schema, uiSchema)) {
409
- return this.renderFiles();
410
- }
411
- return this.renderNormalArray();
412
- }
413
-
414
- /** Renders a normal array without any limitations of length
415
- */
416
- renderNormalArray() {
417
- const {
418
- schema,
419
- uiSchema = {},
420
- errorSchema,
421
- idSchema,
422
- name,
423
- disabled = false,
424
- readonly = false,
425
- autofocus = false,
426
- required = false,
427
- registry,
428
- onBlur,
429
- onFocus,
430
- idPrefix,
431
- idSeparator = "_",
432
- rawErrors,
433
- } = this.props;
434
- const { keyedFormData } = this.state;
435
- const title = schema.title === undefined ? name : schema.title;
436
- const { schemaUtils, formContext } = registry;
437
- const uiOptions = getUiOptions<T[], F>(uiSchema);
438
- const _schemaItems = isObject(schema.items) ? (schema.items as F) : {};
439
- const itemsSchema = schemaUtils.retrieveSchema(_schemaItems as F);
440
- const formData = keyedToPlainFormData(this.state.keyedFormData);
441
- const arrayProps: ArrayFieldTemplateProps<T[], F> = {
442
- canAdd: this.canAddItem(formData),
443
- items: keyedFormData.map((keyedItem, index) => {
444
- const { key, item } = keyedItem;
445
- // While we are actually dealing with a single item of type T, the types require a T[], so cast
446
- const itemCast = item as unknown as T[];
447
- const itemSchema = schemaUtils.retrieveSchema(
448
- _schemaItems as F,
449
- itemCast
450
- );
451
- const itemErrorSchema = errorSchema
452
- ? (errorSchema[index] as ErrorSchema<T[]>)
453
- : undefined;
454
- const itemIdPrefix = idSchema.$id + idSeparator + index;
455
- const itemIdSchema = schemaUtils.toIdSchema(
456
- itemSchema,
457
- itemIdPrefix,
458
- itemCast,
459
- idPrefix,
460
- idSeparator
461
- );
462
- return this.renderArrayFieldItem({
463
- key,
464
- index,
465
- name: name && `${name}-${index}`,
466
- canMoveUp: index > 0,
467
- canMoveDown: index < formData.length - 1,
468
- itemSchema: itemSchema,
469
- itemIdSchema,
470
- itemErrorSchema,
471
- itemData: itemCast,
472
- itemUiSchema: uiSchema.items,
473
- autofocus: autofocus && index === 0,
474
- onBlur,
475
- onFocus,
476
- rawErrors,
477
- });
478
- }) as ArrayFieldTemplateItemType<T[], F, any>[],
479
- className: `field field-array field-array-of-${itemsSchema.type}`,
480
- disabled,
481
- idSchema,
482
- uiSchema,
483
- onAddClick: this.onAddClick,
484
- readonly,
485
- required,
486
- schema,
487
- title,
488
- formContext,
489
- formData,
490
- rawErrors,
491
- registry,
492
- };
493
-
494
- const Template = getTemplate<"ArrayFieldTemplate", T[], F>(
495
- "ArrayFieldTemplate",
496
- registry,
497
- uiOptions
498
- );
499
- return <Template {...arrayProps} />;
500
- }
501
-
502
- /** Renders an array using the custom widget provided by the user in the `uiSchema`
503
- */
504
- renderCustomWidget() {
505
- const {
506
- schema,
507
- idSchema,
508
- uiSchema,
509
- disabled = false,
510
- readonly = false,
511
- autofocus = false,
512
- required = false,
513
- hideError,
514
- placeholder,
515
- onBlur,
516
- onFocus,
517
- formData: items = [],
518
- registry,
519
- rawErrors,
520
- name,
521
- } = this.props;
522
- const { widgets, formContext } = registry;
523
- const title = schema.title || name;
524
-
525
- const { widget, ...options } = getUiOptions<T[], F>(uiSchema);
526
- const Widget = getWidget<T[], F>(schema, widget, widgets);
527
- return (
528
- <Widget
529
- id={idSchema && idSchema.$id}
530
- multiple
531
- name={name}
532
- onChange={this.onSelectChange}
533
- onBlur={onBlur}
534
- onFocus={onFocus}
535
- options={options}
536
- schema={schema}
537
- uiSchema={uiSchema}
538
- registry={registry}
539
- value={items}
540
- disabled={disabled}
541
- readonly={readonly}
542
- hideError={hideError}
543
- required={required}
544
- label={title}
545
- placeholder={placeholder}
546
- formContext={formContext}
547
- autofocus={autofocus}
548
- rawErrors={rawErrors}
549
- />
550
- );
551
- }
552
-
553
- /** Renders an array as a set of checkboxes
554
- */
555
- renderMultiSelect() {
556
- const {
557
- schema,
558
- idSchema,
559
- uiSchema,
560
- formData: items = [],
561
- disabled = false,
562
- readonly = false,
563
- autofocus = false,
564
- required = false,
565
- placeholder,
566
- onBlur,
567
- onFocus,
568
- registry,
569
- rawErrors,
570
- name,
571
- } = this.props;
572
- const { widgets, schemaUtils, formContext } = registry;
573
- const itemsSchema = schemaUtils.retrieveSchema(schema.items as F, items);
574
- const title = schema.title || name;
575
- const enumOptions = optionsList(itemsSchema);
576
- const { widget = "select", ...options } = getUiOptions<T[], F>(uiSchema);
577
- const Widget = getWidget<T[], F>(schema, widget, widgets);
578
- return (
579
- <Widget
580
- id={idSchema && idSchema.$id}
581
- name={name}
582
- multiple
583
- onChange={this.onSelectChange}
584
- onBlur={onBlur}
585
- onFocus={onFocus}
586
- options={{ ...options, enumOptions }}
587
- schema={schema}
588
- uiSchema={uiSchema}
589
- registry={registry}
590
- value={items}
591
- disabled={disabled}
592
- readonly={readonly}
593
- required={required}
594
- label={title}
595
- placeholder={placeholder}
596
- formContext={formContext}
597
- autofocus={autofocus}
598
- rawErrors={rawErrors}
599
- />
600
- );
601
- }
602
-
603
- /** Renders an array of files using the `FileWidget`
604
- */
605
- renderFiles() {
606
- const {
607
- schema,
608
- uiSchema,
609
- idSchema,
610
- name,
611
- disabled = false,
612
- readonly = false,
613
- autofocus = false,
614
- required = false,
615
- onBlur,
616
- onFocus,
617
- registry,
618
- formData: items = [],
619
- rawErrors,
620
- } = this.props;
621
- const title = schema.title || name;
622
- const { widgets, formContext } = registry;
623
- const { widget = "files", ...options } = getUiOptions<T[], F>(uiSchema);
624
- const Widget = getWidget<T[], F>(schema, widget, widgets);
625
- return (
626
- <Widget
627
- options={options}
628
- id={idSchema && idSchema.$id}
629
- name={name}
630
- multiple
631
- onChange={this.onSelectChange}
632
- onBlur={onBlur}
633
- onFocus={onFocus}
634
- schema={schema}
635
- uiSchema={uiSchema}
636
- title={title}
637
- value={items}
638
- disabled={disabled}
639
- readonly={readonly}
640
- required={required}
641
- registry={registry}
642
- formContext={formContext}
643
- autofocus={autofocus}
644
- rawErrors={rawErrors}
645
- label=""
646
- />
647
- );
648
- }
649
-
650
- /** Renders an array that has a maximum limit of items
651
- */
652
- renderFixedArray() {
653
- const {
654
- schema,
655
- uiSchema = {},
656
- formData = [],
657
- errorSchema,
658
- idPrefix,
659
- idSeparator = "_",
660
- idSchema,
661
- name,
662
- disabled = false,
663
- readonly = false,
664
- autofocus = false,
665
- required = false,
666
- registry,
667
- onBlur,
668
- onFocus,
669
- rawErrors,
670
- } = this.props;
671
- const { keyedFormData } = this.state;
672
- let { formData: items = [] } = this.props;
673
- const title = schema.title || name;
674
- const uiOptions = getUiOptions<T[], F>(uiSchema);
675
- const { schemaUtils, formContext } = registry;
676
- const _schemaItems = isObject(schema.items) ? (schema.items as F[]) : [];
677
- const itemSchemas = _schemaItems.map((item: F, index: number) =>
678
- schemaUtils.retrieveSchema(item, formData[index] as unknown as T[])
679
- );
680
- const additionalSchema = isObject(schema.additionalItems)
681
- ? schemaUtils.retrieveSchema(
682
- schema.additionalItems as unknown as F,
683
- formData
684
- )
685
- : null;
686
-
687
- if (!items || items.length < itemSchemas.length) {
688
- // to make sure at least all fixed items are generated
689
- items = items || [];
690
- items = items.concat(new Array(itemSchemas.length - items.length));
691
- }
692
-
693
- // These are the props passed into the render function
694
- const arrayProps: ArrayFieldTemplateProps<T[], F> = {
695
- canAdd: this.canAddItem(items) && !!additionalSchema,
696
- className: "field field-array field-array-fixed-items",
697
- disabled,
698
- idSchema,
699
- formData,
700
- items: keyedFormData.map((keyedItem, index) => {
701
- const { key, item } = keyedItem;
702
- // While we are actually dealing with a single item of type T, the types require a T[], so cast
703
- const itemCast = item as unknown as T[];
704
- const additional = index >= itemSchemas.length;
705
- const itemSchema =
706
- additional && isObject(schema.additionalItems)
707
- ? schemaUtils.retrieveSchema(
708
- // @ts-ignore
709
- schema.additionalItems,
710
- itemCast
711
- )
712
- : itemSchemas[index];
713
- const itemIdPrefix = idSchema.$id + idSeparator + index;
714
- const itemIdSchema = schemaUtils.toIdSchema(
715
- itemSchema,
716
- itemIdPrefix,
717
- itemCast,
718
- idPrefix,
719
- idSeparator
720
- );
721
- const itemUiSchema = additional
722
- ? uiSchema.additionalItems || {}
723
- : Array.isArray(uiSchema.items)
724
- ? uiSchema.items[index]
725
- : uiSchema.items || {};
726
- const itemErrorSchema = errorSchema
727
- ? (errorSchema[index] as ErrorSchema<T[]>)
728
- : undefined;
729
-
730
- return this.renderArrayFieldItem({
731
- key,
732
- index,
733
- name: name && `${name}-${index}`,
734
- canRemove: additional,
735
- canMoveUp: index >= itemSchemas.length + 1,
736
- canMoveDown: additional && index < items.length - 1,
737
- itemSchema,
738
- itemData: itemCast,
739
- itemUiSchema,
740
- itemIdSchema,
741
- itemErrorSchema,
742
- autofocus: autofocus && index === 0,
743
- onBlur,
744
- onFocus,
745
- rawErrors,
746
- });
747
- }) as ArrayFieldTemplateItemType<T[], F, any>[],
748
- onAddClick: this.onAddClick,
749
- readonly,
750
- required,
751
- registry,
752
- schema,
753
- uiSchema,
754
- title,
755
- formContext,
756
- rawErrors,
757
- };
758
-
759
- const Template = getTemplate<"ArrayFieldTemplate", T[], F>(
760
- "ArrayFieldTemplate",
761
- registry,
762
- uiOptions
763
- );
764
- return <Template {...arrayProps} />;
765
- }
766
-
767
- /** Renders the individual array item using a `SchemaField` along with the additional properties required to be send
768
- * back to the `ArrayFieldItemTemplate`.
769
- *
770
- * @param props - The props for the individual array item to be rendered
771
- */
772
- renderArrayFieldItem(props: {
773
- key: string;
774
- index: number;
775
- name: string;
776
- canRemove?: boolean;
777
- canMoveUp?: boolean;
778
- canMoveDown?: boolean;
779
- itemSchema: F;
780
- itemData: T[];
781
- itemUiSchema: UiSchema<T[], F>;
782
- itemIdSchema: IdSchema<T[]>;
783
- itemErrorSchema?: ErrorSchema<T[]>;
784
- autofocus?: boolean;
785
- onBlur: FieldProps<T[], F>["onBlur"];
786
- onFocus: FieldProps<T[], F>["onFocus"];
787
- rawErrors?: string[];
788
- }) {
789
- const {
790
- key,
791
- index,
792
- name,
793
- canRemove = true,
794
- canMoveUp = true,
795
- canMoveDown = true,
796
- itemSchema,
797
- itemData,
798
- itemUiSchema,
799
- itemIdSchema,
800
- itemErrorSchema,
801
- autofocus,
802
- onBlur,
803
- onFocus,
804
- rawErrors,
805
- } = props;
806
- const {
807
- disabled,
808
- hideError,
809
- idPrefix,
810
- idSeparator,
811
- readonly,
812
- uiSchema,
813
- registry,
814
- formContext,
815
- } = this.props;
816
- const {
817
- fields: { SchemaField },
818
- } = registry;
819
- const { orderable = true, removable = true } = getUiOptions<T[], F>(
820
- uiSchema
821
- );
822
- const has: { [key: string]: boolean } = {
823
- moveUp: orderable && canMoveUp,
824
- moveDown: orderable && canMoveDown,
825
- remove: removable && canRemove,
826
- toolbar: false,
827
- };
828
- has.toolbar = Object.keys(has).some((key: keyof typeof has) => has[key]);
829
-
830
- return {
831
- children: (
832
- <SchemaField
833
- name={name}
834
- index={index}
835
- schema={itemSchema}
836
- uiSchema={itemUiSchema}
837
- formData={itemData}
838
- formContext={formContext}
839
- errorSchema={itemErrorSchema}
840
- idPrefix={idPrefix}
841
- idSeparator={idSeparator}
842
- idSchema={itemIdSchema}
843
- required={this.isItemRequired(itemSchema)}
844
- onChange={this.onChangeForIndex(index)}
845
- onBlur={onBlur}
846
- onFocus={onFocus}
847
- registry={registry}
848
- disabled={disabled}
849
- readonly={readonly}
850
- hideError={hideError}
851
- autofocus={autofocus}
852
- rawErrors={rawErrors}
853
- />
854
- ),
855
- className: "array-item",
856
- disabled,
857
- hasToolbar: has.toolbar,
858
- hasMoveUp: has.moveUp,
859
- hasMoveDown: has.moveDown,
860
- hasRemove: has.remove,
861
- index,
862
- key,
863
- onAddIndexClick: this.onAddIndexClick,
864
- onDropIndexClick: this.onDropIndexClick,
865
- onReorderClick: this.onReorderClick,
866
- readonly,
867
- registry,
868
- };
869
- }
870
- }
871
-
872
- /** `ArrayField` is `React.ComponentType<FieldProps<T[], F>>` (necessarily) but the `registry` requires things to be a
873
- * `Field` which is defined as `React.ComponentType<FieldProps<T, F>>`, so cast it to make `registry` happy.
874
- */
875
- export default ArrayField as unknown as Field;
1
+ import React, { Component } from "react";
2
+ import {
3
+ getTemplate,
4
+ getWidget,
5
+ getUiOptions,
6
+ isFixedItems,
7
+ allowAdditionalItems,
8
+ isCustomWidget,
9
+ optionsList,
10
+ ITEMS_KEY,
11
+ } from "@rjsf/utils";
12
+ import type {
13
+ ArrayFieldTemplateProps,
14
+ ErrorSchema,
15
+ Field,
16
+ FieldProps,
17
+ IdSchema,
18
+ UiSchema,
19
+ GenericObjectType,
20
+ ArrayFieldTemplateItemType,
21
+ } from "@rjsf/utils";
22
+ import get from "lodash/get";
23
+ import isObject from "lodash/isObject";
24
+ import set from "lodash/set";
25
+ import { nanoid } from "nanoid";
26
+
27
+ /** Type used to represent the keyed form data used in the state */
28
+ type KeyedFormDataType<T> = { key: string; item: T };
29
+
30
+ /** Type used for the state of the `ArrayField` component */
31
+ type ArrayFieldState<T> = {
32
+ /** The keyed form data elements */
33
+ keyedFormData: KeyedFormDataType<T>[];
34
+ /** Flag indicating whether any of the keyed form data has been updated */
35
+ updatedKeyedFormData: boolean;
36
+ };
37
+
38
+ /** Used to generate a unique ID for an element in a row */
39
+ function generateRowId() {
40
+ return nanoid();
41
+ }
42
+
43
+ /** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
44
+ *
45
+ * @param formData - The data for the form
46
+ * @returns - The `formData` converted into a `KeyedFormDataType` element
47
+ */
48
+ function generateKeyedFormData<T>(formData: T[]): KeyedFormDataType<T>[] {
49
+ return !Array.isArray(formData)
50
+ ? []
51
+ : formData.map((item) => {
52
+ return {
53
+ key: generateRowId(),
54
+ item,
55
+ };
56
+ });
57
+ }
58
+
59
+ /** Converts `KeyedFormDataType` data into the inner `formData`
60
+ *
61
+ * @param keyedFormData - The `KeyedFormDataType` to be converted
62
+ * @returns - The inner `formData` item(s) in the `keyedFormData`
63
+ */
64
+ function keyedToPlainFormData<T>(
65
+ keyedFormData: KeyedFormDataType<T> | KeyedFormDataType<T>[]
66
+ ): T[] {
67
+ if (Array.isArray(keyedFormData)) {
68
+ return keyedFormData.map((keyedItem) => keyedItem.item);
69
+ }
70
+ return [];
71
+ }
72
+
73
+ /** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal
74
+ * and fixed array, allowing user to add and remove elements from the array data.
75
+ */
76
+ class ArrayField<T = any, F extends GenericObjectType = any> extends Component<
77
+ FieldProps<T[], F>,
78
+ ArrayFieldState<T>
79
+ > {
80
+ /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData`
81
+ *
82
+ * @param props - The `FieldProps` for this template
83
+ */
84
+ constructor(props: FieldProps<T[], F>) {
85
+ super(props);
86
+ const { formData = [] } = props;
87
+ const keyedFormData = generateKeyedFormData<T>(formData);
88
+ this.state = {
89
+ keyedFormData,
90
+ updatedKeyedFormData: false,
91
+ };
92
+ }
93
+
94
+ /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It
95
+ * regenerates the keyed form data and returns it
96
+ *
97
+ * @param nextProps - The next set of props data
98
+ * @param prevState - The previous set of state data
99
+ */
100
+ static getDerivedStateFromProps<T = any, F extends GenericObjectType = any>(
101
+ nextProps: Readonly<FieldProps<T[], F>>,
102
+ prevState: Readonly<ArrayFieldState<T>>
103
+ ) {
104
+ // Don't call getDerivedStateFromProps if keyed formdata was just updated.
105
+ if (prevState.updatedKeyedFormData) {
106
+ return {
107
+ updatedKeyedFormData: false,
108
+ };
109
+ }
110
+ const nextFormData = Array.isArray(nextProps.formData)
111
+ ? nextProps.formData
112
+ : [];
113
+ const previousKeyedFormData = prevState.keyedFormData || [];
114
+ const newKeyedFormData =
115
+ nextFormData.length === previousKeyedFormData.length
116
+ ? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
117
+ return {
118
+ key: previousKeyedFormDatum.key,
119
+ item: nextFormData[index],
120
+ };
121
+ })
122
+ : generateKeyedFormData<T>(nextFormData);
123
+ return {
124
+ keyedFormData: newKeyedFormData,
125
+ };
126
+ }
127
+
128
+ /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to
129
+ * the description from the schema.items, and finally the string "Item"
130
+ */
131
+ get itemTitle() {
132
+ const { schema } = this.props;
133
+ return get(
134
+ schema,
135
+ [ITEMS_KEY, "title"],
136
+ get(schema, [ITEMS_KEY, "description"], "Item")
137
+ );
138
+ }
139
+
140
+ /** Determines whether the item described in the schema is always required, which is determined by whether any item
141
+ * may be null.
142
+ *
143
+ * @param itemSchema - The schema for the item
144
+ * @return - True if the item schema type does not contain the "null" type
145
+ */
146
+ isItemRequired(itemSchema: F) {
147
+ if (Array.isArray(itemSchema.type)) {
148
+ // While we don't yet support composite/nullable jsonschema types, it's
149
+ // future-proof to check for requirement against these.
150
+ return !itemSchema.type.includes("null");
151
+ }
152
+ // All non-null array item types are inherently required by design
153
+ return itemSchema.type !== "null";
154
+ }
155
+
156
+ /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding
157
+ * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the
158
+ * `formData` matches that value, then false is returned, otherwise true is returned.
159
+ *
160
+ * @param formItems - The list of items in the form
161
+ * @returns - True if the item is addable otherwise false
162
+ */
163
+ canAddItem(formItems: any[]) {
164
+ const { schema, uiSchema } = this.props;
165
+ let { addable } = getUiOptions<T[], F>(uiSchema);
166
+ if (addable !== false) {
167
+ // if ui:options.addable was not explicitly set to false, we can add
168
+ // another item if we have not exceeded maxItems yet
169
+ if (schema.maxItems !== undefined) {
170
+ addable = formItems.length < schema.maxItems;
171
+ } else {
172
+ addable = true;
173
+ }
174
+ }
175
+ return addable;
176
+ }
177
+
178
+ /** Returns the default form information for an item based on the schema for that item. Deals with the possibility
179
+ * that the schema is fixed and allows additional items.
180
+ */
181
+ _getNewFormDataRow = (): T => {
182
+ const { schema, registry } = this.props;
183
+ const { schemaUtils } = registry;
184
+ let itemSchema = schema.items as F;
185
+ if (isFixedItems(schema) && allowAdditionalItems(schema)) {
186
+ itemSchema = schema.additionalItems as F;
187
+ }
188
+ // Cast this as a T to work around schema utils being for T[] caused by the FieldProps<T[], F> call on the class
189
+ return schemaUtils.getDefaultFormState(itemSchema as F) as unknown as T;
190
+ };
191
+
192
+ /** Callback handler for when the user clicks on the add button. Creates a new row of keyed form data at the end of
193
+ * the list, adding it into the state, and then returning `onChange()` with the plain form data converted from the
194
+ * keyed data
195
+ *
196
+ * @param event - The event for the click
197
+ */
198
+ onAddClick = (event: MouseEvent) => {
199
+ if (event) {
200
+ event.preventDefault();
201
+ }
202
+
203
+ const { onChange } = this.props;
204
+ const { keyedFormData } = this.state;
205
+ const newKeyedFormDataRow: KeyedFormDataType<T> = {
206
+ key: generateRowId(),
207
+ item: this._getNewFormDataRow(),
208
+ };
209
+ const newKeyedFormData = [...keyedFormData, newKeyedFormDataRow];
210
+ this.setState(
211
+ {
212
+ keyedFormData: newKeyedFormData,
213
+ updatedKeyedFormData: true,
214
+ },
215
+ () => onChange(keyedToPlainFormData(newKeyedFormData))
216
+ );
217
+ };
218
+
219
+ /** Callback handler for when the user clicks on the add button on an existing array element. Creates a new row of
220
+ * keyed form data inserted at the `index`, adding it into the state, and then returning `onChange()` with the plain
221
+ * form data converted from the keyed data
222
+ *
223
+ * @param index - The index at which the add button is clicked
224
+ */
225
+ onAddIndexClick = (index: number) => {
226
+ return (event: MouseEvent) => {
227
+ if (event) {
228
+ event.preventDefault();
229
+ }
230
+ const { onChange } = this.props;
231
+ const { keyedFormData } = this.state;
232
+ const newKeyedFormDataRow: KeyedFormDataType<T> = {
233
+ key: generateRowId(),
234
+ item: this._getNewFormDataRow(),
235
+ };
236
+ const newKeyedFormData = [...keyedFormData];
237
+ newKeyedFormData.splice(index, 0, newKeyedFormDataRow);
238
+
239
+ this.setState(
240
+ {
241
+ keyedFormData: newKeyedFormData,
242
+ updatedKeyedFormData: true,
243
+ },
244
+ () => onChange(keyedToPlainFormData(newKeyedFormData))
245
+ );
246
+ };
247
+ };
248
+
249
+ /** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of
250
+ * keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted
251
+ * from the keyed data
252
+ *
253
+ * @param index - The index at which the remove button is clicked
254
+ */
255
+ onDropIndexClick = (index: number) => {
256
+ return (event: MouseEvent) => {
257
+ if (event) {
258
+ event.preventDefault();
259
+ }
260
+ const { onChange, errorSchema } = this.props;
261
+ const { keyedFormData } = this.state;
262
+ // refs #195: revalidate to ensure properly reindexing errors
263
+ let newErrorSchema: ErrorSchema<T>;
264
+ if (errorSchema) {
265
+ newErrorSchema = {};
266
+ for (const idx in errorSchema) {
267
+ const i = parseInt(idx);
268
+ if (i < index) {
269
+ set(newErrorSchema, [i], errorSchema[idx]);
270
+ } else if (i > index) {
271
+ set(newErrorSchema, [i - 1], errorSchema[idx]);
272
+ }
273
+ }
274
+ }
275
+ const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
276
+ this.setState(
277
+ {
278
+ keyedFormData: newKeyedFormData,
279
+ updatedKeyedFormData: true,
280
+ },
281
+ () =>
282
+ onChange(
283
+ keyedToPlainFormData(newKeyedFormData),
284
+ newErrorSchema as ErrorSchema<T[]>
285
+ )
286
+ );
287
+ };
288
+ };
289
+
290
+ /** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the
291
+ * row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the
292
+ * plain form data converted from the keyed data
293
+ *
294
+ * @param index - The index of the item to move
295
+ * @param newIndex - The index to where the item is to be moved
296
+ */
297
+ onReorderClick = (index: number, newIndex: number) => {
298
+ return (event: React.MouseEvent<HTMLButtonElement>) => {
299
+ if (event) {
300
+ event.preventDefault();
301
+ event.currentTarget.blur();
302
+ }
303
+ const { onChange, errorSchema } = this.props;
304
+ let newErrorSchema: ErrorSchema<T>;
305
+ if (this.props.errorSchema) {
306
+ newErrorSchema = {};
307
+ for (const idx in errorSchema) {
308
+ const i = parseInt(idx);
309
+ if (i == index) {
310
+ set(newErrorSchema, [newIndex], errorSchema[index]);
311
+ } else if (i == newIndex) {
312
+ set(newErrorSchema, [index], errorSchema[newIndex]);
313
+ } else {
314
+ set(newErrorSchema, [idx], errorSchema[i]);
315
+ }
316
+ }
317
+ }
318
+
319
+ const { keyedFormData } = this.state;
320
+ function reOrderArray() {
321
+ // Copy item
322
+ const _newKeyedFormData = keyedFormData.slice();
323
+
324
+ // Moves item from index to newIndex
325
+ _newKeyedFormData.splice(index, 1);
326
+ _newKeyedFormData.splice(newIndex, 0, keyedFormData[index]);
327
+
328
+ return _newKeyedFormData;
329
+ }
330
+ const newKeyedFormData = reOrderArray();
331
+ this.setState(
332
+ {
333
+ keyedFormData: newKeyedFormData,
334
+ },
335
+ () =>
336
+ onChange(
337
+ keyedToPlainFormData(newKeyedFormData),
338
+ newErrorSchema as ErrorSchema<T[]>
339
+ )
340
+ );
341
+ };
342
+ };
343
+
344
+ /** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the
345
+ * `onChange` callback with the updated form data
346
+ *
347
+ * @param index - The index of the item being changed
348
+ */
349
+ onChangeForIndex = (index: number) => {
350
+ return (value: any, newErrorSchema?: ErrorSchema<T>) => {
351
+ const { formData, onChange, errorSchema } = this.props;
352
+ const arrayData = Array.isArray(formData) ? formData : [];
353
+ const newFormData = arrayData.map((item: T, i: number) => {
354
+ // We need to treat undefined items as nulls to have validation.
355
+ // See https://github.com/tdegrunt/jsonschema/issues/206
356
+ const jsonValue = typeof value === "undefined" ? null : value;
357
+ return index === i ? jsonValue : item;
358
+ });
359
+ onChange(
360
+ newFormData,
361
+ errorSchema &&
362
+ errorSchema && {
363
+ ...errorSchema,
364
+ [index]: newErrorSchema,
365
+ }
366
+ );
367
+ };
368
+ };
369
+
370
+ /** Callback handler used to change the value for a checkbox */
371
+ onSelectChange = (value: any) => {
372
+ const { onChange } = this.props;
373
+ onChange(value);
374
+ };
375
+
376
+ /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements
377
+ */
378
+ render() {
379
+ const { schema, uiSchema, idSchema, registry } = this.props;
380
+ const { schemaUtils } = registry;
381
+ if (!(ITEMS_KEY in schema)) {
382
+ const uiOptions = getUiOptions<T[], F>(uiSchema);
383
+ const UnsupportedFieldTemplate = getTemplate<
384
+ "UnsupportedFieldTemplate",
385
+ T[],
386
+ F
387
+ >("UnsupportedFieldTemplate", registry, uiOptions);
388
+
389
+ return (
390
+ <UnsupportedFieldTemplate
391
+ schema={schema}
392
+ idSchema={idSchema}
393
+ reason="Missing items definition"
394
+ registry={registry}
395
+ />
396
+ );
397
+ }
398
+ if (schemaUtils.isMultiSelect(schema)) {
399
+ // If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
400
+ return this.renderMultiSelect();
401
+ }
402
+ if (isCustomWidget<T[], F>(uiSchema)) {
403
+ return this.renderCustomWidget();
404
+ }
405
+ if (isFixedItems(schema)) {
406
+ return this.renderFixedArray();
407
+ }
408
+ if (schemaUtils.isFilesArray(schema, uiSchema)) {
409
+ return this.renderFiles();
410
+ }
411
+ return this.renderNormalArray();
412
+ }
413
+
414
+ /** Renders a normal array without any limitations of length
415
+ */
416
+ renderNormalArray() {
417
+ const {
418
+ schema,
419
+ uiSchema = {},
420
+ errorSchema,
421
+ idSchema,
422
+ name,
423
+ disabled = false,
424
+ readonly = false,
425
+ autofocus = false,
426
+ required = false,
427
+ registry,
428
+ onBlur,
429
+ onFocus,
430
+ idPrefix,
431
+ idSeparator = "_",
432
+ rawErrors,
433
+ } = this.props;
434
+ const { keyedFormData } = this.state;
435
+ const title = schema.title === undefined ? name : schema.title;
436
+ const { schemaUtils, formContext } = registry;
437
+ const uiOptions = getUiOptions<T[], F>(uiSchema);
438
+ const _schemaItems = isObject(schema.items) ? (schema.items as F) : {};
439
+ const itemsSchema = schemaUtils.retrieveSchema(_schemaItems as F);
440
+ const formData = keyedToPlainFormData(this.state.keyedFormData);
441
+ const arrayProps: ArrayFieldTemplateProps<T[], F> = {
442
+ canAdd: this.canAddItem(formData),
443
+ items: keyedFormData.map((keyedItem, index) => {
444
+ const { key, item } = keyedItem;
445
+ // While we are actually dealing with a single item of type T, the types require a T[], so cast
446
+ const itemCast = item as unknown as T[];
447
+ const itemSchema = schemaUtils.retrieveSchema(
448
+ _schemaItems as F,
449
+ itemCast
450
+ );
451
+ const itemErrorSchema = errorSchema
452
+ ? (errorSchema[index] as ErrorSchema<T[]>)
453
+ : undefined;
454
+ const itemIdPrefix = idSchema.$id + idSeparator + index;
455
+ const itemIdSchema = schemaUtils.toIdSchema(
456
+ itemSchema,
457
+ itemIdPrefix,
458
+ itemCast,
459
+ idPrefix,
460
+ idSeparator
461
+ );
462
+ return this.renderArrayFieldItem({
463
+ key,
464
+ index,
465
+ name: name && `${name}-${index}`,
466
+ canMoveUp: index > 0,
467
+ canMoveDown: index < formData.length - 1,
468
+ itemSchema: itemSchema,
469
+ itemIdSchema,
470
+ itemErrorSchema,
471
+ itemData: itemCast,
472
+ itemUiSchema: uiSchema.items,
473
+ autofocus: autofocus && index === 0,
474
+ onBlur,
475
+ onFocus,
476
+ rawErrors,
477
+ });
478
+ }) as ArrayFieldTemplateItemType<T[], F, any>[],
479
+ className: `field field-array field-array-of-${itemsSchema.type}`,
480
+ disabled,
481
+ idSchema,
482
+ uiSchema,
483
+ onAddClick: this.onAddClick,
484
+ readonly,
485
+ required,
486
+ schema,
487
+ title,
488
+ formContext,
489
+ formData,
490
+ rawErrors,
491
+ registry,
492
+ };
493
+
494
+ const Template = getTemplate<"ArrayFieldTemplate", T[], F>(
495
+ "ArrayFieldTemplate",
496
+ registry,
497
+ uiOptions
498
+ );
499
+ return <Template {...arrayProps} />;
500
+ }
501
+
502
+ /** Renders an array using the custom widget provided by the user in the `uiSchema`
503
+ */
504
+ renderCustomWidget() {
505
+ const {
506
+ schema,
507
+ idSchema,
508
+ uiSchema,
509
+ disabled = false,
510
+ readonly = false,
511
+ autofocus = false,
512
+ required = false,
513
+ hideError,
514
+ placeholder,
515
+ onBlur,
516
+ onFocus,
517
+ formData: items = [],
518
+ registry,
519
+ rawErrors,
520
+ name,
521
+ } = this.props;
522
+ const { widgets, formContext } = registry;
523
+ const title = schema.title || name;
524
+
525
+ const { widget, ...options } = getUiOptions<T[], F>(uiSchema);
526
+ const Widget = getWidget<T[], F>(schema, widget, widgets);
527
+ return (
528
+ <Widget
529
+ id={idSchema && idSchema.$id}
530
+ multiple
531
+ name={name}
532
+ onChange={this.onSelectChange}
533
+ onBlur={onBlur}
534
+ onFocus={onFocus}
535
+ options={options}
536
+ schema={schema}
537
+ uiSchema={uiSchema}
538
+ registry={registry}
539
+ value={items}
540
+ disabled={disabled}
541
+ readonly={readonly}
542
+ hideError={hideError}
543
+ required={required}
544
+ label={title}
545
+ placeholder={placeholder}
546
+ formContext={formContext}
547
+ autofocus={autofocus}
548
+ rawErrors={rawErrors}
549
+ />
550
+ );
551
+ }
552
+
553
+ /** Renders an array as a set of checkboxes
554
+ */
555
+ renderMultiSelect() {
556
+ const {
557
+ schema,
558
+ idSchema,
559
+ uiSchema,
560
+ formData: items = [],
561
+ disabled = false,
562
+ readonly = false,
563
+ autofocus = false,
564
+ required = false,
565
+ placeholder,
566
+ onBlur,
567
+ onFocus,
568
+ registry,
569
+ rawErrors,
570
+ name,
571
+ } = this.props;
572
+ const { widgets, schemaUtils, formContext } = registry;
573
+ const itemsSchema = schemaUtils.retrieveSchema(schema.items as F, items);
574
+ const title = schema.title || name;
575
+ const enumOptions = optionsList(itemsSchema);
576
+ const { widget = "select", ...options } = getUiOptions<T[], F>(uiSchema);
577
+ const Widget = getWidget<T[], F>(schema, widget, widgets);
578
+ return (
579
+ <Widget
580
+ id={idSchema && idSchema.$id}
581
+ name={name}
582
+ multiple
583
+ onChange={this.onSelectChange}
584
+ onBlur={onBlur}
585
+ onFocus={onFocus}
586
+ options={{ ...options, enumOptions }}
587
+ schema={schema}
588
+ uiSchema={uiSchema}
589
+ registry={registry}
590
+ value={items}
591
+ disabled={disabled}
592
+ readonly={readonly}
593
+ required={required}
594
+ label={title}
595
+ placeholder={placeholder}
596
+ formContext={formContext}
597
+ autofocus={autofocus}
598
+ rawErrors={rawErrors}
599
+ />
600
+ );
601
+ }
602
+
603
+ /** Renders an array of files using the `FileWidget`
604
+ */
605
+ renderFiles() {
606
+ const {
607
+ schema,
608
+ uiSchema,
609
+ idSchema,
610
+ name,
611
+ disabled = false,
612
+ readonly = false,
613
+ autofocus = false,
614
+ required = false,
615
+ onBlur,
616
+ onFocus,
617
+ registry,
618
+ formData: items = [],
619
+ rawErrors,
620
+ } = this.props;
621
+ const title = schema.title || name;
622
+ const { widgets, formContext } = registry;
623
+ const { widget = "files", ...options } = getUiOptions<T[], F>(uiSchema);
624
+ const Widget = getWidget<T[], F>(schema, widget, widgets);
625
+ return (
626
+ <Widget
627
+ options={options}
628
+ id={idSchema && idSchema.$id}
629
+ name={name}
630
+ multiple
631
+ onChange={this.onSelectChange}
632
+ onBlur={onBlur}
633
+ onFocus={onFocus}
634
+ schema={schema}
635
+ uiSchema={uiSchema}
636
+ title={title}
637
+ value={items}
638
+ disabled={disabled}
639
+ readonly={readonly}
640
+ required={required}
641
+ registry={registry}
642
+ formContext={formContext}
643
+ autofocus={autofocus}
644
+ rawErrors={rawErrors}
645
+ label=""
646
+ />
647
+ );
648
+ }
649
+
650
+ /** Renders an array that has a maximum limit of items
651
+ */
652
+ renderFixedArray() {
653
+ const {
654
+ schema,
655
+ uiSchema = {},
656
+ formData = [],
657
+ errorSchema,
658
+ idPrefix,
659
+ idSeparator = "_",
660
+ idSchema,
661
+ name,
662
+ disabled = false,
663
+ readonly = false,
664
+ autofocus = false,
665
+ required = false,
666
+ registry,
667
+ onBlur,
668
+ onFocus,
669
+ rawErrors,
670
+ } = this.props;
671
+ const { keyedFormData } = this.state;
672
+ let { formData: items = [] } = this.props;
673
+ const title = schema.title || name;
674
+ const uiOptions = getUiOptions<T[], F>(uiSchema);
675
+ const { schemaUtils, formContext } = registry;
676
+ const _schemaItems = isObject(schema.items) ? (schema.items as F[]) : [];
677
+ const itemSchemas = _schemaItems.map((item: F, index: number) =>
678
+ schemaUtils.retrieveSchema(item, formData[index] as unknown as T[])
679
+ );
680
+ const additionalSchema = isObject(schema.additionalItems)
681
+ ? schemaUtils.retrieveSchema(
682
+ schema.additionalItems as unknown as F,
683
+ formData
684
+ )
685
+ : null;
686
+
687
+ if (!items || items.length < itemSchemas.length) {
688
+ // to make sure at least all fixed items are generated
689
+ items = items || [];
690
+ items = items.concat(new Array(itemSchemas.length - items.length));
691
+ }
692
+
693
+ // These are the props passed into the render function
694
+ const arrayProps: ArrayFieldTemplateProps<T[], F> = {
695
+ canAdd: this.canAddItem(items) && !!additionalSchema,
696
+ className: "field field-array field-array-fixed-items",
697
+ disabled,
698
+ idSchema,
699
+ formData,
700
+ items: keyedFormData.map((keyedItem, index) => {
701
+ const { key, item } = keyedItem;
702
+ // While we are actually dealing with a single item of type T, the types require a T[], so cast
703
+ const itemCast = item as unknown as T[];
704
+ const additional = index >= itemSchemas.length;
705
+ const itemSchema =
706
+ additional && isObject(schema.additionalItems)
707
+ ? schemaUtils.retrieveSchema(
708
+ // @ts-ignore
709
+ schema.additionalItems,
710
+ itemCast
711
+ )
712
+ : itemSchemas[index];
713
+ const itemIdPrefix = idSchema.$id + idSeparator + index;
714
+ const itemIdSchema = schemaUtils.toIdSchema(
715
+ itemSchema,
716
+ itemIdPrefix,
717
+ itemCast,
718
+ idPrefix,
719
+ idSeparator
720
+ );
721
+ const itemUiSchema = additional
722
+ ? uiSchema.additionalItems || {}
723
+ : Array.isArray(uiSchema.items)
724
+ ? uiSchema.items[index]
725
+ : uiSchema.items || {};
726
+ const itemErrorSchema = errorSchema
727
+ ? (errorSchema[index] as ErrorSchema<T[]>)
728
+ : undefined;
729
+
730
+ return this.renderArrayFieldItem({
731
+ key,
732
+ index,
733
+ name: name && `${name}-${index}`,
734
+ canRemove: additional,
735
+ canMoveUp: index >= itemSchemas.length + 1,
736
+ canMoveDown: additional && index < items.length - 1,
737
+ itemSchema,
738
+ itemData: itemCast,
739
+ itemUiSchema,
740
+ itemIdSchema,
741
+ itemErrorSchema,
742
+ autofocus: autofocus && index === 0,
743
+ onBlur,
744
+ onFocus,
745
+ rawErrors,
746
+ });
747
+ }) as ArrayFieldTemplateItemType<T[], F, any>[],
748
+ onAddClick: this.onAddClick,
749
+ readonly,
750
+ required,
751
+ registry,
752
+ schema,
753
+ uiSchema,
754
+ title,
755
+ formContext,
756
+ rawErrors,
757
+ };
758
+
759
+ const Template = getTemplate<"ArrayFieldTemplate", T[], F>(
760
+ "ArrayFieldTemplate",
761
+ registry,
762
+ uiOptions
763
+ );
764
+ return <Template {...arrayProps} />;
765
+ }
766
+
767
+ /** Renders the individual array item using a `SchemaField` along with the additional properties required to be send
768
+ * back to the `ArrayFieldItemTemplate`.
769
+ *
770
+ * @param props - The props for the individual array item to be rendered
771
+ */
772
+ renderArrayFieldItem(props: {
773
+ key: string;
774
+ index: number;
775
+ name: string;
776
+ canRemove?: boolean;
777
+ canMoveUp?: boolean;
778
+ canMoveDown?: boolean;
779
+ itemSchema: F;
780
+ itemData: T[];
781
+ itemUiSchema: UiSchema<T[], F>;
782
+ itemIdSchema: IdSchema<T[]>;
783
+ itemErrorSchema?: ErrorSchema<T[]>;
784
+ autofocus?: boolean;
785
+ onBlur: FieldProps<T[], F>["onBlur"];
786
+ onFocus: FieldProps<T[], F>["onFocus"];
787
+ rawErrors?: string[];
788
+ }) {
789
+ const {
790
+ key,
791
+ index,
792
+ name,
793
+ canRemove = true,
794
+ canMoveUp = true,
795
+ canMoveDown = true,
796
+ itemSchema,
797
+ itemData,
798
+ itemUiSchema,
799
+ itemIdSchema,
800
+ itemErrorSchema,
801
+ autofocus,
802
+ onBlur,
803
+ onFocus,
804
+ rawErrors,
805
+ } = props;
806
+ const {
807
+ disabled,
808
+ hideError,
809
+ idPrefix,
810
+ idSeparator,
811
+ readonly,
812
+ uiSchema,
813
+ registry,
814
+ formContext,
815
+ } = this.props;
816
+ const {
817
+ fields: { SchemaField },
818
+ } = registry;
819
+ const { orderable = true, removable = true } = getUiOptions<T[], F>(
820
+ uiSchema
821
+ );
822
+ const has: { [key: string]: boolean } = {
823
+ moveUp: orderable && canMoveUp,
824
+ moveDown: orderable && canMoveDown,
825
+ remove: removable && canRemove,
826
+ toolbar: false,
827
+ };
828
+ has.toolbar = Object.keys(has).some((key: keyof typeof has) => has[key]);
829
+
830
+ return {
831
+ children: (
832
+ <SchemaField
833
+ name={name}
834
+ index={index}
835
+ schema={itemSchema}
836
+ uiSchema={itemUiSchema}
837
+ formData={itemData}
838
+ formContext={formContext}
839
+ errorSchema={itemErrorSchema}
840
+ idPrefix={idPrefix}
841
+ idSeparator={idSeparator}
842
+ idSchema={itemIdSchema}
843
+ required={this.isItemRequired(itemSchema)}
844
+ onChange={this.onChangeForIndex(index)}
845
+ onBlur={onBlur}
846
+ onFocus={onFocus}
847
+ registry={registry}
848
+ disabled={disabled}
849
+ readonly={readonly}
850
+ hideError={hideError}
851
+ autofocus={autofocus}
852
+ rawErrors={rawErrors}
853
+ />
854
+ ),
855
+ className: "array-item",
856
+ disabled,
857
+ hasToolbar: has.toolbar,
858
+ hasMoveUp: has.moveUp,
859
+ hasMoveDown: has.moveDown,
860
+ hasRemove: has.remove,
861
+ index,
862
+ key,
863
+ onAddIndexClick: this.onAddIndexClick,
864
+ onDropIndexClick: this.onDropIndexClick,
865
+ onReorderClick: this.onReorderClick,
866
+ readonly,
867
+ registry,
868
+ };
869
+ }
870
+ }
871
+
872
+ /** `ArrayField` is `React.ComponentType<FieldProps<T[], F>>` (necessarily) but the `registry` requires things to be a
873
+ * `Field` which is defined as `React.ComponentType<FieldProps<T, F>>`, so cast it to make `registry` happy.
874
+ */
875
+ export default ArrayField as unknown as Field;