Can't control Downshift selectedItem prop from parent component using controlled props - reactjs

I wrote a reusable autocomplete component using Downshift. The component encapsulates a lot of Relay logic for fetching data from my GraphQL endpoint. I have a use case where the parent component needs to receive the selectedItem, display the name property of the selectedItem, then clear out the selectedItem of the autocomplete (think selecting and displaying multiple tags).
Problem is I can't seem to control the selectedItem of the autocomplete from the parent component. I'm sending down the selectedItem as null to the autocomplete yet the inputValue/selectedItem remains the chosen item.
Sample code with Relay stripped out for simplicity:
https://codesandbox.io/embed/spring-fire-um1xh
Steps to reproduce
Type the word "Item" into the textbox
Click on one of the three results that are displayed
Actual outcome
Title of parent component and inputValue/selectedItem of autocomplete are both set to value of item chosen
Desired Outcome
Title of parent component set to value of item chosen
inputValue/selectedItem of autocomplete cleared out

Try below code. This code needs alot of refactor but it works anyway as desired. So, i just added inputValue props on Downshift as per doc.
----- index.js -------
function App() {
const [title, setTitle] = useState("Select an Item");
const [selectedItem, setSelectedItem] = useState(null);
const [inputValue, setInputValue] = useState(""); //added these two to control input of inputbox
const onItemSelected = selectedItem => {
setTitle(selectedItem.name);
setSelectedItem(null);
setInputValue("") // after selection clear input
};
return (
<div className="App">
<h1>{title}</h1>
<Autocomplete
selectedItem={selectedItem}
inputValue={inputValue} // passed down inputValue and setInputValue
setInputValue={setInputValue}
onItemSelected={onItemSelected}
/>
</div>
);
}
----Autocomplete.js--------
function Autocomplete({ selectedItem, onItemSelected, inputValue, setInputValue }) { // get those props
const [items, setItems] = useState([]);
const onInputValueChanged = e => {
setInputValue(e.currentTarget.value) //change a input on inputchange
const items = data.filter(f => f.name.includes(e.currentTarget.value));
setItems(items);
};
return (
<Downshift
inputValue={inputValue} //added
selectedItem={selectedItem}
onChange={onItemSelected}
itemToString={i => (i ? i.name : "")}
>
{({ getInputProps, getItemProps, getMenuProps, isOpen }) => (
<div>
<input {...getInputProps({ onChange: onInputValueChanged })} />
<ul {...getMenuProps()}>
{isOpen &&
items.map(item => (
<li {...getItemProps({ item, key: item.value })}>
{item.name}
</li>
))}
</ul>
</div>
)}
</Downshift>
);
}

Related

Handling state of multiple instances of same component

I have this Item component that I am using in another component:
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name }) => {
const [isSelected, setIsSelected] = useState(false);
const toggle = () => {
setIsSelected(!isSelected);
};
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={toggle} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;
In the other component, I have multiple instances of this Item component, representing different items. The Item component can with a click change property, like text and color for the button.
The problem is that in the other component, multiple of these Items can be toggled at the same time.
I would like that out of every instance of the Item component in the other component, only a single one can be toggled on at the same time. So if I select one item, the previous (if any selected) will be "unselected", changing the text and color back to the original state.
Can this be solved by only making changes in the Item component, or do I also need to make changes where it's being imported?
Can this be solved by only making changes in the Item component
Isolating state is good, but in this situation, Item state has dependencies on other components, so we cannot isolate that state completely.
I'd suggest that you should lift your state isSelected up to the parent component, and pass that state down to each Item for UI update.
import React, { useState } from "react";
const ParentComponent = () => {
const [selectedIndex, setSelectedIndex] = useState();
//[...Array(5)] is representing your actual items
return (
<div>
[...Array(5)].map((value, index) => <Item key={index} isSelected={selectedIndex === index} index={index} toggle={(i) => setSelectedIndex(i)} />)
</div>
);
};
And then change Item props with a little logic modification
import React, { useState } from "react";
import Button from "./Button";
const Item = ({ name, isSelected, index, toggle }) => {
var buttonColor;
var buttonText;
if (isSelected === true) {
buttonColor = "bg-button-blue";
buttonText = "Selected";
} else {
buttonColor = "bg-button-gray";
buttonText = "Select";
}
return (
<div onClick={() => toggle(index)} className="flex ml-2 items-center">
<div className="text-misc-gray text-xs w-40">{name}</div>
<div>
<Button
text={buttonText}
color={buttonColor}
height={"h-8"}
width={"w-18"}
></Button>
</div>
</div>
);
};
export default Item;
Can this be solved by only making changes in the Item component
No good way using the React paradigm, because you want one instance to affect another instance, where the other instance is not a child.
In the ancestor component of the <Item>s, create a state variable that holds the index (or ID, or name, or some other uniquely identifying aspect) of the Item component currently toggled on, and then pass down an isSelected prop to each Item, as well as the state setter. (The individual Items should no longer have an isSelected state.) Perhaps something along the lines of:
const numItems = 5;
const Parent = () => {
const [selectedIndex, setSelectedIndex] = useState(-1);
const makeToggle = (i) => () => {
setSelectedIndex(i === selectedIndex ? -1 : i);
};
return (
<div>
{ Array.from(
{ length: numItems },
(_, i) => <Item
isSelected={selectedIndex == i}
toggle={makeToggle(i)}
/>
)}
</div>
);
};

onClick handler is applying on all elements after map

I have the following data:
export const Bookings = ({ booking, selectedBookings }) => {
const [selected, setSelected] = React.useState(false)
const handleSelect = (data) => {
selectedBookings(data)
}
{booking.map(data => (
<div
key={nanoid()}
className={selected ? styles.selectedDescription : styles.NonSelectedDescription}
onClick={() => {
handleSelect(data.name); // when I add this it does not trigger the style toggling
setSelected(!selected) // if I keep only this handler it works but for all childrens
}}
>
{description}
</div>
In this code I want to send data to parent component and apply a specific styles when the booking is clicked, BUT I have 2 issues:
1- when I remove the handleSelect(data.name) the styles apply on whole childrens and not one single chidren.
2- when I add the handleSelect(data.name) handler the styles does not work at all, but the handler send the data to the parent
So is there any problem here?
You need to store different selected boolean for all bookings;
Try something like below:-
Change selected state to object:-
const [selected, setSelected] = React.useState({})
Change handleSelect like below:-
const handleSelect = (dataName) => {
setSelected({...selected, selected[dataName]: !selected[dataName]});
}
Change className in div like below:-
{booking.map(data => (
<div
key={nanoid()}
className={selected[data.name] ? styles.selectedDescription : styles.NonSelectedDescription}
onClick={() => { handleSelect(data.name); }}
>
{description}
</div>
))}
Note:- All booking name should be unique, If they are not unique you need to pass unique value in handleSelect function.

Passing OnChange Function using React for drop down

I have a payment component and custom dropdown component. I'm trying to pass down a function called handlePaymentImageChange from the parent (payment) to child (dropdown) so as to control the image change. However, it does not work well as I expect. What I'm trying to do is displaying the image based on the selection of the dropdown. In my case, if the value = 'Visa' -> render visa image only.
Details: https://codesandbox.io/s/serene-noether-s8pqc?file=/src/components/Payment/Payment.js
In my Payment.js
function Payment() {
const [paymentImage, setPaymentImage] = useState({
id: 0,
value: ""
});
const handlePaymentImageChange = (e) => {
const { name, value } = e.target;
setPaymentImage({
...paymentImage,
[name]: value
});
};
return (
<div className="payment-container">
<Dropdown
title="Select payment"
items={items}
multiSelect={false}
handlePaymentImageChange={handlePaymentImageChange}
/>
{/* render specifed image based on the selected choice */}
//REST RENDER CODE...
// for example, value = Visa -> render visa image only
</div>
);
In my Dropdown.js
import React, { useState } from "react";
import "./Dropdown.css";
function Dropdown({
title,
items = [],
multiSelect = false,
handlePaymentImageChange
}) {
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState([]);
const [selectedValue, setSelectedValue] = useState(title);
//REST DROPDOWN TOGGLE FUNCTION
...
return (
<div className="dropdown-container">
// pass the item.value to change the Payment state, then render the correct image
{open && (
<ul className="dropdown-list">
{items.map((item) => (
<li
className="dropdown-list-item"
key={item.id}
onChange={() => handlePaymentImageChange(item.value)}
>
<button
type="button"
onClick={() => handleOnClick(item)}
value={item.value}
>
<span>{item.value}</span>
<span>{isItemInSelection(item) && "Selected"}</span>
</button>
</li>
))}
</ul>
)
}
</div>
);
}
export default Dropdown;
Any solution?
There are multiple issue,
In Dropdown component you should add eventListener for onClick not onChange.
Inside handlePaymentImageChange method you are using e.target.value for the value. But in your case e itself is the value. So you should write,
setPaymentImage({
...paymentImage,
value: e
});
When you are rendering the image there is no check. So check if value is "Visa" and render visa image and so on.
I have updated the code here please check.

React toggle visibility of clicked element

I have a simple list of items. When I click on one item I want the text inside span dissapear, but in the rest I want to make them visible. Now when I click on any all spans dissapear. Here is a simple demo:
link: https://codepen.io/rosy654/pen/VwaZJNb
You only have a single value called visible in the state of your Example component and your handleClick funciton updates that value independently of the element clicked. (You're using the same function and same state value for both spans.)
This can easily be solved in two ways:
Creating a new component (example: SpanItem) which only contains the span and the state of that span. You're Example component doesn't need any state in that case and can just render the SpanItem component multiple times.
const SpanItem = ({ label }) => {
const [visible, setVisible] = useState(true);
const handleClick = () => {
setVisible(!visible);
}
return <li onClick={handleClick}>{label} <span className={!visible && 'hide'}>Visible</span></li>
}
const Example = () => (
<div>
<ul>
<SpanItem label="First Item">
<SpanItem label="Second Item">
</ul>
</div>
);
You could refactor your usage of the state in the Example component and save one visible value per item:
const Example = () => {
const defaultVisibillity = true;
const [visible, setVisible] = useState({});
const isVisible = (id) => visible[id] || defaultVisibillity;
const handleClick = (id) => () => {
setVisible({
...visible
[id]: !isVisible(id)
})
}
return (
<div>
<ul>
<li id={1} onClick={handleClick(1)}>First Item <span className={!isVisible(1) && 'hide'}>Visible</span></li> <li id={2} onClick={handleClick(2)}>Second Item <span className={!isVisible(2) && 'hide'}>Visible</span></li>
</ul>
</div>
);
}
You are using same visible flag/state for both the span items. Hence either both will be hidden or shown at the same time when user clicks any one item,
Either use different states like visible1/visible2 and use them respectively
Or keep the ids to span element and change code as below inside handleClick
const el = document.getElementById(e.currentTarget.id);
el.style.visibility = "hidden"; // or "visible" accordingly

How can I set focus on a dynamically added field with React

I have a basic task list app that gives users the ability to add items to the task list. When the "Add Item" button is clicked I will insert a new row to the bottom of the list. The row contains an empty text field, where the user can enter their task name. I want to set the focus on this field as soon as it's push()ed into the array. I know how to set the focus using a ref if the field already exists, but I can't seem to figure it out for a dynamically added field. How can I do this?
Here is my code:
const tasks = [array_of_task_objects];
const [state, setState] = React.useState({tasks: tasks});
const newTask = {title: ''};
const addTask = () => {
let newTasks = [...state.tasks];
newTasks.push(newTask);
setState({...state, tasks: newTasks});
// Now, set focus in the input field...(how?)
};
Elsewhere:
<button onClick={addTask}>Add Task</button>
<ul>
{
state.tasks.map(task => {
return(
<li><input value={task.title}></li>
);
})
}
</ul>
One way to do this is to have a ref that's always referring to the last textbox and then running an effect that sets focus on that last element when tasks are updated. This is a shell of an example that should basically get you there:
export default function App() {
const [tasks, setTasks] = useState([newTask]);
const lastRef = useRef(null);
useEffect(() => {
if (lastRef.current)
lastRef.current.focus();
}, [tasks]);
return (
<div className="App">
{tasks.map((task, i) => (
<>
<input key={i} ref={i === tasks.length - 1 ? lastRef : undefined} />
<br />
</>
))}
<button
onClick={() => {
setTasks(tasks => [...tasks, newTask]);
}}
>
Add
</button>
</div>
);
}
You can make the task input focus itself when it is rendered the first time.
const Task = ({value}) => {
const ref = useRef(null);
useEffect(() => if (ref.current) {ref.current.focus()}, [ref.current])
return <li><input ref={ref} value={value} /></li>
}
This will work if you are only mounting one at a time. If you have multiple inputs rendered in an initial state for example you could introduce a shouldTakeFocus prop. Then you limit the effect to only run when shouldTakeFocus is true.

Resources