Ternary condition on onclick event - reactjs

I am trying to create a to-do application in React. Code I have so far adds to-do items to the to-do list. When I click on the edit icon I had put turnery condition for the done icon but it's not working. Can someone explain what is wrong with my code?
App.js
import './App.css';
import React, { useState } from 'react';
import TodoList from './TodoList';
import { v4 as uuidv4 } from 'uuid';
function App() {
// const [input, setInput] = useState('');
const [todos, setTodo] = useState([]);
const input = React.useRef();
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([...todos, { id: id, text: input.current.value }])
input.current.value='';
}
const deleteTodo = (id) => {
setTodo(todos.filter(todo => todo.id !== id));
}
const editTodo = (id) => {
}
return (
<div className="App">
<form>
<input type="text" ref={input}/>
<button type="submit" onClick={addTodo}>Enter</button>
</form>
<TodoList todos={todos} deleteTodo={deleteTodo} editTodo={editTodo}/>
</div>
);
}
export default App;
TodoItem.js
import React from 'react'
import DeleteIcon from '#material-ui/icons/Delete';
import EditIcon from '#material-ui/icons/Edit';
import CheckBoxOutlineBlankIcon from '#material-ui/icons/CheckBoxOutlineBlank';
import DoneIcon from '#material-ui/icons/Done';
const TodoItem = ({todo, deleteTodo, editTodo}) => {
return (
<>
<div>
<CheckBoxOutlineBlankIcon/>
<input type="text" value={todo.text} readOnly={true}/>
</div>
<div>
{ <EditIcon/> ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
<DeleteIcon onClick={deleteTodo}/>
</div>
</>
)
}
export default TodoItem

There are a few problems with your code. One, also pointed out by Abu Sufian is that your ternary operator will always trigger whatever is immediately after ?, because <EditIcon/> is just a component and will always be true.
But more fundamentally, to do what you want, you will need to add another properly to your todo list, say, status. So when a task goes in for the first time, it will be in pending status, then once you click your Edit icon, it will change to done. And that's how we will toggle that icon with a ternary operator.
So I would change your addTodo function to
const addTodo = (e) => {
e.preventDefault();
const id = uuidv4();
setTodo([
...todos,
{ id: id, text: input.current.value, status: "pending" }
]);
input.current.value = "";
};
Then I would change your editTodo to:
const editTodo = (id) => {
console.log(id);
setTodo(
todos.map((todo) => {
if (todo.id === id) todo.status = "done";
return todo;
})
);
};
And finally, I would change your ternary part to:
{todo.status === "pending" ? (
<EditIcon onClick={() => editTodo(todo.id)} />
) : (
<DoneIcon />
)}
Here is a complete Sandbox for you. Sorry I don't have your CSS so I can't make it look super pretty.

Maybe you are looking for something like this.
{ !todo.done ? <EditIcon onClick={editTodo}/> : <DoneIcon/> }
I believe checking whether a todo item is done or not should happen with a property of todo object itself.
In a ternary you need to start with a condition.
condition ? do something when true : do something when false
So you have to have a condition in the first place. In your case EditIcon is not a condition.
If you are looking for a way to mark a todo as completed so you need to do more things.
const markAsCompleted = id => {
const todo = todos.find(todo => todo.id !== id);
setTodo([...todos, {...todo, done: true }]);
}
Then you can decide based on whether a todo is done or not.

Related

How to use state on one element of map on typescript?

I want to use onClick on one element of my map and set "favorite" for it. Basically, I'm trying to change the SVG of a Icon to the filled version, but with the map, all of items are changing too.
I already try to pass this to onClick, but doesn't work.
My code:
import React, { Component, useState, useEffect } from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { ForwardArrow } from "../../../assets/images/ForwardArrow";
import { BackArrow } from "../../../assets/images/BackArrow";
import * as S from "./styled";
import { IconFavoriteOffer } from "../../../assets/images/IconFavoriteOffer";
import { Rating } from "../../../assets/images/Rating";
import { TruckFill } from "../../../assets/images/TruckFill";
import { OpenBox } from "../../../assets/images/OpenBox";
import { IconCartWht } from "../../../assets/images/IconCartWht";
import axios from "axios";
import { off } from "process";
import SwitcherFavorite from "../SwitcherFavorite";
export default function Carousel() {
const [offers, setOffers] = useState<any[]>([]);
useEffect(() => {
axios.get("http://localhost:5000/offers").then((response) => {
setOffers(response.data);
});
}, []);
const [favorite, setFavorite] = useState(true);
const toggleFavorite = () => {
setFavorite((favorite) => !favorite);
};
return (
<>
<Slider {...settings}>
{offers.map((offer, index) => {
return (
<S.Offer key={index}>
<>
<S.OfferCard>
<S.OfferCardTop>
<S.OfferContentTop>
<S.OfferFavorite>
<S.OfferFavoriteButton onClick={toggleFavorite}> // Want to get this element of mapping
<SwitcherFavorite favorite={favorite} />
</S.OfferFavoriteButton>
</S.OfferFavorite>
<S.OfferStars>
<Rating />
</S.OfferStars>
</S.OfferContentTop>
</S.OfferCardTop>
</S.OfferCard>
</>
</S.Offer>
);
})}
</Slider>
</>
);
}
So, how can I do it?
Instead of using a single boolean flag with your current [favorite, setFavorite] = useState(false) for all the offers, which wouldn't work, you can store the list of offer IDs in an array. In this way you can also have multiple favourited offers.
Assuming your offer item has a unique id property or similar:
// This will store an array of IDs of faved offers
const [favorite, setFavorite] = useState([]);
const toggleFavorite = (id) => {
setFavorite((previousFav) => {
if (previousFav.includes(id)) {
// remove the id from the list
// if it already existed
return previousFav.filter(favedId => favedId !== id);
}
// add the id to the list
// if it has not been here yet
return [...previousFav, id]);
}
};
And then in your JSX:
/* ... */
<S.OfferFavoriteButton onClick={() => toggleFavorite(offer.id) }>
<SwitcherFavorite favorite={favorite.includes(offer.id)} />
// Similar to your original boolean flag to switch icons
</S.OfferFavoriteButton>
/* ... */

Update only a specific item on button click

I need to know what I must do to ensure that the texts of all the items(except for the one that needs to) don't have the line-through effect upon clicking the purchase button. Right now, every single item on the list tends to have this effect when I click either one of them. Also, another behavior of this component should be to change the name of the button to Purchased from Purchase and vice-versa on clicking upon it. This is working as it should except for the fact that all the buttons show this behavior rather than only the one that was clicked. I have used Context API to pass data around. The screenshot to show what I mean is attached as well.
The code is as follows:
context.js
import React, {useState, useEffect, createContext} from "react";
export const FoodContext = createContext();
export const FoodProvider = (props) => {
const[food, setFood] = useState([]);
const getData = () => {
const request = {
method : 'GET',
}
fetch("http://localhost:3008/api/get", request)
.then(res => res.json())
.then(data => {
setFood(data)
})
};
useEffect(() => {
getData()
}, []);
return(
<FoodContext.Provider value={[food, setFood]}>
{props.children}
</FoodContext.Provider>
);
}
content_screen.js - This is where the ListItems component is getting rendered.
import React, {Component} from 'react';
import Caption from '../components/caption/caption';
import InputBar from '../components/input_bar/input_bar';
import ListItems from '../components/list_items/list_items';
import "./content_screen.css";
//This is where the ListItems component is getting rendered
export default class ContentScreen extends React.Component {
render() {
return(
<div className="content_screen">
<div className="caption">
<Caption></Caption>
<div className="search_box">
<InputBar></InputBar>
</div>
<div className="box">
<ListItems></ListItems>
</div>
</div>
</div>
);
}
}
list_items.jsx - The component in question
import React,{useState, useContext} from "react";
import { FoodContext } from "../../context";
import "./list_items.css";
const ListItems = () => {
const [food, setFood] = useContext(FoodContext);
const [purchase, setPurchase] = useState(false);
const deleteItem = (id) => {
const request = {
method : 'DELETE'
};
fetch(`http://localhost:3008/api/delete/${id}`, request)
.then(res => res.json())
.then(data => console.log(data));
};
const clicked = () => {
setPurchase(!purchase);
}
return(
<div>
{!food.length ? <p>No Items Added</p> : food.map((key, value) => <div className="list_items" key={key._id}>
{purchase ? <span>{food[value].name}</span> : (food[value].name)}
<button className="x_button" onClick={() => deleteItem(food[value]._id)}>X</button>
<button className="purchase_button" onClick={clicked}>{purchase ? 'Purchased' : 'Purchase'}</button>
</div>)}
</div>
);
}
export default ListItems;
The problem is that you are using the same state for all of them.
You need to have each item that has its own state of being purchased or not.
Make a NEW component ListItem
import React, { useState } from 'react';
export const ListItem = ({ item, deleteItem }) => {
const [purchased, setPurchased] = useState(false);
const purchase = () => {
setPurchased(!purchased)
}
return (
<div className="list_items">
{purchased ? <span>{item.name}</span> : (item.name)}
<button className="x_button" onClick={() => deleteItem(item._id)}>X</button>
<button className="purchase_button" onClick={purchase}>{purchased ? 'Purchased' : 'Purchase'}</button>
</div>
)
}
and then in your ListItems component
import { ListItem } from "./ListItem";
const ListItems = () => {
const [food, setFood] = useContext(FoodContext);
const deleteItem = (id) => {
const request = {
method : 'DELETE'
};
fetch(`http://localhost:3008/api/delete/${id}`, request)
.then(res => res.json())
.then(data => console.log(data));
};
return (
<div>
{food.length
? food.map((item, key) => <ListItem item={item} key={key} deleteItem={deleteItem} />)
: <div> No food available </div>
}
</div>
)
}
for the second part: you have 1 single state (purchase). All your items are conditioned to that same state, meaning if it is true, the will show purchased and if it's false they will show purchase. You need to have different states for them or you can have one state containing all of them ( I'm talking about an array). This is how you should go about it:
const [purchsed, setPurchased] = useState([])
//let's say this is the list of your items ( items)
for ( let i in items.length()){
setPurchased((prevState)=>[...prevState,false]
}
// Up to here we have an array containing false's. The length of the array is
// the same as your items
// give your items a name ( or another attribute that could be read from event)
<button name="item.id" className="purchase_button" onClick={clicked}>{purchase ? 'Purchased' : 'Purchase'}</button>
// This was happened when mapping
//now you can target the name attribute when you call your function
const clicked = (e) => {
let copy = [...purchased]
//make a copy of your state
copy[e.target.name] = !purchased[e.target.name]
setPurchased([...copy])
}
and your first problem is caused by the same mistake. You can follow the same pattern to fix that too. Bear in mind that this was not a quality fix, since there are some mistakes in setting up the foundation of this project.

In react, how can I deselect an element after it has been set as active with onClick?

I have an image grid in react, with an onclick function that highlights the selected image. Clicking another image will change the active element but I'd like to be able to re-click the selected icon to deselect it and return the grid to default.
Here is my codesandbox and the script is below
import { useState, useRef, useEffect } from "react";
import "./App.css";
import { Data } from "./Data.js";
function App() {
const scrollRef = useRef();
useEffect(() => {
const el = scrollRef.current;
if (el) {
const wheelListener = (e) => {
e.preventDefault();
el.scrollTo({
left: el.scrollLeft + e.deltaY * 5,
behavior: "smooth"
});
};
el.addEventListener("wheel", wheelListener);
return () => el.removeEventListener("wheel", wheelListener);
}
}, []);
const [active, setActive] = useState(-1);
const [active2, setActive2] = useState(false);
return (
<div ref={scrollRef} className="grid_container">
{Data.map((prev, i) => {
return (
<div
onClick={() => {
setActive(i);
setActive2(true);
console.log(prev.Team);
}}
className={`${
(active === i && "scale") || (active2 && "notScale")
} card`}
key={i}
>
<img src={prev.TeamBadge} alt="" />
</div>
);
})}
</div>
);
}
export default App;
If I understand the problem correctly, I think this solves the problem, based on the codesandbox:
onClick={() => {
setActive(i);
if (active === i) {
setActive2(null);
setActive(null);
} else {
setActive2(true);
}
console.log(i);
console.log(prev.Team);
}}
since every team has its unique number, the logic here is to check if the same number is in the state (if that makes sense). Let me know if this was you are looking for!
You can set your state like that :
setActive2(!active2);

onClickHandler sometimes work, sometimes not - React

The onClickHandler in the following code, in this component, 'SearchResult', sometimes work and sometimes not.
I can't figure out any logic that can explain why it works when it works, and why it's not working, when it's not working.
I've put a debugger inside the onClickHandler, at the beginning of it, and when it's not working, it doesn't get to the debugger at all - what indicates that the function sometimes isn't even called, and I can't figure out why.
Furthermore, I've tried to move all the code in function to the onClick, inline, but then, it's not working at all.
In addition, I've tried to use a function declaration instead of an arrow function, and it still behaves the same - sometimes it works, and sometimes it's not...
This is the site, you can see the behavior for yourself, in the search box.
This is the GitHub repository
Here you can see a video demonstrating how it's not working, except for one time it did work
Please help.
The problematic component:
import { useDispatch } from 'react-redux'
import { Col } from 'react-bootstrap'
import { getWeatherRequest } from '../redux/weather/weatherActions'
import { GENERAL_RESET } from '../redux/general/generalConstants'
const SearchResult = ({ Key, LocalizedName, setText }) => {
const dispatch = useDispatch()
const onClickHandler = () => {
dispatch({ type: GENERAL_RESET })
dispatch(
getWeatherRequest({
location: Key,
cityName: LocalizedName,
})
)
setText('')
}
return (
<Col className='suggestion' onClick={onClickHandler}>
{LocalizedName}
</Col>
)
}
export default SearchResult
This is the parent component:
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Form } from 'react-bootstrap'
import { getAutoCompleteResultsRequest } from '../redux/autoComplete/autoCompleteActions'
import { AUTO_COMPLETE_RESET } from '../redux/autoComplete/autoCompleteConstants'
import SearchResult from './SearchResult'
const SearchBox = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const autoComplete = useSelector((state) => state.autoComplete)
const { results } = autoComplete
const onChangeHandler = (e) => {
if (e.target.value === '') {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}
setText(e.target.value)
dispatch(getAutoCompleteResultsRequest(e.target.value))
}
const onBlurHandler = () => {
setTimeout(() => {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}, 100)
}
return (
<div className='search-box'>
<Form inline>
<div className='input-group search-md search-sm'>
<input
type='search'
name='q'
value={text}
onChange={onChangeHandler}
onBlur={onBlurHandler}
placeholder='Search Location...'
className='mr-sm-2 ml-sm-3 form-control'
/>
</div>
</Form>
<div className='search-results'>
{results &&
results.map((result) => {
return (
<SearchResult key={result.Key} {...result} setText={setText} />
)
})}
</div>
</div>
)
}
export default SearchBox
I played a bit with your code and it looks like a possible solution may be the following addition in the SearchResult.js:
const onClickHandler = (e) => {
e.preventDefault();
...
After some tests
Please remove the onBlurHandler. It seams to fire ahaed of the onClickHandler of the result.
Can you put console.log(e.target.value) inside the onChangeHandler,
press again search results and make sure that one of it doesn't working and show us the console.
In searchResult component print to the console LocalizedName as well

Child Component doesn't rerender when state of parent component changes

I have the following issue: I have an Component that renders other components in it. One of this component gets state variables of my parent component as parameter and are using them actively, but they don't rerender when the state of the parent component changes. Another problem that I am facing is that I have an additional item in my list that navigates that is activated when the user has a special roleID. The changing of the state works completely fine, but in this situation the additional item only gets visible after I changed the path param of my url.
parent component:
import React, { useEffect, useState } from 'react';
import {Row, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../../App.css';
import ProfileSettings from './profileSettings';
import SettingsChooser from './settingsChooser';
// import SettingRoutings from '../settingRoutings';
import {BrowserRouter as Router, useHistory, useLocation, useParams} from 'react-router-dom';
// import Routings from '../Routings.js';
import UserRequests from './userRequests';
import useAuth from '../../API/useAuthentification';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
function UserSettings({user}) {
const {title: path} = useParams();
const [acst, setAcst] = useState(localStorage.accessToken);
const [rft, setRft] = useState(localStorage.refreshToken);
const history = useHistory();
const [items, setItems] = useState(['Profile', 'Requests','Log Out', 'Delete Account']);
const [authError, setAuthError] = useState(false);
const [userValues, authentificate] = useBackend(authError, setAuthError, user);
const [component, setComponent] = useState(<></>);
const [defaultItem, setDefaultItem] = useState(0);
useEffect(() => {
console.log('render');
authentificate(CONTROLLERS.USERS.getUserByAccessToken());
}, [acst, rft]);
window.addEventListener('storage', () => localStorage.accessToken !== acst ? setAcst(localStorage.accessToken) : '');
window.addEventListener('storage', () => localStorage.refreshToken !== rft ? setRft(localStorage.refreshToken) : '');
useEffect(() => {
if(userValues?.roleID === 1) {
items.splice(0, 0, 'Admin Panel');
setItems(items);
}
console.log(items);
}, [userValues]);
useEffect(() => {
// if(path==='logout') setDefaultItem(2);
// else if(path==='deleteAccount') setDefaultItem(3);
// else if(path==='requests') setDefaultItem(1);
}, [])
const clearTokens = () => {
localStorage.accessToken = undefined;
localStorage.refreshToken = undefined;
}
useEffect(() => {
console.log(path);
if(path ==='logout' && !authError) {
setDefaultItem(2);
clearTokens();
}
else if(path === 'deleteaccount') {
setDefaultItem(3);
if(userValues?.userID && !authError) {
authentificate(CONTROLLERS.USERS.delete(userValues.userID));
}
clearTokens();
history.push('/movies/pages/1');
}
else if(path==='requests') {
setDefaultItem(1);
setComponent(<UserRequests user={userValues} setAuthError={setAuthError} authError={authError}/>);
} else {
setComponent(<ProfileSettings user={userValues} setAuthError={setAuthError} authError={authError}/>);
}
}, [path]);
useEffect(() => {
console.log(defaultItem);
}, [defaultItem])
return (
<div >
<Row className="">
<Col className="formsettings2" md={ {span: 3, offset: 1}}>
<SettingsChooser items={items} headline={'Your Details'} defaultpath='userSettings' defaultactive={defaultItem} />
</Col>
<Col className="ml-5 formsettings2"md={ {span: 6}}>
{authError ? <p>No Access, please Login first</p> : component}
</Col>
</Row>
</div>
);
}
export default UserSettings;
Child component (settingsChooser):
import React, {useEffect, useState} from 'react';
import {Card, Form, Button, Nav, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { LinkContainer } from 'react-router-bootstrap';
import '../../App.css'
function SettingsChooser({items, headline, defaultpath, defaultactive}) {
const [selected, setSelected] = useState(defaultactive);
const handleClick = (e, key) => {
setSelected(key);
}
useEffect(() => console.log("rerender"), [items, defaultactive]);
useEffect(() => {
setSelected(defaultactive);
}, [])
return(
<>
<Card className="shadow-sm">
<Card.Header className="bg-white h6 ">{headline}</Card.Header>
{items.map((item, idx) =>{
return(
<LinkContainer to={`/${defaultpath}/${(item.replace(/\s/g,'').toLowerCase())}`}><Nav.Link onClick={(e) => handleClick(this, idx)} className={'text-decoration-none text-secondary item-text ' + (selected === idx? 'active-item' : 'item')}>{item}</Nav.Link></LinkContainer>
);
})}
</Card>
</>
);
}
export default SettingsChooser;
Firstly, in your parent component when you do
setItems(items)
you are not actually modifying the state, since items already is stored in the state. React will check the value you pass, and not cause a re-render if the value is already stored in the state. When you modify your array with splice, it is still the "same" array, just different contents.
One way around this is to do setItems([...items]), which will call setItems with a new array, containing the same items.
Secondly, in your child class, the following currently has no effect:
useEffect(() => {
setSelected(defaultactive);
}, [])
Since the dependency array is empty, it will only be called on the first render. If you want it to be called any time defaultactive changes, you need to do this instead:
useEffect(() => {
setSelected(defaultactive);
}, [defaultactive])

Resources