I have a landing Page with an array that is passed to my TemplateList component. And my TemplateList component is made up by TemplateCard components.
Each card on my TemplateList can be selected, and if more than 2 of those cards are selected, the button on the page activates onOnboardingComplete, and it continues to the next page.
What I would like to do, is to be able to grab each card's unique Id that I selected on the page And once I select one or more, convert those ids into an array (called: selectedTemplatesIds) that I can pass to onOnboardingComplete (the button).
Using the id as an identifier, and based on that we are getting an array of selected cards and passing it with onOnboardingComplete.
I hear I can use Object.keys, but never done this before. How can I do this?
Summary:
1. I want to be able to grab my card unique ids when selected and ignore them when unselected
2. Once I select one or more, convert those ids into an array (selectedTemplatesIds) in order to pass them to onOnboardingComplete (the button in TemplateList).
Here is the array I use in my LandingPage
templates = [
{
title: "Grocery List",
description: "Description of what this things does so the reader can have info of",
imgURL: "https://res.cloudinary.com/deruwllkv/image/upload/v1625753993/groceries.png",
id: 0,
},
{
title: "Shopping Space",
description: "blablabalblabla",
imgURL: "https://res.cloudinary.com/deruwllkv/image/upload/v1625753766/shopping.png",
id: 1,
},
{
title: "Travel Planning",
description: "blablabalblabla",
imgURL: "https://res.cloudinary.com/deruwllkv/image/upload/v1625753885/travel.png",
id: 2,
},
{
title: "Travel",
description: "blablabalblabla",
imgURL: "https://res.cloudinary.com/deruwllkv/image/upload/v1625753993/groceries.png",
id: 3,
},
];
My Templateslist component:
export type Template = {
title: string;
description: string;
imgURL: string;
id?: number;
};
type Props = {
templates: Template[];
onOnboardingComplete: Function;
};
const TemplateList = ({ templates, onOnboardingComplete }: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
const [noOfSelectedCards, setNoOfSelectedCards] = useState(0);
const handleSelect = () => setNoOfSelectedCards(noOfSelectedCards + 1);
const handleDeselect = () => setNoOfSelectedCards(noOfSelectedCards - 1);
let buttonText;
if (noOfSelectedCards === 1) {
buttonText = "Select at least 1 more option";
} else if (noOfSelectedCards >= 2) {
buttonText = "Create my initial cuadds!";
} else {
buttonText = "Select at least 2 options";
}
return (
<>
<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
onClick={onOnboardingComplete}
style={actionButton}
className={
noOfSelectedCards >= 2 ? `${styles.actionBlue}` : `${styles.actionNormal}`
}
>
{buttonText}
</MenuButton>
</>
);
};
export default TemplateList;
And my TemplateCards:
type Props = {
title: string;
description: string;
img: string;
classNameToAdd?: string;
classNameOnSelected?: string;
onSelectCard: any;
onDeselectCard: any;
};
const TemplateCard = ({
title,
description,
img,
classNameToAdd,
classNameOnSelected,
onSelectCard,
onDeselectCard,
}: Props) => {
const { aspectRatio, vmin } = useWindowResponsiveValues();
let className = `${styles.card} ${classNameToAdd}`;
const [selected, setSelected] = useState(false);
const handleClick = () => {
setSelected(!selected);
if (selected) {
onDeselectCard();
} else {
onSelectCard();
}
};
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;
Add:
const selectedTemplatesIds = [];
Change handlers:
const handleSelect = (id) => {
selectedTemplatesIds.push(id);
setNoOfSelectedCards(noOfSelectedCards + 1);
};
const handleDeselect = (id) => {
selectedTemplatesIds.splice(selectedTemplatesIds.indexOf(id),1);
setNoOfSelectedCards(noOfSelectedCards - 1);
}
Change:
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
onSelectCard={() => {
handleSelect(item.id)
}}
onDeselectCard={() => {
handleDeselect(item.id)
}}
/>
Rather than just storing the noOfSelectedCards. Just store the array of ids:
const [selectedIds, setSelectedIds] = useState([]);
Send in the id into the handleSelect function and push the id into the array.
<TemplateCard
title={item.title}
description={item.description}
img={item.imgURL}
classNameToAdd={styles.cardContainer}
key={item.id}
onSelectCard={() => handleSelect(item.id)}
onDeselectCard={handleDeselect}
/>
handleSelect(id) {
setSelectedIds(prev => [...prev, id])
}
And you can do a similar thing to remove the id when deselecting a card.
Related
I would like to keep the coloring of the selected item in the state, however, when changing pages (Ex: from 1 to 2 and back to 1), it loses the coloring and as default states start as false, a request is sent to remove all items from the state in useEffect.
is filtering 10 items per page, and when I change pages, apparently the state resets and starts from scratch, even though there are selected items on the page.
Types
type ISkySelected = {
id: string
}
interface ISkusProps {
id: string
description: string
code_sap: string
stock_pe: string
stock_atc: string
enableSku: boolean
skusSelectedList: (selected: ISkySelected) => void
removeSkuSelected: (selected: ISkySelected) => void
}
export function CardList ({ id, description, code_sap, stock_pe, stock_atc, enableSku, skusSelectedList, removeSkuSelected }: ISkusProps) {
const [selected, setSelected] = useState<boolean>(false)
const [color, setColor] = useState<string>('red.500')
const [cursor, setCursor] = useState<string>('')
function selectedSku (event: MouseEvent) {
event.preventDefault()
if (!enableSku) {
return
}
setSelected(!selected)
}
useEffect(() => {
if (selected) {
setColor('red.500')
skusSelectedList({ id: id })
}
if (!selected) {
removeSkuSelected({ id: id })
setColor('white')
}
}, [selected])
useEffect(() => {
if (enableSku) {
setCursor('pointer')
}
if (!enableSku) {
setSelected(false)
setCursor('')
}
}, [enableSku])
return (
<Box
cursor={cursor}
borderRadius='2px'
overflow='hidden'
h='400px'
w='225px'
mt={5}
borderWidth='4px'
borderColor={color}
onClick={(e) => selectedSku(e)}
> ...myComponente </Box>
)
})
PRINTS
selected an item: enter image description here
back one page: enter image description here
returning to the page that was: enter image description here
The problem is that when CardList is unmounted and remounted, the state of the component is reinitialized. You can store IDs array of selected cards higher up the tree. And then pass the selected prop to the card.
For example like this:
interface CardProps {
id: string;
selected: boolean;
toggleSelect: (id: string) => void;
}
const Card = ({ id, selected, toggleSelect }: CardProps) => {
const [color, setColor] = useState<string>('red.500');
const handleClick = () => {
toggleSelect(id);
setColor(selected ? 'red.500' : 'white');
};
return (
<Box onClick={handleClick} color={color}>
...Component
</Box>
);
};
const List = () => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const toggleCardSelected = (cardId: string) => {
const isSelected = selectedIds.includes(cardId);
if (isSelected) {
setSelectedIds((prev) => prev.filter((id) => id !== cardId));
} else {
setSelectedIds((prev) => [...prev, cardId]);
}
};
return (
<>
{cards.map(({ id }) => (
<Card id={id} selected={selectedIds.includes(id)} toggleSelect={toggleCardSelected} />
))}
</>
);
};
I'm beginnig my journey into TypeScript in React and to experiment what I've learn, I've try a simple Todo App.
Everything is working fine except ONE things !
When I'm pushing 'newTask'
When I'm hovering 'newTask' here's the hint (Google Trad from French) :
The 'Todo | undefined 'is not attributable to the parameter of type' Todo '.
Cannot assign type 'undefined' to type 'Todo'.
I guess it's related to something here :
let [newTask, setNewTask] = useState<Todo>();
because if I type useState<any>(); I don't have any error..
Here's the full code :
import React, { useState } from "react";
// INTERFACES
interface Todo {
id: number;
text: string;
completed: boolean;
}
export const TodoComponent = () => {
// STATE
const initialTodos: Todo[] = [
{ id: 0, text: "Todo 1", completed: false },
{ id: 1, text: "Todo 2", completed: true },
{ id: 2, text: "Todo 3", completed: false },
];
const [todos, setTodos] = useState<Todo[]>(initialTodos);
let [newTask, setNewTask] = useState<Todo>();
// ACTIONS
const handleClickOnComplete = (id: number, completed: boolean) => {
const newTodos = [...todos];
newTodos[id].completed = !completed;
setTodos(newTodos);
};
const handleRemove = (todo: Todo) => {
const newTodos = todos.filter((t) => t !== todo);
setTodos(newTodos);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewTask({
id: todos.length,
text: event.target.value,
completed: false,
});
};
const handleSubmitNewTodo = () => {
const newTodos = [...todos];
console.log(newTask, newTodos);
newTodos.push(newTask);
setTodos(newTodos);
};
return (
<div>
<h1>Todo App !</h1>
<div>
{todos.map((todo) => {
return (
<div key={todo.id}>
{todo.id} - {todo.text} -{" "}
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleClickOnComplete(todo.id, todo.completed)}
/>
<button onClick={() => handleRemove(todo)}>Remove task</button>
</div>
);
})}
</div>
<hr />
<div>
<input placeholder="Add todo" type="text" onChange={handleChange} />
<button onClick={handleSubmitNewTodo}>Add todo</button>
</div>
</div>
);
};
Problem is in handleSubmitTodo
Thanks for your help and advices.
Take care.
Updated
You can try this:
const handleSubmitNewTodo = () => {
let newTodos = [...todos];
console.log(newTask, newTodos);
if (newTask) {
newTodos.push(newTask);
setTodos(newTodos);
setNewTask(undefined);
}
};
I have added the setNewTask to undefined to maintain the initial state after adding the new todo to the todo list.
Adding a condition around the push seems to do the trick.
const handleSubmitNewTodo = () => {
const newTodos = [...todos];
if (newTask) {
newTodos.push(newTask);
}
setTodos(newTodos);
};
I'm using this react-beautiful-dnd library to be able to reorder lists. However, even though I'm able to drag and drop and re-order, there is a flicker when I try to reorder lists.
You can see in the video:
enter image description here
Here is my code:
I added sorting to the array by order before mapping.
const Board = () => {
const [currentId, setCurrentId] = useState(null);
const lists = useSelector((state) => state.lists);
const dispatch = useDispatch();
const classes = useStyles();
useEffect(() => {
dispatch(getLists());
}, [currentId, dispatch]);
const onDragEnd = (result) => {
const { destination, source, draggableId, type } = result;
if(!destination) return;
const droppableIdStart = source.droppableId;
const droppableIdEnd = destination.droppableId;
const droppableIndexStart = source.index;
const droppableIndexEnd = destination.index;
const newState = [...lists];
// drag lists
if(type === 'list') {
const dragList = newState.splice(droppableIndexStart, 1);
newState.splice(droppableIndexEnd, 0, ...dragList);
// update order list to be index
newState.forEach((list, index) => {
list.order = index;
dispatch(updateList(list._id , { ...list }));
});
}
return newState;
}
// Arranging lists by order
const newArrange = (a,b) => {
return (a.order - b.order);
}
lists.sort(newArrange);
return (
<>
<DragDropContext onDragEnd={onDragEnd} >
<div>
<h1>Board</h1>
<Droppable droppableId="all-lists" direction="horizontal" type="list">
{ provided => (
<div className={classes.listContainer} {...provided.droppableProps} ref={provided.innerRef} >
{ lists.map((list, index) =>
(user?.result?.googleId === list?.creator || user?.result?._id === list?.creator) ?
<List key={list._id} title={list.title} cards={list.cards} currentId={list._id} index={index} /> :
null
)}
{addListFlag && (
<InputItem
value={listData.title}
btnText={"Add list"}
type={"list"}
placeholder={"Enter a list title..."}
changedHandler={handleChange}
closeHandler={closeHandlerBtn}
addItem={submitHandler}
/>
)}
{!addListFlag && (
<AddBtn btnText={"Add another list"} type={"list"} handleClick={handleAddition} />
)}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
</DragDropContext>
</>
)
}
export default Board;
Sample of data:
{
_id: "6163cdd306d27b",
title: "a",
name: "first one",
order: "0",
cards:[
{
id: "0",
text: "a1",
_id: {_id: "61d0ca2c20d27e"}
},
{
id: "1",
text: "a2",
_id: {_id: "616541ec90630"}
},
{
id: "2",
text: "a3",
_id: {_id: "61606e609"}
}
]
}
Thank :)
It has probably todo with your getLists() call in the useEffect hook.
Is there an async function behind it? Do you get your lists from a server? If so, I suppose that useEffect is firing twice (once on drag end and once when it gets back the data from the backend or whatever getLists is doing), which leads to the flickering.
It would probably help to know what exactly getLists is.
I'm trying to make a shopping app which repeats the same card. Instead of manually rendering them, I used map to render array objects like this:
Parent component:
const Home = () => {
const dummyData = [
{
id: 1,
title: tshirt,
price: 10
},
{
id: 2,
title: hat,
price: 20
}
]
const [totalPrice, setTotalPrice] = useState(0);
const itemNo = 2;
const handleClick = (price) => {
setTotalPrice(price * itemNo);
}
const RenderCards = () => {
return (
dummyData.map(
(d) =>
<Card key={d.id} title={d.title} price={d.price} totalPrice={totalPrice}/>
)
)
}
return(
<>
<RenderCards />
</>
)
}
and the child component:
const Card = (id, title, price, totalPrice) => {
return (
<>
<div key={id}>
<p>{title}</p>
<p>{totalPrice}</p>
<button onClick={() => handleClick(price)}>Click for total price</button>
</div>
</>
)
}
When clicking the button on each card, I'd like to display total price for each card, i.e. for card 1, total price should be 10 * 2 = 20, and for card 2 should be 40. Clicking card 1 should only change {totalPrice} of card 1, card 2 should not be affected, vice versa.
However what I have so far is when clicking the button, both card would show the same total price. I understand this behaviour as the same data is passed to the card component, but how can I individually set data for each card in this case when components are rendered from array map?
const Home = () => {
const dummyData = [
{
id: 1,
title: tshirt,
price: 10
},
{
id: 2,
title: hat,
price: 20
}
]
const [totalPrice, setTotalPrice] = useState([]); //<-- an empty array for start
const handleClick = (id, qty) => {
let newState = [...totalPrice]; //<--- copy the state
if (newState.find(item => item.id === id) != undefined) { // find the item to add qty, if not exists, add one
newState.find(item => item.id === id).qty += qty
} else {
newState.push({id:id, qty:qty});
}
setTotalPrice(newState); //<-- set the new state
}
const RenderCards = () => {
return (
dummyData.map(
(d) => {
const stateItem = totalPrice.find(item=> item.id === d.id); // return the item or undefined
const qty = stateItem ? stateItem.qty : 0 // retreive qty from state by id or 0 if the product is not in the array
return (
<Card key={d.id} title={d.title} price={d.price} totalPrice={d.price * qty}/> //calculate the total
)
}
)
)
}
return(
<>
<RenderCards />
</>
)
}
and card:
const Card = (id, title, price, totalPrice) => {
return (
<>
<div>
<p>{title}</p>
<p>{totalPrice}</p>
<button onClick={() => handleClick(id, 1)}>Click for add one</button> // add one, total price will be good on the next render
</div>
</>
)
}
maybe buggy but the idea is here
I am trying to achive a shared counter between components for example
I have 3 buttons
<Button />
<Button />
<Button />
and each of them has a label that will show a number when its clicked on it
when I click one of them it will start by 1 and only the one i clicked will show that number 1 and others will be 0 or no label at all then if i click other button that button wil show 2 on it
and previous one will stay at 1 and so on when i click third button it will have state of 3
which is a shared state between 3 same component instance
I am trying to achive a gallery image selection by this i will show a label selected items and their order by numbers on it how to achive it with react hooks
Sharing logic between siblings can be handled via a parent component that passes callback functions to each child.
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
interface AppProps { }
interface AppState {
photos: PhotoData[];
}
type PhotoData = { id: number; name: string; }
type GalleryProps = { photos: PhotoData[] }
const Gallery : React.FunctionComponent<GalleryProps> = ({
photos
}) => {
// Create an array to store the unique ID of the selected "photos".
// As items are selected they will be inserted into the array in the order
// they were selected.
const [selected, setSelected] = React.useState<number[]>([]);
// Create a function to handle the selection of a button.
const onSelect = React.useCallback((selectedId?: number) => {
setSelected(currentSelected => {
// If item already in the array, remove it.
if (currentSelected.some(id => id === selectedId)) {
return currentSelected.filter(id => id !== selectedId);
}
// if item not in the array, add it.
return [...currentSelected, selectedId ]
});
}, [setSelected])
return (
<div>
{
!!photos && photos.map(photo => {
// find the selection order using index.
const index = selected.indexOf(photo.id);
// change index to 1 based (or undefined if not selected).
const selectionOrder = index >= 0 ? index + 1 : undefined
return (
<Photo
key={photo.id}
{...photo}
onClick={onSelect}
index={selectionOrder}
/>
);
})
}
</div>
);
}
type PhotoProps = PhotoData & { index?: number; onClick: (selectedId: number) => void; }
const Photo : React.FunctionComponent<PhotoProps> = ({
id,
name,
onClick,
index
}) => {
// Create function to handle button click.
const onButtonClick = React.useCallback(() => {
onClick(id)
}, [onClick, id])
return (
<button
onClick={onButtonClick}
>
{name} {!!index && `(Selected ${index})`}
</button>
);
};
class App extends Component<AppProps, AppState> {
constructor(props) {
super(props);
this.state = {
photos: [
{
id: 1,
name: 'Photo 1'
},
{
id: 2,
name: 'Photo 2'
},
{
id: 3,
name: 'Photo 3'
}
]
};
}
render() {
return (
<div>
<Gallery photos={this.state.photos} />
</div>
);
}
}
render(<App />, document.getElementById('root'));
Runnable Version of the above.