multiple dropdown state and handling outside click in React - javascript

I have a parent component that holds multiple Dropdown child components. The state is managed within the parent component to show only one dropdown at a time. I currently have part of it working but am having some trouble wrapping my head around the logic to make it so that when you click outside of a dropdown if its open then it will close the dropdown. I have tried using useRef hook to detect click outside, but still having trouble wrapping my head around the logic to make things display correctly.
const MultipleDropdownPage = () => {
const [dropdown, setDropdown] = useState(null);
const handleDropdown = id => {
if (dropdown === id) {
setDropdown(null);
}
if (dropdown !== id) {
setDropdown(id);
}
};
return (
<div>
{dropdown ? dropdown : 'Nothing'}
<Dropdown handleDropdown={handleDropdown} dropdown={dropdown} id='1' />
<Dropdown handleDropdown={handleDropdown} dropdown={dropdown} id='2' />
</div>
);
};
import React from 'react';
const Dropdown = ({ handleDropdown, id, dropdown }) => {
return (
<div>
<button onClick={() => handleDropdown(id)}>Click me</button>
{id === dropdown && (
<div className='dropdown'>
<ul>
<li>Lorem, ipsum.</li>
<li>Dolore, eligendi.</li>
<li>Quam, itaque!</li>
</ul>
</div>
)}
</div>
);
};
export default Dropdown;

Needed to set a class on the button itself and then check if when the document is clicked it doesn't match that button class
import React, { useRef, useEffect } from 'react';
const Dropdown = ({ handleDropdown, id, dropdown }) => {
const ref = useRef();
useEffect(() => {
const handleClick = e => {
if (!e.target.classList.contains('dropdown-toggle')) {
handleDropdown(null);
}
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [handleDropdown]);
return (
<>
<button
onClick={() => handleDropdown(id)}
ref={ref}
className='dropdown-toggle'
>
Click me
</button>
{id === dropdown && (
<div className='dropdown'>
<ul>
<li>Lorem, ipsum.</li>
<li>Dolore, eligendi.</li>
<li>Quam, itaque!</li>
</ul>
</div>
)}
</>
);
};
export default Dropdown;

This solution takes advantage of the custom HTML data-* attribute.
const MultipleDropdownPage = (props) => {
const [dropdown, setDropdown] = React.useState(null);
const handleDropdown = id => {
if (dropdown === id || dropdown && id == undefined) {
setDropdown(null);
}
if (dropdown !== id) {
setDropdown(id);
}
};
React.useEffect(() => {
const handleClick = ({target}) => {
handleDropdown(target.dataset.id);
};
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick)
}, [handleDropdown]);
return (
<div className="dropdown-container">
{dropdown ? dropdown : 'Nothing'}
<Dropdown dropdown={dropdown} id='1' />
<Dropdown dropdown={dropdown} id='2' />
</div>
);
};
const Dropdown = ({ id, dropdown }) => {
return (
<div>
<button data-id={id}>Click me</button>
{id === dropdown && (
<div className='dropdown'>
<ul>
<li>Lorem, ipsum.</li>
<li>Dolore, eligendi.</li>
<li>Quam, itaque!</li>
</ul>
</div>
)}
</div>
);
};
ReactDOM.render(<MultipleDropdownPage />, document.getElementById("root"));
.dropdown-container {
padding: 5px;
border: 2px solid red;
width: fit-content;
margin: 0 auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

ReactJS - Dropdown events cant be triggered because Input looses focus

I need a search box in React that opens a dropdown as soon as an entry is made. The dropdown should contain buttons that can trigger an event.
The problem is that the dropdown has to disappear as soon as another input box is used in the application.
I could also implement this, but now I have the problem that the event of the button in question is not triggered because the focus of the input field is lost beforehand as soon as I press the button. As a result, the dropdown disappears and the event is never triggered.
This is roughly my Searchbox Component:
import React, { useState } from 'react';
import Dropdown from './Dropdown';
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0)
props.apiCall(params);
}
const buttonHandler = (id) => {
console.log(id);
}
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)} // Problem area
/>
{
focused === true && props.apiData.length > 0 ?
props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
)
})
: null
}
</>
)
}
export default Search;
This is my Dropdown Component:
import React from 'react';
function Dropdown(props) {
return (
<ul key={props.id}>
<li>{props.name}</li>
<li>even more data</li>
<li>
<input
type="button"
value="Select"
onClick={() => {
props.buttonHandler(props.id)
}}
/>
</li>
</ul>
)
}
export default Dropdown;
To resolve this issue blur event in Search Component can be handled with delay.
function Search(props) {
const [focused, setFocused] = useState(false);
const inputHandler = (params) => {
if (params.length > 0) props.apiCall(params);
};
const buttonHandler = (id) => {
console.log(id);
};
return (
<>
<input
type="text"
placeholder="Suchen.."
onChange={(event) => inputHandler(event.target.value)}
onFocus={() => setFocused(true)}
- onBlur={() => setFocused(false)} // Problem area --> remove this line
+ onBlur={() => setTimeout(()=> setFocused(false),500)} // ---> delay added
/>
{focused === true && props.apiData.length > 0
? props.apiData.map((mappedData, key) => {
return (
<Dropdown
key={key}
id={mappedData.id}
name={mappedData.name}
/*
even more Data
*/
buttonHandler={buttonHandler}
/>
);
})
: null}
</>
);
}

Clicking a child component affects parent in an unexpected way

I have two components, Container and Item.
Container contains Item.
Item contains a button and a div.
These components have the following behaviors:
Container: When I click outside of Container it should disappear, I'm achieving this by using a custom hook that detects clicks outside of components. This works just fine.
Item: When I click in the div which is inside Item it should disappear, I'm achieving this by setting a boolean state. This also works but the problem here is that Container also disappears.
Container
const Container = ({ setDisplay }) => {
const containerRef = useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
setDisplay(false)
});
return (
<div ref={containerRef} className='container'>
<Item />
</div>
);
};
Item
const Item = () => {
const [displayItem, setDisplayItem] = useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
/>
)}
</div>
);
};
useClickOutside
const useClickOutside = (ref, handler) => {
useEffect(() => {
const trigger = e => {
if (!(ref?.current?.contains(e.target))) handler();
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
Why is this happening and how can I prevent it?
Note: I have to use that hook.
Both the listeners are being attached to the bubbling phase, so the inner ones trigger first.
When the item is shown, and when it's clicked, this runs:
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
As a result, before the event propagates outward, setDisplayItem(false) causes this .item-content element to be removed from the DOM. See here, how the parent no longer exists afterwards:
const Container = ({ setDisplay }) => {
const containerRef = React.useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
console.log('making container disappear');
});
return (
<div ref={containerRef} className='container'>
container
<Item />
</div>
);
};
const Item = () => {
const [displayItem, setDisplayItem] = React.useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
)}
</div>
);
};
const useClickOutside = (ref, handler) => {
React.useEffect(() => {
const trigger = e => {
console.log(e.target.parentElement);
if (!(ref.current.contains(e.target))) handler();
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
You can fix this by changing useClickOutside to also check whether the node is connected or not. If it's not connected, the element is no longer in the DOM due to a state change and rerender - so the click that was made wasn't definitely outside the ref.current, so the handler shouldn't run.
const trigger = e => {
const { current } = ref;
if (e.target.isConnected && !current.contains(e.target)) {
const Container = ({ setDisplay }) => {
const containerRef = React.useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
console.log('making container disappear');
});
return (
<div ref={containerRef} className='container'>
container
<Item />
</div>
);
};
const Item = () => {
const [displayItem, setDisplayItem] = React.useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
)}
</div>
);
};
const useClickOutside = (ref, handler) => {
React.useEffect(() => {
const trigger = e => {
const { current } = ref;
if (e.target.isConnected && !current.contains(e.target)) {
console.log(current.parentElement);
handler();
}
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>

Closing a list item dropdown on selecting an item or clicking outside it

I have made a dropdown in my react js project with list items in it (Dropdown items are shown by ul and li tags here).
The issue i am facing is that on selecting any item the value in state changes but the dropdown doesn't close not do it closes when i click anywhere outside of it.
Please help me out, here is the working codesandbox url for the same repo
Check here
Also i am sharing the code below .
App.js
import React, { Component, useState } from "react";
import Dropdown from "./dropdown";
import "./dropdown.css";
const App = () => {
const [value, setValue] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const selectedValue = (value) => {
console.log(value);
setValue(value);
};
const onSelectItem = () => {
console.log("12");
setShowDropdown(false);
};
return (
<section className="shadow-border" id="FlightsForm">
<div className="top-sec d-flex">
<div className="left-side">
<span
className="custm-dropdown"
onClick={() => setShowDropdown(true)}
>
<span style={{ backgroundColor: "grey", color: "white" }}>
{value.tripType}
</span>
<span className="dropdown-icon"> </span>
<Dropdown
selectedValue={selectedValue}
onSelectItem={onSelectItem}
showDropdown={showDropdown}
/>
</span>
</div>
</div>
</section>
);
};
export default App;
Dropdown.js
import React from "react";
import { useState, useEffect } from "react";
const TripTypeDropdown = (props) => {
const [values, setValues] = useState([
{ tripType: "One Way", value: 1 },
{ tripType: "Return", value: 2 },
{ tripType: "Multi- City", value: 3 },
]);
const [selectedItem, setSelectedItem] = useState({
tripType: "One Way",
value: 1,
});
useEffect(() => {
props.selectedValue(selectedItem);
console.log(selectedItem);
}, [selectedItem]);
const selectItemFromList = (index) => {
const itemSelected = values[index];
setSelectedItem(itemSelected);
props.onSelectItem();
};
const getActiveClassName = (item) => {
if (selectedItem) {
if (item.tripType == selectedItem.tripType) return "active";
else return "";
}
} ;
return (
<React.Fragment>
<div
className={`dropdown-modal sm-modal ripple trip-type-dropdown`}
style={{ display: `${props.showDropdown ? "block" : "none"}` }}
>
<ul>
{console.log(values)}
{values.map((item, index) => (
<li
className={getActiveClassName(item)}
onClick={() => selectItemFromList(index)}
key={index}
style={{backgroundColor:'yellow',border:"1px solid black",listStyle:"none"}}
>
{item.tripType}
</li>
))}
</ul>
</div>
</React.Fragment>
);
};
export default TripTypeDropdown;
You need to stop bubbling up of click event from your dropdown component to it's parent span element. On click you need to pass thevent argument and call stopPropagation function of event object
Here is the condesandbox
Dropdown.js
const selectItemFromList = (e,index) => {
e.stopPropagation();
...
<li
className={getActiveClassName(item)}
onClick={(e) => selectItemFromList(e,index)}
key={index}
Also added code for outside click.
const ref = useRef();
...
useEffect(() => {
document.addEventListener("click", handleDropdownClick);
}, [ref]);
const handleDropdownClick = (e) => {
e.stopPropagation();
if (ref.current && ref.current.contains(e.target)) {
setShowDropdown(true);
} else {
setShowDropdown(false);
}
};
...
<span
ref={ref}
className="custm-dropdown"
onClick={handleDropdownClick}
>
You should stop the propagation of the click event from the list item to the outer span, this is defeating any attempts to toggle the dropdown closed again from the parent.
const selectItemFromList = (index) => (e) => {
e.stopPropagation();
const itemSelected = values[index];
setSelectedItem(itemSelected);
props.onSelectItem();
};
...
<li
key={index}
...
onClick={selectItemFromList(index)}
...
>
{item.tripType}
</li>
To handle outside clicks you attach a React ref to the dropdown div and use an useEffect hook to add an onClick event listener to the widow object and check that the onClick's event target is not contained within the dropdown div.
useEffect(() => {
const outsideClickHandler = (e) => {
if (props.showDropdown && !outsideRef.current?.contains(e.target)) {
props.onSelectItem(false);
}
};
window.addEventListener("click", outsideClickHandler);
return () => window.removeEventListener("click", outsideClickHandler);
}, [props]);
...
<div
ref={outsideRef}
...
>
...
</div>
Demo

React how to change an individual button in a list

I have a list that renders content from a base, each list item has a "Favorite" button to move the item to the start of the render. The problem is that I do not know how to change the icon of an individual element when I click on the "Favorite" (asterisk) button.
I tried to do it through LocalState, but because of this, when I click on a separate button "Add to favorites", everything changes at once
Before pressing
After
Code
import { useState } from "react"
const WaysItem = (props) => {
let [change, setChange] = useState(false)
return props.directionsToRender.map((item, index) => (
<li
className={`ways-item`}
key={index}
onClick={() => {
props.getCurrentDirections(index)
}}
>
<div>
<h5>{item.title}</h5>
</div>
<div className="ways-kilometrs">
<div>{item.direction.routes[0].legs[0].distance.text}</div>
<div>{item.direction.routes[0].legs[0].duration.text}</div>
</div>
<button onClick={() => setChange((change) => !change)} key={index}>
<i className={`${!change ? "far" : "fas"} fa-star`}></i>
</button>
</li>
))
}
export default WaysItem
You can have array as a state . And add or remove index on button click.
import { useState } from "react"
const WaysItem = (props) => {
let [selectedItems, setSelectedItems] = useState([])
const onButtonClick = (index) => {
// if index is already there in the selectedItems then remove it
if(selectedItems.includes(index)){
setSelectedItems(selectedItems.filter(item => item !== index))
} else {
setSelectedItems(prevSelectedItems => [...prevSelectedItems, index]);
}
}
return props.directionsToRender.map((item, index) => {
const showStar = selectedItems.includes(index);
return (
<li
className={`ways-item`}
key={index}
onClick={() => {
props.getCurrentDirections(index)
}}
>
<div>
<h5>{item.title}</h5>
</div>
<div className="ways-kilometrs">
<div>{item.direction.routes[0].legs[0].distance.text}</div>
<div>{item.direction.routes[0].legs[0].duration.text}</div>
</div>
<button onClick={() => onButtonClick(index)} key={index}>
<i className={`${showStar ? "far" : "fas"} fa-star`}></i>
</button>
</li>
)})
}
export default WaysItem

How to make Multi select without using bootstrap and with only pure css?

Now, I am making a multi-select component that has a heading inside the select box.
I made the tag inside the div component and every item is tag.
This is my code.
import React, { useState, useRef, useEffect } from "react";
import "./style.css";
function Select({ title, data, changeSelect, selectedItem }) {
const [categories, setCategories] = useState(data);
const [selectedItems, setSelectedItems] = useState([]);
const wrapperRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const handleClickOutside = event => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsVisible(false);
}
};
useEffect(() => {
document.addEventListener("click", handleClickOutside, false);
return () => {
document.removeEventListener("click", handleClickOutside, false);
};
}, []);
const selectItem = (item) => {
if (selectedItems.indexOf(item) === -1) {
if (categories.length === 1) {
}
setCategories((prevCategories) =>
prevCategories.filter((value) => value !== item)
);
setSelectedItems((prevItems) => [...prevItems, item]);
}
};
const removeItem = (item) => {
setSelectedItems((items) => items.filter((value) => value !== item));
setCategories((prevCategories) => [...prevCategories, item]);
};
return (
<div className="custom-select" ref={wrapperRef} >
{/* <label className="append-label">{title}</label> */}
<div className="multi-select" >
<title>{title}</title>
<div className="multi-select-wrapper">
{selectedItems.length > 0 &&
selectedItems.map((item) => (
<span onClick={() => removeItem(item)}>{item}</span>
))}
</div>
</div>
<div className={`dropDown-wrapper ${isVisible ? "active" : ""}`}>
<ul>
{categories.length > 0 &&
categories.map((item) => (
<li onClick={() => selectItem(item)}>{item}</li>
))}
<li
style={{
display: `${categories.length === 0 ? "block" : "none"}`,
}}
>
No Result
</li>
</ul>
</div>
</div>
);
}
export default Select;
When I click the select button, it shows a dropdown box.
The most important thing here is that I used react-onclickoutside npm library to get the click event outside the current element but it is not working well.
I used several libraries but all of them do not work at all.
PS: I have to use 3 multi-selects.
react: 16.14.0
react-scripts: 3.4.3
react-onclickoutside: 6.10.0
I had a solution by using another npm library and stopPropagation() function.
import React, { useState, useRef, useEffect } from "react";
import ClickOutside from "react-click-outside";
import "./style.css";
function Select({ title, data, changeSelect, selectedItem }) {
const [categories, setCategories] = useState(data);
const [selectedItems, setSelectedItems] = useState([]);
const [isActive, setIsActive] = useState(false);
// const onClickSelect = (e, active) => {
// setIsActive(active);
// console.log(e.target.value, "asdfasdfasdfs");
// };
const selectItem = (item) => {
if (selectedItems.indexOf(item) === -1) {
if (categories.length === 1) {
setIsActive(false);
}
setCategories((prevCategories) =>
prevCategories.filter((value) => value !== item)
);
setSelectedItems((prevItems) => [...prevItems, item]);
}
};
const removeItem = (e, item) => {
e.stopPropagation();
setSelectedItems((items) => items.filter((value) => value !== item));
setCategories((prevCategories) => [...prevCategories, item]);
};
return (
<ClickOutside onClickOutside={() => setIsActive(false)}>
<div className="custom-select" onClick={() => setIsActive(!isActive)}>
<div className="multi-select">
<title>{title}</title>
<div className="multi-select-wrapper">
{selectedItems.length > 0 &&
selectedItems.map((item) => (
<span onClick={(e) => removeItem(e, item)}>{item}</span>
))}
</div>
</div>
<div className={`dropDown-wrapper ${isActive ? "active" : ""}`}>
<ul>
{categories.length > 0 &&
categories.map((item) => (
<li onClick={() => selectItem(item)}>{item}</li>
))}
<li
style={{
display: `${categories.length === 0 ? "block" : "none"}`,
}}
>
No Result
</li>
</ul>
</div>
</div>
</ClickOutside>
);
}
export default Select;
I hope this will help you who has same issue with me.

Categories