I've successfully implemented Beautiful DnD in a React app where the draggable items are Material UI Expansion Panels. I have the list of items resorting onDragEnd and saving the new sorted list in state.
I'm using React hooks, useState, Material-UI Expansion Panel, and React-Beautiful-DnD.
When this part of the app loads, the first expansion panel is expanded and all others are collapsed.
What I've been trying to get working is how to close the expansion panel [onDragStart | onBeforeDragStart | onDragUpdate] and then open the expansion panel onDragEnd.
I have stored the state of each expansion panel along with other info in an array that loops over and renders each expansion panel:
{
name: string,
expanded: string,
...
}
I'm thinking this is an issue where the state is not getting updated to where the expansion panel is not picking up the change.
I've tried using the snapshot.isDragging on the item to change the expansion panels expanded state and even tried targeting the specific expansion panel, finding the corresponding item in the state list and updating the expanded prop onDragStart, onBeforeDragStart, and onDragUpdate. None of these have worked so far.
Here's part of the component handling the DnD
const newArray = ReorderList(srcList, ixSelectedItem, 0);
const panelState: Array<ExpansionState> = [];
// eslint-disable-next-line array-callback-return
newArray.map(item => {
panelState.push({
name: item as string,
expanded: item === selectedItem,
isDragging: false
});
});
const [itemListWithState, setItemListWithState] = useState(panelState);
const handleOnDragEnd = (result: any): void => {
const { destination, draggableId, source } = result;
if (!destination) {
return;
}
if (destination.droppableId === source.droppableId && destination.index === source.index) {
return;
}
const newList = ReorderList(itemListWithState, source.index, destination.index) as Array<ExpansionState>;
const ix = newList.findIndex(item => item.name === draggableId);
newList[ix].isDragging = false;
newList[ix].expanded = !newList[ix].expanded;
setItemListWithState(newList);
};
const handleOnDragUpdate = (result: any): void => {
const { draggableId } = result;
const ix = itemListWithState.findIndex(item => item.name === draggableId);
if (!itemListWithState[ix].isDragging) {
itemListWithState[ix].expanded = !itemListWithState[ix].expanded;
itemListWithState[ix].isDragging = true;
}
};
...
...
return (
<DragDropContext onDragEnd={handleOnDragEnd} onDragUpdate={handleOnDragUpdate}>
<Droppable droppableId="list">
{(provided, snapshot) => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
{itemListWithState.map((item: ExpansionState, index: number) => {
return (
<Draggable key={item.name} draggableId={item.name} index={index}>
{(provided, snapshot) => {
return (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<GraphContainer id={item.name} isGraphExpanded={item.expanded} {...props}></GraphContainer>
</div>
);
}}
</Draggable>
);
})}
{provided.placeholder}
</div>
);
}}
</Droppable>
</DragDropContext>
);
Here's the component housing the expansion panel:
return (
<div style={{ margin: '20px 0' }}>
<Panel
expanded={props.isGraphExpanded}
summaryTitle={getResourceString(props.id, Resources.Performance)}
summaryDetail={props.graph}
isDraggable={true}
subPanel={
<Panel
expanded={false}
summaryTitle={getResourceString(props.id, Resources.DataTable)}
summaryDetail={props.table}
isDraggable={false}
></Panel>
}
></Panel>
</div>
);
Related
Continuing the question
I have a simple react project stackblitz (update) with react-beautiful-dnd.
const onDragEnd = async (result) => {
const { source, destination } = result;
if (!destination) {
return;
}
if (destination.droppableId !== source.droppableId) {
setItems((items) =>
items.map((item) =>
item.id === parseInt(result.draggableId)
? {
...item,
category: parseInt(result.destination.droppableId),
}
: item
)
);
} else {
const itemsCopy = [...items];
const [removed] = itemsCopy.splice(source.index, 1);
itemsCopy.splice(destination.index, 0, removed);
setItems(itemsCopy);
}
};
....
<DragDropContext onDragEnd={onDragEnd}>
{categories.map((category, index) => (
<Droppable droppableId={category.id.toString()}>
{(provided) => (
<div ref={provided.innerRef}>
<Category
key={category.id}
provided={provided}
category={category}
index={index}
items={items}
/>
</div>
)}
</Droppable>
))}
</DragDropContext>
How can I move categories around (drgging category)? At the moment they are static.
How can I implement dragging items from one category to another with changing the value of item.category ?
It is exactly the same as my previous reply.
You have some DOM element you want to drop things inside, you make it a Droppable. You want something to be draggable inside a droppable you make it a Draggable.
I refactored my previous example so the categories can be moved.
https://stackblitz.com/edit/react-1j37eg?file=src/App.js
Hopefully you understand better how it works.
Right now I have a page that looks like this:
Im displaying some ClosedExerciseComp-components basically small cards with their respective id on them. When I press the Add exercise-button a new exercise card will emerge.
Using react-beautiful-dnd the cards are fully drag-and-droppable.However sometimes when I drop an Item I get this error:
It seems to occur at random when I drop an Item but I can get the error to trigger more reliable if I spam drag-and-drop in the small gap between the cards. I demonstrate here.
My code looks like this (I know it's a tad long) (simplified aesthetically):
import React, { useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import ExerciseComp from "../components/ExerciseComp/ExerciseComp";
const EditorMainPage = () => {
const [exercises, setExercises] = useState([]);
const addExercise = () => {
const exercise = {
id: "exercise-" + Date.now(),
...,
};
setExercises((exercises) => [...exercises, exercise]);
};
const handleDragEnd = (result) => {
if (!result.destination) return;
if (result.destination.index === result.source.index) return;
const items = [...exercises];
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setExercises(items);
};
const Exercises = () => {
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="exercises-dnd">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{exercises.map((exercise, index) => (
<Draggable
key={exercise.id}
draggableId={exercise.id}
index={index}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ExerciseComp
id={exercise.id}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
return (
<div>
<Exercises />
<button onClick={() => addExercise()}>Add exercise</button>
</div>
);
};
export default EditorMainPage;
I hope you can help me understand why suddenly react decides that an existing draggable doesn't exist???
I am working on React and DND, I am experimenting with an example. For example, I have two columns between which I can drag-and-drop the cards back and forth that works fine, now I want the second column to contain no more than 4 items, but items can be dragged out. In the documentation I read that this was done by setting 'isDropDisabled' to true. Then I can indeed drag no more to this column, and I can leave. Now I've tried influencing that with the this.state.isUnlocked variable. So if there are more than 4 this value will be true otherwise this value will be false. Only the code does not respond to this.state.isUnlocked. Only if I type it directly in but then it is no longer variable. Is there anyone who can help me with this?
This is my code so far:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import { Row, Col, Card, CardHeader, CardBody, Button } from "shards-react";
// fake data generator
const getItems = (count, offset = 0) =>
Array.from({ length: count }, (v, k) => k).map(k => ({
id: `item-${k + offset}`,
content: `item ${k + offset}`
}));
// a little function to help us with reordering the result
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
/**
* Moves an item from one list to another list.
*/
const move = (source, destination, droppableSource, droppableDestination) => {
const sourceClone = Array.from(source);
const destClone = Array.from(destination);
const [removed] = sourceClone.splice(droppableSource.index, 1);
destClone.splice(droppableDestination.index, 0, removed);
const result = {};
result[droppableSource.droppableId] = sourceClone;
result[droppableDestination.droppableId] = destClone;
return result;
};
const grid = 8;
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: 'none',
padding: grid * 2,
margin: `0 0 ${grid}px 0`,
// change background colour if dragging
background: isDragging ? 'lightgreen' : 'grey',
// styles we need to apply on draggables
...draggableStyle
});
const getListStyle = isDraggingOver => ({
background: isDraggingOver ? 'lightblue' : 'lightgrey',
padding: grid,
width: 250
});
class qSortOverview extends React.Component {
constructor(props) {
super(props);
this.state = {
items: getItems(10),
selected: getItems(2, 10),
iUnlocked: false,
};
this.canvasRef = React.createRef();
}
/**
* A semi-generic way to handle multiple lists. Matches
* the IDs of the droppable container to the names of the
* source arrays stored in the state.
*/
id2List = {
droppable: 'items',
droppable2: 'selected'
};
getList = id => this.state[this.id2List[id]];
onDragEnd = result => {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
const items = reorder(
this.getList(source.droppableId),
source.index,
destination.index
);
let state = { items };
if (source.droppableId === 'droppable2') {
state = { selected: items };
}
this.setState(state);
} else {
const result = move(
this.getList(source.droppableId),
this.getList(destination.droppableId),
source,
destination
);
this.setState({
items: result.droppable,
selected: result.droppable2
});
//Linker rij
console.log(this.state.items);
if(this.state.items.length > 4){
console.log('to much items');
}
//Rechter rij
console.log(this.state.selected);
if(this.state.selected.length > 4){
this.setState({
isUnlocked: true,
})
}
}
};
// Normally you would want to split things out into separate components.
// But in this example everything is just done in one place for simplicity
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<Row>
<Col lg="6" md="6" sm="6" className="mb-4">
<Droppable droppableId="droppable" >
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}>
{this.state.items.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}>
{item.content}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</Col>
<Col lg="6 " md="6" sm="6" className="mb-4">
<Droppable droppableId="droppable2" isDropDisabled={this.state.isUnlocked}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}>
{this.state.selected.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}>
{item.content}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</Col>
</Row>
</DragDropContext>
);
}
};
export default qSortOverview;
At the moment the right column contains 2 items, when there are 4 items it should no longer be possible to add items, just remove them.
You need change this rows:
if (this.state.selected.length > 4) {
this.setState({
isUnlocked: true
});
}
to
if (this.state.selected.length > 4) {
this.setState({
isUnlocked: true
});
} else {
this.setState({
isUnlocked: false
});
}
I'm building an infinite loading list of users with react-window. In the list, every item has an icon button from Material-UI for further action.
But I can't mount the menu near the icon as the icon button would be re-rendered when setting anchorEl for the menu to be opened. A gif clip:
The question is related to React Material-UI menu anchor broken by react-window list but has more HOC. The code is listed here. I wish I could use my codesandbox for demonstration but the react-measure keeps growing height.
function App() {
const [anchorEl, setAnchorEl] = useState(null);
const openMenu = React.useCallback(e => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
console.log("target", e.currentTarget);
}, []);
const handleClose = () => {
setAnchorEl(null);
};
const [items, setItems] = React.useState([]);
const isItemLoaded = index => {
const c = index < items.length;
// console.log("isItemLoaded", index, c);
return c;
};
const loadMoreItems = (startIndex, stopIndex) => {
console.log("loadMoreItems", startIndex, items);
setItems(items.concat(Array(10).fill({ name: "1", size: startIndex })));
};
const innerET = React.forwardRef((props, ref) => (
<div ref={ref} {...props} />
));
const Row = React.useCallback(
({ index, style }) => {
console.log("Row", items, index);
return items[index] ? (
<ListItem style={style} key={index}>
<Button variant="contained" color="primary" onClick={openMenu}>
Row {index}: {items[index].size}
</Button>
</ListItem>
) : null;
},
[items, openMenu]
);
const innerListType = React.forwardRef((props, ref) => (
<List ref={ref} {...props} />
));
return (
<div className="App">
<div className="ceiling">Something at top</div>
<div className="interest">
<Menu anchorEl={anchorEl} onClose={handleClose} />
<Measure bounds offset>
{({ measureRef, contentRect }) => {
const height = Math.min(
contentRect && contentRect.offset
? document.getElementById("root").getBoundingClientRect()
.height - contentRect.offset.top
: itemSize * items.length,
itemSize * items.length
);
console.log(
"bounds",
height,
contentRect.bounds,
contentRect.offset
);
return (
<div>
<div />
<div ref={measureRef} className="measurement">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={height}
width={
contentRect.bounds !== undefined &&
contentRect.bounds.width !== undefined
? contentRect.bounds.width
: -1
}
itemCount={itemCount}
itemSize={itemSize}
onItemsRendered={onItemsRendered}
ref={ref}
innerElementType={innerET}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
}}
</Measure>
</div>
</div>
);
}
As far as I understand, the ripple effect would trigger a re-render in the box with the first click. Moreover, the second click after the re-render upon clicking would not trigger a re-render. That feels even more peculiar to me.
EDIT: I fixed the first sandbox. And by using Material UI's list, this issue is reproducible. https://codesandbox.io/s/blissful-butterfly-qn3g7
So the problem lies in using innerElementType property.
It turns out that a hook is needed.
const innerListType = React.useMemo(() => {
return React.forwardRef((props, ref) => (
<List component="div" ref={ref} {...props} />
));
}, []);
To fix my problems, hooks for handling events are needed to be handled more carefully.
I have react-beautiful-dnd horizontal multiple list(6 rows with the same items in each row) with the same items in each list.
I want to delete individual selected items from each list, but just having a button component with onClick fires the onClick while rendering the lists itself. How do i configure the list so that an individual item is deleted from that list when i click on the close/delete (x) button?
Below, is my code,
import React, { Component } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import {Button, Icon} from 'semantic-ui-react'
// a little function to help us with reordering the result
const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const grid = 12;
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: 'none',
padding: grid / 2,
margin: `0 ${grid}px 0 0`,
// change background colour if dragging
background: isDragging ? 'lightgreen' : 'lightgray',
// styles we need to apply on draggables
...draggableStyle,
});
const getListStyle = isDraggingOver => ({
background: isDraggingOver ? 'lightblue' : 'white',
display: 'flex',
padding: grid,
overflow: 'auto',
});
class DragAndDrop extends Component {
constructor(props) {
super(props);
this.state = {
items: this.props.uniqueEntries
};
this.onDragEnd = this.onDragEnd.bind(this)
this.removeSubject = this.removeSubject.bind(this)
}
onDragEnd(result) {
// dropped outside the list
if (!result.destination) {
return;
}
const items = reorder(
this.state.items,
result.source.index,
result.destination.index
);
this.setState({
items,
});
}
componentWillReceiveProps(newProps){
this.setState({
items : newProps.uniqueEntries
})
}
removeItem = (index) => {
this.state.items.splice(index, 1)
}
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<Droppable droppableId="droppable" direction="horizontal">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
{...provided.droppableProps}
>
{this.state.items.map((item, index) => (
<Draggable key={item.Id} draggableId={item.Id}
index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<Button icon size='mini'
style={{backgroundColor : 'lightgray' ,padding:
'0', float: 'right'}}
onClick = {this.removeItem(index)}>
<Icon name='close' />
</Button>
{item.name}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}
}
export default DragAndDrop
A Note for this bug that might hit you later and future viewers. 😋
You are not supposed to mutate the state directly. It will have some side effects or none at all.
I also recommend you generate unique IDs because if you create more list, it will also have some unexpected result even react-beautiful-dnd won't notice.
If you want to update or remove state data, use setState(). First modify existing data with a copy. Then assign the new value.
removeItem(index) {
// do magic here to filter out the unwanted element
// Update via 'setState'
this.setState({
items : newModifiedItems
})
}
For an example, my trial method below:
removeItem(e) {
e.preventDefault();
// give your single list a className so you can select them all
const sourceList = document.querySelectorAll(".list-name");
const arrList= Array.from(sourceList);
// make shallow copy of your state data
const newItems = Array.from(this.state.items);
// Find index element from whole list Arr by traversing the DOM
const removeItemIndex = arrList.indexOf(e.target.parentElement);
// Remove it
newItems.splice(removeItemIndex, 1);
this.setState({
items: newItems
})
}
I found out why it was firing at render, instead of passing the function i was initiating it so through the loop it was getting called. Then i did this and it worked. May be this will help someone who might face a similar issue.
<Button icon size='mini'
style={{backgroundColor : 'lightgray' ,padding: '0',
float: 'right', marginLeft:'15px'}}
onClick = {() => this.removeItem(index)}>
<Icon name='close' />
</Button>