React-dnd multiple elements - javascript

I can make react-dnd drag easily having a single element to drag over however I have array of 4 fields I'd like to make draggable. In my example code down below it creates four boxes from mapping the array and each box has a className of 'element'. Which should make them all draggable however they won't move.
Here is my drag code:
const ELEMENT = 'element';
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: ELEMENT,
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
}))
Here is my draggable element:
{FieldDetail.map((e,i) =>
<div key={i} ref={dragPreview} style={{ opacity: isDragging ? 0.5 : 1}}>
<div className='element' ref={drag}></div>
</div>
)}
Any ideas? Do I need to do something more within the type or className?

I had a similar problem - I discovered that you can call drag(ref) to take an ordinary ref and link it up with DnD.
For your use case, I would try pre-allocating the refs and calling drag on each one:
// generate refs
const refs = FieldDetal.map(x => useRef());
// call drag on each ref, linking it up with React DnD
refs.forEach(x => drag(x));
// And then when you render your component, use the refs
{FieldDetail.map((e,i) =>
<div key={i} ref={dragPreview} style={{ opacity: isDragging ? 0.5 : 1}}>
<div className='element' ref={refs[i]}></div>
</div>
)}
Not tested but this general idea should get you where you need to be. You can even make something both a drag and a drop target by calling drop(drag(ref)).

Related

How to expand MaterialUI Accordion by clicking on a separate component in React?

I have a map with 5 markers on it and accordion with 5 elements on the side. Each marker has a corresponding accordion.
I want to click on a marker and expand the corresponding accordion. Both accordion and markers on the map have the same key values (screenshot below).
I use Map() function to generate accordion as well as markers. Simplified code looks something like this:
function Markers() {
const map = useMap();
return (
loc.map(loc => {
return (
<Marker
icon={locationMarker}
key={loc.properties.id}
position={[loc.properties.y, loc.properties.x]}}}>
</Marker>
)
})
)
}
export default Markers
function LocationCard() {
return (
<Container>
{loc.map(loc => (
<Accordion key={loc.properties.id}>
<AccordionSummary>
<Typography> {loc.properties.title} </Typography>
</AccordionSummary>
<AccordionDetails>
<Typography> { loc.properties.description } </Typography>
</AccordionDetails>
</Accordion>
))
}
</Container>
);
}
export default LocationCard
I am basically looking for a functionality "On marker click, expand accordion".
Any idea how I can achieve this with my current setup?
Thanks
An accordion will internally store its own state (an uncontrolled component) to track if it's expanded but you can override it, making it a controlled component.
Go up the component tree of your markers + accordions to find the lowest common intersection where we can store some state and pass it down to both components.
Store the selected marker ID (or an array if you want to have many accordions open at once) and an event handler function to update the state when a marker is pressed.
const [selectedMarkerID, setSelectedMarkerID] = useState<number | undefined>();
const handleMarkerPressed = (id: number) => () => {
// If a marker is pressed a second time, close the accordion
setSelectedMarkerID(selectedMarkerID !== id ? id : undefined);
}
return (
<>
<MyMarkers markers={markers} handleMarkerPressed={handleMarkerPressed />
<MyAccordions markers={markers} selectedMarkerID={selectedMarkerID} />
</>)
Then in your markers.map function, set the expanded property like the following:
<Accordion
key={loc.properties.id}
expanded={selectedMarkerID}>
// If you want to disable opening of accordions, override the onClick
onClick={undefined}
>
And your marker should look something like:
<Marker
...
onClick={handleOpenMarker(marker.id)}
/>
An alternative to prop drilling the state + markers + event handler down both components would be some kind of store or context API
More info available in the MUI accordion docs
Edit
Here's a Codebox example of passing the state + event handler around to manage which accordions are expanded: https://codesandbox.io/s/intelligent-feather-x9grwv?file=/src/App.tsx
The goal is to keep the markers in sync with the accordions, to summarise:
The accordion state is hoisted up a level to the App.tsx component
The marker event handler + accordion event handler is passed down from App.tsx

How to get the position of a moving element in React Native

I am currently trying to develop an app with draggable elements using React Native and the library react Native draggable (https://www.npmjs.com/package/react-native-draggable).
My goal is to get the position of my Draggable element. (So I can change their color depending on where it is on the screen).
I tried different method but nothing worked for me. Here is the last thing I tried :
return (
<View>
<Draggable
onDragRelease={event =>
{const layout = event.nativeEvent.layout;
console.log('x:', layout.x);
console.log('y:', layout.y);
}
}
x={positionX}
y={positionY}
renderSize={56}
renderColor='#BDFF00'
renderText={'Drag me'}
// onDrag={getPosition}
/>
</View>
)
The code above returns this error (TypeError: undefined is not an object (evaluating 'layout.x'))
I also tried to do this :
const getPosition = (event) => {
console.log(event.locationX)
setPositionX(event.locationX)
setPositionY(event.locationY)
};
const changerColorFunction = () => {
}
return (
<View>
<Draggable
x={positionX}
y={positionY}
renderSize={56}
renderColor='#BDFF00'
renderText={'Drag me'}
onDrag={getPosition}
/>
</View>
)
Based on the react native Draggable documentation, but it is not working too. I think, I didn't fully understand how it should be working. Any help would be great.

Dynamic permanent property for Tooltip leaflet

I'm trying to show a tooltip when a Legend component get hover. For that I have a Father component that has an useState hook in order to pass to the Leaflet Map component the index for an array Location and change the Permanent property if these index are equal.
const permanent = i === showLocationPosition;
The showLocationPosition is the index for the location that is getting hover, getting by props for its Father component.
<Marker
position={[position[0], position[1]]}
key={index}
icon={divIcon({ className: 'marker-dot dot-hover', html: ReactDOMServer.renderToString(stringHTML), iconSize: [30, 30] })}
>
<Tooltip direction="bottom" opacity={1} offset={new Point(xPoint, 10)} permanent={permanent}>
<div className={`popup-container-header ${item.count ? 'w-80' : 'w-40 text-center'}`}>
<p className="w-full">
{type_of_country_operation ?? item.name}
</p>
{item.count && <p>{item.count}</p>}
</div>
{item.summary && <p className="popup-container-main">{item.summary}</p>}
</Tooltip>
</Marker>
I could validate that the permanent variable changes but the Tooltip does not apear.
Any advice ? Thanks!
The reason why the change in permanent doesn't help is because underlying leaflet options are treated as immutable by react-leaflet. So even as your showLocationPosition might change (which changes permanent), the Tooltip was already created and will not respond to changes in that prop.
A quick and dirty way would be to use the key prop on the tooltip also, which can be a combination of the index and the permanent status:
<Tooltip {...otherProps} key={`${index}-${permanent}`}>
This would force a rerender of that Tooltip component when the value of permanent changes.
I would consider a different approach. If you don't need to also render the Tooltip when you mouseover the Marker it originates from, just conditionally render it based on permanent:
<Marker {...markerprops}>
{permanent && <Tooltip direction="..." offset={...} permanent={permanent}>
{stuff}
</Tooltip>}
</Marker>
You may want to change the name permanent to something else, like currentOpenTooltip. Now, if you also want to have the tooltip open and close properly when the user mouses over a marker, you'll need to add a condition for that. You can use a state variable to keep track of what Marker is being moused over, and use event handlers to control that state variable.
const Father = () => {
const [currentlyMousedOverMarker, setCurrentlyMousedOverMarker] = useState(-1);
return (
<MapContainer>
{markersData.map((marker, index) => {
<Marker
{...markerprops}
eventHandlers={{
mouseenter: () => { setCurrentlyMousedOverMarker(index) },
mouseleave: () => { setCurrentlyMousedOverMarker(-1) }
}}
>
{permanent || currentlyMousedOverMarker === index &&
(
<Tooltip permanent={permanent || currentlyMousedOverMarker}>
{stuff}
</Tooltip>
)
}
</Marker>
})}
</MapContainer>
)
}
Obviously this code example is simplified and doesn't contain any of the logic you already had for permanent, but its just to give you an idea that the tooltip should only be rendered if either condition is true.

Add/Modify className Dynamically onMouseEnter

I want to add a new className when the user hovers on a slick slider Image and perform some CSS transition for that particular Image card in the slider. https://stackblitz.com/edit/react-slick-slider-issues how do I add className to the slider whenever the user hovers on the image or change the parent className based on the hover position?
I tried the document.getElementsByClassName('unique-image') but all the images have this className as they are looped inside a map function. how can I only change unique-image className if the user hovers on a certain image to unique-image-hover?
You may access Event.target that triggered mouseEnter and use Element.classList add() method to add your desired className
So, your mouseEnter handler may look like that:
const mouseHover = e =>{
e.target.classList.add('someClassNameYouWantedToAdd')
}
I can use React.useState
const [hoveredClass, setHoveredClass] = React.useState("");
const updateHovered = (e) => {
setHoveredClass(e.target.id)
}
const removeHovered = (e) => {
setHoveredClass('')
}
return (
<div className={`someStaticClass ${hoveredClass ? "hoveredClass" : ""}`}
onMouseEnter={updateHovered}
onMouseExit={removeHovered}
>
{list. map(item => (
<ImageChildComponent {...item} />
)}
</div>
)
Target will give you a child element, but you can add an event listener to the parent.
As #YevgenGorbunkov mention, change in state will trigger rerendering, so
consider wrapping ImageChildComponent with React memo to prevent unnecessary rendering

How to select the clickable parent in my React component even if I click on its children or anywhere else inside the parent

I have started an application which I want to work same as weather.com next 36 hours section. The idea is when you click on each weatherCard which has a seperate component in my app you will update the below section which is my weatherDetails component based on the selected weatherCard /weather box. So I made the entire component clickable by giving it the click event via props from my stateful component which is my weatherLocation component. This is my WeatherCard component:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={props.clicked}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
And here in render method in WeatherLocation component I loop through data coming from state and give props the WeatherCard component:
const WeatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
{...report}
clicked={() => this.handleCardClick(event)}
/>
);
});
And this is the handleCardClick that I added for it just for testing:
handleCardClick = event => {
// const { reports , selectedCardInfo , activeCard } = this.state;
const selectedDate = document.getElementById(event.target.id);
console.log(event.target.id);
}
I don't want to use anchor tag as I don't need href. The click works fine by itself. But because I need to get the id of the parent which is the div with the class of weatherCard. At the moment when I click on other elements inside the card I cannot get the id because they are not the parent. The reason I need its id is when I get data with from the API I need a unique value for each card so that when you click on the card the data for that card will be shown in the other component which is the WeatherDetails component. But for now I need to be able to somehow choose that selected card and pull out the state for that unique card. Could someone help me out? Thanks.
You just need to pass the Parent component ID to your onClick function in Weather Card.
Here is your WeatherCard - Component
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
You can see that I have added props.id to your onClick function and with help of event now you can access that id from the parent component.
Now here is your Parent Component- WeatherCards
const WeatherCards = this.state.reports.map( (report, i) => {
return(
<WeatherCard
key={report.id}
id={i}
{...report}
clicked={this.handleCardClick}
/>
);
});
You can see in the code I am passing index number as id to your child component.
So this will give you an id (for now it's an index number) of the card in your onClick handler.
and Finally, here is your on click handler.
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
As of now, I am using the index as id if you want to use a unique identifier, you can change that easily.
General JavaScript solution is to differentiate the elements and .stopPropogation after you've captured the event you are targeting. A nested unordered list, <ul>would be an example. Tag the containing <li> with an .opened class upon rendering/displaying each level of nesting, tag those <li> elements accordingly, e.g. a dataset attribute such as data-make, then data-model, then data-option. You then attach and fire event listeners on the different level <li>'s.
Thank you #RutulPatel. I made your answer as the answer. But I changed your code a bit as I got your point so I wrote an answer as it is long. I think we might not need to change the WeatherCard at all and I don't pass event or any logic there. so it will be intact:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
But I use your tip changing my weatherCards array to look like this:
const weatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
id={report.date}
{...report}
clicked={() => this.handleCardClick(event, report.date)}
/>
);
});
So I use the report.date which is a unique value as my id. Also I don't pass event as a parameter to the arrow function I just pass it with the report.date to the handler:
clicked={() => this.handleCardClick(event, report.date)}
And the handler will be the same as you did:
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
I might even remove event later on from both if there was no need fo that.
Thank you again.

Categories