Skip to content

Commit

Permalink
feat: enable drag-and-drop of array items
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Oct 17, 2023
1 parent fdb6f32 commit 12800f8
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 67 deletions.
168 changes: 113 additions & 55 deletions packages/core/components/InputOrGroup/fields/ArrayField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,39 @@ import styles from "../../styles.module.css";
import { List, Trash } from "react-feather";
import { InputOrGroup, type InputProps } from "../..";
import { IconButton } from "../../../IconButton";
import { replace } from "../../../../lib";
import { reorder, replace } from "../../../../lib";
import DroppableStrictMode from "../../../DroppableStrictMode";
import { DragDropContext } from "react-beautiful-dnd";
import { Draggable } from "../../../Draggable";
import { generateId } from "../../../../lib/generate-id";
import { useEffect, useState } from "react";

const getClassName = getClassNameFactory("Input", styles);

type ItemWithId = {
_arrayId: string;
data: any;
};

export const ArrayField = ({
field,
onChange,
value,
name,
label,
}: InputProps) => {
const [valueWithIds, setValueWithIds] = useState<ItemWithId[]>(value);

// Create a mirror of value with IDs added for drag and drop
useEffect(() => {
const newValueWithIds = value.map((item, idx) => ({
_arrayId: valueWithIds[idx]?._arrayId || generateId("ArrayItem"),
data: item,
}));

setValueWithIds(newValueWithIds);
}, [value]);

if (!field.arrayFields) {
return null;
}
Expand All @@ -26,65 +48,101 @@ export const ArrayField = ({
</div>
{label || name}
</b>
<div className={getClassName("array")}>
{Array.isArray(value) ? (
value.map((item, i) => (
<details key={`${name}_${i}`} className={getClassName("arrayItem")}>
<summary>
{field.getItemSummary
? field.getItemSummary(item, i)
: `Item #${i}`}
<DragDropContext
onDragEnd={(event) => {
if (event.destination) {
const newValue: ItemWithId[] = reorder(
valueWithIds,
event.source.index,
event.destination?.index
);

setValueWithIds(newValue);
onChange(newValue.map(({ _arrayId, data }) => data));
}
}}
>
<DroppableStrictMode droppableId="array">
{(provided) => {
return (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={getClassName("array")}
>
{Array.isArray(value) ? (
valueWithIds.map(({ _arrayId, data }, i) => (
<Draggable id={_arrayId} index={i} key={_arrayId}>
<details className={getClassName("arrayItem")}>
<summary>
{field.getItemSummary
? field.getItemSummary(data, i)
: `Item #${i}`}

<div className={getClassName("arrayItemAction")}>
<IconButton
onClick={() => {
const existingValue = value || [];
const existingValueWithIds = valueWithIds || [];

existingValue.splice(i, 1);
existingValueWithIds.splice(i, 1);

<div className={getClassName("arrayItemAction")}>
<IconButton
onClick={() => {
const existingValue = value || [];
onChange(existingValue);
setValueWithIds(existingValueWithIds);
}}
title="Delete"
>
<Trash size={21} />
</IconButton>
</div>
</summary>
<fieldset className={getClassName("fieldset")}>
{Object.keys(field.arrayFields!).map((fieldName) => {
const subField = field.arrayFields![fieldName];

existingValue.splice(i, 1);
onChange(existingValue);
}}
title="Delete"
>
<Trash size={21} />
</IconButton>
</div>
</summary>
<fieldset className={getClassName("fieldset")}>
{Object.keys(field.arrayFields!).map((fieldName) => {
const subField = field.arrayFields![fieldName];
return (
<InputOrGroup
key={`${name}_${i}_${fieldName}`}
name={`${name}_${i}_${fieldName}`}
label={subField.label || fieldName}
field={subField}
value={data[fieldName]}
onChange={(val) =>
onChange(
replace(value, i, {
...data,
[fieldName]: val,
})
)
}
/>
);
})}
</fieldset>
</details>
</Draggable>
))
) : (
<div />
)}

return (
<InputOrGroup
key={`${name}_${i}_${fieldName}`}
name={`${name}_${i}_${fieldName}`}
label={subField.label || fieldName}
field={subField}
value={item[fieldName]}
onChange={(val) =>
onChange(
replace(value, i, { ...item, [fieldName]: val })
)
}
/>
);
})}
</fieldset>
</details>
))
) : (
<div />
)}
{provided.placeholder}

<button
className={getClassName("addButton")}
onClick={() => {
const existingValue = value || [];
onChange([...existingValue, field.defaultItemProps || {}]);
<button
className={getClassName("addButton")}
onClick={() => {
const existingValue = value || [];
onChange([...existingValue, field.defaultItemProps || {}]);
}}
>
+ Add item
</button>
</div>
);
}}
>
+ Add item
</button>
</div>
</DroppableStrictMode>
</DragDropContext>
</div>
);
};
21 changes: 9 additions & 12 deletions packages/core/components/InputOrGroup/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,15 @@
padding: 16px;
}

.Input-arrayItem
> .Input-fieldset
.Input
+ .Input-arrayItem
> .Input-fieldset
.Input {
margin-top: 16px;
.Input-array {
display: flex;
flex-direction: column;
}

.Input-arrayItem {
display: block;
overflow: hidden;
margin-bottom: 12px;
}

.Input-arrayItem > .Input-fieldset .Input-label {
Expand All @@ -128,10 +130,6 @@
cursor: pointer;
}

.Input-arrayItem + .Input-arrayItem {
margin-top: 12px;
}

.Input-addButton {
background-color: white;
border: none;
Expand All @@ -140,7 +138,6 @@
cursor: pointer;
width: 100%;
margin: 0;
margin-top: 12px;
padding: 12px 16px;
text-align: left;
}
Expand Down

0 comments on commit 12800f8

Please sign in to comment.