If two or more cards from a component are selected how to pass data to a button component?
I have a landing page that holds two components; Template list and a button.
<TemplateList templates={templates} />
<MenuButton style={actionButton} onClick={onOnboardingComplete}>
Select at least 2 options
</MenuButton>
The TemplateList brings in an array of information for my Template and creates Template Cards with it. I am then able to select my cards and unselect them.
My Button when pressed just takes me to the next step of the onboarding.
I would like to know how I should approach linking these two components. My button now is gray and would like this to happen:
1. Button is gray and user cant go on to next step, until selecting two or more cards.
2. When two or more cards are selected, button turn blue and they are able to press the button to continue.
This is my Template List:
export type Template = {
title: string;
description: string;
imgURL: string;
id?: number;
};
type Props = {
templates: Template[];
};
const TemplateList = ({ templates }: Props) => {
return (
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
/>
))}
</div>
);
};
export default TemplateList;
And this my Template Card:
type Props = {
title: string;
description: string;
img: string;
classNameToAdd?: string;
classNameOnSelected?: string;
};
const TemplateCard = ({ title, description, img, classNameToAdd, classNameOnSelected }: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
const [selected, setSelected] = useState(false);
const handleClick = () => {
setSelected(!selected);
};
if (selected) {
className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
}
return (
<div style={card} className={className} onClick={handleClick}>
<img style={imageSize} src={img}></img>
<div style={cardTitle}>
{title}
{selected ? <BlueCheckIcon style={blueCheck} className={styles.blueCheck} /> : null}
</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
TemplateCard.defaultProps = {
classNameOnSelected: styles.selected,
};
export default TemplateCard;
This is how it looks like now.
There are at least 3 ways to implement your requirement:
OPTION 1
Put Button component inside the <TemplateList> component
Add one useState tied to <TemplateList> component to hold the number of
selected cards
Add two new props onSelectCard and onDeselectCard to <TemplateCard> to increment/decrement newly created state by 1 for each selected/deselected item
Implement callback functions inside <TemplateList component (code below)
Call onSelectCard inside <TemplateCard> when needed (code below)
TemplateList Component
const TemplateList = ({ templates }: Props) => {
const [noOfSelectedCards, setNoOfSelectedCards] = useState(0);
handleSelect = () => setNoOfSelectedCards(noOfSelectedCards + 1);
handleDeselect = () => setNoOfSelectedCards(noOfSelectedCards - 1);
return (
<div classNae="template-list-container">
<div className={styles.scrollContainer}>
{templates.map((item) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
onSelectCard={handleSelect}
onDeselectCard={handleDeselect}
/>
))}
</div>
<MenuButton style={actionButton} onClick={onOnboardingComplete} className={noOfSelectedCards === 2 ? 'active' : ''}>
Select at least 2 options
</MenuButton>
</div>
);
};
TemplateCard Component
const TemplateCard = ({ ..., onSelectCard, onDeselectCard }: Props) => {
...
const [selected, setSelected] = useState(false);
const handleClick = () => {
if(selected) {
onDeselectCard();
} else {
onSelectCard();
}
setSelected(!selected);
};
...
};
Now, you'll have the current number of selected cards in your state noOfSelectedCards (<TemplateList> component) so you can conditionally render whatever className you want for your button or do something else with it.
OPTION 2
Using React Context to share state between components
It's okay to use React Context for such cases, but if your requirements contains other similar cases for handling/sharing states between components across the app, I suggest you take a look at the 3rd option.
OPTION 3
Using State Management like Redux to handle global/shared states between components.
This is probably the best option for projects where sharing states across the app is quite common and important. You'll need some time to understand concepts around Redux, but after you do that I assure you that you'll enjoy working with it.
Related
Here is the relevant code:
const Members = () => {
// array of each video in selected grade
const videosMap = (videos) => {
return videos.map((video) => (
<VideoCard
key={video.id}
thumbnail={video.thumbnail}
title={video.title}
description={video.description}
onClick={() => {
handleVideoClick();
}}
/>
));
};
// updates state of shown videos & page heading
const handleGradeButtonClick = (videos, heading) => {
setShowVideos(videosMap(videos));
setVideosHeading(heading);
};
const handleVideoClick = () => {
console.log("test");
};
// controls state of which grade's videos to show
const [showVideos, setShowVideos] = useState(videosMap(kinder_videos));
// controls states heading to display depending on selected grade
const [videosHeading, setVideosHeading] = useState("Kindergarten");
const [showVideoDetails, setShowVideoDetails] = useState(null);
The handleVideoClick is the function that is not working when I click on one of the mapped VideoCard components.
Here is the full code if you want to see that:
https://github.com/dblinkhorn/steam-lab/blob/main/src/components/pages/Members.js
When I look in React DevTools at one of the VideoCard components, it shows the following:
onClick: *f* onClick() {}
If I don't wrap it in an arrow function it does execute, but on component load instead of on click. I have a feeling it has something to do with my use of .map to render this component, but haven't been able to figure it out.
Thanks for any help!
There's no problem with your mapping method, you just need to pass the onClick method as a prop to your VideoCard component :
On your VideoCard component do this :
const VideoCard = (props) => {
const { thumbnail, description, title, onClick } = props;
return (
<div className="video-card__container" onClick={onClick}>
<div className="video-card__thumbnail">
<img src={thumbnail} />
</div>
<div className="video-card__description">
<div className="video-card__title">
<h3>{title}</h3>
</div>
<div className="video-card__text">{description}</div>
</div>
</div>
);
};
export default VideoCard;
I'm new to React, learning by coding, here i have component A, which has select element with menuItems (all material ui), when user clicks select element and chooses from drop down, right after user has chosen whole component should go display:none, is this possible ? i mean user should not be able to see select element anymore on the page
English is not my mother language, so there might be mistakes.
suggestions/help is appreciated.
component A:
const A: React.FC<AProps> = (props) => {
const handleChange = (e: React.ChangeEvent<{ value: unknown }>) => {
const site = e.target.value as string;
dispatch(changeActiveSite(site));
if (site) {
dispatch(getAnalysers(site));
} else {
dispatch(clearSiteData(site));
}
};
const sites = [
{
ident: "",
name: "None",
},
].concat(sitess);
return (
<React.Fragment>
<FormControl className={classes.formControl}>
<InputLabel id="site-select-input-label">site</InputLabel>
<Select
id="site-select"
value={currentSiteId}
labelId="site-select-input-label"
onChange={(e) => handleChange(e)}
>
{sites.map((site) => {
return (
<MenuItem key={site.ident} value={site.ident}>
{site.name}
</MenuItem>
);
})}
</Select>
</FormControl>
</React.Fragment>
);
};
that component is in component B like this: <div > <A site={site} /> </div>
Let's imagine your MenuItem.js, specifically, its render() and constructor(). You'll want it to be able to be hidden/not-displayed, or visible/displayed. Use a state attribute for hidden to control this, your render will probably look like...
constructor(props) {
super(props);
this.state = {
'hidden':false,
};
}
render () {
if(this.state.hidden) {
return '';
}
return (
<div
onClick={(e) => this.handleOnClick(e)}
>
{this.props.value}
</div>
);
}
Notice I also added a handleOnClick(e) handler up above! That will simply call this.setState({'hidden':true}), so like...
handleOnClick(e) {
this.setState({'hidden':true});
}
I have an answer to a similar question elsewhere, if it might also help: How to set one component's state from another component in React
I have one Component which shows a list of data in a dropdown and there is an option to search these data which works as a filter. Here is my code:
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Popover from '../../Popover';
import Input from '../../Input';
import Icon from '../../Icon';
import IconButton from '../../IconButton';
const DropDownFilter = props => {
const { label, options, onChange, isSearchEnabled } = props;
const [activeOption, setActiveOption] = useState({});
const [filter, setfilter] = useState('');
const searchFilter = event => {
setfilter(event.target.value);
};
const removeFilter = () => {
setfilter('');
};
const lowercasedFilter = filter.toLowerCase();
const filteredData = options.filter(item => {
return Object.keys(item).some(
key => typeof item[key] === 'string' && item[key].toLowerCase().includes(lowercasedFilter)
);
});
const labelText = activeOption.label ? activeOption.label : label;
const handleSelectedOption = option => {
setActiveOption(option);
onChange(option);
};
return (
<div className="filter">
<Popover linkText={labelText} size="small" direction="bottom-left">
{isSearchEnabled && (
<div className="filter__search">
<Input
value={filter}
onChange={searchFilter}
preIcon={
<div role="presentation">
<Icon name="search" />
</div>
}
placeholder="Search"
postIcon={
filter.length > 0 && (
<IconButton
icon={<Icon name="close" />}
size="tiny"
onClick={removeFilter}
standalone={true}
isIconOnly={true}
/>
)
}
/>
</div>
)}
<ul className="filter__options filter__options--scrollbar">
{filteredData.map(option => (
<li
key={option.value}
role="presentation"
className={classNames('filter__options-option', {
'filter__options-option--active': option.value === activeOption.value,
})}
onClick={() => handleSelectedOption(option)}
>
{option.label}
</li>
))}
</ul>
</Popover>
</div>
);
};
DropDownFilter.defaultProps = {
label: 'Filter Menu',
options: [],
isSearchEnabled: true,
};
DropDownFilter.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})
),
onChange: PropTypes.func.isRequired,
isSearchEnabled: PropTypes.bool,
};
export default DropDownFilter;
Here is a gif of it: https://recordit.co/HtalUtuPsj
Now during searching I want to send the value of the search param to another component, the value will be used to search from a DB or any other external data source which is being handled in that new component. Such as, if I am searching for Ratings, this component should search for it in the existing options list it has in its own component, as well as the same time it will search for Ratings in any other external data source or DB. This external network call, search or any other functionality will be processed in the other component. So this component will only send the search param; for example Ratings to the other component in real time.
I can think of an idea like I will get the searchParam in a state and pass the setState value to a new props which will be called through an onSearchParamChange function, this new function will pass the data through a callback and the other component will get the data through calling that props of this component. I am not sure if this is the correct way and also I am not able to implement this thought in the code either. Is there any better way to do it? if so what would be that coding implementation?
If you need to pass to a parent component you should be able to use for example the onChange prop which is passed to your component, like you are doing in the handleSelectedOption function. That function is in fact passing the chosen option to the parent component. If you want to pass to the parent component when the user is typing, then you should call the onChange function also in searchFilter:
const searchFilter = event => {
const option = event.target.value);
setfilter(option);
onChange(option);
};
If you want to pass it to a child component, the you can just pass it as prop:
<ChildComponent filter={ filter } />
I have a similar situation like the one in the sandbox.
https://codesandbox.io/s/react-typescript-fs0em
Basically what I want to achieve is that Table.tsx is my base component and App component is acting like a wrapper component. I am returning the JSX from the wrapper file.
Everything is fine but the problem is whenever I hover over any name, getData() is called and that is too much rerendering. Here it is a simple example but in my case, in real, the records are more.
Basically Table is a generic component which can be used by any other component and the data to be displayed in can vary. For e.g. rn App is returning name and image. Some other component can use the Table.tsx component to display name, email, and address. Think of App component as a wrapper.
How can I avoid this getData() to not to be called again and again on hover?
Can I use useMemo or what approach should I use to avoid this?
Please help
Every time you update the "hover" index state in Table.jsx it rerenders, i.e. the entire table it mapped again. This also is regenerating the table row JSX each time, thus why you see the log "getData called!" so much.
You've effectively created your own list renderer, and getData is your "renderRow" function. IMO Table shouldn't have any state and the component being rendered via getData should handle its own hover state.
Create some "row component", i.e. the thing you want to render per element in the data array, that handles it's own hover state, so when updating state it only rerenders itself.
const RowComponent = ({ index, name }) => {
const [hov, setHov] = useState();
return (
<div
key={name}
onMouseEnter={() => setHov(index)}
onMouseLeave={() => setHov(undefined)}
style={{ display: "flex", justifyContent: "space-around" }}
>
<div> {name} </div>
<div>
<img
src={hov === index ? img2 : img1}
height="30px"
width="30px"
alt=""
/>
</div>
</div>
);
};
Table.jsx should now only take a data prop and a callback function to render a specific element, getData.
interface Props {
data: string[];
getData: () => JSX.Element;
}
export const Table: React.FC<Props> = ({ data, getData }) => {
return (
<div>
{data.map((name: string, index: number) => getData(name, index))}
</div>
);
};
App
function App() {
const data = ["Pete", "Peter", "John", "Micheal", "Moss", "Abi"];
const getData = (name: string, index: number, hov: number) => {
console.log("getData called!", index);
return <RowComponent name={name} index={index} />;
};
return <Table data={data} getData={getData} />;
}
I have two functional components built in React, one is an Item component - it holds some data about stuff, with optional graphics, some text data and price information. On the bottom there is a button, that allows you to select this particular item. It also keeps information in its props on ID of currently selected Item - that's how i planned to solve this problem.
My second component is a ItemList - it basically holds a list of aforemetioned Items - plus it sorts all the items and must keep information about which component is currently selected - the selected one basically looks different - some stuff like the border box and button's colour gets switched via CSS.
My logic to implement goes like this - when user clicks on a "Select" button of a particular Item, the Item should change its look (unless it's already selected, then do nothing), after that somehow propagate info up onto the ItemList, so that it can "disable" the previously selected component. There can be only one selected Item, and once user decide to select another one, the previously selected should change its state and go back to unselected standard graphic style.
I've ran across a solution with state in the ItemList component plus passing a function via props into Item, but that doesn't solve the second part - ItemList needs to get info about a change, so it can rerender all the components according to actual state. What part of React API should I dive into to solve this issue?
Here is code for my components:
Item
interface Props {
receivedObject: itemToDisplay;
selectedItemId: string;
onClick?: () => void;
}
export default function Item(props: Props) {
const {name, description, price} = props.receivedObject;
const imageUrl = props.receivedObject?.media?.mainImage?.small?.url;
const priceComponent = <Price price={price}/>;
const [isItemSelected, setSelection] = useState(props.selectedItemId == props.receivedObject.id);
const onClick = props.onClick || (() => {
setSelection(!isItemSelected)
});
return (
<>
<div className="theDataHolderContainer">
// displayed stuff goes here
<div className="pickButtonContainer">
// that's the button which should somehow send info "upwards" about the new selected item
<Button outline={isItemSelected} color="danger" onClick={onClick}>{isItemSelected ? "SELECTED" : "SELECT"}</Button>
</div>
</div>
</>)
};
ItemList
interface Props {
packageItems: Array<itemToDisplay>
}
export default function ItemList(props: Props) {
const itemsToDisplay = props.packageItems;
itemsToDisplay.sort((a, b) =>
a.price.finalPrice - b.price.finalPrice
);
let selectedItemId = itemsToDisplay[0].id;
const [currentlySelectedItem, changeCurrentlySelectedItem] = useState(selectedItemId);
const setSelectedItemFunc = () => {
/* this function should be passed down as a prop, however it can only
* have one `this` reference, meaning that `this` will refer to singular `Item`
* how do I make it change state in the `ItemList` component?
*/
console.log('function defined in list');
};
return(
<div className="packageNameList">
<Item
key={itemsToDisplay[0].id}
receivedObject={itemsToDisplay[0]}
onClick={setSelectedItemFunc}
/>
{itemsToDisplay.slice(1).map((item) => (
<Item
key={item.id}
receivedObject={item}
onClick={setSelectedItemFunc}
/>
))}
</div>
);
}
In React, the data flows down, so you'd better hold state data in a stateful component that renders presentation components.
function ListItem({ description, price, selected, select }) {
return (
<li className={"ListItem" + (selected ? " selected" : "")}>
<span>{description}</span>
<span>${price}</span>
<button onClick={select}>{selected ? "Selected" : "Select"}</button>
</li>
);
}
function List({ children }) {
return <ul className="List">{children}</ul>;
}
function Content({ items }) {
const [selectedId, setSelectedId] = React.useState("");
const createClickHandler = React.useCallback(
id => () => setSelectedId(id),
[]
);
return (
<List>
{items
.sort(({ price: a }, { price: b }) => a - b)
.map(item => (
<ListItem
key={item.id}
{...item}
selected={item.id === selectedId}
select={createClickHandler(item.id)}
/>
))}
</List>
);
}
function App() {
const items = [
{ id: 1, description: "#1 Description", price: 17 },
{ id: 2, description: "#2 Description", price: 13 },
{ id: 3, description: "#3 Description", price: 19 }
];
return (
<div className="App">
<Content items={items} />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
.App {
font-family: sans-serif;
}
.List > .ListItem {
margin: 5px;
}
.ListItem {
padding: 10px;
}
.ListItem > * {
margin: 0 5px;
}
.ListItem:hover {
background-color: lightgray;
}
.ListItem.selected {
background-color: darkgray;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>