Skip to content

Commit 20796c2

Browse files
fstegerepicfaace
authored andcommitted
Array items now have unique, stable keys (rjsf-team#1046) (rjsf-team#1335)
* Array items now have unique, stable keys (rjsf-team#1046) * update package-lock.json * update package-lock.json * fix: ensure state has been updated before calling onChange * Add tests for array item keys. Include item key for fixed item arrays. * Update ArrayField to use getDerivedStateFromProps via polyfill * fix: remove id; use custom array template for tests. * fix: use custom arraytemplate for key test.
1 parent 07b07b1 commit 20796c2

File tree

6 files changed

+344
-20
lines changed

6 files changed

+344
-20
lines changed

docs/advanced-customization.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ The following props are part of each element in `items`:
112112
- `hasRemove`: A boolean value stating whether the array item can be removed.
113113
- `hasToolbar`: A boolean value stating whether the array item has a toolbar.
114114
- `index`: A number stating the index the array item occurs in `items`.
115+
- `key`: A stable, unique key for the array item.
115116
- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`.
116117
- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`.
117118
- `readonly`: A boolean value stating if the array item is read-only.

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
"lodash.pick": "^4.4.0",
5151
"lodash.topath": "^4.5.2",
5252
"prop-types": "^15.5.8",
53-
"react-is": "^16.8.4"
53+
"react-is": "^16.8.4",
54+
"react-lifecycles-compat": "^3.0.4",
55+
"shortid": "^2.2.14"
5456
},
5557
"devDependencies": {
5658
"@babel/cli": "^7.4.4",

playground/samples/customArray.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function ArrayFieldTemplate(props) {
55
<div className={props.className}>
66
{props.items &&
77
props.items.map(element => (
8-
<div key={element.index}>
8+
<div key={element.key} className={element.className}>
99
<div>{element.children}</div>
1010
{element.hasMoveDown && (
1111
<button

src/components/fields/ArrayField.js

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AddButton from "../AddButton";
22
import IconButton from "../IconButton";
33
import React, { Component } from "react";
4+
import { polyfill } from "react-lifecycles-compat";
45
import includes from "core-js/library/fn/array/includes";
56
import * as types from "../../types";
67

@@ -18,6 +19,7 @@ import {
1819
toIdSchema,
1920
getDefaultRegistry,
2021
} from "../../utils";
22+
import shortid from "shortid";
2123

2224
function ArrayFieldTitle({ TitleField, idSchema, title, required }) {
2325
if (!title) {
@@ -44,7 +46,7 @@ function DefaultArrayItem(props) {
4446
fontWeight: "bold",
4547
};
4648
return (
47-
<div key={props.index} className={props.className}>
49+
<div key={props.key} className={props.className}>
4850
<div className={props.hasToolbar ? "col-xs-9" : "col-xs-12"}>
4951
{props.children}
5052
</div>
@@ -174,6 +176,25 @@ function DefaultNormalArrayFieldTemplate(props) {
174176
);
175177
}
176178

179+
function generateRowId() {
180+
return shortid.generate();
181+
}
182+
183+
function generateKeyedFormData(formData) {
184+
return !Array.isArray(formData)
185+
? []
186+
: formData.map(item => {
187+
return {
188+
key: generateRowId(),
189+
item,
190+
};
191+
});
192+
}
193+
194+
function keyedToPlainFormData(keyedFormData) {
195+
return keyedFormData.map(keyedItem => keyedItem.item);
196+
}
197+
177198
class ArrayField extends Component {
178199
static defaultProps = {
179200
uiSchema: {},
@@ -185,6 +206,32 @@ class ArrayField extends Component {
185206
autofocus: false,
186207
};
187208

209+
constructor(props) {
210+
super(props);
211+
const { formData } = props;
212+
const keyedFormData = generateKeyedFormData(formData);
213+
this.state = {
214+
keyedFormData,
215+
};
216+
}
217+
218+
static getDerivedStateFromProps(nextProps, prevState) {
219+
const nextFormData = nextProps.formData;
220+
const previousKeyedFormData = prevState.keyedFormData;
221+
const newKeyedFormData =
222+
nextFormData.length === previousKeyedFormData.length
223+
? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
224+
return {
225+
key: previousKeyedFormDatum.key,
226+
item: nextFormData[index],
227+
};
228+
})
229+
: generateKeyedFormData(nextFormData);
230+
return {
231+
keyedFormData: newKeyedFormData,
232+
};
233+
}
234+
188235
get itemTitle() {
189236
const { schema } = this.props;
190237
return schema.items.title || schema.items.description || "Item";
@@ -217,24 +264,40 @@ class ArrayField extends Component {
217264

218265
onAddClick = event => {
219266
event.preventDefault();
220-
const { schema, formData, registry = getDefaultRegistry() } = this.props;
267+
const { schema, registry = getDefaultRegistry(), onChange } = this.props;
221268
const { definitions } = registry;
222269
let itemSchema = schema.items;
223270
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
224271
itemSchema = schema.additionalItems;
225272
}
226-
this.props.onChange([
227-
...formData,
228-
getDefaultFormState(itemSchema, undefined, definitions),
229-
]);
273+
const newFormDataRow = getDefaultFormState(
274+
itemSchema,
275+
undefined,
276+
definitions
277+
);
278+
const newKeyedFormData = [
279+
...this.state.keyedFormData,
280+
{
281+
key: generateRowId(),
282+
item: newFormDataRow,
283+
},
284+
];
285+
286+
this.setState(
287+
{
288+
keyedFormData: newKeyedFormData,
289+
},
290+
() => onChange(keyedToPlainFormData(newKeyedFormData))
291+
);
230292
};
231293

232294
onDropIndexClick = index => {
233295
return event => {
234296
if (event) {
235297
event.preventDefault();
236298
}
237-
const { formData, onChange } = this.props;
299+
const { onChange } = this.props;
300+
const { keyedFormData } = this.state;
238301
// refs #195: revalidate to ensure properly reindexing errors
239302
let newErrorSchema;
240303
if (this.props.errorSchema) {
@@ -249,7 +312,13 @@ class ArrayField extends Component {
249312
}
250313
}
251314
}
252-
onChange(formData.filter((_, i) => i !== index), newErrorSchema);
315+
const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
316+
this.setState(
317+
{
318+
keyedFormData: newKeyedFormData,
319+
},
320+
() => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema)
321+
);
253322
};
254323
};
255324

@@ -259,7 +328,7 @@ class ArrayField extends Component {
259328
event.preventDefault();
260329
event.target.blur();
261330
}
262-
const { formData, onChange } = this.props;
331+
const { onChange } = this.props;
263332
let newErrorSchema;
264333
if (this.props.errorSchema) {
265334
newErrorSchema = {};
@@ -275,18 +344,24 @@ class ArrayField extends Component {
275344
}
276345
}
277346

347+
const { keyedFormData } = this.state;
278348
function reOrderArray() {
279349
// Copy item
280-
let newFormData = formData.slice();
350+
let _newKeyedFormData = keyedFormData.slice();
281351

282352
// Moves item from index to newIndex
283-
newFormData.splice(index, 1);
284-
newFormData.splice(newIndex, 0, formData[index]);
353+
_newKeyedFormData.splice(index, 1);
354+
_newKeyedFormData.splice(newIndex, 0, keyedFormData[index]);
285355

286-
return newFormData;
356+
return _newKeyedFormData;
287357
}
288-
289-
onChange(reOrderArray(), newErrorSchema);
358+
const newKeyedFormData = reOrderArray();
359+
this.setState(
360+
{
361+
keyedFormData: newKeyedFormData,
362+
},
363+
() => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema)
364+
);
290365
};
291366
};
292367

@@ -367,7 +442,8 @@ class ArrayField extends Component {
367442
const itemsSchema = retrieveSchema(schema.items, definitions);
368443
const arrayProps = {
369444
canAdd: this.canAddItem(formData),
370-
items: formData.map((item, index) => {
445+
items: this.state.keyedFormData.map((keyedItem, index) => {
446+
const { key, item } = keyedItem;
371447
const itemSchema = retrieveSchema(schema.items, definitions, item);
372448
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
373449
const itemIdPrefix = idSchema.$id + "_" + index;
@@ -379,6 +455,7 @@ class ArrayField extends Component {
379455
idPrefix
380456
);
381457
return this.renderArrayFieldItem({
458+
key,
382459
index,
383460
canMoveUp: index > 0,
384461
canMoveDown: index < formData.length - 1,
@@ -544,7 +621,8 @@ class ArrayField extends Component {
544621
disabled,
545622
idSchema,
546623
formData,
547-
items: items.map((item, index) => {
624+
items: this.state.keyedFormData.map((keyedItem, index) => {
625+
const { key, item } = keyedItem;
548626
const additional = index >= itemSchemas.length;
549627
const itemSchema = additional
550628
? retrieveSchema(schema.additionalItems, definitions, item)
@@ -565,6 +643,7 @@ class ArrayField extends Component {
565643
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
566644

567645
return this.renderArrayFieldItem({
646+
key,
568647
index,
569648
canRemove: additional,
570649
canMoveUp: index >= itemSchemas.length + 1,
@@ -597,6 +676,7 @@ class ArrayField extends Component {
597676

598677
renderArrayFieldItem(props) {
599678
const {
679+
key,
600680
index,
601681
canRemove = true,
602682
canMoveUp = true,
@@ -658,6 +738,7 @@ class ArrayField extends Component {
658738
hasMoveDown: has.moveDown,
659739
hasRemove: has.remove,
660740
index,
741+
key,
661742
onDropIndexClick: this.onDropIndexClick,
662743
onReorderClick: this.onReorderClick,
663744
readonly,
@@ -669,4 +750,6 @@ if (process.env.NODE_ENV !== "production") {
669750
ArrayField.propTypes = types.fieldProps;
670751
}
671752

753+
polyfill(ArrayField);
754+
672755
export default ArrayField;

0 commit comments

Comments
 (0)