Changing State on Multiple React Elements in a Grid - javascript

I have a grid that has multiple buttons in it, the number of buttons varying by row. For example, row 1 could have 4 buttons, row 2 could have 3, row 3 could have 3, etc. I'm trying to make it to where when I click on one button, it changes not only its background color, but every button in the grid that also has the same text. So if my buttons kind of look like this:
[ text ] [ moreText ] [ someMoreText ]
[ texting] [ textAgain ] [text]
[textAgain] [ someMoreText]
The idea is that when I click on the button [ text ] in row 1, it'll also change the button in row 2 column 3 that is also [ text ].
Right now, I can get one button to change, but I'm stuck on getting the rest of them to change. Below is my code.
GridComponent.js
import React from "react";
import ButtonComponent from "./ButtonComponent";
const GridComponent = ({ arrayOfArray }) => {
const renderColumns = (array) => {
const columns = array.map((buttonText, index) => {
return (
<div key={index}>
<div className="column">
<ButtonComponent buttonText={buttonText} />
</div>
</div>
);
});
return columns;
};
const renderRows = () => {
const rows = arrayOfArry.map((array, index) => {
return (
<div key={index} className="row">
{renderColumns(array)}
</div>
);
});
return rows;
};
return (
<div>
<div className="ui grid">{renderRows()}</div>
</div>
);
};
export default GridComponent;
ButtonComponent.js
import React, { useState } from "react";
const ButtonComponent = ({ buttonText }) => {
const [status, setStatus] = useState(false);
const color = status ? "green" : "blue";
return (
<div className={`ui ${color} button`} onClick={() => setStatus(!status)}>
{buttonText}
</div>
);
};
export default ButtonComponent;

You need to maintain the state at the GridComponent level, not on each ButtonComponent, which has no knowledge of other buttons.
You can do this by using a "map" object that maps the button text to its status.
const GridComponent = ({ arrayOfArray }) => {
const [statuses, setStatuses] = useState({});
...
And pass this map and the update function to the ButtonComponent:
<ButtonComponent
buttonText={buttonText}
status={statuses}
updateStatus={setStatuses}
/>
And in the ButtonComponent set the color bases on the status in the map for this button text:
const ButtonComponent = ({ buttonText, status, updateStatus }) => {
const color = status[buttonText] ? "green" : "blue";
return (
<div
className={`ui ${color} button`}
onClick={() =>
updateStatus({ ...status, [buttonText]: !status[buttonText] })
}
>
{buttonText}
</div>
);
};
You can see how it works on codesandbox

Related

Filtering "All" category by props with React

I'm having a filter logic on the page. Clicking on different buttons I'm filtering the initial array. How can I display all the items in the array to the sibling component by clicking the "All" button in the filtering component. Need to pass function to the parent component if I'm getting it right.
https://codesandbox.io/s/trusting-moon-djocul?file=/src/components/Filters.js.
-----
Parent component
-----
const ShopPage = () => {
const [data, setData] = useState(Categories);
const filterResult = (catItem) => {
if (!catItem) {
console.log(Categories);
setData(Categories);
} else {
const result = Categories.filter(
(curData) => curData.category === catItem
);
setData(result);
}
};
return (
<>
<div className={styles.wrapper}>
<Filters filterResult={filterResult} />
<Products products={data} />
</div>
</>
);
};
export default ShopPage;
-----
Child component
-----
const Filters = ({ filterResult }) => {
return (
<>
<div className={styles.filterbtns}>
<div onClick={() => filterResult("Cap")} className={styles.filterbtn}>
Cap
</div>
<div onClick={() => filterResult("Shirt")} className={styles.filterbtn}>
Shirt
</div>
<div
onClick={() => filterResult("Jogging")}
className={styles.filterbtn}
>
Jogging
</div>
// needed to change the useState data of the compnonent and show all of the items
<div onClick={() => filterResult()} className={styles.filterbtn}>
All
</div>
</div>
</>
);
};
export default Filters;
**
Consider passing null as filterResult parameter from the all button:
<div onClick={() => filterResult(null)} className={styles.filterbtn}>
All
</div>
This can be captured in the filterResult function where you set the result back to the original Categories if no filter category was passed:
const result = catItem
? Categories.filter((curData) => curData.category === catItem)
: Categories;
Updated SandBox:

How to render a button in parent component based on children action in react js?

I have a list of movie cards when a user clicks on them, they become selected and the id of each movie card is transferred to an array named "selectedList".
I want to add a "let's go" button below the movie card but conditionally.
I mean when the array is empty the button should not be displayed and when the user clicked on at least a movie the button displays. the array should be checked each time and whenever it becomes equal to zero the button should disappear.
the thing is all the movie cards are the children of this page and I want to render the parent component based on children's behavior.
MY MAIN PAGE:
export default function Index(data) {
const info = data.data.body.result;
const selectedList = [];
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<SearchBar />
</div>
<div className={style.card_container}>
{info.map((movie, i) => {
return (
<MovieCard
movieName={movie.name}
key={i}
movieId={movie._id}
selected={selectedList}
isSelected={false}
/>
);
})}
</div>
</main>
<button className={style.done}>Let's go!</button>
</>
);
}
**MOVIE CARD COMPONENT:**
export default function Index({ selected, movieName, movieId, visibility }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
}
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
id={movieId}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}
You can use conditional rendering for that:
{selectedList.length > 0 && <button className={style.done}>Let's go!</button>}
Plus, you should change your selectedList to a state, and manage the update via the setSelectedList function:
import { useState } from 'react';
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
Add the method to the MovieCardĀ as a property:
<MovieCard
movieName={movie.name}
key={i}
movieId={movie._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>;
And update the list in the pushToSelected method:
export default function MovieCard({
selected,
setSelected,
movieName,
movieId,
visibility
}) {
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
}
setSelected([...selected]);
toggleClass();
};

React: How can I remove a specific div element after clicking its associated button?

I have this component which adds a div and its elements to the dom on button click. The adding part works fine as expected but the issue arises when I want to delete.
Right now when I click on the delete button, it does remove the item but it doesn't remove that specific item which the button is associated with. It just removes the div from the top or bottom.
I have been trying to remove that specific div whose button has been clicked to remove. How can I achieve that?
Here's the CodeSandbox.
And here's the code:
import { useState } from "react";
const App = () => {
const [ counter, setCounter ] = useState( 1 );
const handleAddDiv = () => {
setCounter( counter + 1 );
};
const handleRemoveDiv = () => {
setCounter( counter - 1 );
};
return (
<div className="App">
{
Array.from(Array( counter )).map(( item, idx ) => (
<div>
<div>
<input type="text" />
<button onClick={handleRemoveDiv}>Remove</button>
</div>
</div>
))
}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
export default App;
This is not prefered react way of doing things, but this will work:
import "./styles.css";
import { useState } from "react";
const App = () => {
const [counter, setCounter] = useState(1);
const handleAddDiv = () => {
setCounter(counter + 1);
};
const removeNode = (idx) => document.getElementById(`id-${idx}`).remove();
return (
<div className="App">
{Array.from(Array(counter)).map((item, idx) => (
<div key={idx} id={`id-${idx}`}>
<div>
<input type="text" />
<button onClick={() => removeNode(idx)}>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
};
export default App;
Generaly if you would like to have it made correactly then you would want to map on a real array and have every item in array eighter having an unique id or simply use map index and then based on which item you click write a function to remove from that array our specific element.
Map over an array of unique Ids
First of all, you should map over an array of items instead of an integer value.
So, on click of add button, you should push a unique ID to the array of items where each ID would denote an item being rendered in your app.
Now, when you click on remove button, you would need to remove that ID from the array of items, which would result in "deletion" of that div from the app.
In my case, I have considered timestamp as a unique ID but should explore other options for generating unique IDs. Working with indices is anti pattern in React especially when you are mapping over an array in JSX as you would encounter issues at one point of time. So, it's a good idea to maintain unique Ids.
Note: Damian's solution is not ideal as DOM Manipulation is avoided in React.
const { useState, useCallback } = React;
const Item = ({ id, removeDiv }) => {
const clickHandler = useCallback(() => {
removeDiv(id);
}, [id, removeDiv]);
return (
<div>
<input type="text" />
<button onClick={clickHandler}>Remove</button>
</div>
);
};
const App = () => {
const [items, setItems] = useState([]);
const addDiv = useCallback(() => {
// using timestamp as a unique ID
setItems([...items, new Date().getTime()]);
}, [items]);
const removeDiv = useCallback((itemId) => {
// filter out the div which matches the ID
setItems(items.filter((id) => id !== itemId));
}, [items]);
return (
<div className="app">
{items.map((id) => (
<Item key={id} id={id} removeDiv={removeDiv} />
))}
<button onClick={addDiv}>Add</button>
</div>
);
};
ReactDOM.render(<App />,document.getElementById("react"));
.app {
text-align: center;
font-family: sans-serif;
}
<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="react"></div>
I would use an array state instead of a counter state, because otherwise you don't know which element has to be removed.
import { useState } from "react";
import "./styles.css";
let counter = 1;
export default function App() {
const [array, setArray] = useState([0]);
const handleAddDiv = () => {
setArray((prev) => [...prev, counter++]);
};
const handleRemoveDiv = (idx) => {
var arrayCopy = [...array];
arrayCopy.splice(idx, 1);//remove the the item at the specific index
setArray(arrayCopy);
};
return (
<div className="App">
{array.map((item, idx) => (
<div key={item}>
<div>
<input type="text" />
<button onClick={()=>handleRemoveDiv(idx)}
>Remove</button>
</div>
</div>
))}
<button onClick={handleAddDiv}>Add</button>
</div>
);
}
When I am adding a new item, I give it the value counter++, because I will use it as a key, and a key should be unique.

Reactjs: How to select one or multiple cards when clicked with a state hook?

I have a card component TemplateList that is used to map my template cards (I add them through an array).
I would like to add an onClick state hook functionality that helps me select one or multiple cards when clicked, How can I do this?
This is my TemplateList component:
import TemplateCard from
import styles from "./styles/actionpage.m.css";
export type Template = {
title: string;
description: string;
imgURL: string;
};
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}
/>
))}
</div>
);
};
export default TemplateList;
This is my TemplateCard component:
import React from "react";
import styles from "./styles/cards.m.css";
type Props = {
title: string;
description: string;
img: string;
classNameToAdd?: string;
selected?: boolean;
classNameOnSelected?: string;
};
const TemplateCard = ({
title,
description,
img,
classNameToAdd,
classNameOnSelected,
selected,
}: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
if (selected) {
className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
}
return (
<div style={card} className={className}>
<img style={imageSize} src={img}></img>
<div style={cardTitle}>{title}</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
TemplateCard.defaultProps = {
classNameOnSelected: styles.selected,
};
export default TemplateCard;
At the moment, I added a "selected" prop that gives my cards a border when true, but this of course selects all cards when true now.
This is how my cards are supposed to look like when selected.
To approach this, you have to give each card its own state; selected.
This way each card will have their own tiny logic through which they will know if they're selected or not.
// TemplateCard.js
...
const TemplateCard = ({
title,
description,
img,
classNameToAdd,
classNameOnSelected,
/* selected, we will make this a state instead of prop */
}: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
const [selected, setSelected] = useState(false); // Added state
// Added handler
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}</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
...
Add an onClick handler on your TemplateCard component and fire it on the main div. Moreover you need some identification for your individual card so we can use the index of array its coming from.
Like this:
const TemplateCard = ({
title,
description,
img,
classNameToAdd,
classNameOnSelected,
selected,
handleClick
index
}: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
if (selected) {
className += `${styles.card} ${classNameToAdd} ${classNameOnSelected}`;
}
return (
<div style={card} className={className} onClick={()=>handleClick(index)}>
<img style={imageSize} src={img}></img>
<div style={cardTitle}>{title}</div>
<div style={descriptionCard}>{description}</div>
</div>
);
};
In your TemplateList component, you need to set an array of indexes (using useState) that stores the indexes of clicked TemplateCards so you can check which card is selected and which is not. And set the state using the function which we passed onto the child component i.e. handleClick
Like this:
const [selectedArray,setSelectedArray]=useState([])
const handleClick=(i)=>{
const tempArray =[...selectedArray]
if(tempArray[i]==i){tempArray[i]=undefined}
else {tempArray[i]=i}
setSelectedArray(tempArray)
}
The component will be like this:
const TemplateList = ({ templates }: Props) => {
return (
<div className={styles.scrollContainer}>
{templates.map((item,index) => (
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
index={index}
selected={selectedArray[index]==index? true:false}
handleClick={handleClick}
/>
))}
</div>
);
};
Now what's happening here is that we are using the index as an identifier, and based on that we are setting an array of selected cards. On the basis of which we send 'selected' prop to the Card component. And there you can apply your styling based on that prop.
Hope your question was answered!!

Displaying one List at a time on Clicking button

I have a web page where i am displaying list of records using React JS table and each row has a button that shows user a list of action item.If the user clicks on a button in a row and clicks on another button in another row the previous list should hide but both the list are displaying.How to solve this?
const dataset = [
{"id" : 1, "buttons" : (<ActionItemList />)},
{"id" : 2, "buttons" : (<ActionItemList />)}
]
<ReactTable data={dataset} />
const ActionItemList = () => {
return (
<div>
<button>...</button>
</div>
<div>
<ul>
<li>action-item1</li>
<li>action-item2</li>
<li>action-item3</li>
</ul>
/>
</div>
</div>
)
}
If you can use hooks you can have your menu set the openId with a method passed by a wrapper. Here is an example of how you can do it:
import React, { useState, memo, useMemo } from 'react';
import ReactTable from 'react-table';
//make ActionItemList a pure component using React.memo
const ActionItemList = memo(
({ isOpen, setOpenId, id }) =>
console.log('in render', id) || (
<button
onClick={() =>
isOpen ? setOpenId() : setOpenId(id)
}
>
{isOpen ? 'Close' : 'Open'} Menu
</button>
)
);
const dataset = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
];
const columns = [
{ id: 1, accessor: 'id' },
{
id: 2,
//if accessor is a function table will render what function returns
// passing in the data where we added isOpen and setOpenId
accessor: ({ Menu, ...props }) => (
//passing in the needed props to pure component Menu
<Menu {...props} />
),
},
];
//wrap this on your react table, adding isOpen and setOpenId
export default () => {
//set state for open id
const [openId, setOpenId] = useState();
//memoize creating the data prop based on openId changing
const dataWithProps = useMemo(
() =>
dataset.map(item => ({
...item,
isOpen: item.id === openId,
setOpenId,
Menu: ActionItemList,
})),
[openId]
);
//return the react table with the extra props in data
return (
<ReactTable data={dataWithProps} columns={columns} />
);
};

Categories

Resources