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.
Related
I'm creating a todo app and I have some problem
I got a feature to add a todo item to favorite and so objects with "favorite: true" should be first at the array of all todos.
useEffect(() => {
// Sort array of objects with favorite true were first
setTodos(todos.sort((x, y) => Number(y.favorite) - Number(x.favorite)));
}, [todos]);
//Add to favorite function
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: e.favorite,
};
}
return e;
})
);
};
<div className="favorite-button" onClick={() => favoriteHandler()}>
{favorite ? (
<img src={FavoriteFilledIcon} alt="Remove from favorite" />
) : (
<img src={FavoriteIcon} alt="Add to favorite" />
)}
</div>
but if click on a favoriteHandler console log tells me that objects with favorite: true is at the start of array, but todos.map doesn't re-render this changes, why?
// App.js
{todos.map((e, i) => (
<TodoItem
completed={e.completed}
id={e.id}
key={i}
text={e.name}
setTodos={setTodos}
todos={todos}
favorite={e.favorite}
/>
))}
There is no re-render because no state is being change.
In favoriteHandler function you are just setting the favorite as the initial favorite you supplied as props. If you're trying to toggle favorite on click you should consider using boolean operator as below.
const favoriteHandler = () => {
setTodos(
todos.map((e) => {
if (e.id === id) {
return {
...e,
favorite: !e.favorite,
};
}
return e;
})
);
};
In terms of sorting you won't need to use useEffect hook but re-write as following:
todos
.sort((x, y) => Number(y.favorite) - Number(x.favorite))
.map((todo) => {
return (
<TodoItem
completed={todo.completed}
id={todo.id}
key={todo.id}
text={todo.text}
setTodos={setTodos}
todos={todos}
favorite={todo.favorite}
/>
);
});
For Improvement in your code I suggest the following.
Try to use a more straightforward variable name like todo and stay away from variables name like e.
Instead of attaching onClick on div tag, use button tags.
When you are trying to map over an array and have a unique key, you should use that key instead of index.
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???
What's up ?
I'm trying to reproduce the sliding button effect from frontity home page with ReactJS (NextJS).
Sliding buttons from Frontity
I managed to create the sliding button effect BUT I'm struggling with state management.
I have all my objects mapped with a "isActive : true/false" element and I would to create a function that put "isActive : true" on the clicked button BUT put "isActive: false" on all the other buttons.
I don't know the syntax / method for that kind of stuff.
Please, take a look at my codesandbox for more clarity (using react hooks):
https://codesandbox.io/s/busy-shirley-lgx96
Thank you very much people :)
UPDATE: As pointed out above by Drew Reese, even more cleaner/easier is to have just one activeIndex state:
const TabButtons = () => {
const [activeIndex, setActiveIndex] = useState(0);
const handleButtonClick = (index) => {
setActiveIndex(index);
};
return (
<>
<ButtonsWrapper>
{TabButtonsItems.map((item, index) => (
<div key={item.id}>
<TabButtonItem
label={item.label}
ItemOrderlist={item.id}
isActive={index === activeIndex}
onClick={() => handleButtonClick(index)}
/>
</div>
))}
<SlidingButton transformxbutton={activeIndex}></SlidingButton>
</ButtonsWrapper>
</>
);
};
I have made a slight modification of your TabButtons:
const TabButtons = () => {
const [buttonProps, setButtonProps] = useState(TabButtonsItems);
// //////////// STATE OF SLIDING BUTTON (TRANSLATE X ) ////////////
const [slidingbtn, setSlidingButton] = useState(0);
// //////////// HANDLE CLIK BUTTON ////////////
const HandleButtonState = (item, index) => {
setButtonProps((current) =>
current.map((i) => ({
...i,
isActive: item.id === i.id
}))
);
setSlidingButton(index);
};
return (
<>
<ButtonsWrapper>
{buttonProps.map((item, index) => (
<div key={item.id}>
<TabButtonItem
label={item.label}
ItemOrderlist={item.id}
isActive={item.isActive}
onClick={() => HandleButtonState(item, index)}
/>
</div>
))}
<SlidingButton transformxbutton={slidingbtn}></SlidingButton>
</ButtonsWrapper>
</>
);
};
When we click on a button, we set its isActive state to true and all the rest buttons to isActive: false. We also should use state, since we also declared it. Changing state will force component to re-render, also we are not mutating anything, but recreating state for buttons.
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>
);
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.