Deleting individual items from react-beautiful-dnd horizontal list - reactjs

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>

Related

React gallery App. I want Add tags to an image individually but the tag is being added to all images. How can I solve this?

**> This is my Gallery Component **
import React, {useState} from 'react';
import useFirestore from '../hooks/useFirestore';
import { motion } from 'framer-motion';
const Gallery = ({ setSelectedImg }) => {
const { docs } = useFirestore('images');
here im setting the state as a Tags array
const [tags, setTags] = useState([""]);
const addTag = (e) => {
if (e.key === "Enter") {
if (e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = "";
}
}
};
functions for adding and removing Tags
const removeTag = (removedTag) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
return (
<>
<div className="img-grid">
{docs && docs.map(doc => (
< motion.div className="img-wrap" key={doc.id}
layout
whileHover={{ opacity: 1 }}s
onClick={() => setSelectedImg(doc.url)}
>
here Im adding the Tag input to each Image...the problem is that when adding a Tag is added to all the pictures. I want to add the tags for the image that I´m selecting.
<div className="tag-container">
{tags.map((tag, ) => {
return (
<div key={doc.id} className="tag">
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
<motion.img src={doc.url} alt="uploaded pic"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
</motion.img>
</motion.div>
))}
</div>
</>
)
}
export default Gallery;
The tags array that you are using to store values entered by the user are not unique with respect to each image item. Meaning, every image item in your program is using the same instance of the tags array, what you need to do is
Either create an object that stores an array of tags for each image:
const [tagsObj, setTagsObj] = {}, then while adding a new tag for say image_1, you can simply do setTagsObj(prevObj => {...prevObj, image_1: [...prevObj?.image_1, newTagValue]},
Or create an Image Component which would then handle tags for a single image:
Gallery Component:
{
imageList.map(imageEl =>
<ImageItem key={imageEl} image={imageEl} />
)
}
ImageItem Component:
import {useState} from 'react';
export default function ImageItem({image}) {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === "Enter") {
const newVal = e.target.value;
if (newVal.length > 0) {
setTags(prevTags => [...prevTags, newVal]);
e.target.value = '';
}
}
};
const removeTag = (removedTag) => {
setTags(prevTags => prevTags.filter((tag) => tag !== removedTag));
}
return (
<div style={{margin: '12px', padding: '12px', width: '100px', height:'100px', display:'flex', flexDirection: 'column', alignItems:'center'}}>
<span>{image}</span>
{tags.map((tag, index) => {
return (
<div key={tag+index}>
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
);
}
Refer this sandbox for ease, if available Gallery unique image tags sandbox
I suggest using the second method, as it is easy to understand and debug later on.
I hope this helps, please accept the answer if it does!

React Spring translate animation not working and clicking on a list item seems to be delayed

I have a simple list of options in a menu like so:
Option 1
Option 2
Option 3
When the user clicks on an option, there should be a highlighted bar that shows which one they selected. And when the user clicks on different options, the highlighted bar should slide up and down depending on what they chose. I'm trying to use react-spring, but I can't seem to get the animation and clicking behavior to happen properly.
With my current code, the highlighted bar does not slide up and down; it just shows and hides upon user selection. And clicking on an option once does not put the highlighted bar on it, instead, I have to click twice for it to show up correctly on the selected option.
Help is appreciated! This is my first time using react-spring so I'm a bit lost on this.
Below is the code snippet for the animations and rendering the component:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
And here is the custom component, TextWithIcon:
// Interfaces
interface TextWithIconProps {
containerStyle?: Record<any, any>;
icon: ReactElement;
text: string;
textStyle?: Record<any, any>;
}
// TextWithIcon component
const TextWithIcon: React.FC<TextWithIconProps> = ({ containerStyle, icon, text, textStyle}) => {
return (
<div id='menu-items' style={{...styles.container, ...containerStyle}}>
{icon}
<Text style={{...styles.text, ...textStyle}}>{text}</Text>
</div>
)
};
You have to set the duration property of the config property inside the useSpring's parameter to a value. Try the following:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
config: {
duration: 1250 //this can be a different number
}
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
References:
StackOverflow. Animation duration in React Spring.https://stackoverflow.com/a/54076843/8121551. (Accessed August 23, 2021).

Closing/Opening Expansion Panel onDragStart/onDragEnd with React Beautiful DnD

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>
);

react-beautifull-dnd limit number of items

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
});
}

Callback function, responsible for updating state, passed as props to child component not triggering a state update

The callback function (lies in Images component) is responsible for making a state update. I'm passing that function as props to the Modal component, and within it it's being passed into the ModalPanel component.
That function is used to set the state property, display, to false which will close the modal. Currently, that function is not working as intended.
Image Component:
class Images extends Component {
state = {
display: false,
activeIndex: 0
};
handleModalDisplay = activeIndex => {
this.setState(() => {
return {
activeIndex,
display: true
};
});
};
closeModal = () => {
this.setState(() => {
return { display: false };
});
}
render() {
const { imageData, width } = this.props;
return (
<div>
{imageData.resources.map((image, index) => (
<a
key={index}
onClick={() => this.handleModalDisplay(index)}
>
<Modal
closeModal={this.closeModal}
display={this.state.display}
activeIndex={this.state.activeIndex}
selectedIndex={index}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</Modal>
</a>
))}
</div>
);
}
}
export default Images;
Modal Component:
const overlayStyle = {
position: 'fixed',
zIndex: '1',
paddingTop: '100px',
left: '0',
top: '0',
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.9)'
};
const button = {
borderRadius: '5px',
backgroundColor: '#FFF',
zIndex: '10'
};
class ModalPanel extends Component {
render() {
const { display } = this.props;
console.log(display)
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
</div>
);
return <div>{display ? overlay : null}</div>;
}
}
class Modal extends Component {
render() {
const {
activeIndex,
children,
selectedIndex,
display,
closeModal
} = this.props;
let modalPanel = null;
if (activeIndex === selectedIndex) {
modalPanel = (
<ModalPanel display={this.props.display} closeModal={this.props.closeModal} />
);
}
return (
<div>
{modalPanel}
{children}
</div>
);
}
}
export default Modal;
links to code
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Images.js
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Modal.js
You're dealing with this modal through a very non-react and hacky way.
Essentially, in your approach, all the modals are always there, and when you click on image, ALL modals display state becomes true, and you match the index number to decide which content to show.
I suspect it's not working due to the multiple children of same key in Modal or Modal Panel.
I strongly suggest you to ditch current approach. Here's my suggestions:
Only a single <Modal/> in Images component.
Add selectedImage state to your Images component. Every time you click on an image, you set selectedImage to that clicked image object.
Pass selectedImage down to Modal to display the content you want.
This way, there is only ONE modal rendered at all time. The content changes dynamically depending on what image you click.
This is the working code I tweaked from your repo:
(I'm not sure what to display as Modal content so I display public_id of image)
Images Component
class Images extends Component {
state = {
display: false,
selectedImage: null
};
handleModalDisplay = selectedImage => {
this.setState({
selectedImage,
display: true
})
};
closeModal = () => {
//shorter way of writing setState
this.setState({display: false})
}
render() {
const { imageData, width } = this.props;
return (
<div>
<Modal
closeModal={this.closeModal}
display={this.state.display}
selectedImage={this.state.selectedImage}
/>
{imageData.resources.map((image, index) => (
<a
//Only use index as key as last resort
key={ image.public_id }
onClick={() => this.handleModalDisplay(image)}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</a>
))}
</div>
);
}
}
Modal Component
class Modal extends Component {
render() {
const { display, closeModal, selectedImage } = this.props;
const overlayContent = () => {
if (!selectedImage) return null; //for when no image is selected
return (
//Here you dynamically display the content of modal using selectedImage
<h1 style={{color: 'white'}}>{selectedImage.public_id}</h1>
)
}
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
{
//Show Modal Content
overlayContent()
}
</div>
);
return <div>{display ? overlay : null}</div>;
}
}

Resources