본문 바로가기
개발/React

리액트 Material - 런타임에 Tab 추가, 삭제하기 (Mui Dynamic Tabs)

by 피로물든딸기 2024. 2. 3.
반응형

리액트 전체 링크

 

참고

- https://mui.com/material-ui/react-tabs/

- SweetAlert2로 모달, 팝업 만들기

 

Mui Tabs를 이용해서 동적으로 Tab을 추가 / 삭제해 보자. (링크에서 확인)

 

먼저 위의 링크를 참고하여 Tab 아래에 Text Area를 추가해 보자.

 

코드는 다음과 같다.

import React, { useState } from "react";

import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";

const TextAreaTabPanel = (props) => {
  const { children, value, index, tabs, setTabs, ...other } = props;

  const handleChange = (event) => {
    const updatedTabs = [...tabs];
    updatedTabs[index].content = event.target.value;
    setTabs(updatedTabs);
  };

  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && (
        <Box sx={{ p: 3 }}>
          <Textarea
            id={`textarea-${index}`}
            placeholder="Input Text ..."
            variant="outlined"
            color="primary"
            minRows={15}
            maxRows={15}
            value={tabs[index].content}
            onChange={handleChange}
          />
        </Box>
      )}
    </div>
  );
};

const DynamicTabs = () => {
  const [tabs, setTabs] = useState([
    { title: "Tab 1", content: "Content 1" },
    { title: "Tab 2", content: "Content 2" },
    { title: "Tab 3", content: "Content 3" },
  ]);

  const [activeTab, setActiveTab] = useState(0);

  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
  };

  return (
    <Box sx={{ m: 2 }}>
      <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          aria-label="dynamic tabs example"
        >
          {tabs.map((tab, index) => (
            <Tab key={index} label={<span>{tab.title}</span>} />
          ))}
        </Tabs>
      </Box>
      {tabs.map((tab, index) => (
        <TextAreaTabPanel
          key={index}
          value={activeTab}
          index={index}
          content={tab.content}
          tabs={tabs}
          setTabs={setTabs}
        />
      ))}
    </Box>
  );
}

export default DynamicTabs;

탭 이름 변경하기

 

탭을 더블 클릭하면 탭의 이름을 변경하도록 해보자.

 

선택한 탭이 편집 모드인지 판단하기 위한 변수가 필요하다.

  const [editIndex, setEditIndex] = useState(null);

 

Tab은 아래와 같이 바뀐다.

  <Tab
    key={index}
    label={
      editIndex === index ? (
        <input
          type="text"
          value={tab.title}
          autoFocus
          onFocus={(e) => e.target.select()}
          onChange={(e) => changeTabTitle(index, e.target.value)}
          onBlur={(e) => handleTitleBlur(index, e)}
          style={{ border: "none", outline: "none" }}
        />
      ) : (
        <span onDoubleClick={() => setEditIndex(index)}>
          {tab.title}
        </span>
      )
    }
  />

 

편집 모드가 아니라면 span에서 tab의 제목을 그대로 보여준다.

doubleClick을 하게 되면 현재 선택한 tab의 index가 editIndex로 설정되어 편집 모드가 된다.

그리고 편집 모드가 된 tab은 input에 설정한 값이 된다.

 

onFocus / onBlur로 input의 포커스가 설정되거나 해제되도록 조절한다.

그리고 onChange로 제목을 변경하면 된다.

 

각 메서드는 전체 코드를 확인하자.

import React, { useState } from "react";

import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";

const TextAreaTabPanel = (props) => {
  const { children, value, index, tabs, setTabs, ...other } = props;

  const handleChange = (event) => {
    const updatedTabs = [...tabs];
    updatedTabs[index].content = event.target.value;
    setTabs(updatedTabs);
  };

  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && (
        <Box sx={{ p: 3 }}>
          <Textarea
            id={`textarea-${index}`}
            placeholder="Input Text ..."
            variant="outlined"
            color="primary"
            minRows={15}
            maxRows={15}
            value={tabs[index].content}
            onChange={handleChange}
          />
        </Box>
      )}
    </div>
  );
};

const DynamicTabs = () => {
  const [tabs, setTabs] = useState([
    { title: "Tab 1", content: "Content 1" },
    { title: "Tab 2", content: "Content 2" },
    { title: "Tab 3", content: "Content 3" },
  ]);

  const [activeTab, setActiveTab] = useState(0);
  const [editIndex, setEditIndex] = useState(null);

  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
  };

  const changeTabTitle = (index, newTitle) => {
    const updatedTabs = [...tabs];
    updatedTabs[index].title = newTitle;
    setTabs(updatedTabs);
  };

  const handleTitleBlur = (index, event) => {
    const newTitle = event.target.value.trim();
    if (newTitle !== "" && newTitle !== tabs[index].title) {
      changeTabTitle(index, newTitle);
    }
    setEditIndex(null);
  };

  return (
    <Box sx={{ m: 2 }}>
      <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          aria-label="dynamic tabs example"
        >
          {tabs.map((tab, index) => (
          <Tab
            key={index}
            label={
              editIndex === index ? (
                <input
                  type="text"
                  value={tab.title}
                  autoFocus
                  onFocus={(e) => e.target.select()}
                  onChange={(e) => changeTabTitle(index, e.target.value)}
                  onBlur={(e) => handleTitleBlur(index, e)}
                  style={{ border: "none", outline: "none" }}
                />
              ) : (
                <span onDoubleClick={() => setEditIndex(index)}>
                  {tab.title}
                </span>
              )
            }
          />
          ))}
        </Tabs>
      </Box>
      {tabs.map((tab, index) => (
        <TextAreaTabPanel
          key={index}
          value={activeTab}
          index={index}
          tabs={tabs}
          setTabs={setTabs}
        />
      ))}
    </Box>
  );
}

export default DynamicTabs;

탭 추가하기

 

탭 추가하기 기능을 위해 + 버튼을 탭 옆에 추가하자.

import AddIcon from "@mui/icons-material/Add";

...

  {tabs.map((tab, index) => (
  <Tab
    key={index}
    label={ ... }
  />
  ))}
  <Tab onClick={addTab} label={<AddIcon />} />

 

addTab은 다음과 같다.

useState로 관리되던 Tab에서 신규 탭을 추가하면 된다.

  const addTab = () => {
    const newTab = {
      title: `New Tab`,
      content: `New Content`,
    };
    setTabs([...tabs, newTab]);
    setActiveTab(tabs.length); // Make the newly added tab active
  };

 

이제 동적으로 TAB을 추가할 수 있게 되었다.

 

TabsscrollablescrollButtonsauto로 추가하면

탭이 많아지는 경우 자동으로 버튼이 만들어진다.

  <Tabs
    value={activeTab}
    onChange={handleTabChange}
    aria-label="dynamic tabs example"
    variant="scrollable" // 'standard' | 'scrollable' | 'fullWidth';
    scrollButtons="auto"
  >


탭 삭제하기

 

탭에서 마우스 오른쪽 버튼을 누르면 팝업 창(Swal)이 나오고, Yes를 누르면 탭이 삭제되도록 해보자.

마우스 오른쪽 버튼은 onContextMenu에서 이벤트를 등록하면 된다.

  <span
    onDoubleClick={() => setEditIndex(index)}
    onContextMenu={(event) =>
      handleTabContextMenu(event, index)
    }
  >
    {tab.title}
  </span>

 

삭제 메서드는 다음과 같다.

삭제할 index를 filter를 이용해 지우고 useState로 갱신하였다.

  const deleteTab = (index) => {
    const updatedTabs = tabs.filter((tab, i) => i !== index);
    setTabs(updatedTabs);
    if (activeTab === index) {
      setActiveTab(Math.max(activeTab - 1, 0));
    }
  };

  const handleTabContextMenu = (event, index) => {
    event.preventDefault();
    Swal.fire({
      title: "Are you sure?",
      text: "Do you want to delete this tab?",
      icon: "question",
      showCancelButton: true,
      confirmButtonText: "Yes",
      cancelButtonText: "No",
    }).then((result) => {
      if (result.isConfirmed) {
        deleteTab(index);
      }
    });
  };

 

이제 탭을 삭제해 보자.

 

최종 코드는 다음과 같다.

import React, { useState } from "react";

import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";
import AddIcon from "@mui/icons-material/Add";

import Swal from "sweetalert2";

const TextAreaTabPanel = (props) => {
  const { children, value, index, tabs, setTabs, ...other } = props;

  const handleChange = (event) => {
    const updatedTabs = [...tabs];
    updatedTabs[index].content = event.target.value;
    setTabs(updatedTabs);
  };

  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && (
        <Box sx={{ p: 3 }}>
          <Textarea
            id={`textarea-${index}`}
            placeholder="Input Text ..."
            variant="outlined"
            color="primary"
            minRows={15}
            maxRows={15}
            value={tabs[index].content}
            onChange={handleChange}
          />
        </Box>
      )}
    </div>
  );
};

const DynamicTabs = () => {
  const [tabs, setTabs] = useState([
    { title: "Tab 1", content: "Content 1" },
    { title: "Tab 2", content: "Content 2" },
    { title: "Tab 3", content: "Content 3" },
  ]);

  const [activeTab, setActiveTab] = useState(0);
  const [editIndex, setEditIndex] = useState(null);

  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
  };

  const changeTabTitle = (index, newTitle) => {
    const updatedTabs = [...tabs];
    updatedTabs[index].title = newTitle;
    setTabs(updatedTabs);
  };

  const handleTitleBlur = (index, event) => {
    const newTitle = event.target.value.trim();
    if (newTitle !== "" && newTitle !== tabs[index].title) {
      changeTabTitle(index, newTitle);
    }
    setEditIndex(null);
  };

  const addTab = () => {
    const newTab = {
      title: `New Tab`,
      content: `New Content`,
    };
    setTabs([...tabs, newTab]);
    setActiveTab(tabs.length); // Make the newly added tab active
  };

  const deleteTab = (index) => {
    const updatedTabs = tabs.filter((tab, i) => i !== index);
    setTabs(updatedTabs);
    if (activeTab === index) {
      setActiveTab(Math.max(activeTab - 1, 0));
    }
  };

  const handleTabContextMenu = (event, index) => {
    event.preventDefault();
    Swal.fire({
      title: "Are you sure?",
      text: "Do you want to delete this tab?",
      icon: "question",
      showCancelButton: true,
      confirmButtonText: "Yes",
      cancelButtonText: "No",
    }).then((result) => {
      if (result.isConfirmed) {
        deleteTab(index);
      }
    });
  };

  return (
    <Box sx={{ m: 2 }}>
      <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          aria-label="dynamic tabs example"
          variant="scrollable"
          scrollButtons="auto"
        >
          {tabs.map((tab, index) => (
            <Tab
              key={index}
              label={
                editIndex === index ? (
                  <input
                    type="text"
                    value={tab.title}
                    autoFocus
                    onFocus={(e) => e.target.select()}
                    onChange={(e) => changeTabTitle(index, e.target.value)}
                    onBlur={(e) => handleTitleBlur(index, e)}
                    style={{ border: "none", outline: "none" }}
                  />
                ) : (
                  <span
                    onDoubleClick={() => setEditIndex(index)}
                    onContextMenu={(event) =>
                      handleTabContextMenu(event, index)
                    }
                  >
                    {tab.title}
                  </span>
                )
              }
            />
          ))}
          <Tab onClick={addTab} label={<AddIcon />} />
        </Tabs>
      </Box>
      {tabs.map((tab, index) => (
        <TextAreaTabPanel
          key={index}
          value={activeTab}
          index={index}
          tabs={tabs}
          setTabs={setTabs}
        />
      ))}
    </Box>
  );
}

export default DynamicTabs;
반응형

댓글