1

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:

Dropdown Image

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. :)

Joel Hager
  • 2,990
  • 3
  • 15
  • 44

3 Answers3

0

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>

enter image description here

emamones
  • 77
  • 7
  • I forgot about this. The way I fixed it was to have a single state obj that inits to null, and I wrapp all collapse panels' in like this: { Boolean(stateObj === id) }. That's just how I did it. – Joel Hager Aug 25 '21 at 16:13
0

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

  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31865590) – mc-user May 30 '22 at 06:15
0

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;
Mujahidul Islam
  • 265
  • 4
  • 8