참고
- https://mui.com/material-ui/react-drawer/
- 오브젝트 순회하기 (Iterate JavaScript Object)
- width, height, placeholder, sort
- afterSelection으로 수식 입력줄 구현하기
- Download CSV 구현 (콤마, 줄바꿈, 따옴표 처리)
- Mui Drawer로 Handsontable Option 관리하기
- Column Width, Row Height 로컬 스토리지에 저장하기
- Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기)
Material UI의 Drawer 코드를 참고해서 아래와 같은 템플릿을 만들자.
예시 코드는 다음과 같다.
import React, { useState } from "react";
import CustomHansOnTable from "./CustomHansOnTable";
import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import FormGroup from "@mui/material/FormGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
const MyHandsonTable = () => {
const [state, setState] = useState({ right: false });
const toggleDrawer = (anchor, open) => (event) => {
if (
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const list = () => (
<Box sx={{ width: 1000 }}>
<FormGroup>
<FormControlLabel control={<Checkbox checked={true} />} label="Label" />
<FormControlLabel required control={<Checkbox />} label="Required" />
<FormControlLabel disabled control={<Checkbox />} label="Disabled" />
</FormGroup>
<Divider />
<FormGroup>
<FormControlLabel control={<Checkbox checked={true} />} label="Label" />
<FormControlLabel required control={<Checkbox />} label="Required" />
<FormControlLabel disabled control={<Checkbox />} label="Disabled" />
</FormGroup>
</Box>
);
return (
<div>
<div>
{["right"].map((anchor) => (
<React.Fragment key={anchor}>
<Button
sx={{ m: 2 }}
variant="contained"
color="secondary"
onClick={toggleDrawer(anchor, true)}
>
Options Setting
</Button>
<Drawer
anchor={anchor}
open={state[anchor]}
onClose={toggleDrawer(anchor, false)}
>
{list(anchor)}
</Drawer>
</React.Fragment>
))}
{/* <CustomHansOnTable /> */}
</div>
</div>
);
};
export default MyHandsonTable;
True False Options 구현
커스터마이징 하고 싶은 True / False 옵션을 아래와 같이 기본값으로 설정한다.
const [myOptions, setMyOptions] = useState({
trueFalseOptions: {
colHeaders: true,
rowHeaders: true,
wordWrap: false /* 줄 바꿈 off */,
manualColumnResize: true,
manualRowResize: true,
manualColumnMove: true,
manualRowMove: true,
allowInsertColumn: true,
allowInsertRow: true,
allowRemoveColumn: true,
allowRemoveRow: true,
autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
outsideClickDeselects: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
readOnly: true /* true : 모든 셀을 readOnly로 설정*/,
enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
copyable: true /* 복사 가능 여부 */,
copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
undo: true /* false : ctrl + z 비활성화 */,
trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
comments: true /* 주석, 메모 기능 context menu에 추가 */,
manualColumnFreeze: true /* freezeColumn context menu에 추가 */,
observeChanges: true,
},
numberOptions: { ... },
});
True / False 옵션이기 때문에 Mui Check Box를 만든다.
myOptions.trueFalseOptions를 배열로 만들고 map을 이용하였다.
그리고 onChange에 해당 값만 변경할 수 있도록 이벤트를 추가하였다.
const changeTrueFalseOptions = (option, value) => {
let temp = { ...myOptions };
temp.trueFalseOptions[option] = !value;
setMyOptions(temp);
};
const makeTrueFalseCheckBox = () => {
let pair = Object.entries(myOptions.trueFalseOptions);
pair = pair.map((item) => [item[0], Boolean(item[1])]);
return pair.map((item, idx) => (
<FormControlLabel
key={idx}
control={<Checkbox checked={item[1]} />}
label={item[0]}
onChange={() => changeTrueFalseOptions(item[0], item[1])}
/>
));
};
Box 안에 makeTrueFalseCheckBox를 추가한다.
const list = () => (
<Box sx={{ width: 600 }}>
<Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>
<Divider />
</Box>
);
이제 Drawer에 아래와 같이 옵션이 나타나게 된다.
Handsontable에는 현재 옵션을 props로 넘겨준다.
<CustomHansOnTable myOptions={myOptions} />
그리고 myTable을 만들 때, 해당 options에 myOptions를 덮어씌운다.
const CustomHansOnTable = ({ myOptions }) => {
...
let myTable;
const makeTable = () => {
const container = document.getElementById("hot-app");
container.innerHTML = "";
myTable = new Handsontable(container, {
...options,
...myOptions.trueFalseOptions,
});
...
};
이제 True / False 옵션을 원하는 대로 커스터마이징 할 수 있다.
Number Options 구현
위와 마찬가지로 커스터마이징 하고 싶은 Number Options을 추가한다.
const [myOptions, setMyOptions] = useState({
trueFalseOptions: { ... },
numberOptions: {
width: 1000,
height: 1000,
startCols: 5 /* data가 없는 경우 기본 설정 */,
startRows: 5 /* data가 없는 경우 기본 설정 */,
maxCols: 100 /* 주어진 값보다 큰 Column은 제거 */,
maxRows: 100 /* 주어진 값보다 큰 Row는 제거 */,
minCols: 1 /* 최소한의 Column */,
minRows: 1 /* 최소한의 Row */,
minSpareRows: 0 /* 빈 열 자동 추가 */,
minSpareCols: 0 /* 빈 행 자동 추가 */,
fixedColumnsLeft: 0,
fixedRowsTop: 0,
fixedRowsBottom: 0,
rowHeaderWidth: 55 /* 행 헤더 너비 */,
},
});
Number는 값을 입력하기 때문에 Mui Input을 사용하였다.
그리고 숫자 외의 값은 입력되지 않도록 처리한다.
const changeNumberOptions = (option, value) => {
let temp = { ...myOptions };
if (isNaN(Number(value))) return;
temp.numberOptions[option] = Number(value);
setMyOptions(temp);
};
const makeNumberInput = () => {
let pair = Object.entries(myOptions.numberOptions);
pair = pair.map((item) => [item[0], Number(item[1])]);
return pair.map((item, idx) => (
<FormControl key={idx} sx={{ m: 2 }} variant="standard">
<InputLabel htmlFor="component-error">{item[0]}</InputLabel>
<Input
value={item[1]}
onChange={(e) => changeNumberOptions(item[0], e.target.value)}
/>
</FormControl>
));
};
list에 makeNumberInput을 추가한다.
const list = () => (
<Box sx={{ width: 600 }}>
<Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>
<Divider />
<Box sx={{ m: 2, flexDirection: "row" }}>
<FormHelperText sx={{ color: "blue" }}>
0 이상 숫자를 입력하세요.
</FormHelperText>
{makeNumberInput()}
</Box>
</Box>
);
테이블을 생성하는 위치에 numberOptions도 덮어씌우도록 한다.
let myTable;
const makeTable = () => {
const container = document.getElementById("hot-app");
container.innerHTML = "";
myTable = new Handsontable(container, {
...options,
...myOptions.trueFalseOptions,
...myOptions.numberOptions,
});
...
};
원하는 대로 커스터마이징 되는지 확인해 보자.
전체 코드는 다음과 같다.
import React, { useState, useEffect } from "react";
import CustomHansOnTable from "./CustomHansOnTable";
import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import Input from "@mui/material/Input";
import InputLabel from "@mui/material/InputLabel";
const MyHandsonTable = () => {
const [myOptions, setMyOptions] = useState({
trueFalseOptions: {
colHeaders: true,
rowHeaders: true,
wordWrap: false /* 줄 바꿈 off */,
manualColumnResize: true,
manualRowResize: true,
manualColumnMove: true,
manualRowMove: true,
allowInsertColumn: true,
allowInsertRow: true,
allowRemoveColumn: true,
allowRemoveRow: true,
autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
outsideClickDeselects: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
readOnly: true /* true : 모든 셀을 readOnly로 설정*/,
enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
copyable: true /* 복사 가능 여부 */,
copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
undo: true /* false : ctrl + z 비활성화 */,
trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
comments: true /* 주석, 메모 기능 context menu에 추가 */,
manualColumnFreeze: true /* freezeColumn context menu에 추가 */,
observeChanges: true,
},
numberOptions: {
width: 1000,
height: 1000,
startCols: 5 /* data가 없는 경우 기본 설정 */,
startRows: 5 /* data가 없는 경우 기본 설정 */,
maxCols: 100 /* 주어진 값보다 큰 Column은 제거 */,
maxRows: 100 /* 주어진 값보다 큰 Row는 제거 */,
minCols: 1 /* 최소한의 Column */,
minRows: 1 /* 최소한의 Row */,
minSpareRows: 0 /* 빈 열 자동 추가 */,
minSpareCols: 0 /* 빈 행 자동 추가 */,
fixedColumnsLeft: 0,
fixedRowsTop: 0,
fixedRowsBottom: 0,
rowHeaderWidth: 55 /* 행 헤더 너비 */,
},
});
const [state, setState] = useState({ right: false });
const toggleDrawer = (anchor, open) => (event) => {
if (
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const changeTrueFalseOptions = (option, value) => {
let temp = { ...myOptions };
temp.trueFalseOptions[option] = !value;
setMyOptions(temp);
};
const makeTrueFalseCheckBox = () => {
let pair = Object.entries(myOptions.trueFalseOptions);
pair = pair.map((item) => [item[0], Boolean(item[1])]);
return pair.map((item, idx) => (
<FormControlLabel
key={idx}
control={<Checkbox checked={item[1]} />}
label={item[0]}
onChange={() => changeTrueFalseOptions(item[0], item[1])}
/>
));
};
const changeNumberOptions = (option, value) => {
let temp = { ...myOptions };
if (isNaN(Number(value))) return;
temp.numberOptions[option] = Number(value);
setMyOptions(temp);
};
const makeNumberInput = () => {
let pair = Object.entries(myOptions.numberOptions);
pair = pair.map((item) => [item[0], Number(item[1])]);
return pair.map((item, idx) => (
<FormControl key={idx} sx={{ m: 2 }} variant="standard">
<InputLabel htmlFor="component-error">{item[0]}</InputLabel>
<Input
value={item[1]}
onChange={(e) => changeNumberOptions(item[0], e.target.value)}
/>
</FormControl>
));
};
const list = () => (
<Box sx={{ width: 600 }}>
<Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>
<Divider />
<Box sx={{ m: 2, flexDirection: "row" }}>
<FormHelperText sx={{ color: "blue" }}>
0 이상 숫자를 입력하세요.
</FormHelperText>
{makeNumberInput()}
</Box>
</Box>
);
return (
<div>
<div>
{["right"].map((anchor) => (
<React.Fragment key={anchor}>
<Button
sx={{ m: 2 }}
variant="contained"
color="secondary"
onClick={toggleDrawer(anchor, true)}
>
Options Setting
</Button>
<Drawer
anchor={anchor}
open={state[anchor]}
onClose={toggleDrawer(anchor, false)}
>
{list()}
</Drawer>
</React.Fragment>
))}
<CustomHansOnTable myOptions={myOptions} />
</div>
</div>
);
};
export default MyHandsonTable;
로컬 스토리지에 옵션 저장하기
마지막으로 커스터마이징한 옵션이 로컬에 남도록 해보자.
로컬 스토리지의 key로 MY_OPTIONS를 정의한다.
그리고 useState에 있던 기본값을 myDefaultOptions로 정의한다.
const MY_OPTIONS = "MY_OPTIONS";
const myDefaultOptions = {
trueFalseOptions: { ... },
numberOptions: { ... },
}
const MyHandsonTable = () => {
const [myOptions, setMyOptions] = useState(myDefaultOptions);
로컬 스토리지의 초기화는 useEffect를 이용한다.
const initLocalStorage = () => {
let localOptions = localStorage.getItem(MY_OPTIONS);
if (localOptions === null) return;
setMyOptions(JSON.parse(localOptions));
};
useEffect(() => {
initLocalStorage();
}, []);
그리고 setMyOptions에 설정한 옵션을 localStorage에도 저장하도록 코드를 추가한다.
const changeTrueFalseOptions = (option, value) => {
console.log(myOptions);
let temp = { ...myOptions };
temp.trueFalseOptions[option] = !value;
setMyOptions(temp);
localStorage.setItem(MY_OPTIONS, JSON.stringify(temp)); // 추가
};
const changeNumberOptions = (option, value) => {
let temp = { ...myOptions };
if (isNaN(Number(value))) return;
temp.numberOptions[option] = Number(value);
setMyOptions(temp);
localStorage.setItem(MY_OPTIONS, JSON.stringify(temp)); // 추가
};
이제 새로고침을 해도 설정이 저장되는 것을 확인할 수 있다.
전체 코드는 다음과 같다.
import React, { useState, useEffect } from "react";
import CustomHansOnTable from "./CustomHansOnTable";
import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import Input from "@mui/material/Input";
import InputLabel from "@mui/material/InputLabel";
const MY_OPTIONS = "MY_OPTIONS";
const myDefaultOptions = {
trueFalseOptions: {
colHeaders: true,
rowHeaders: true,
wordWrap: false /* 줄 바꿈 off */,
manualColumnResize: true,
manualRowResize: true,
manualColumnMove: true,
manualRowMove: true,
allowInsertColumn: true,
allowInsertRow: true,
allowRemoveColumn: true,
allowRemoveRow: true,
autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
outsideClickDeselects: true /* 셀 외부 클릭 시, 셀 선택 해제 */,
readOnly: true /* true : 모든 셀을 readOnly로 설정*/,
enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
copyable: true /* 복사 가능 여부 */,
copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
undo: true /* false : ctrl + z 비활성화 */,
trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
comments: true /* 주석, 메모 기능 context menu에 추가 */,
manualColumnFreeze: true /* freezeColumn context menu에 추가 */,
observeChanges: true,
},
numberOptions: {
width: 1000,
height: 1000,
startCols: 5 /* data가 없는 경우 기본 설정 */,
startRows: 5 /* data가 없는 경우 기본 설정 */,
maxCols: 100 /* 주어진 값보다 큰 Column은 제거 */,
maxRows: 100 /* 주어진 값보다 큰 Row는 제거 */,
minCols: 1 /* 최소한의 Column */,
minRows: 1 /* 최소한의 Row */,
minSpareRows: 0 /* 빈 열 자동 추가 */,
minSpareCols: 0 /* 빈 행 자동 추가 */,
fixedColumnsLeft: 0,
fixedRowsTop: 0,
fixedRowsBottom: 0,
rowHeaderWidth: 55 /* 행 헤더 너비 */,
},
};
const MyHandsonTable = () => {
const [myOptions, setMyOptions] = useState(myDefaultOptions);
const [state, setState] = useState({ right: false });
const toggleDrawer = (anchor, open) => (event) => {
if (
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const changeTrueFalseOptions = (option, value) => {
console.log(myOptions);
let temp = { ...myOptions };
temp.trueFalseOptions[option] = !value;
setMyOptions(temp);
localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
};
const makeTrueFalseCheckBox = () => {
let pair = Object.entries(myOptions.trueFalseOptions);
pair = pair.map((item) => [item[0], Boolean(item[1])]);
return pair.map((item, idx) => (
<FormControlLabel
key={idx}
control={<Checkbox checked={item[1]} />}
label={item[0]}
onChange={() => changeTrueFalseOptions(item[0], item[1])}
/>
));
};
const changeNumberOptions = (option, value) => {
let temp = { ...myOptions };
if (isNaN(Number(value))) return;
temp.numberOptions[option] = Number(value);
setMyOptions(temp);
localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
};
const makeNumberInput = () => {
let pair = Object.entries(myOptions.numberOptions);
pair = pair.map((item) => [item[0], Number(item[1])]);
return pair.map((item, idx) => (
<FormControl key={idx} sx={{ m: 2 }} variant="standard">
<InputLabel htmlFor="component-error">{item[0]}</InputLabel>
<Input
value={item[1]}
onChange={(e) => changeNumberOptions(item[0], e.target.value)}
/>
</FormControl>
));
};
const list = () => (
<Box sx={{ width: 600 }}>
<Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>
<Divider />
<Box sx={{ m: 2, flexDirection: "row" }}>
<FormHelperText sx={{ color: "blue" }}>
0 이상 숫자를 입력하세요.
</FormHelperText>
{makeNumberInput()}
</Box>
</Box>
);
const initLocalStorage = () => {
let localOptions = localStorage.getItem(MY_OPTIONS);
if (localOptions === null) return;
setMyOptions(JSON.parse(localOptions));
};
useEffect(() => {
initLocalStorage();
}, []);
return (
<div>
<div>
{["right"].map((anchor) => (
<React.Fragment key={anchor}>
<Button
sx={{ m: 2 }}
variant="contained"
color="secondary"
onClick={toggleDrawer(anchor, true)}
>
Options Setting
</Button>
<Drawer
anchor={anchor}
open={state[anchor]}
onClose={toggleDrawer(anchor, false)}
>
{list()}
</Drawer>
</React.Fragment>
))}
<CustomHansOnTable myOptions={myOptions} />
</div>
</div>
);
};
export default MyHandsonTable;
'개발 > React' 카테고리의 다른 글
리액트 - Mui 토글 버튼으로 Handsontable 셀 스타일 편집 기능 만들기 (0) | 2023.09.30 |
---|---|
리액트 - Handsontable Column Width, Row Height 로컬 스토리지에 저장하기 (0) | 2023.09.30 |
리액트 - Handsontable Download CSV 구현 (콤마, 줄바꿈, 따옴표 처리) (0) | 2023.09.30 |
리액트 - Handsontable afterSelection으로 수식 입력줄 구현하기 (0) | 2023.09.30 |
리액트 - Handsontable Customization Options (Cell 커스터마이징) (0) | 2023.09.30 |
댓글