Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array items now have unique, stable keys (#1046) #1335

Merged
merged 8 commits into from
Jul 9, 2019
Merged
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
13 changes: 13 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"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",
"shortid": "^2.2.14"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
Expand Down
127 changes: 112 additions & 15 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
toIdSchema,
getDefaultRegistry,
} from "../../utils";
import shortid from "shortid";

function ArrayFieldTitle({ TitleField, idSchema, title, required }) {
if (!title) {
Expand Down Expand Up @@ -174,6 +175,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 +205,51 @@ class ArrayField extends Component {
autofocus: false,
};

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

componentWillReceiveProps(nextProps) {
const nextFormData = nextProps.formData;
const previousKeyedFormData = this.state.keyedFormData;
const newKeyedFormData =
nextFormData.length === previousKeyedFormData.length
? previousKeyedFormData.map((previousKeyedFormDatum, index) => {
return {
key: previousKeyedFormDatum.key,
item: nextFormData[index],
};
})
: generateKeyedFormData(nextFormData);
epicfaace marked this conversation as resolved.
Show resolved Hide resolved
this.setState({
keyedFormData: newKeyedFormData,
});
}

/*
// React 16.3 replaces componentWillReceiveProps with getDerivedStateFromProps
//
static getDerivedStateFromProps(nextProps, prevState) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be cleaner (and easier to migrate to React 16) just to use getDerivedStateFromProps and https://github.com/reactjs/react-lifecycles-compat for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in latest commit.

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 +282,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 +330,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 +346,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 +362,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 +460,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 +473,7 @@ class ArrayField extends Component {
idPrefix
);
return this.renderArrayFieldItem({
key,
index,
canMoveUp: index > 0,
canMoveDown: index < formData.length - 1,
Expand Down Expand Up @@ -597,6 +692,7 @@ class ArrayField extends Component {

renderArrayFieldItem(props) {
const {
key,
index,
canRemove = true,
canMoveUp = true,
Expand Down Expand Up @@ -658,6 +754,7 @@ class ArrayField extends Component {
hasMoveDown: has.moveDown,
hasRemove: has.remove,
index,
key,
onDropIndexClick: this.onDropIndexClick,
onReorderClick: this.onReorderClick,
readonly,
Expand Down