React/MUI Popover positioning incorrectly with anchorPosition - javascript

I'm using a React/MUI Popover inside a react-window List element and am unable to get the Popover to position correctly -- it always winds up in the top left corner of the window (the component is unable to perform a getBoundingClientRctd() on the anchor element [anchorEl in the docs]).
So to get around that problem temporarily, I decided to use the anchorPosition parameter which allows to set an absolute position -- in my case, just the middle of the window. That's not working either.
I've reviewed the values in Chrome DevTools and everything seems to be OK (i.e., I do get an anchorEl when I'm using that; I get valid positionLeft/Top values; etc...
Probably something really simple and hoping someone can point out what I did wrong.
Edited: Key elements of the solution
Row component must be defined outside of the containing component.
the <List> component has an itemData attribute which is used to pass custom data to Row.
Edited to add react-window List renderer.
Here's the basic setup:
Popover renderer
renderPopover(template, itemId) {
const { anchorEl, anchorId } = this.state;
const { classes } = this.props;
const open = Boolean(anchorEl) && anchorId === itemId;
const bgColor = '#babaef';
const { innerHeight, innerWidth } = window;
const positionLeft = innerWidth / 2;
const positionTop = innerHeight / 2;
console.log(`renderPopover: ${positionLeft} / ${positionTop}`);
<Popover
id="simple-popper"
open={open}
style={{ color: 'Black' }}
anchorEl={anchorEl}
onClose={event => this.handlePopoverClose(event)}
anchorPosition={{ left: {positionLeft}, top: {positionTop} }}
anchorReference="anchorPosition"
>
<Typography style={{ backgroundColor: bgColor }} className={classes.typography}>
{this.renderScheduleElements(template, itemId)}
</Typography>
</Popover>
);
}
Button element renderer
renderScheduleComponent(template, itemId) {
const { anchorEl, anchorId } = this.state;
const open = Boolean(anchorEl) && anchorId === itemId;
const { classes } = this.props;
const id = open ? 'simple-popper' : undefined;
return (
<Grid key={itemId} item>
<Paper className={classes.paper}>
<div style={{ padding: '4px' }}>
<Button
NO_ref={itemId}
NODE_ref={(node) => this.buttonRef = node}
id={itemId}
name={itemId}
aria-owns={id}
aria-haspopup="true"
variant="outlined"
color="primary"
style={{
fontWeight: 'bold',
padding: '8px',
margin: 'auto',
display: 'block',
width: '100%',
}}
onClick={event => this.handlePopoverClick(event, itemId)}
>
{template.templateName}
</Button>
{(this.renderPopover).call(this, template, itemId)}
</div>
</Paper>
</Grid>
);
}
Click event handler
handlePopoverClick(event, id) {
event.preventDefault();
console.log(`handlePopoverClick : ${event.currentTarget.name}`);
this.setState({
anchorEl: event.currentTarget,
anchorId: id,
});
}
react-window List renderer
renderScheduleColumn(columnData) {
const { classes } = this.props;
const { scheduleDate, scheduleTemplates } = columnData;
this.scheduleTemplates = scheduleTemplates;
const Row = ({ index, style }) => {
return (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{this.renderScheduleComponent(scheduleTemplates[index], `${scheduleDate}:${index}`)}
</div>
);
}
const { columnHeight, columnWidth } = this.state;
return (
<Grid id={scheduleDate} key={scheduleDate} item>
<Paper className={classes.paper}>
<div style={{ width: '100%', textAlign: 'center' }}>
<Typography variant="h6" style={{ padding: '24px', color: 'white', backgroundColor: '#3f51b5' }}>
{scheduleDate}
</Typography>
</div>
<List
className="List"
height={columnHeight}
itemCount={scheduleTemplates.length}
itemSize={50}
width={columnWidth}
>
{Row}
</List>
</Paper>
</Grid>
);
}

It looks like a similar problem as here: React Material-UI menu anchor broken by react-window list.
You should move the definition of your Row function out of renderScheduleColumn so that it is a consistent type. This will require moving/reworking renderScheduleComponent as well. You can use the itemData property on the List to pass information to the Row.

Related

Custom Pagination in Material-Table

I want to override the default pagination in Material-Table to look different, but keep the same default elements there. My override allows me to move between pages in the table, but I cannot change how many rows appear in the table. I cannot skip to the last or first page either. I want to keep the exact same options as what is there by default, but change how they look.
All of my Icon, Grid, Typography, etc, imports are #material-ui/core/ or #material-ui/icons/
function IntakeList() {
const CustomPaginationComponent = (props) => {
const { page, rowsPerPage, count, onChangePage } = props;
let from = rowsPerPage * page + 1;
let to = rowsPerPage * (page + 1);
if (to > count) {
to = count;
}
return (
<td>
<Grid container alignItems="center" style={{ paddingTop: 8 }}>
<Grid item>
<IconButton disabled={page === 0} onClick={(e) => onChangePage(e, page - 1)}>
<SkipPreviousIcon fontSize="small" color={page === 0 ? "disabled" : "primary"} />
<Typography>Prev</Typography>
</IconButton>
</Grid>
<Grid item>
<Typography variant="caption" style={{ color: "black" }}>
{from}-{to} of {count}
</Typography>
</Grid>
<Grid item>
<IconButton disabled={to >= count} onClick={(e) => onChangePage(e, page + 1)}>
<Typography>Next</Typography>
<SkipNextIcon fontSize="small" color={to < count ? "primary" : "disabled"} />
</IconButton>
</Grid>
</Grid>
</td>
);
};
return (
<MaterialTable
title="Title"
data={data}
columns={columns}
options={{
pageSize: 10,
pageSizeOptions: [10, 15, 25, 50, 100],
}}
components={{
Pagination: (props) => {
return <CustomPaginationComponent {...props} />;
},
}}
/>
);
}
What you have done so far is right. You just simply have to code for the "skip to last page", "skip to first page" and "select row options" just like you did for "next" and "previous" page.
https://codesandbox.io/s/solution-q1cmer?file=/src/App.js
When you console log the props of pagination you will get this
Using the rowsPerPageOptions , onChangeRowsPerPage and some of the code you have coded I was able to code the number of rows per page.
To be honest I don't think you have to code to next page and previous page functionality. It's already in props. You simply has to declare them in the right place.
Happy Coding

Why is an element found using getElementById in Next.js returning null even when document is defined?

Using SSR in my React/Next app.
Trying to find an element with the id but is returning null
even when document is present (and I can see the div with the id plTable),
and even when getElementById is called after 6 seconds to ensure the element has been loaded on screen.
What is the issue and how can I fix this?
Here is the component:
const LineItemTable: React.FC<LineItemTableProps> = ({ reportName }) => {
const classes = useStyles({});
const dispatch = useDispatch();
const [page, setPage] = useState<number>(0);
const selectedCompanyId = useSelector((state) => state.company.selectedId);
const company = useSelector((state) => state.company.current);
useEffect(() => {
if (reportName && selectedCompanyId) {
dispatch(
getReportByName({
name: reportName, // 'profit and loss' or 'balance sheet'
includeLineItems: true,
page: page,
}),
);
}
}, [reportName, selectedCompanyId]);
let plTable: any = 'kk';
useEffect(() => {
console.log('uef');
if (typeof document !== 'undefined') {
setTimeout(() => {
plTable = document.querySelector('plTable');// ***** NEVER FOUND ******
console.log('doc', document); // ***** is found and defined correctly *****
console.log('plTable', plTable); // ***** null *****
}, 6000);
}
});
const endObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (!entry.isIntersecting) {
//Put what you want to happen if the end is NOT visible
console.log('not visible');
} else {
//Put what you want to happen if the end is visible
//For instance firing your function
// setPage(page + 1);
console.log('visible');
}
},
{ root: null, threshold: 1 },
);
// endObserver.observe(plTable);
const getLineItems = useMemo(() => makeGetAllLineItemsByReport(reportName), [
reportName,
]);
const lineItems = useSelector((state) => getLineItems(state));
if (!lineItems) return null;
// ADDED
// Add an elemnt in your html with the class of "end" at the end of the chart
// I recommend adding an empty div with the class of "end" and setting it's opacity to 0
return (
<div
id="plTable" // ****** Defined here *******
style={{
display: 'flex',
alignItems: 'flex-end',
margin: 'auto 0px',
}}
>
<Grid container spacing={3}>
<Grid item xs={12}>
<Card
sx={{
padding: '20px',
}}
>
<CardContent
sx={{
alignItems: 'center',
display: 'flex',
height: '1000px',
}}
>
<Scrollbar className={classes.scrollBar}>
<Table className={classes.root}>
<TableHead>
<TableRow>
<th>
<TableCell className={classes.headerStyle}>
ANALYSIS CATEGORY
</TableCell>
<TableCell
className={classes.headerStyle}
sx={{ marginRight: '10px' }}
>
NAME
</TableCell>
{company &&
company.dates.map((header) => (
<TableCell
className={classes.headerStyle}
sx={{
width: '200px !important',
marginLeft: '10px',
}}
key={header}
>
{header}
</TableCell>
))}
</th>
</TableRow>
</TableHead>
<TableBody>
{lineItems.map((lineItem, i) => (
<TableRow key={lineItem.id}>
<LineItemRow
i={i}
id={lineItem.id}
reportName={reportName}
level={lineItem.level}
/>
</TableRow>
))}
</TableBody>
</Table>
</Scrollbar>
</CardContent>
</Card>
</Grid>
</Grid>
</div>
);
};
According to MDN docs, querySelector either takes an element to look for:
querySelector('plTable')
/* Looking for html tag plTable */
or an identifier:
querySelector('#plTable')
/* Looking for an element with id of plTable */
Don't use DOM selectors in React, use refs to access DOM nodes.
You can create a ref with useRef or React.createRef or you can pass a callback to the ref attribute of an element, that will receive the DOM node reference when the virtual DOM has done with the reconciliation.
To check if the node is mounted and do something with it being sure it is mounted, try this:
<div
id="plTable" // ****** Defined here *******
style={{
display: 'flex',
alignItems: 'flex-end',
margin: 'auto 0px',
}}
ref={node => {
if (node) console.log("p1Table", node)
//Do something with node
}}
>

Changing "likes" based on what picture is shown

I am making an app based off Nasa's api that shows a picture or video each day. I have also created a like button that toggles if a person clicks it to like a picture or to toggle it back to default. I created the app in a way where a user can select a date using a calendar component that is through material-ui and when they select a date, it will bring the picture or video from that date. I am trying to figure out how to attach the like button to each picture, so that if they like one picture, they can go to another day and it will let them like another picture. Currently, for example, if I like today's picture, select the calendar and go to yesterday's picture, my like button is still toggled. Here is the code I used for getting the pictures and other information to show:
export default function PictureOfDay(props) {
const [picture, setPicture] = useState([])
const {date} = props;
let selectDate = moment(date, "ddd MMM DD YYYY HH:mm:ss Z-HHmm");
let formatDate = selectDate.format("YYYY-MM-DD");
useEffect(() => {
axios.get(
`https://api.nasa.gov/planetary/apod?api_key=QQpTYaQHDUvPAyVorMgxfKhQEoSQikBYt5WuFCf6&date=${formatDate}`
)
.then((response) => {
console.log(response.data);
setPicture(response.data);
})
.catch((err) => {
console.log(err)
});
}, [formatDate]);
return (
<div className="info">
{props.fetchingData && (
<div className="key spinner">
<Loader type="Puff" color="#800080" height="60" width="60" />
<p>Loading Data</p>
</div>
)}
<PictureInfo
date={picture.date}
title={picture.title}
image={picture.url}
media={picture.media_type}
explanation={picture.explanation}
/>
</div>
);
}
I also have my Like button logic on a different component. Right now, it is not attached to the pictures and I am unsure how to go about doing that:
import React, {useState, useEffect} from "react";
import ToggleHearts from "./ToggleHearts"
import {
Button,
Typography,
} from "#mui/material";
import {makeStyles} from "#mui/styles";
const useStyles = makeStyles((theme) => ({
button: {
...theme.typography.buttons,
fontFamily: "Orbitron",
padding: "0.7em",
fontSize: "1.5em",
width: "3.2em",
height: "3.2em",
background: theme.palette.primary.mainGradient,
border: "2px solid pink",
"&:hover": {
background: theme.palette.secondary.mainGradient,
},
},
}));
const Likes = () => {
const classes = useStyles();
const [liked, setLiked] = useState(false)
const handleChangeHeart = () => {
setLiked((previousHeart) => {
return !previousHeart;
});
};
useEffect(() => {
setLiked(JSON.parse(window.localStorage.getItem("liked")));
}, [])
useEffect(() => {
window.localStorage.setItem('liked', liked);
}, [liked])
return (
<Button
className={classes.button}
variant="contained"
onClick={handleChangeHeart}
>
<Typography style={{ fontFamily: "Orbitron" }}>
<ToggleHearts liked={liked}/>
</Typography>
</Button>
);
}
export default Likes
The Likes component is just attached to my component that renders everything on the page, here is the code to that:
export default function PictureInfo(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const newDate = moment(props.date).format("dddd, MMMM, Do YYYY");
return (
<React.Fragment>
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid item className={classes.formGridItem}>
<Typography variant="h2">Date: {newDate}</Typography>
</Grid>
<Grid item className={classes.formGridItem}>
<Typography variant="h2">Title of Photo: {props.title}</Typography>
</Grid>
</Grid>
<Grid
container
direction="column"
justifyContent="center"
alignItems="center"
>
<Grid item className={classes.formGridItem}>
{props.media === "video" ? (
<iframe
className={classes.image}
title="video"
src={props.image}
width="800px"
height="600px"
></iframe>
) : (
<img className={classes.image} alt="nasa" src={props.image} />
)}
</Grid>
<Likes />
<Grid item className={classes.formGridItem} align="center">
<Button
className={classes.button}
variant="contained"
onClick={handleOpen}
>
Click here for more information!
</Button>
</Grid>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography
id="modal-modal-title"
style={{
borderBottom: "1px solid pink",
fontFamily: "'Orbitron', sans-serif",
marginBottom: "1em",
fontSize: "1.4em",
}}
>
Explanation of: {props.title}
</Typography>
<Box
style={{
background: "white",
border: "1px solid purple",
padding: "2em",
}}
>
<Typography
id="modal-modal-description"
variant="subtitle2"
style={{ fontSize: "1.2em" }}
>
{props.explanation}
</Typography>
</Box>
</Box>
</Modal>
<footer>
<Typography
variant="subtitle2"
style={{ fontSize: "1rem", color: "black" }}
>
©2021 Charlene Johnson
</Typography>
</footer>
</Grid>
</React.Fragment>
);
}
If i right to understand you
all picture or video posts, must have an unique ID in you data represent, you data for this must look like this for example:
[{
id:'id of post',
likes:[idOfUsers],
...other needed for you data
}]
So, now you can check for likes id of user, and toggle you 'like button', if ID of user exist in this post into a 'likes' array,
or store IDs of posts to 'postsWhichwasLiked':[idsOfPosts],
and then to define which of posts was liked across getting id from this array and then to comparing with ID of post

How to handle multiple menu state with Material-UI Menu component?

I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;

React - Splice removing too many elements from state array

I'm trying to create an image gallery that has a variable number of inputs. I have successfully created an add button which will add a new element to my array that is in the state. However, when I click the button to remove an element from the array, it removes all elements except the first one. Could someone help me figure out where I'm going wrong here?
My initialization/add/remove logic in parent component:
const newImage = {
fileName: 'placeholder.png',
description: '',
}
const [galleryImages, setGalleryImages] = useState([newImage])
const addNewImage = () => {
setGalleryImages(galleryImages.concat(newImage))
}
const removeImage = (index) => {
setGalleryImages(galleryImages.splice(index, 1))
}
My image gallery component:
const ImageGallery = ({galleryImages, setGalleryImages, addNewImage, removeImage}) => {
console.log('gallery images:', galleryImages)
return(
galleryImages.map((image, index) => {
const fileId = 'image' + (index + 1) + 'File'
const descriptionId = 'image' + (index + 1) + 'Description'
return(
<Card key={index} style={{marginTop: '10px'}}>
<Card.Body>
<div style={{position: 'absolute', top:'5px', right:'5px'}}>
<IconButton aria-label="remove" color="secondary" onClick={() => removeImage(index)}>
<CancelIcon />
</IconButton>
</div>
<Card.Title>Image {index+1}</Card.Title>
<Form.Group>
<Form.File id={fileId} />
<Form.Label>Image Description</Form.Label>
<Form.Control id={descriptionId} type="text" placeholder="Image description..."/>
</Form.Group>
</Card.Body>
{ index === (galleryImages.length - 1) &&
<div style={{left: '0px', right:'0px', flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', bottom: '-30px', position: 'absolute'}}>
<IconButton aria-label="add another image" onClick={() => addNewImage()}>
<AddCircleIcon style={{color: 'green', fontSize: 40, backgroundColor: 'white', borderRadius: '50%'}}/>
</IconButton>
</div>
}
</Card>
)
})
)
}
Splice mutates the array directly, which is generally disapproved in React.
While the recommended approach is using the filter method to remove, you can do it in this way if u want to use splice -
const removeImage = (index) => {
//create a new array here with the spread operator of the original array.
//Without this, react won't recognize the change and the child component won't be re-rendered!
const galleryImagesData = [...galleryImages];
galleryImagesData.splice(index, 1)
setGalleryImages(galleryImagesData)
}

Categories