is there a way to prevent dropdown menu from react bootstrap to close when selecting an item? I created this component which I use multiple times in my project, but when props.name is trans('Tło') I need to keep dropdown menu open on select, how can I achive this?
export const DropdownList = ({
name,
currentValue,
items,
action,
displayValue = (i, className) => <span className={className}>{i.name.pl}</span>,
getItemId = i => i.id,
getKey = i => `${name}-${i.id || i}`,
displayCurrentValue = displayValue,
}) => {
const handleAction = i => {
if (name !== trans('Tło')) {
action(i);
}
};
return (
<Dropdown>
<Dropdown.Toggle variant="none" className={`w-100 ${styles.toggle}`}>
<div className="row mx-auto d-flex flex-nowrap">
<div className="col px-0 d-flex flex-column align-items-baseline">
<div style={{ fontSize: '12px', color: '#878787' }}>{name}</div>
<div className={`${styles.item} font-weight-bold text-left`}>
{currentValue ? displayCurrentValue(currentValue, 'w-50') : trans('Wybierz')}
</div>
</div>
<div className="col-auto px-0 d-flex align-items-center">
<img src={arrowDownIcon} alt="" style={{ width: '12px' }} />
</div>
</div>
</Dropdown.Toggle>
<Dropdown.Menu align="left" style={{ minWidth: '100%' }}>
{items.map(i => (
<div
key={getKey(i)}
className={currentValue && getItemId(currentValue) === getItemId(i) ? styles.active : ''}
>
<Dropdown.Item
onSelect={e => handleAction(i)}
className={`current-item ${name === trans('Tło') && styles.noHover}`}
>
{displayValue(i, 'w-100')}
</Dropdown.Item>
</div>
))}
</Dropdown.Menu>
</Dropdown>
);
};
You can control the state of whether or not the dropdown is open using the show prop:
const { Dropdown } = ReactBootstrap;
const { useState } = React;
function App() {
const [show, setShow] = useState(false);
return (
<Dropdown
show={show}
onToggle={(isOpen, event, metadata) => {
if (metadata.source !== "select") {
setShow(isOpen);
}
}}
>
<Dropdown.Toggle>foo</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onSelect={() => setShow(false)}>bar</Dropdown.Item>
<Dropdown.Item>baz</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<!-- react -->
<script
crossorigin
src="https://unpkg.com/react#17/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"
></script>
<!-- react-bootstrap -->
<script
crossorigin
src="https://unpkg.com/react-bootstrap#1.6.3/dist/react-bootstrap.min.js"
></script>
<!-- bootstrap -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap#4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l"
crossorigin="anonymous"
/>
<div id="root"></div>
In the above example:
When bar is selected, show is set to false and the dropdown closes.
When baz is selected, we do not change show, and so the dropdown does not close.
When the dropdown is toggled for any reason other than a selection was made, we set show to whatever the component wanted to toggle to. This allows the dropdown to open and close normally.
Related
I want to add a border when the class is "transporation active" but when I click one of the icons, all of them become active. I want only the clicked one to become active. This is for selecting categories. My react code is like this:
const [transportationClick, setTransportationClick] = useState(false);
const handleTransportationClick = () => setTransportationClick(!transportationClick);
/* where I use it */
<div className="transportation-types">
<div className={transportationClick ? "transportation active" : "transportation"} onClick={handleTransportationClick}>
<div className="transportation-icon">
<FaCar />
</div>
</div>
<div className={transportationClick ? "transportation active" : "transportation"} onClick={handleTransportationClick}>
<div className="transportation-icon">
<FaBus />
</div>
</div>
<div className={transportationClick ? "transportation active" : "transportation"} onClick={handleTransportationClick}>
<div className="transportation-icon">
<FaWalking />
</div>
</div>
</div>
The issue is that all the icons share the same state, which results in all of the icons becoming active when one of them is clicked. There are several ways to solve this - you can refactor each icon into it's own component:
const Icon = ({ icon }) => {
const [transportationClick, setTransportationClick] = useState(false);
const handleTransportationClick = () => setTransportationClick(!transportationClick);
return (
<div className={transportationClick ? "transportation active" : "transportation"} onClick={handleTransportationClick}>
<div className="transportation-icon">
{icon}
</div>
</div>
)
}
const Parent = () => {
return (
<div className="transportation-types">
<Icon icon={<FaCar />} />
<Icon icon={<FaBus />} />
<Icon icon={<FaWalking />} />
</div>
)
}
Or set the state to an index corresponding to the order your showing the icon in:
const Parent = () => {
const [transportationClick, setTransportationClick] = useState(0)
return (
<div className="transportation-types">
<div
className={transportationClick === 0 ? 'transportation active' : 'transportation'}
onClick={() => setTransportationClick(0)}
>
<div className="transportation-icon">
<FaCar />
</div>
</div>
<div
className={transportationClick === 1 ? 'transportation active' : 'transportation'}
onClick={() => setTransportationClick(1)}
>
<div className="transportation-icon">
<FaBus />
</div>
</div>
<div
className={transportationClick === 2 ? 'transportation active' : 'transportation'}
onClick={() => setTransportationClick(2)}
>
<div className="transportation-icon">
<FaWalking />
</div>
</div>
</div>
)
}
The code below illustrates a normal drop down list. To indicate a drop down list, I use a down arrow with
arrow_drop_down
This arrow remains static for me in any state of the list (open or closed). However, I would like that when clicking on the list, the arrow changes to
arrow_drop_up
.
Those. so that with two different states of the list, there would be two different arrows.
export default function FilterStatusCode() {
const [values, setValues] = React.useState([]);
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return <>
<div className="item-toggle-statuscode" onClick={toggleExpand}>
<h6>Status Code</h6>
<span class="material-icons">
arrow_drop_down
</span>
</div>
{ isExpanded &&
<div>
<TagInput
inputProps={{ placeholder: 'Add status code...' }}
values={values}
onChange={(values) => {
setValues(values)}}>
</TagInput>
</div>
}
</>;
}
try
<div className="item-toggle-statuscode" onClick={toggleExpand}>
<h6>Status Code</h6>
<span class="material-icons">
{ isExpanded ? arrow_drop_up : arrow_drop_down }
</span>
</div>
You can choose which arrow you use depending on the current state:
// If the list is open show the `up` arrow
// otherwise show the `down` arrow
<span className={open ? "up" : "down"}></span>
I had to improvise in this example and used unicode in the class names.
const { useState } = React;
function Example() {
return (
<div>
<Item />
<Item />
</div>
);
}
function Item() {
const [ input, setInput ] = useState('');
const [ open, setOpen ] = useState(false);
function handleChange(e) {
setInput(e.target.value);
}
function handleOpen() {
setOpen(!open);
}
function handleClick() {
console.log(input);
}
return (
<div className="item">
<div onClick={handleOpen} className="heading">
<span>Status code</span>
<span className={open ? "up" : "down"}></span>
</div>
{open && (
<div>
<input
type="text"
onChange={handleChange}
value={input}
/>
<button
type="button"
onClick={handleClick}
>Submit
</button>
</div>
)}
</div>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
.down:after { content: '\25BC'; }
.up:after { content: '\25B2'; }
.heading:hover { cursor: pointer; color: red; }
.item { margin-bottom: 1em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Additional documentation
Conditional (ternary) operator
Struggling with this issue with the earlier answers not utilizing map function / functional components. When I click my Card, the modal only shows the data of the last Modal:
export const ModalCard = (props) => {
const productData = props.data;
const [modal, setModal] = React.useState(false);
const toggle = () => setModal(!modal);
return (
<Row>
{productData.map((v, i) => (
<Col className="py-4 btn" key={i} xs={12} md={4} lg={2}>
<div className="pb-4" onClick={toggle}>
<div className="product_card_padding">
<div className="pt-4">
<span className="card_product_subtitle">{v.headline}</span>
</div>
</div>
<Modal isOpen={modal}
toggle={toggle}
centered
>
<ModalBody className="product_modal" onClick={toggle}>
<div className="row pt-3 bg_white">
<Col>
<div>
<span className="card_product_subtitle">{v.headline}</span>
</div>
</Col>
</div>
</ModalBody>
</Modal>
</div>
</Col>
))}
</Row>
);
}
According to your code, multiple modals will be opened and you will see the last modal.
If you have 10 products, then 10 modals will be opened.
My suggestion is that you need to define a global modal outside map function and you need to define a new state variable to represent the selected product to be rendered on the modal.
selectedInd holds the data index to be rendered on modal.
const [selectedInd, setSelectedInd] = React.useState(-1);
Then toggle function would be changed to set -1 to hide modal.
const toggle = () => setSelectedInd(-1);
And move the modal outside map.
Try to use the following code pattern.
export const ModalCard = (props) => {
const productData = props.data;
const [selectedInd, setSelectedInd] = React.useState(-1);
const toggle = () => setSelectedInd(-1);
const modal = selectedInd >= 0 && (productData && productData.length > selectedInd);
return (
<React.Fragment>
<Row>
{productData.map((v, i) => (
<Col className="py-4 btn" key={i} xs={12} md={4} lg={2}>
<div className="pb-4" onClick={()=>setSelectedInd(i)}>
<div className="product_card_padding">
<div className="pt-4">
<span className="card_product_subtitle">{v.headline}</span>
</div>
</div>
</div>
</Col>
))}
</Row>
{modal && <Modal isOpen={modal} toggle={toggle} centered>
<ModalBody className="product_modal" onClick={toggle}>
<div className="row pt-3 bg_white">
<Col>
<div>
<span className="card_product_subtitle">{productData[selectedInd].headline}</span>
</div>
</Col>
</div>
</ModalBody>
</Modal>}
</React.Fragment>
);
}
I only want to show the display block on the hovered item. but when I hover it shows on every item in a map function. what I'm doing wrong.
basically, I just want to show hovered movie item's title. for now, it shows every movie when I hover.
MovieList.js
const [popular, setPopular] = useState([]);
const [hover, setHover] = useState(false);
return (
<>
<div className="movie-list">
<h2 style={{ fontSize: "49px", marginLeft: "60px" }}>What's Popular</h2>
<div className="popular">
{popular.map((pop, index) => (
<>
<div className="movie" key={index}>
<div className="tot" onMouseEnter={() => setHover(true)}>
<h4
id="pop-title"
style={{ display: hover ? "block" : "none" }}
key={index}
>
{pop.title}
</h4>
</div>
<img
src={"https://image.tmdb.org/t/p/w500" + pop.poster_path}
id="movie-img"
/>
</div>
</>
))}
</div>
</div>
</>
);
};
export default MovieList;
The same hover state variable is used for all your movies, so when it becomes true, all the movies are affected by the change. You could use something like an object instead of just a boolean to store one hover state per movie.
Another problem with your code that isn't helping:
You are missing a unique key prop on each item of the map (it must be on the direct child).
Solution 1: Remove the Fragment
Everything is already under one div so you don't need the React Fragment (<>) in that case. Also, you might wanna use something unique to the current map item other than the index in the array.
{popular.map((pop) => (
<div className="movie" key={pop.somethingUnique}>
<div className="tot" onMouseEnter={() => setHover(true)}>
<h4 id="pop-title" style={{ display: hover ? "block" : "none" }}>
{pop.title}
</h4>
</div>
<img
src={"https://image.tmdb.org/t/p/w500" + pop.poster_path}
id="movie-img"
/>
</div>
))}
Solution 2: Set the key on the Fragment
{popular.map((pop) => (
<Fragment key={pop.somethingUnique}>
<div className="movie">
<div className="tot" onMouseEnter={() => setHover(true)}>
<h4 id="pop-title" style={{ display: hover ? "block" : "none" }}>
{pop.title}
</h4>
</div>
<img
src={"https://image.tmdb.org/t/p/w500" + pop.poster_path}
id="movie-img"
/>
</div>
</Fragment>
))}
I a week new in learning react coming from an angular background. I have the following unordered list in React.
const QueueManage: React.FC = () => {
const { queue, setQueue, loading, error } = useGetQueue();
const [btnState, setBtnState] = useState(state);
const enterIconLoading = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
const item = '';
const btn = '';
console.log(item, btn);
setBtnState({ loading: true, iconLoading: true, item: item, btnType: btn });
};
<ul className="listCont">
{queue.map(queueItem => (
<li className="col-12" key={queueItem.id}>
<div className="row">
<div className="listName col-3">
<p>{queueItem.user.firstName} {queueItem.user.lastName}</p>
</div>
<div className="listName col-5">
<div className="row">
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Assign
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Absent
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Done
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Cancel
</Button>
</div>
</div>
</div>
</div>
</li>
)
)}
</ul>
}
For each list item, the list item will have for buttons, namely Assign, Absent, Done, Cancel. My goal is to identify which button was clicked and for which list item so that I can apply a loader for that specific button. Can any one please assist me with an explanation of how I can achieve this in my code
Here is a visual representation of the list that i get
https://i.imgur.com/kxcpxOo.png
At the moment went i click one button, all buttons are applied a spinner like below:
Your assistance and explanation is highly appreciated.
The Reactful approach involved splitting the li into a separate component. This will help keep each item's state separate. Let's call that QueueItem.
const QueueItem = ({ user }) => {
const [loading, setLoading] = useState(false)
function onClickAssign() {
setLoading(true)
// do something
setLoading(false)
}
function onClickAbsent() {
setLoading(true)
// do something
setLoading(false)
}
function onClickDone() {
setLoading(true)
// do something
setLoading(false)
}
function onClickCancel() {
setLoading(true)
// do something
setLoading(false)
}
return (
<li className='col-12'>
<div className='row'>
<div className='listName col-3'>
<p>
{user.firstName} {user.lastName}
</p>
</div>
<div className='listName col-5'>
<div className='row'>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickAssign}>
Assign
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickAbsent}>
Absent
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickDone}>
Done
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickCancel}>
Cancel
</Button>
</div>
</div>
</div>
</div>
</li>
)
}
Here I've also split out each button's onClick into a separate callback since they are well defined and probably have unique behaviours. Another approach mentioned above in a comment is
function onClickButton(action) {
...
}
<Button type='primary' loading={loading} onClick={() => onClickButton('cancel')}>
Cancel
</Button>
This follows the action / reducer pattern which might be applicable here instead of state (useState)
Move the buttons or the whole li to a component and let each list manage it's state.
// Get a hook function
const {useState} = React;
//pass the index of li as prop
const Buttons = ({ listId }) => {
const [clicked, setClickedButton] = useState(0);
return (
<div>
<button
className={clicked === 1 && "Button"}
onClick={() => setClickedButton(1)}
>
Assign
</button>
<button className={clicked === 2 && "Button"} onClick={() => setClickedButton(2)}>Absent</button>
<button className={clicked === 3 && "Button"} onClick={() => setClickedButton(3)}>Done</button>
<button className={clicked === 4 && "Button"} onClick={() => setClickedButton(4)}>Cancel</button>
</div>
);
};
// Render it
ReactDOM.render(
<Buttons />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<style>
.Button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
</style>
<div id="react"></div>
In addition to the previous answer it's worth adding that making simple components (in our case buttons) stateful is often considered a bad practice as it gets harder to track all the state changes, and to use state from different buttons together (e.g. if you want to disable all 4 buttons in a row after any of them is pressed)
Take a look at the following implementation, where entire buttons state is contained within parent component
enum ButtonType {
ASSIGN, ABSENT, DONE, CANCEL
}
// this component is stateless and will render a button
const ActionButton = ({ label, loading, onClick }) =>
<Button type="primary" loading={loading} onClick={onClick}>
{label}
</Button>
/* inside the QueueManage component */
const [buttonsState, setButtonsState] = useState({})
const updateButton = (itemId: string, buttonType: ButtonType) => {
setButtonsState({
...buttonsState,
[itemId]: {
...(buttonsState[itemId] || {}),
[buttonType]: {
...(buttonsState[itemId]?.[buttonType] || {}),
loading: true,
}
}
})
}
const isButtonLoading = (itemId: string, buttonType: ButtonType) => {
return buttonsState[itemId]?.[buttonType]?.loading
}
return (
<ul className="listCont">
{queue.map(queueItem => (
<li className="col-12" key={queueItem.id}>
<div className="row">
<div className="listName col-3">
<p>{queueItem.user.firstName} {queueItem.user.lastName}</p>
</div>
<div className="listName col-5">
<div className="row">
<div className="col-3">
<ActionButton
label={'Assign'}
onClick={() => updateButton(queueItem.id, ButtonType.ASSIGN)}
loading={isButtonLoading(queueItem.id, ButtonType.ASSIGN)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Absent'}
onClick={() => updateButton(queueItem.id, ButtonType.ABSENT)}
loading={isButtonLoading(queueItem.id, ButtonType.ABSENT)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Done'}
onClick={() => updateButton(queueItem.id, ButtonType.DONE)}
loading={isButtonLoading(queueItem.id, ButtonType.DONE)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Cancel'}
onClick={() => updateButton(queueItem.id, ButtonType.CANCEL)}
loading={isButtonLoading(queueItem.id, ButtonType.CANCEL)}
/>
</div>
</div>
</div>
</div>
</li>
)
)}
</ul>
)
The goal here is to keep buttons loading state in parent component and manage it from here. buttonsState is a multilevel object like
{
'23': {
[ButtonType.ASSIGN]: { loading: false },
[ButtonType.ABSENT]: { loading: false },
[ButtonType.DONE]: { loading: false },
[ButtonType.CANCEL]: { loading: false },
},
...
}
where keys are ids of queueItems and values describe the state of the 4 buttons for that item. It is usually preferred to use useReducer instead of nested spreading in updateButton but it is good to start with