This is the code for a custom MultiSelect component I'm writing. I want each button to have class="selected" when that value is selected.
import React from 'react'
import './styles.scss'
export default function MultiSelect({
name = '',
options = [],
onChange = () => {},
}) {
const clickOption = e => {
e.preventDefault()
onChange(
options.map(o => {
if (o.value === e.target.value) o.selected = !o.selected
return o
}),
)
}
return (
<div className="multiselect" name={name}>
{options.map(option => (
<button
key={option.value}
value={option.value}
onClick={e => clickOption(e)}
{/* here */}
className={option.selected ? 'selected' : ''}
>
{option.name}
</button>
))}
</div>
)
}
The class name never displays as selected, and doesn't change when option.selected changes. When I add {option.selected ? 'selected' : ''} under {option.name} inside the button as raw text, it displays and changes as expected.
When I change it to either of the following, it works:
<button className={`${option.selected ? 'selected' : ''}`}>
<!-- OR -->
<button className={'' + (option.selected ? 'selected' : '')}>
Can anybody explain why plain old className={option.selected ? 'selected' : ''} isn't working?
I'm going to analyze your solution.
className={option.selected ? 'selected' : ''} could be rewrite to className={option.selected && 'selected' }if the property is defined the operation result will be 'selected' for operator precedence, javascript always evaluate from left to right.
MultiSelect is a stateless component so your options props come from the hight order component, one way is an onClick event send as a parameter the id of the option, and in the parent change the value of the option.
import React, { useState } from 'react';
import './styles.scss';
const MultiSelect = ({
name = '',
options = [],
onChange = () => {},
}) => {
const handleClick = (id) => () => onChange(id);
return (
<div className="multiselect" name={name}>
{options.map((option) => (
<button
key={option.value}
value={option.value}
onClick={handleClick(option.id)}
className={option.selected && 'selected'}
>
{option.name}
</button>
))}
</div>
);
};
const Parent = ({ }) => {
const [options, setOptions] = useState([{
id: 1,
value: 1,
selected: true,
name: 'Hello',
},
{
id: 2,
value: 2,
selected: false,
name: 'World',
},
]);
const handleChange = (id) => {
const option = options.find((option) => option.id === id);
setOptions(
...options.filter((option) => option.id !== id),
{
...option,
selected: !option.selected,
},
);
};
return <MultiSelect options={options} onChange={handleChange} name="Example" />;
};
Related
I'm working on adding arrow key functionality to a react-select input dropdown with submenus. I am able to open the submenu, however I don't know how to be able focus an option from the submenu.
As you can see in the image, the submenu parent is selected. And I want to be able to focus the options on the right.
export const CustomOption = () => {
const [showNestedOptions, setShowNestedOptions] = useState(false);
const [subMenuOpen, setSubMenuOpen] = useState(null);
// this is used to give styling when an option is hovered over or chosen with arrow key
const [isFocusedState, setIsFocusedState] = useState(false);
// this basically opens the submenu on ArrowRight keyboard click and gives it isFocusedState(true) which is used to render the SELECTED submenu
const handleKeys = (e: any) => {
if (e.key === 'ArrowRight') {
!props.isMobile && setShowNestedOptions(true);
e.setIsFocusedState(true);
} else if (e.key === 'ArrowLeft') {
!props.isMobile && setShowNestedOptions(false);
e.setIsFocusedState(false);
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeys);
return () => {
return window.removeEventListener('keydown', handleKeys);
};
}, []);
// this does the same but on mouseOver (hover)
const handleMouseOver = (e: any) => {
!props.isMobile && setShowNestedOptions(true);
setIsFocusedState(true);
};
const handleMouseLeave = (e: any) => {
!props.isMobile && setShowNestedOptions(false);
setIsFocusedState(false);
};
return (
<Box>
{props.data.nestedOptions ? (
<Box
onMouseLeave={handleMouseLeave}
onMouseOver={handleMouseOver}
onKeyDown={handleKeys}
onClick={() => setIsFocusedState(!isFocusedState)}
>
<MainOption
renderOption={props.renderOption}
option={props.data}
hasNestedOptions={true}
setSubMenuOpen={() => setSubMenuOpen(props.data.value)}
selectOption={selectOption}
isFocused={isFocusedState}
/>
{showNestedOptions && (
<Box>
{(isFocusedState || props.isFocused) &&
map(props.data.nestedOptions, (nestedOption, index: number) => {
const isFirst = index === 0;
const value = props.getOptionValue?.(nestedOption) || props.value;
const label = props.getOptionLabel?.(nestedOption) || props.label;
const nestedInnerProps = innerProps;
nestedInnerProps.onClick = (e: React.ChangeEvent<HTMLInputElement>) =>
selectOption(props.data.nestedOptions.find((o: SelectOption) => o.label === e.target.textContent));
const optionProps = {
...props,
data: { ...nestedOption, parentValue: subMenuOpen },
value: value,
label: label,
children: label,
innerProps: { ...nestedInnerProps, parentValue: subMenuOpen },
};
// here the submenu is rendered and opened
return (
<Box
key={index}
>
<Option {...optionProps} key={value} isFocused={false}>
<Center>
{label}
</Center>
</Option>
</Box>
);
})}
</Box>
)}
</Box>
) : (
// if there's no submenu, simply render the list of options
<Option {...props}>
<MainOption
isMobile={props.isMobile}
option={props.data}
getOptionValue={props.getOptionValue}
renderOption={props.renderOption}
wrapperOptionArg={props.wrapperOptionArg}
getOptionLabel={props.getOptionLabel}
/>
</Option>
)}
</Box>
);
};
I have tried to add onKeyDown to change the isFocused prop conidtionally, like so but somehow it only works on mouseOver and it sets the condition inappropriately ( for all options, or none at all )
return (
<Box
key={index}
>
<Option
{...optionProps}
key={value}
isFocused={props.data.nestedOptions[0] ? true : false}
<Center>
{label}
</Center>
</Option>
</Box>
Unfortunately there's not much information about this certain keyboard functionality that I was able to find online.
In short, how to focus the first element of a submenu when ArrowRight is already used to open the submenu?
The SelectedColumn value doesn't come in the CustomHeader component. However, setSelectedColumn works! Why🧐 ?
Also, I'm passing CustomHeader to constant components that use useMemo. Without useMemo CustomHeader doesn't work.
const [selectedColumn, setSelectedColumn] = useState(null);
console.log("selected Column Outside:", selectedColumn); // It works!
const CustomHeader = (props) => {
const colId = props.column.colId;
console.log("selected Column In CustomHeader:", selectedColumn); // Doesn't work
return (
<div>
<div style={{float: "left", margin: "0 0 0 3px"}} onClick={() => setSelectedColumn(props.column.colId)}>{props.displayName}</div>
{ selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
)
}
const components = useMemo(() => {
return {
agColumnHeader: CustomHeader
}
}, []);
UPDATE: If I use the useState hook inside the CustomHeader component, it adds a "+" sign to each column and does not remove from the previous one. Here is a picture:
After reading your comment, your issue is clearly about where you want to place your useState.
First of all, you should always place useState inside a component. But in your case, apparently what you're trying to achieve is that when you select a column, the other columns get deselected.
Therefore, you need to pass both selectedColumn and setSelectedColumn as props to your component, and create the useState on the parent component.
Assuming all your CustomHeader components share the same parent component, in which my example I'll call CustomHeadersParent, you should do something like this:
// added mock headers to have a working example
const headers = [
{
displayName: "Game Name",
column: {
colId: 1,
},
},
{
displayName: "School",
column: {
colId: 2,
},
},
];
const CustomHeadersParent = (props) => {
const [selectedColumn, setSelectedColumn] = useState(null);
return headers.map((h) => (
<CustomHeader
column={h.column}
displayName={h.displayName}
setSelectedColumn={setSelectedColumn}
selectedColumn={selectedColumn}
/>
));
};
const CustomHeader = (props) => {
const colId = props.column.colId;
return (
<div>
<div
style={{ float: "left", margin: "0 0 0 3px" }}
onClick={() => props.setSelectedColumn(props.column.colId)}
>
{props.displayName}
</div>
{props.selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
);
};
const components = useMemo(() => {
return {
agColumnHeader: CustomHeader,
};
}, []);
You should use hooks inside your component
const CustomHeader = (props) => {
const colId = props.column.colId;
const [selectedColumn, setSelectedColumn] = useState(null);
console.log("selected Column In CustomHeader:", selectedColumn); // Should work
return (
<div>
<div style={{float: "left", margin: "0 0 0 3px"}} onClick={() => setSelectedColumn(props.column.colId)}>{props.displayName}</div>
{ selectedColumn === colId ? <FontAwesomeIcon icon={faPlus} /> : null}
</div>
)
}
I am loading a child component on parent component in React.js. With a click on the button, data will be pass to child component through props, and child component will map through that data and render on screen. I am getting data from localstorage and processing its data structure to child component.
But, the issue is when I click on the button and data is being passed and rendered, the button is shown and after the child component is rendered that shows up. I need the loading spinner when I click on the button and it disappears and shows the actual component.
I have tried methods like loading: false in the state but to no avail.
Thanks for your help
import ShowPatientAppointments from './ShowPatientAppointments.component';
class PatientAppointmnent extends Component {
state = {
doctorSlots: null,
timingSlot: null,
bookDay: null,
bookTime: null,
hasTiming: false,
}
getSlots = () => {
let slot = [];
let time = [];
for (let i=0; i< localStorage.length; i++) {
let key = localStorage.key(i);
let value = JSON.parse(localStorage[key]);
slot.push(key);
time.push(value);
this.setState({doctorSlots: slot, timingSlot: time});
}
console.log(this.state.doctorSlots, this.state.timingSlot);
}
render() {
const { doctorSlots, timingSlot, hasTiming } = this.state;
return(
<div>
<button onClick={this.getSlots} className='button'>Show me dates</button>
{doctorSlots === null ? <p></p> : <PatientSelectDay props={doctorSlots} timing={timingSlot} getTimings={this.getTiming} />}
</div>
)
}
}
export default PatientAppointmnent;
class PatientSelectDay extends Component {
state = {
options: [...this.props.props].map(obj => {
return {value: `${obj}`, label: `${obj}`}
}),
timingOptions: [...this.props.timing],
open_id: [],
part_id: '',
doctorDay: 'day',
doctorTiming: 'timing',
}
changeSingleHandler = e => {
this.setState({ part_id: e ? e.value : '' });
};
changeHandler = e => {
let add = this.state.open_id;
add.push(e.map(x => x.value));
this.setState({ open_id: e ? add : [] });
};
saveState = (option) => {
...save selected options
}
render() {
const {options, timingOptions} = this.state;
return (
<div>
<div className='carousel'>
{options.map((option, index) => {
const timing = timingOptions[index].map(obj => {
return {value: `${obj}`, label: `${obj}`}});
return(
<div key={index}>
<Select
name="open_id"
value={option}
onChange={this.changeSingleHandler}
options={option}
className='select'
/>
<Select
name="open_id"
value={this.state.open_id}
onChange={this.changeHandler}
options={timing}
className='select'
/>
<button onClick={() => this.saveState(option)} className='button-left'>Select Days</button>
</div>
)
})}
</div>
</div>
)
}
}
export default PatientSelectDay;
You need to update your code adding a loading state variable.
class PatientAppointmnent extends Component {
state = {
doctorSlots: null,
timingSlot: null,
bookDay: null,
bookTime: null,
hasTiming: false,
loading: false
}
getSlots = () => {
let slot = [];
let time = [];
this.setState({
loading: true
})
for (let i=0; i< localStorage.length; i++) {
let key = localStorage.key(i);
let value = JSON.parse(localStorage[key]);
slot.push(key);
time.push(value);
this.setState({doctorSlots: slot, timingSlot: time, loading: false});
}
console.log(this.state.doctorSlots, this.state.timingSlot);
}
renderButton = () => {
const { doctorSlots, timingSlot, loading } = this.state;
if(doctorSlots === null && timingSlot === null) {
return <div>
{loading ? <p>Loading...</p> : <button onClick={this.getSlots} className='button'>Show me dates</button>}
</div>
}
return null;
}
render() {
const { doctorSlots, timingSlot, hasTiming, loading } = this.state;
return(
<div>
{this.renderButton()}
{doctorSlots === null ? <p></p> : <PatientSelectDay props={doctorSlots} timing={timingSlot} getTimings={this.getTiming} />}
</div>
)
}
}
export default PatientAppointmnent;
class PatientSelectDay extends Component {
state = {
options: [...this.props.props].map(obj => {
return {value: `${obj}`, label: `${obj}`}
}),
timingOptions: [...this.props.timing],
open_id: [],
part_id: '',
doctorDay: 'day',
doctorTiming: 'timing',
}
changeSingleHandler = e => {
this.setState({ part_id: e ? e.value : '' });
};
changeHandler = e => {
let add = this.state.open_id;
add.push(e.map(x => x.value));
this.setState({ open_id: e ? add : [] });
};
saveState = (option) => {
...save selected options
}
render() {
const {options, timingOptions} = this.state;
return (
<div>
<div className='carousel'>
{options.map((option, index) => {
const timing = timingOptions[index].map(obj => {
return {value: `${obj}`, label: `${obj}`}});
return(
<div key={index}>
<Select
name="open_id"
value={option}
onChange={this.changeSingleHandler}
options={option}
className='select'
/>
<Select
name="open_id"
value={this.state.open_id}
onChange={this.changeHandler}
options={timing}
className='select'
/>
<button onClick={() => this.saveState(option)} className='button-left'>Select Days</button>
</div>
)
})}
</div>
</div>
)
}
}
export default PatientSelectDay;
Still learning some ropes of React.
I have the following code where I render a list of buttons:
import React, { useState, useEffect } from 'react';
const MyComponent = (props) => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => { getButtonList()}, [])
const getButtonList = () => {
let data = [
{id: 1, name: 'One', selected: false },
{id: 2, name: 'Two', selected: false },
{id: 3, name: 'Three', selected: false }
]
setButtonList(data)
}
const ButtonItem = ({ item }) => {
const btnClick = (event) => {
const id = event.target.value
buttonList.forEach((el) => {
el.isSelected = (el.id == id) ? true : false
})
setButtonList(buttonList)
console.log('buttonList', buttonList)
}
return (
<button type="button"
className={ "btn mx-2 " + (item.isSelected ? 'btn-primary' : 'btn-outline-primary') }
onClick={btnClick} value={item.id}>
{item.name + ' ' + item.isSelected}
</button>
)
}
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{
buttonList.map(item => (
<ButtonItem key={item.id} item={item} />
))
}
</div>
</div>
</div>
)
}
export default MyComponent;
So the button renders:
[ One false ] [ Two false ] [ Three false ]
And when I click on any Button, I can see on Chrome React Tools that the value for isSelected of that button becomes true. I can also confirm that the specific array item for the button clicked in the dev tools for State (under hooks), the value is true.
The text for the button clicked does not show [ One true ] say if I clicked on button One. What am I missing here?
P.S. Note that I also want to change the class of the button, but I think that part will be resolved if I get the button isSelected value to be known across the component.
Code Demo:
https://codesandbox.io/s/laughing-keller-o5mds?file=/src/App.js:666-735
Issue: You are mutating the state object instead of returning a new state reference. You were also previously using === to compare a string id to a numerical id which was returning false for all comparisons.
Solution: Use a functional update and array.map to update state by returning a new array.
const ButtonItem = ({ item }) => {
const btnClick = event => {
const id = event.target.value;
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id == id
}))
);
};
...
};
Suggestion: Factor out the btnClick handler, it only needs to be defined once. Curry the id property of item so you can use ===.
const btnClick = id => event => {
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id === id
}))
);
};
Update the attaching of click handler to pass the item id
const ButtonItem = ({ item }) => {
return (
<button
type="button"
className={
"btn mx-2 " +
(item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={btnClick(item.id)} // <-- pass item.id to handler
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
In your btnClick handler you are mutating your state, you should create a new value and assign it instead:
import React from "react";
import "./styles.css";
import { useState, useEffect } from "react";
const ButtonItem = ({ item, onClick }) => {
return (
<button
type="button"
className={
"btn mx-2 " + (item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={() => onClick(item.id)}
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
const MyComponent = props => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => {
getButtonList();
}, []);
const getButtonList = () => {
let data = [
{ id: 1, name: "One", isSelected: false },
{ id: 2, name: "Two", isSelected: false },
{ id: 3, name: "Three", isSelected: false }
];
setButtonList(data);
};
const btnClick = id => {
const updatedList = buttonList.map(el => ({
...el,
isSelected: el.id === id
}));
setButtonList(updatedList);
console.log("buttonList", updatedList);
};
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{buttonList.map(item => (
<ButtonItem key={item.id} item={item} onClick={btnClick} />
))}
</div>
</div>
</div>
);
};
export default MyComponent;
I've been building an application that pulls data from a database onto a React frontend in the form of items in an array. Each item then has information on it with a button to visit the URL on that item plus a button to delete the item from the database. This functionality works, I have made API's that interact with the database and everything is great. When the delete button is pressed, the event gets removed from the database but it doesn't remove the item from the UI. Could someone please show me where I could be going wrong?
I've created a component for the events, with logic to map all items to a grid on the front end:
const Events = () => {
const eventContext = useContext(EventContext);
const { events, loading } = eventContext;
if (loading) {
return <Spinner />;
} else {
return (
<div style={userStyle}>
{events.map(event => (
<EventItem key={event._id} event={event} />
))}
</div>
);
}
};
const userStyle = {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gridGap: "1rem"
};
export default Events;
I've then created an EventItem component to show data from each item in the array:
const EventItem = ({ event }) => {
const eventContext = useContext(EventContext);
const { deleteEvent } = eventContext;
const { _id, Client, Keyword, Source, Url, URL, Shodan_URL, Title, Date, Ip, PhishingAgainst, Preview, Verdict, Port, Product, Version, Expires } = event;
const onDelete = e => {
e.preventDefault();
deleteEvent(_id);
}
return (
<div className="card bg-light text-center">
{Client ? <h3>Client: {Client}</h3> : null}
{Keyword ? <p>Keyword: {Keyword}</p> : null}
{Source ? <h4>Source: {Source}</h4> : null}
<hr></hr>
{Url ? <p>URL: {Url}</p> : null}
{URL ? <p>URL: {URL}</p> : null}
{Shodan_URL ? <p>URL: {Shodan_URL}</p> : null}
{Title ? <p>Title: {Title}</p> : null}
{Date ? <p>Date: {Date}</p> : null}
{Ip ? <p>IP: {Ip}</p> : null}
{PhishingAgainst ? <p>Phishing Target: {PhishingAgainst}</p> : null}
{Preview ? <p>Preview: {Preview}</p> : null}
{Verdict ? <p>Verdict: {Verdict}</p> : null}
{Port ? <p>Port: {Port}</p> : null}
{Product ? <p>Product: {Product}</p> : null}
{Version ? <p>Version: {Version}</p> : null}
{Expires ? <p>Expires: {Expires}</p> : null}
<div>
<a type="button" href={Url || URL || Shodan_URL} className="btn btn-dark btn-sm my-1">Go to Source</a>
<button onClick={onDelete} className="btn btn-danger btn-sm my-1">Delete</button>
</div>
</div>
)
}
Here is the file with my state:
const EventState = props => {
const initialState = {
events: [],
loading: false,
}
const [state, dispatch] = useReducer(EventReducer, initialState);
// Get Events
const getEvents = async () => {
setLoading();
const res = await axios.get("http://localhost:5000/events");
dispatch({
type: GET_EVENTS,
payload: res.data
})
};
// Clear events
const clearEvents = () => {
dispatch({ type: CLEAR_EVENTS });
};
// Delete Event
const deleteEvent = async id => {
await axios.delete(`http://localhost:5000/events/${id}`);
dispatch({
type: DELETE_EVENT,
payload: id
})
};
const setLoading = () => {
dispatch({ type: SET_LOADING });
}
return <EventContext.Provider
value={{
events: state.events,
loading: state.loading,
getEvents,
clearEvents,
deleteEvent,
}}
>
{props.children}
</EventContext.Provider>
}
export default EventState;
Here is where the state passes the deletion of the event to the reducer:
import {
GET_EVENTS,
CLEAR_EVENTS,
DELETE_EVENT,
SET_LOADING
} from '../types';
export default (state, action) => {
switch (action.type) {
case GET_EVENTS:
return {
...state,
events: action.payload,
loading: false,
};
case SET_LOADING:
return {
...state,
loading: true
};
case CLEAR_EVENTS:
return {
...state,
events: [],
loading: false
};
case DELETE_EVENT:
return {
...state,
events: state.events.filter(event => event._id !== action.payload)
};
default:
return state;
}
}
The item is removed from the database, but not from the front end. Any thoughts?