経緯
表題の通りですが、 React Spreadsheet でセレクトボックスを扱いたいと考えました。
そもそものところですが、「 React アプリケーション上でスプレッドシートを扱いたい、かつ、そのアプリケーションのスプレッドシートでドロップダウン式の選択項目を作りたい」という個人的需要があり、それに対して試行錯誤する中で実装できたのが React Spreadsheet + React Select の組み合わせでした。
成果物
早速成果物としてデモのコードを。
スプレッドシート本体
スプレッドシート本体部分 (セレクトボックスのコンポーネントを除く) のコードは以下。
やっていることは
- サンプルデータでスプレッドシートを作成 (アプリ起動時に
useEffect
でuseState
にデータを保存、保存されたタイミングでスプレッドシートを描画) 。 - セレクトボックスのコンポーネントへデータを渡すために
useContext
使用。
という辺り。実際には1個目の処理が AJAX で API からデータをもらう部分になるのではないでしょうか。
import { useState, useEffect, createContext } from 'react';
import { Spreadsheet, CellBase, createEmptyMatrix, Matrix } from 'react-spreadsheet';
import { SelectComponentGenius } from './SelectComponentGenius';
import { SelectComponentConway } from './SelectComponentConway';
import { SelectComponentBeryl } from './SelectComponentBeryl';
import { SelectComponentShionne } from './SelectComponentShionne';
import { SelectComponentRinwellI } from './SelectComponentRinwellI';
import { SelectComponentRinwellA } from './SelectComponentRinwellA';
import { SelectComponentRubiaI } from './SelectComponentRubiaI';
import { SelectComponentRubiaA } from './SelectComponentRubiaA';
// React Select に選択肢の値を持って行くために useContext 使用
export const Params = createContext({});
// Cell 定義
type Cell = CellBase<string | number | undefined>;
// React Select に渡すには value, label 2つのキーを持ったオブジェクトの型でなければいけないため、その定義
type SelectCell = {
value: string,
label: string,
}
// データの構造定義
type Acuirers = {
acuirers: string,
magic: {
beginner: string,
intermediate: string|SelectCell[],
advanced: string|SelectCell[],
},
};
// サンプルデータ
const acuirersData: Acuirers[] = [
{
acuirers: 'キール',
magic: {
beginner: 'アクアエッジ',
intermediate: 'スプレッド',
advanced: 'メイルシュトローム',
},
},
{
acuirers: 'ジーニアス',
magic: {
beginner: 'アクアエッジ',
intermediate: [
{
value: 'スプレッド',
label: 'スプレッド',
},
{
value: 'アクアレイザー',
label: 'アクアレイザー',
},
],
advanced: 'タイダルウェイブ',
},
},
{
acuirers: 'ウィル',
magic: {
beginner: '',
intermediate: 'スプレッド',
advanced: '',
},
},
{
acuirers: 'イリア',
magic: {
beginner: 'アクアエッジ',
intermediate: 'スプレッド',
advanced: 'タイダルウェイブ',
},
},
{
acuirers: 'コンウェイ',
magic: {
beginner: 'アイスニードル',
intermediate: 'スプレッド',
advanced: [
{
value: 'メイルシュトローム',
label: 'メイルシュトローム',
},
{
value: 'インブレイスエンド',
label: 'インブレイスエンド',
},
],
},
},
{
acuirers: 'ヒスイ',
magic: {
beginner: 'アイスニードル',
intermediate: 'スプレッド',
advanced: 'アクアゲイザー',
},
},
{
acuirers: 'ベリル',
magic: {
beginner: 'アイスニードル',
intermediate: 'スプレッド',
advanced: [
{
value: 'アクアゲイザー',
label: 'アクアゲイザー',
},
{
value: 'インブレイスエンド',
label: 'インブレイスエンド',
},
],
},
},
{
acuirers: 'パスカル',
magic: {
beginner: 'スプレッド',
intermediate: 'メイルシュトローム',
advanced: 'シアンディーム',
},
},
{
acuirers: 'シオン',
magic: {
beginner: 'スプレッド',
intermediate: [
{
value: 'フリーズランサー',
label: 'フリーズランサー',
},
{
value: 'アイストルネード',
label: 'アイストルネード',
},
],
advanced: '',
},
},
{
acuirers: 'リンウェル',
magic: {
beginner: 'スプレッド',
intermediate: [
{
value: 'フリーズランサー',
label: 'フリーズランサー',
},
{
value: 'アローレイン',
label: 'アローレイン',
},
],
advanced: [
{
value: 'タイダルウェーブ',
label: 'タイダルウェーブ',
},
{
value: 'メイルシュトローム',
label: 'メイルシュトローム',
},
],
},
},
{
acuirers: 'ルビア',
magic: {
beginner: 'アイシクル',
intermediate: [
{
value: 'スプレッド',
label: 'スプレッド',
},
{
value: 'アイストーネード',
label: 'アイストーネード',
},
],
advanced: [
{
value: 'アブソリュート',
label: 'アブソリュート',
},
{
value: 'インブレイスエンド',
label: 'インブレイスエンド',
},
],
},
},
];
export const SpreadComponents = (): JSX.Element => {
// 列ラベル
const columnLabels = ['習得者', '初級術', '中級術', '上級術'];
// 初期値として空データの定義
const EMPTY_DATA = createEmptyMatrix<Cell>(1, columnLabels.length);
// データ
const [resultAcuirers, setResultAcuirers] = useState(EMPTY_DATA);
// React Spreadsheet のセレクトボックス用の引数作成 (useContext)
const args = {
genius: acuirersData[1].magic.intermediate,
conway: acuirersData[4].magic.advanced,
beryl: acuirersData[6].magic.advanced,
shionne: acuirersData[8].magic.intermediate,
rinwell_i: acuirersData[9].magic.intermediate,
rinwell_a: acuirersData[9].magic.advanced,
rubia_i: acuirersData[10].magic.intermediate,
rubia_a: acuirersData[10].magic.advanced,
};
const getData = (): void => {
// acuirers データ取得・整形, React Spreadsheet のセル作成
let acuirers: any = [];
let acuirersMatrix: Matrix<Cell>;
if(acuirersData.length > 0) {
for (const v of acuirersData) {
let DataEditorIntermediate;
let DataEditorAdvanded;
// 共通の術名の組み合わせがなく全員バラバラだったので、雑にそれぞれ固有のセレクトボックスのコンポーネントを作成してそれを利用するようにした
switch (v.acuirers) {
case 'ジーニアス':
DataEditorIntermediate = SelectComponentGenius;
break;
case 'コンウェイ':
DataEditorAdvanded = SelectComponentConway;
break;
case 'ベリル':
DataEditorAdvanded = SelectComponentBeryl;
break;
case 'シオン':
DataEditorIntermediate = SelectComponentShionne;
break;
case 'リンウェル':
DataEditorIntermediate = SelectComponentRinwellI;
DataEditorAdvanded = SelectComponentRinwellA;
break;
case 'ルビア':
DataEditorIntermediate = SelectComponentRubiaI;
DataEditorAdvanded = SelectComponentRubiaA;
break;
default:
break;
}
acuirers.push([
// 習得者 (読み取り専用)
{
value: v.acuirers,
readOnly: true
},
// 初級術 (文字列・編集可)
{
value: v.magic.beginner,
},
// 中級術 (文字列または React Select によるセレクトボックスでの選択、編集可)
Array.isArray(v.magic.intermediate)
? {
value: '選択してください',
readOnly: false,
DataEditor: DataEditorIntermediate,
className: 'select-cell',
}
: {
value: v.magic.intermediate,
},
// 上級術 (文字列または React Select によるセレクトボックスでの選択、セレクトボックスの場合のみ編集可)
Array.isArray(v.magic.advanced)
? {
value: '選択してください',
readOnly: false,
DataEditor: DataEditorAdvanded,
className: 'select-cell',
}
: {
value: v.magic.advanced,
readOnly: true,
},
]);
}
}
// Cell の Matrix 型にしないと Spreadsheet コンポーネントの onChange に引っかけられないので変換
acuirersMatrix = acuirers;
// useState で set
setResultAcuirers(acuirersMatrix);
};
useEffect(() => {
// コンポーネントの初期化時にのみ実行 // アプリ起動時のみ実行の意図
getData();
}, []);
return (
<Params.Provider value={args}>
<Spreadsheet
data={resultAcuirers}
columnLabels={columnLabels}
onChange={setResultAcuirers}
/>
</Params.Provider>
);
};
セレクトボックスのコンポーネント
一方セレクトボックスのコンポーネントは以下のような感じ。
- セルの定義。
useContext
で受け取ったデータを基にセレクトボックスを生成。- 値変更の挙動時に
useCallback
で処理実行。 - 状態記憶に
useMemo
使用。
辺りを実装している感じでしょうか。
import { useMemo, useCallback, useContext } from 'react';
import { CellBase, DataEditorComponent } from 'react-spreadsheet';
import Select from 'react-select';
import { Params } from './SpreadComponents';
// 型定義
type Value = string | number | undefined;
type Cell = CellBase<Value> & {
value: Value;
};
export const SelectComponentGenius: DataEditorComponent<Cell> = ({
cell,
onChange,
exitEditMode,
}) => {
const { genius }: any = useContext(Params);
const handleChange = useCallback(
(selection: any) => {
onChange({ ...cell, value: selection ? selection.value : null });
},
[cell, onChange]
);
const option = useMemo(
() => cell && genius.find((option: any) => option.value === cell.value),
[cell]
);
return (
<Select
value={option}
onChange={handleChange}
options={genius}
autoFocus
defaultMenuIsOpen
onMenuClose={() => exitEditMode()}
/>
);
};
以上のようなコードで実装できました。
動作確認
画面全体。単純にスプレッドシートを描画しています。
選択式のセルでダブルクリックすると選択肢が展開されて予め決めておいた要素を選択することができます。
先程とは別のセルで同様に選択。きちんと選択できていることも分かります。
余談 (データの内容)
今回はスプレッドシートということでそれなりの個数のデータが欲しかったので、「スプレッド」シートに関するものということでテイルズシリーズの魔法(作品によって設定や名称は異なりますが)で「スプレッド」という水を地面から噴出させるものがあったので、歴代の「スプレッド」を習得するキャラの一覧にしました。
※ただ、全部の作品をプレイしたわけではないので初級・中級・上級の区分が異なっていたり、氷属性と水属性がゲーム内では異なる属性として設定されているのに一緒くたにしていたり、データに誤りがある可能性があります。
参考
React
起動時のみ実行
スプレッド的なサムシング
React Spreadsheet
セル内にセレクトボックスで選択式にしたい
Storybook を見ると使えることは分かる。ただ、どうやって実装しているかが見えない。
コンポーネントとしてはこれを真似すれば行けそう。ただ、 Spreadsheet コンポーネントの1セル内にどう埋め込むのかが分からない。また、選択肢をハードコーディングではなく変数利用したい。できれば汎用的な作りにしたい。
データを渡すときに DataEditor
キーでコンポーネントを食わせれば良さそう。
- react-spreadsheet/src/Spreadsheet.tsx at master · iddan/react-spreadsheet · GitHub
- react-spreadsheet/src/DataEditor.tsx at master · iddan/react-spreadsheet · GitHub
引数を渡すには……いや、なさそう。
Redux, reducer までは使いたくなかったので useContext
でごまかした。
- react-spreadsheet/src/stories/Spreadsheet.stories.tsx at master · iddan/react-spreadsheet · GitHub
- Usage | React Spreadsheet
- react-spreadsheet/src/matrix.ts at master · iddan/react-spreadsheet · GitHub
セルの値が変更されたらその値を取得したい、という場合は Spreadsheet
コンポーネントに onChenge
を付ける。ただし型が Matrix<CellBase>
であること、基本的に useState
を使うであろうことから、 useState
でセットする値を予め Matrix<CellBase>
にしておく必要がある。
props
型 ‘string’ を型 ‘never’ に割り当てることはできません。
型 ‘string’ を型 ‘never’ に割り当てることはできません。
のようなエラー。
useState
の初期値指定のときに配列ならば型指定(何の配列か)を指定すれば良い。
If you meant to render a collection of children, use an array instead.
If you meant to render a collection of children, use an array instead.
Warning: Cannot update a component (Connect(Form(Change Form))) while rendering a different component (Change Form).
Warning: Cannot update a component (Connect(Form(Change Form))) while rendering a different component (Change Form).
TypeScript
オブジェクトの型定義
- TypeScriptのオブジェクト型の型定義についての覚書
- TypeScriptの型定義まとめ【Reactも対応】
- TypeScript 配列内のオブジェクトのプロパティに配列を追加(分かりにくくてすみません) #JavaScript – Qiita