Skip to content

Commit

Permalink
Array items now have unique, stable keys (#1046) (#1335)
Browse files Browse the repository at this point in the history
* Array items now have unique, stable keys (#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.
  • Loading branch information
fsteger authored and epicfaace committed Jul 9, 2019
1 parent 07b07b1 commit 20796c2
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 20 deletions.
1 change: 1 addition & 0 deletions docs/advanced-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ The following props are part of each element in `items`:
- `hasRemove`: A boolean value stating whether the array item can be removed.
- `hasToolbar`: A boolean value stating whether the array item has a toolbar.
- `index`: A number stating the index the array item occurs in `items`.
- `key`: A stable, unique key for the array item.
- `onDropIndexClick: (index) => (event) => void`: Returns a function that removes the item at `index`.
- `onReorderClick: (index, newIndex) => (event) => void`: Returns a function that swaps the items at `index` with `newIndex`.
- `readonly`: A boolean value stating if the array item is read-only.
Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"lodash.pick": "^4.4.0",
"lodash.topath": "^4.5.2",
"prop-types": "^15.5.8",
"react-is": "^16.8.4"
"react-is": "^16.8.4",
"react-lifecycles-compat": "^3.0.4",
"shortid": "^2.2.14"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
Expand Down
2 changes: 1 addition & 1 deletion playground/samples/customArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function ArrayFieldTemplate(props) {
<div className={props.className}>
{props.items &&
props.items.map(element => (
<div key={element.index}>
<div key={element.key} className={element.className}>
<div>{element.children}</div>
{element.hasMoveDown && (
<button
Expand Down
117 changes: 100 additions & 17 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AddButton from "../AddButton";
import IconButton from "../IconButton";
import React, { Component } from "react";
import { polyfill } from "react-lifecycles-compat";
import includes from "core-js/library/fn/array/includes";
import * as types from "../../types";

Expand All @@ -18,6 +19,7 @@ import {
toIdSchema,
getDefaultRegistry,
} from "../../utils";
import shortid from "shortid";

function ArrayFieldTitle({ TitleField, idSchema, title, required }) {
if (!title) {
Expand All @@ -44,7 +46,7 @@ function DefaultArrayItem(props) {
fontWeight: "bold",
};
return (
<div key={props.index} className={props.className}>
<div key={props.key} className={props.className}>
<div className={props.hasToolbar ? "col-xs-9" : "col-xs-12"}>
{props.children}
</div>
Expand Down Expand Up @@ -174,6 +176,25 @@ function DefaultNormalArrayFieldTemplate(props) {
);
}

function generateRowId() {
return shortid.generate();
}

function generateKeyedFormData(formData) {
return !Array.isArray(formData)
? []
: formData.map(item => {
return {
key: generateRowId(),
item,
};
});
}

function keyedToPlainFormData(keyedFormData) {
return keyedFormData.map(keyedItem => keyedItem.item);
}

class ArrayField extends Component {
static defaultProps = {
uiSchema: {},
Expand All @@ -185,6 +206,32 @@ class ArrayField extends Component {
autofocus: false,
};

constructor(props) {
super(props);
const { formData } = props;
const keyedFormData = generateKeyedFormData(formData);
this.state = {
keyedFormData,
};
}

static getDerivedStateFromProps(nextProps, prevState) {
const nextFormData = nextProps.formData;
const previousKeyedFormData = prevState.keyedFormData;
const newKeyedFormData =
nextFormData.length === previousKeyedFormData.length
? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
return {
key: previousKeyedFormDatum.key,
item: nextFormData[index],
};
})
: generateKeyedFormData(nextFormData);
return {
keyedFormData: newKeyedFormData,
};
}

get itemTitle() {
const { schema } = this.props;
return schema.items.title || schema.items.description || "Item";
Expand Down Expand Up @@ -217,24 +264,40 @@ class ArrayField extends Component {

onAddClick = event => {
event.preventDefault();
const { schema, formData, registry = getDefaultRegistry() } = this.props;
const { schema, registry = getDefaultRegistry(), onChange } = this.props;
const { definitions } = registry;
let itemSchema = schema.items;
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
itemSchema = schema.additionalItems;
}
this.props.onChange([
...formData,
getDefaultFormState(itemSchema, undefined, definitions),
]);
const newFormDataRow = getDefaultFormState(
itemSchema,
undefined,
definitions
);
const newKeyedFormData = [
...this.state.keyedFormData,
{
key: generateRowId(),
item: newFormDataRow,
},
];

this.setState(
{
keyedFormData: newKeyedFormData,
},
() => onChange(keyedToPlainFormData(newKeyedFormData))
);
};

onDropIndexClick = index => {
return event => {
if (event) {
event.preventDefault();
}
const { formData, onChange } = this.props;
const { onChange } = this.props;
const { keyedFormData } = this.state;
// refs #195: revalidate to ensure properly reindexing errors
let newErrorSchema;
if (this.props.errorSchema) {
Expand All @@ -249,7 +312,13 @@ class ArrayField extends Component {
}
}
}
onChange(formData.filter((_, i) => i !== index), newErrorSchema);
const newKeyedFormData = keyedFormData.filter((_, i) => i !== index);
this.setState(
{
keyedFormData: newKeyedFormData,
},
() => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema)
);
};
};

Expand All @@ -259,7 +328,7 @@ class ArrayField extends Component {
event.preventDefault();
event.target.blur();
}
const { formData, onChange } = this.props;
const { onChange } = this.props;
let newErrorSchema;
if (this.props.errorSchema) {
newErrorSchema = {};
Expand All @@ -275,18 +344,24 @@ class ArrayField extends Component {
}
}

const { keyedFormData } = this.state;
function reOrderArray() {
// Copy item
let newFormData = formData.slice();
let _newKeyedFormData = keyedFormData.slice();

// Moves item from index to newIndex
newFormData.splice(index, 1);
newFormData.splice(newIndex, 0, formData[index]);
_newKeyedFormData.splice(index, 1);
_newKeyedFormData.splice(newIndex, 0, keyedFormData[index]);

return newFormData;
return _newKeyedFormData;
}

onChange(reOrderArray(), newErrorSchema);
const newKeyedFormData = reOrderArray();
this.setState(
{
keyedFormData: newKeyedFormData,
},
() => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema)
);
};
};

Expand Down Expand Up @@ -367,7 +442,8 @@ class ArrayField extends Component {
const itemsSchema = retrieveSchema(schema.items, definitions);
const arrayProps = {
canAdd: this.canAddItem(formData),
items: formData.map((item, index) => {
items: this.state.keyedFormData.map((keyedItem, index) => {
const { key, item } = keyedItem;
const itemSchema = retrieveSchema(schema.items, definitions, item);
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemIdPrefix = idSchema.$id + "_" + index;
Expand All @@ -379,6 +455,7 @@ class ArrayField extends Component {
idPrefix
);
return this.renderArrayFieldItem({
key,
index,
canMoveUp: index > 0,
canMoveDown: index < formData.length - 1,
Expand Down Expand Up @@ -544,7 +621,8 @@ class ArrayField extends Component {
disabled,
idSchema,
formData,
items: items.map((item, index) => {
items: this.state.keyedFormData.map((keyedItem, index) => {
const { key, item } = keyedItem;
const additional = index >= itemSchemas.length;
const itemSchema = additional
? retrieveSchema(schema.additionalItems, definitions, item)
Expand All @@ -565,6 +643,7 @@ class ArrayField extends Component {
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;

return this.renderArrayFieldItem({
key,
index,
canRemove: additional,
canMoveUp: index >= itemSchemas.length + 1,
Expand Down Expand Up @@ -597,6 +676,7 @@ class ArrayField extends Component {

renderArrayFieldItem(props) {
const {
key,
index,
canRemove = true,
canMoveUp = true,
Expand Down Expand Up @@ -658,6 +738,7 @@ class ArrayField extends Component {
hasMoveDown: has.moveDown,
hasRemove: has.remove,
index,
key,
onDropIndexClick: this.onDropIndexClick,
onReorderClick: this.onReorderClick,
readonly,
Expand All @@ -669,4 +750,6 @@ if (process.env.NODE_ENV !== "production") {
ArrayField.propTypes = types.fieldProps;
}

polyfill(ArrayField);

export default ArrayField;
Loading

0 comments on commit 20796c2

Please sign in to comment.