1

I have this RenamePopover component with an input field, which I'm using inside a map function located in the Board componenent. As I loop over the array, I'm passing "board.name" into the RenamePopover as a value prop, so that in the end, every rendered element would have its own popover with its input field prepopulated with the name. Unfortunately, that's not the case, since after rendering every input field is set to the name of the last element in the array. What I am missing?

Board.js

import React from 'react'
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getActiveBoard } from '../../state/action-creators/activeBoardActions';

import RenamePopover from '../popovers/RenamePopover';

const Board = () => {
  const dispatch = useDispatch();
  const [anchorEl, setAnchorEl] = React.useState(null);
  const boards = useSelector((state) => state.board.items);
  const open = Boolean(anchorEl);

  useEffect(() => {
    dispatch(getActiveBoard());
  }, [boards])

  const handleClick = (id) => {
    const anchor = document.getElementById(`board-anchor${id}`);
    setAnchorEl(anchor);
  };
  
  const handleClose = () => {
    setAnchorEl(null);
  };
    

  const single_board = boards && boards.map((board) => {
    return (
      <li key={board.id} className={`row hovered-nav-item board-item ${active_board == board.id ? "item-selected" : ""}`}>
        <span className="d-flex justify-content-between">
          <div onClick={() => onBoardClick(board.id)} className="fs-5 text-white board-name" id={`board-anchor${board.id}`}>
            {board.name}
          </div>

          <svg onClick={() => handleClick(board.id)} xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
            className="bi bi-pen-fill rename-add-icon rename-add-icon-sidebar"
            viewBox="0 0 16 16">
            <path
              d="m13.498.795.149-.149a1.207 1.207 0 1 1 1.707 1.708l-.149.148a1.5 1.5 0 0 1-.059 2.059L4.854 14.854a.5.5 0 0 1-.233.131l-4 1a.5.5 0 0 1-.606-.606l1-4a.5.5 0 0 1 .131-.232l9.642-9.642a.5.5 0 0 0-.642.056L6.854 4.854a.5.5 0 1 1-.708-.708L9.44.854A1.5 1.5 0 0 1 11.5.796a1.5 1.5 0 0 1 1.998-.001z" />
          </svg>
          <RenamePopover
            open={open}
            anchorEl={anchorEl} 
            onClose={handleClose}
            placeholder="Enter new name"
            value={board.name}
          />
        </span>
      </li>
    )
  })

  return(
    <div className="container" id="sidebar-boards">
      {single_board}
    </div>
  ) 
  
}

export default Board

RenamePopover.js

import { Popover } from '@material-ui/core'
import React from 'react'

const RenamePopover = (props) => {
  const [value, setValue] = React.useState(props.value);

  const handleChange = (e) => {
    setValue(e.target.value);
  }
  
  return (
    <Popover
      open={props.open}
      anchorEl={props.anchorEl}
      onClose={props.onClose}
      anchorOrigin={{
        vertical: 'center',
        horizontal: 'right',
      }}
      transformOrigin={{
        vertical: 'center',
        horizontal: 'left',
      }}
    >
      <input autoFocus={true} className="card bg-dark text-light add-input" type="text" 
      placeholder={props.placeholder} 
      value={value} 
      onChange={handleChange}
      />
    </Popover>
  )
}

export default RenamePopove

r

2
  • do you get different values on every render for value in RenamePopover? Commented Aug 14, 2021 at 17:46
  • No, they are the same and all set with the same "board.name". Commented Aug 14, 2021 at 18:15

2 Answers 2

1

You have one common anchorElement state and it is being used for all the Popovers. Create a new component called BoardItem which is responsible for rendering each board item and move the anchorEl state inside it. This guarantees that each BoardItem will have its own state for the anchorEl .

const BoardItem = ({ board }) => {
  const [anchorEl, setAnchorEl] = React.useState(null);

  const handleClick = (event) => {
    setAnchorEl(event.currentTarget);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  const open = Boolean(anchorEl);

  return (
    <li>
      <span className="d-flex justify-content-between">
        <div>{board.name}</div>
        <svg
          onClick={handleClick}
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          fill="currentColor"
          className="bi bi-pen-fill rename-add-icon rename-add-icon-sidebar"
          viewBox="0 0 16 16"
        >
          <path d="m13.498.795.149-.149a1.207 1.207 0 1 1 1.707 1.708l-.149.148a1.5 1.5 0 0 1-.059 2.059L4.854 14.854a.5.5 0 0 1-.233.131l-4 1a.5.5 0 0 1-.606-.606l1-4a.5.5 0 0 1 .131-.232l9.642-9.642a.5.5 0 0 0-.642.056L6.854 4.854a.5.5 0 1 1-.708-.708L9.44.854A1.5 1.5 0 0 1 11.5.796a1.5 1.5 0 0 1 1.998-.001z" />
        </svg>
        <RenamePopover
          open={open}
          anchorEl={anchorEl}
          onClose={handleClose}
          placeholder="Enter new name"
          value={board.name}
        />
      </span>
    </li>
  );
};

Now in the Board component render the BoardItem

const Board = () => {

  // your useSelector code goes here .... 

  const single_board =
    boards &&
    boards.map((board) => {
      return <BoardItem board={board} key={board.id} />;
    });

  return (
    <div className="container" id="sidebar-boards">
      {single_board}
    </div>
  );
};

Example Sandbox

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for the explanation and solution. Works flawlessly!
1

Issue

This issue is that you are using a single open "state" for the popover. You set the anchorEl to the element being interacted with, but then open all popovers.

Solution 1

Add an open state to track which specific board element you want to open.

const Board = () => {
  ...

  const [anchorEl, setAnchorEl] = React.useState(null);
  const [open, setOpen] = React.useState(null); // <-- add state to hold id

  ...

  const handleClick = (id) => {
    const anchor = document.getElementById(`board-anchor${id}`);
    setAnchorEl(anchor);
    setOpen(id);
  };

  const handleClose = () => {
    setAnchorEl(null);
    setOpen(null);
  };

  const single_board =
    boards &&
    boards.map((board) => {
      return (
        <li
          key={board.id}
          className={`row hovered-nav-item board-item ${active_board == board.id ? "item-selected" : ""}`}
        >
          <span className="d-flex justify-content-between">
            <div
              onClick={() => handleClick(board.id)}
              className="fs-5 text-white board-name"
              id={`board-anchor${board.id}`}
            >
              {board.name}
            </div>
            ...
            <RenamePopover
              open={open === board.id} // <-- match board id
              anchorEl={anchorEl}
              onClose={handleClose}
              placeholder="Enter new name"
              value={board.name}
            />
          </span>
        </li>
      );
    });

...

Solution 2

Use the open state to hold the entire board data you want to open/render and render a single popover. You'll need to add an useEffect hook to the popover component to handle resetting the local state.

const RenamePopover = (props) => {
  const [value, setValue] = React.useState(props.value);

  React.useEffect(() => {
    setValue(props.value);
  }, [props.value]);

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <Popover
      open={props.open}
      anchorEl={props.anchorEl}
      onClose={props.onClose}
      anchorOrigin={{
        vertical: "center",
        horizontal: "right"
      }}
      transformOrigin={{
        vertical: "center",
        horizontal: "left"
      }}
    >
      <input
        autoFocus={true}
        className="card bg-dark text-light add-input"
        type="text"
        placeholder={props.placeholder}
        value={value}
        onChange={handleChange}
      />
    </Popover>
  );
};
const Board = () => {
  ...

  const [anchorEl, setAnchorEl] = React.useState(null);
  const [open, setOpen] = React.useState(null);

  ...

  const handleClick = (board) => {
    const anchor = document.getElementById(`board-anchor${board.id}`);
    setAnchorEl(anchor);
    setOpen(board);
  };

  const handleClose = () => {
    setAnchorEl(null);
    setOpen(null);
  };

  const single_board =
    boards &&
    boards.map((board) => {
      return (
        <li
          key={board.id}
          className={`row hovered-nav-item board-item ${active_board == board.id ? "item-selected" : ""}`}
        >
          <span className="d-flex justify-content-between">
            <div
              onClick={() => handleClick(board)}
              className="fs-5 text-white board-name"
              id={`board-anchor${board.id}`}
            >
              {board.name}
            </div>
            ...
          </span>
        </li>
      );
    });

  return (
    <div className="container" id="sidebar-boards">
      {single_board}
      <RenamePopover // <-- render 1 popover
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        placeholder="Enter new name"
        value={open?.name}
      />
    </div>
  );
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.