Luka Reeson
Luka Reeson

Reputation: 123

React - map function sets the input values only with the last item in the array

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

Upvotes: 1

Views: 2256

Answers (2)

Shyam
Shyam

Reputation: 5497

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

Upvotes: 1

Drew Reese
Drew Reese

Reputation: 202618

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>
  );
};

Upvotes: 1

Related Questions