게으른개발너D

[React] Handsontable 2 - headers and columns for readonly 본문

프로젝트/Side Project

[React] Handsontable 2 - headers and columns for readonly

lazyhysong 2023. 11. 5. 18:05

 

2023.11.04 - [프로젝트/Side Project] - [React] Handsontable 1 - 설치 및 데이터 추가

 

[React] Handsontable을 이용한 스프레드시트 구현

Handsontable을 이용하여 간단한 기능을 구현하려다 의외로 개고생을 하여 기록한다. https://handsontable.com/docs/react-data-grid/ React Data Grid - Documentation | Handsontable Handsontable documentation What is Handsontable? Hand

lazyhysong.tistory.com

 

이전 글에서 기본적으로 구현한 스프레드시트이다.

이제부터 시작이다. ㅠㅠ

무언가를 구현할 때마다 고민거리가 생겼는데 그것들에 대한 고찰과 결과를 기록하려고 한다.

 

 

1. Header Props

1.1 rowHeader, colHeader

HotTable 에는 rowHeader와 colHeader라는 props가 있다.

export const ExampleComponent = () => {
  return (
      <HotTable
        data={[
          ['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
          ['2019', 10, 11, 12, 13],
          ['2020', 20, 11, 14, 13],
          ['2021', 30, 15, 12, 13]
        ]}
        rowHeaders={true}
        colHeaders={true}
        height="auto"
        licenseKey="non-commercial-and-evaluation" 
      />
  );
};

 

 

예를 들어 rowHeaders가 true이면 행의 갯수에 맞춰서 가장 왼쪽열에 1, 2, 3, 4... 를 나열한다.

colHeaders가 true이면 맨 위 행에 A, B, C... 순으로 행 번호를 나열한다.

 

1, 2, 3 또는 A, B, C 가 아니라 직접 데이터를 넣을 수도 있다.

export const ExampleComponent = () => {
  return (
      <HotTable
        data={[
          [10, 11, 12, 13],
          [20, 11, 14, 13],
          [30, 15, 12, 13]
        ]}
        rowHeaders={['2019', '2020', '2021']}
        colHeaders={['Tesla', 'Volvo', 'Toyota', 'Ford']}
        height="auto"
        licenseKey="non-commercial-and-evaluation"
      />
  );
};

 

1.2 issue

여기서 고민할 점은 우리 데이터 0번째 행인 이름, 생년월일, 모임, 만남횟수를 colHeaders로 지정해야하는가이다.

colHeaders로 지정하면 해당 행이 삭제가 안되게 따로 구현해줄 필요가 없다.

 

결론은 안된다이다! ㅜㅜ

일단 0번째 행을 colHeaders로 넣어보자.

function App() {
  const handleClickSave = () => {};
  return (
    <div className="App">
      <Button callback={handleClickSave}>저장</Button>
      <HotTable
        data={DEFAULT_DATA.slice(1)}
        colHeaders={DEFAULT_DATA[0] as string[]}
        contextMenu={CONTEXT_MENU}
        {...HOTTABLE_PROPS}
      />
    </div>
  );
}

 

결과는 이렇게 깔끔하게 나오는데, 문제는 context menu를 열어서 열을 추가했을 때이다.

아무곳에나 열을 추가하면 저렇게 C 열이 생기는데, 문제는 C를 다른 text로 바꾸지 못한다는 점이다.

그래서 결론은 0번째 행은 삭제가 안되도록 따로 구현을 해줘야한다.

 

1.3 issue result 

열을 추가할 수 있으면서 열 항목의 제목이 있으면 colHeaders prop은 넣어주면 안된다.

혹시나 추가한 열의 colHeader 내용을 변경할 수 있는 방법이 있다면 가능하겠지만 아직 찾지 못했으므로 패스ㅠㅠ

 

결론은 이렇게 할 수밖에 없다는 것

function App() {
  const handleClickSave = () => {};
  return (
    <div className="App">
      <Button callback={handleClickSave}>저장</Button>
      <HotTable
        data={DEFAULT_DATA}
        contextMenu={CONTEXT_MENU}
        {...HOTTABLE_PROPS}
      />
    </div>
  );
}

 

 

 

 

 

이제 readonly 기능을 적용해야 한다.

readonly는 이름, 생년월일, 모임 column에 적용되어야하며, 그 앞이나 뒤에 column이 추가될 수도 있어야한다.

 

 

2. columns

2.1 HotColumn components

HotTable 컴포넌트 안에 원하는 column들을 넣으려면 HotColumn 컴포넌트를 쓸 수 있다.

여기서 원하는 column마다 readonly를 적용할 수 있다면 정말 편할 것이다.

실제로 HotColumn 컴포넌트를 사용하면 각 column아다 readonly 기능을 넣을 수 있다.

function App() {
  const handleClickSave = () => {};
  return (
    <div className="App">
      <Button callback={handleClickSave}>저장</Button>
      <HotTable
        data={DEFAULT_DATA}
        contextMenu={CONTEXT_MENU}
        {...HOTTABLE_PROPS}
      >
        <HotColumn data={0} readonly={true} />
        <HotColumn data={1} readonly={true} />
        <HotColumn data={2} readonly={true} />
        <HotColumn data={3} />
      </HotTable>
    </div>
  );
}

 

HotTable 의 children으로 HotColumn을 넣는다.

그리고 HotColumn의 props인 data엔 각 테이블 column의 index 번호를 넣는다.

여기서 나는 0, 1, 2 번째 column에 readonly 기능을 넣어야하니 해당 column에 readonly={true}라는 prop을 넣었다.

 

2.2 columns prop

또는 HotTable 컴포넌트에 columns라는 prop을 넣을 수 있다.

columns: [
  {
    // column options for the first (by physical index) column
    type: 'numeric',
    numericFormat: {
      pattern: '0,0.00 $'
    }
  },
  {
    // column options for the second (by physical index) column
    type: 'text',
    readOnly: true
  }
],


// or 
columns(index) {
  return {
    type: index > 0 ? 'numeric' : 'text',
    readOnly: index < 1
  }
}

 

위처럼 바로 column들을 정의할 수 있고 아래처럼 원하는 기능을 조건문으로 정한 후에 반환할 수 도 있다.

 

2.3 issue

곧바로 이슈가 생겼다.

HotColumn나 columns prop을 사용하니 Column이 고정이 되었다.

그래서 오른쪽 마우스를 클릭해서 나타나는 context menu에서 새로운 열을 추가하는 부분disabled 되어버린다..!!!! ㅠㅠ

따라서 해당 기능들을 이용하여 미리 column들의 property를 지정하는 것은 포기하기로 했다.

 

2.4 issue result

결국 최후의 수단으로 생각한 좀 비효율적인 방법을 쓰기로 했다.

boolean 배열로 원하는 열의 index에 미리 readonly를 적용할 변수를 만들어 놓고 column이 추가되거나 삭제될 때마다 해당 변수를 변경시칸다. 그때마다 해당 변수를 보고 readonly를 적용할 열에 property를 업데이트한다.

 

여기서 useRef hook을 사용하게되는데, 이건 다음 글에서 다루겠다.

 

1. readonly를 적용시킬 열을 나타내는 readonlyCols라는 state를 만든다.

그리고 그 state가 변경될때마다 테이블의 데이터 property를 업데이트시킨다.

const hotRef = useRef<HotTable>(null);
const [readOnlyCols, setReadOnlyCols] = useState([true, true, true, false]);
 
useEffect(() => {
  const hot = hotRef?.current?.hotInstance;
  if (hot) {
    hot.updateSettings({
      cells(row: number, col: number) {
        const cellProperties: Handsontable.CellMeta = {};
        if (readOnlyCols[col]) {
          cellProperties.readonly = true;
          cellProperties.allowRemoveColumn = false;
        }
        return cellProperties;
      },
    });
  }
}, [readOnlyCols]);

...

return (
  <HotTable
    ref={hotRef}
    data={DEFAULT_DATA}
    contextMenu={CONTEXT_MENU}
    {...HOTTABLE_PROPS}
  />
);

 

2. 새로운 행을 추가하였을 경우엔 해당 행의 이름, 생년월일, 모임 부분은 글자를 넣을 수 있어야한다.

새롭게 추가한 행은 저장 버튼을 누르기전엔 수정할 수 있도록 nowAddedRowsIdx라는 state를 만들어 따로 index 번호를 모아두자.

const hotRef = useRef<HotTable>(null);
const [readOnlyCols, setReadOnlyCols] = useState([true, true, true, false]);
const [nowAddedRowsIdx, setNowAddedRowsIdx] = useState<number[]>([]);
 
useEffect(() => {
  const hot = hotRef?.current?.hotInstance;
  if (hot) {
    hot.updateSettings({
      cells(row: number, col: number) {
        const cellProperties: Handsontable.CellMeta = {};
        if (!nowAddedRowsIdx.includes(row) && readOnlyCols[col]) {
          cellProperties.editor = false;
          cellProperties.allowRemoveColumn = false;
        }
        if (row === 0) {
          cellProperties.allowRemoveColumn = false;
        }
        return cellProperties;
      },
    });
  }
}, [readOnlyCols, nowAddedRowsIdx]);

...

return (
  <HotTable
    ref={hotRef}
    data={DEFAULT_DATA}
    contextMenu={CONTEXT_MENU}
    {...HOTTABLE_PROPS}
  />
);
Comments