onClick Handler in React Picking Up Child Nodes - reactjs

I'm trying to add interactivity to a four quadrant chart whereby the user can click a box to highlight it, and the others will deactivate, similar to how radio boxes work in a form.
The idea was to add an onClick event to each card and have a handler function that will check which box was clicked on, activate it, and then deactivate the rest.
The problem I'm having is that e.target seems to be picking up the child nodes of each card instead of the card itself, so I'm having trouble figuring out which card was clicked.
e.g. console log = '>> event.target <li>A</li>'
I was hoping to determine which card was picked by doing something like event.target.id
I've tried a bunch of things and nothing has worked... How do people normally set this type of interaction up?
import React from "react";
import Card from "#material-ui/core/Card";
import CardContent from "#material-ui/core/CardContent";
import CardActionArea from '#material-ui/core/CardActionArea';
import Typography from "#material-ui/core/Typography";
import Paper from "#material-ui/core/Paper";
function MyCard({ title }) {
return (
<CardActionArea onClick={quadrantClickHandler}>
<Card>
<CardContent>
<Typography>{title}</Typography>
</CardContent>
</Card>
</CardActionArea>
);
}
function quadrantClickHandler(e) {
e.stopPropagation();
console.log('>> event.target ',e.target);
//the idea here is that I will click a "card"
//and then determine which card was clicked so that
//I can highlight it similar to a radio box set in a form.
}
function Quadrants() {
return (
<React.Fragment>
<MyCard
title={
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
</ul>
} />
<MyCard title="Fast but expensive" />
<MyCard title="Slow but Cheap" />
<MyCard title="Slow but Fast" />
</React.Fragment>
);
}
function FourQuadrants() {
const classes = useStyles();
return (
<div>
<h2>Make a choice:</h2>
<Paper className={classes.paper}>
<Typography className={classes.top}>Big</Typography>
<Typography className={classes.bottom}>Small</Typography>
<Typography align="center" className={classes.left}>Expensive</Typography>
<Typography align="center" className={classes.right}>Cheap</Typography>
<Quadrants />
</Paper>
</div>
);
}
export default FourQuadrants;

Rather than trying to pull information out of the event target, you can have the click handler know everything of importance.
The key aspects in my example below are:
State at the top level (selectedId) to track which card is selected
A click-handler that knows the id of its card and sets the selectedId accordingly
I'm providing each card with its own id and the selectedId, so that it can style itself differently based on whether or not it is selected
import ReactDOM from "react-dom";
import React from "react";
import Card from "#material-ui/core/Card";
import CardContent from "#material-ui/core/CardContent";
import CardActionArea from "#material-ui/core/CardActionArea";
import Typography from "#material-ui/core/Typography";
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles({
selected: {
border: "1px solid green"
}
});
function MyCard({ title, id, selectedId, handleClick }) {
const classes = useStyles();
return (
<Card className={id === selectedId ? classes.selected : null}>
<CardActionArea onClick={handleClick(id)}>
<CardContent>
<Typography>{title}</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
function Quadrants() {
const [selectedId, setSelectedId] = React.useState();
const handleClick = id => e => {
setSelectedId(id);
};
const cardProps = { selectedId, handleClick };
return (
<React.Fragment>
<MyCard
{...cardProps}
id={1}
title={
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
</ul>
}
/>
<MyCard {...cardProps} id={2} title="Fast but expensive" />
<MyCard {...cardProps} id={3} title="Slow but Cheap" />
<MyCard {...cardProps} id={4} title="Slow but Fast" />
</React.Fragment>
);
}
function FourQuadrants() {
return (
<div>
<h2>Make a choice:</h2>
<Quadrants />
</div>
);
}
function App() {
return <FourQuadrants />;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Related

Link the result items from a list to the marker on map leaflet

I'm trying to link the list with the leaflet map. I want to click on a result card and see the marker on the map. I'm using fake data for now in json format. I would like a display like airbnb, list plus display the list on map.
This is the map file :
import React, { useState } from "react";
import { Map, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { Icon } from "leaflet";
import FakeData from "../../../../../../data";
import {Div, PopContainer} from "./style"
const MarkerIcon = new Icon({
iconUrl: "../public/markerIcon.png",
iconSize: [50, 50],
});
function MapBox() {
const [nurse, setNurse] = useState(null);
const bull = <span>•</span>;
return (
<Div>
<Map center={[50.6365654, 3.0635282]} zoom={13} scrollWheelZoom={false}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png"
/>
{FakeData.map((data) => (
<Marker
key={data.id}
icon={MarkerIcon}
position={[data.coordonatelat, data.coordonatelng]}
onmouseover={() => {
setNurse(data);
}}
/>
))}
{nurse && (
<Popup
position={[nurse.coordonatelat, nurse.coordonatelng]}
onClose={() => {
setNurse(null);
}}
>
<PopContainer>
<h2>
{bull}
{nurse.first_name} {nurse.last_name}
{bull}
</h2>
<h3>
{nurse.job}
</h3>
<p> {nurse.address}
<br />
{nurse.phone_number}
</p>
</PopContainer>
</Popup>
)}
</Map>
</Div>
);
}
export default MapBox;
This is the resultPage :
import React from "react";
import FakeData from "../../../../../../data";
import MapBox from "../map/index";
import { Scrollbars } from "react-custom-scrollbars";
import {
Container,
H1,
Card,
CardContent,
H2,
Address,
Phone,
NavLink,
CardActions,
Job,
MapContainer,
BigContainer,
CardContainer,
Box,
Li,
} from "./resultPageStyle";
function Result(props) {
const bull = <span>•</span>;
return (
<BigContainer>
<Container>
<H1>Résultats</H1>
<Box>
<Scrollbars style={{ width: 650, height: 580 }}>
<CardContainer>
{FakeData.slice(0, 30).map((item) => (
<Li key={item.id}>
<Card>
<CardContent>
<Job>{item.job}</Job>
<H2>
{bull}
{item.first_name} {item.last_name}
{bull}
</H2>
<Address>{item.address}</Address>
<Phone>{item.phone_number}</Phone>
</CardContent>
<CardActions>
<NavLink to={"/detailsPage"}>Plus d'infos</NavLink>
</CardActions>
</Card>
</Li>
))}
</CardContainer>
</Scrollbars>
<MapContainer>
<MapBox />
</MapContainer>
</Box>
</Container>
</BigContainer>
);
}
export default Result;
I tried diferent thing but it is not working.
Create a state variable on Result comp to keep track of the card item that holds the lat lng info
Pass it as a prop to Mapbox comp
On the Mapbox comp create a local variable to save the map instance and use it to change the map view every time you click on a card.
divider
function Result() {
const bull = <span>•</span>;
const [cardItem, setCardItem] = useState(null);
const handleCardClick = (item) => {
console.log(item);
setCardItem(item);
};
...
<Card onClick={() => handleCardClick(item)}>
...
<MapContainer>
<MapBox cardItem={cardItem} />
</MapContainer>
}
divider
function MapBox({ cardItem }) {
const [nurse, setNurse] = useState(null);
const bull = <span>•</span>;
const [map, setMap] = useState(null);
useEffect(() => {
if (!cardItem || !map) return;
const { coordonatelat, coordonatelng } = cardItem;
map.setView([coordonatelat, coordonatelng], 13);
}, [cardItem, map]);
...
}
I fixed also your demo issues and added a cursor when you hover on each card.
You can use also map.flyTo which gives a nice animation out of the box instead of map.setView. It's up to you.
Demo

Cart data is available at navbar component but not in Cart component (reactJs commerceJs)

this is the code I am working with. When the cart component is not active the cart data loads perfectly, but when the cart component is active cart data is undefined.
import './App.css';
import { commerce } from './lib/commerce';
import { NavBar, Products, Cart } from './components';
import { useEffect, useState } from 'react';
function App() {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const fetchProducts = async () => {
const { data} = await commerce.products.list();
setProducts(data);
}
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
}
const handleAddToCart = async (productId, quantity) => {
const item = await commerce.cart.add(productId, quantity);
setCart(item);
fetchCart();
}
useEffect(() => {
fetchProducts();
fetchCart();
}, [])
return (
<div>
<NavBar totalItems={cart.total_items}></NavBar>
<Products products={products} handleAddToCart={ handleAddToCart}/>
<Cart cart={cart}></Cart>
</div>
);
}
export default App;
NavBar component is receiving the prop. But whenever I pass the cart data into the cart component, data is not being received by Cart component. Even in App.js, the data shows undefined.
When Cart component is active
When cart component is not active the top right corner shows cart data
Cart Component
import React from 'react';
import { Container, Typography, Button, Grid } from '#material-
ui/core';
import useStyles from './styles';
const Cart = ({ cart}) => {
const classes = useStyles();
// alert(cart);
const isEmpty = !cart.line_items.length;
const EmptyCart = () => {
<Typography variant="subtitle1">You have no items in your shopping
cart. Start adding some!</Typography>
}
const FilledCart = () => {
<>
<Grid container spacing={ 3}>
{cart.line_items.map((item) => (
<Grid item xs={12} sm={4} key={item.id}>
<div>{ item.name}</div>
</Grid>
))}
</Grid>
<div className={classes.cardDetails}>
<Typography variant="h4">Subtotal:
{cart.subtotal.formatted_with_symbol}</Typography>
<div>
<Button className={ classes.emptyButton} size="large"
type="button" variant="contained" color="secondary">Empty
Cart</Button>
<Button className={ classes.checkoutButton} size="large"
type="button" variant="contained"
color="primary">Checkout</Button>
</div>
</div>
</>
}
return (
<Container>
<div className={ classes.toolbar}>
<Typography className={classes.title} variant="h3"> Your
Shopping
Cart</Typography>
{ isEmpty? <EmptyCart /> : <FilledCart />}
</div>
</Container>
);
};
export default Cart;
Navbar Component
import React from 'react';
import { AppBar, Toolbar, IconButton, Badge, MenuItem, Menu,
Typography }
from '#material-ui/core';
import { ShoppingCart } from '#material-ui/icons';
import useStyles from './styles';
import image from '../../images/commerce.png'
const NavBar = ({ totalItems}) => {
const classes = useStyles();
return (
<>
<AppBar position="fixed" className={ classes.appBar}
color="inherit">
<Toolbar>
<Typography variant="h6" className={
classes.title}
color="inherit">
<img src={image} alt="My Shop" className=
{classes.image} height="25px" />
My Shop
</Typography>
<div className={classes.grow}></div>
<div className={classes.button}>
<IconButton aria-label="Show Cart Button"
color="inherit">
<Badge badgeContent={totalItems}
color="secondary">
<ShoppingCart></ShoppingCart>
</Badge>
</IconButton>
</div>
</Toolbar>
</AppBar>
</>
);
};
export default NavBar;

Getting error when trying to use functional component in class component

I have two main components. The Recipe.js component which is the parent component, and RecipeCard.js which is the child. I am getting the error Error: Invalid hook call. Hooks can only be called inside of the body of a function component. I am not exactly sure as to why I am getting this error. I was hoping I could get some insight as to what the problem is.
Recipe.js:
import React, { Component } from "react";
import AddRecipe from "./addRecipe";
import "./Recipe.css";
import { Route, BrowserRouter as Router, Switch, Link } from "react-router-dom";
import { useAuth0 } from "#auth0/auth0-react";
import RecipeCard from "./RecipeCard";
class Recipe extends Component {
constructor() {
super();
this.state = {
recipes: [], // State array to hold recipe objects that are fetched from the database
search: ''
};
}
updateSearch(event) {
this.setState({ search: event.target.value.substr(0, 20) });
}
componentDidMount() {
fetch("/getRecipes") //Api call using route from server.js to obtain recipe data
.then((res) => res.json())
.then((recipes) =>
this.setState(
{ recipes },
() =>
//inserts data to the state array of the component
console.log("recipes fetched...", recipes) //confirm that the recipes were fetched in the console
)
);
}
render() {
let filteredRecipes = this.state.recipes.filter(
(recipe) => {
return recipe.recipeName.toLowerCase().indexOf(this.state.search) !== -1;
}
);
return (
<Router>
<div>
<input type="text" value={this.state.search} onChange={this.updateSearch.bind(this)}></input>
<ul>
{filteredRecipes.map((
recipe //iterate through each recipe object in the state array display the id, name and instructions of each recipe
) => (
<li className="Recipes" key={recipe.idrecipe}>
<RecipeCard imgUrl={recipe.imgUrl} id={recipe.idrecipe} name={recipe.recipeName} instructions={recipe.recipeInstruction} />
</li>
))}
</ul>
<Switch>
<Route path="/addRecipe">
<AddRecipe />
</Route>
</Switch>
</div>
</Router>
);
}
}
export default Recipe; //Export the recipe component to be used in the main index.js file
RecipeCard.js:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Card from '#material-ui/core/Card';
import CardActionArea from '#material-ui/core/CardActionArea';
import CardActions from '#material-ui/core/CardActions';
import CardContent from '#material-ui/core/CardContent';
import CardMedia from '#material-ui/core/CardMedia';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
const useStyles = makeStyles({
root: {
maxWidth: 345,
},
media: {
height: 140,
},
});
export default function RecipeCard(props) {
const classes = useStyles();
return (
<Card className={classes.root}>
<CardActionArea>
<CardMedia
className={classes.media}
image={props.imgUrl}
title=""
/>
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{props.name}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{props.instructions}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button size="small" color="primary">
Share
</Button>
<Button size="small" color="primary">
Learn More
</Button>
</CardActions>
</Card>
);
}
Hooks can be only used in functional components.
Your Recipe.js is a class component which extends from React.Component.
That's why useAuth0 will fail since it is a hook.
You have two options now. You can either change your Recipe.js into a functional component or use an alternative to useAuth0 which I might guess is a Higher-Order-Component provided by #auth0/auth0-react.
withAuth0 might be a good alternative.
Refs :
https://auth0.com/docs/libraries/auth0-react#use-with-a-class-component
https://reactjs.org/docs/hooks-intro.htm

React passing down a function that modifies a third component

I have been trying to create a component per function in my app, but I am facing the following issue.
I have the component DisplayAllData that sends the data and an actionable button to DisplayDataWithButton, the issue is that when someone clicks on the Button send in the props, the function modifies the state of the parent component, which is also sent as a parameter to FullScreenDialog, and that throws a Warning: Cannot update a component while rendering a different component.
I designed the components in this particular way because:
DisplayAllData is the only function that has the data to render and the actionable button. (Model)
DisplayDataWithButton only renders the data and displays the actionable components for that particular data, in this case a button that opens a Dialog in screen. (Viewer)
You can find a running example here: https://codesandbox.io/s/material-demo-forked-8oyef
import React from "react";
import Button from "#material-ui/core/Button";
import DisplayDataWithButton from "./DisplayDataWithButton";
import FullScreenDialog from "./fullscreendialog";
export default function App(props) {
const [openFullScreen, setopenFullScreen] = React.useState(false);
var items = ["John", "Melinda"];
var dataDisplayFunction = (data) => {
return data.map((item) => {
return [
item,
<Button
color="success"
size="small"
className="px-2"
variant="contained"
onClick={setopenFullScreen()}
>
Show Dialog
</Button>
];
});
};
return (
<>
<DisplayDataWithButton
shapeDataFunction={dataDisplayFunction}
data={items}
/>
<FullScreenDialog open={openFullScreen} />
</>
);
}
DisplayDataWithButton.js
export default function DisplayDataWithButton(props) {
return props.shapeDataFunction(props.data);
}
I suspect that there is another way to implement this model, any suggestion, or ideas on how to fix this one.
Thanks
A couple of things: "I have been trying to create a component per function in my app". Forget that - the pattern you have opted for here is called render props but I don't see how it is necessary. Keep it simple. If a big component is simpler to understand than a small component I always opt for the bigger component. Splitting your components will not magically make them easier to understand.
All of the warnings have been dealt with. Most of them were simple mistakes, for example: onClick={setopenFullScreen()} should be onClick={setopenFullScreen}. You can compare your sandbox with my sandbox for all of the changes.
import React from "react";
import ReactDOM from "react-dom";
import Button from "#material-ui/core/Button";
import FullScreenDialog from "./fullscreendialog";
export default function App() {
const [openFullScreen, setopenFullScreen] = React.useState(false);
const items = ["John", "Melinda"];
return (
<>
{items.map((item) => [
item,
<Button
key={item}
color="primary"
size="small"
className="px-2"
variant="contained"
onClick={() => setopenFullScreen((prev) => !prev)}
>
Show Dialog
</Button>
])}
<FullScreenDialog open={openFullScreen} />
</>
);
}
ReactDOM.render(<App />, document.querySelector("#root"));
import React from "react";
import { makeStyles } from "#material-ui/core";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import ListItemText from "#material-ui/core/ListItemText";
import ListItem from "#material-ui/core/ListItem";
import List from "#material-ui/core/List";
import Divider from "#material-ui/core/Divider";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import IconButton from "#material-ui/core/IconButton";
import Typography from "#material-ui/core/Typography";
import CloseIcon from "#material-ui/icons/Close";
import Slide from "#material-ui/core/Slide";
const useStyles = makeStyles((theme) => ({
appBar: {
position: "relative"
},
title: {
marginLeft: theme.spacing(2),
flex: 1
}
}));
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
export default function FullScreenDialog(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Dialog
fullScreen
open={props.open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Sound
</Typography>
<Button autoFocus color="inherit" onClick={handleClose}>
save
</Button>
</Toolbar>
</AppBar>
<List>
<ListItem button>
<ListItemText primary="Phone ringtone" secondary="Titania" />
</ListItem>
<Divider />
<ListItem button>
<ListItemText
primary="Default notification ringtone"
secondary="Tethys"
/>
</ListItem>
</List>
</Dialog>
</div>
);
}

makeStyle does not work with custom component

I am trying to make a simple navbar using material ui with a few buttons and a custom drop down menu component. When I try to style it using the makeStyle hook, the styling only applies to the material ui's buttons and heading but not the custom drop down component.
import React, { useContext } from "react";
import { makeStyles } from "#material-ui/core/styles";
import { AppBar, Toolbar, Typography } from "#material-ui/core";
import DropDown from "./DropDown";
import { Button } from "#material-ui/core";
import { AlgoContext } from "../AlgoContext";
const useStyles = makeStyles((theme) => ({
item: {
marginRight: theme.spacing(5),
},
}));
const MainHeader = () => {
const classes = useStyles();
const [algo, setAlgo] = useContext(AlgoContext);
return (
<div>
<AppBar elevation={0} position='static'>
<Toolbar>
<Typography variant='h6' className={classes.item}>
Pathfinding Visualiser
</Typography>
<Button variant='contained' className={classes.item}>
Visualise {algo.type}
</Button>
<DropDown className={classes.item}></DropDown>
<Button variant='contained' className={classes.item}>
Clear walls
</Button>
<Button variant='contained' className={classes.item}>
Clear path
</Button>
</Toolbar>
</AppBar>
</div>
);
};
export default MainHeader;
className is a default attribute of React element. You can not style your custom component by passing style object via className. Instead of that, you should pass it as a prop to DropDown component. Try this:
const MainHeader = () => {
const classes = useStyles();
const [algo, setAlgo] = useContext(AlgoContext);
return (
<DropDown itemStyle={classes.item}></DropDown>
);
};
export default MainHeader;
const DropDown = (props) => {
...
return (
<div className={props.itemStyle}>
...
</div>
)
}

Resources