背景
MUIにはCascaderがありませんがコンポーネントを組み合わせことで作れるということを知りました。
業務でMUIを使ってる為、勉強がてらコード書いてみようと思った為です。
Cascader
定義
ツリー構造化データの表示と選択に使用されます。
斜めに少しずつずらしながら重ねて表示していくことをカスケード表示といいます。
用途
- 関連するデータセットから選択する必要がある場合。県/市/区、会社レベル、物事の分類など
- 大規模なデータセットから選択する場合、多段階の分類で簡単に選択が可能
- より良いユーザーエクスペリエンスのために、1つのフロート層でカスケード項目を選択
コンポーネントの説明
TreeView
階層データをノードとしてツリー状に表現するコンポーネントです。
TreeItem
TreeViewと一緒に階層的なリストを構成する一つの要素として使われます。
sampleのソース
sampleData.tsx
export interface RenderTree {
id: string;
name: string;
children?: RenderTree[];
}
export const data: RenderTree = {
id: "0",
name: "Parent",
children: [
{
id: "1",
name: "Child - 1"
},
{
id: "3",
name: "Child - 3",
children: [
{
id: "4",
name: "Child - 4",
children: [
{
id: "7",
name: "Child - 7"
},
{
id: "8",
name: "Child - 8"
}
]
}
]
},
{
id: "5",
name: "Child - 5",
children: [
{
id: "6",
name: "Child - 6"
}
]
}
]
};
App.tsx
import React from "react";
import TreeView from "@material-ui/lab/TreeView";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import { Checkbox, FormControlLabel } from "@material-ui/core";
import { RenderTree, data } from "./sampleData";
export default function RecursiveTreeView() {
const [selected, setSelected] = React.useState<string[]>([]);
// idの値をidにもつ自身と子のidを再起的に取り出した配列を返す
function getChildById(node: RenderTree, id: string) {
let array: string[] = [];
// 自身と子のidを再起的に取り出した配列を返す
function getAllChild(nodes: RenderTree | null) {
if (nodes === null) return [];
array.push(nodes.id);
if (Array.isArray(nodes.children)) {
nodes.children.forEach(node => {
array = [...array, ...getAllChild(node)];
array = array.filter((v, i) => array.indexOf(v) === i);
});
}
return array;
}
// idから再起的にnodeを問い合わせて取得して返す
function getNodeById(nodes: RenderTree, id: string) {
if (nodes.id === id) {
return nodes;
} else if (Array.isArray(nodes.children)) {
let result = null;
nodes.children.forEach(node => {
if (!!getNodeById(node, id)) {
result = getNodeById(node, id);
}
});
return result;
}
return null;
}
return getAllChild(getNodeById(node, id));
}
//checkされたidのリストの更新をする
function getOnChange(checked: boolean, nodes: RenderTree) {
const allNode: string[] = getChildById(data, nodes.id);
let array = checked
? [...selected, ...allNode]
: selected.filter(value => !allNode.includes(value));
array = array.filter((v, i) => array.indexOf(v) === i);
setSelected(array);
}
const renderTree = (nodes: RenderTree) => (
<TreeItem
key={nodes.id}
nodeId={nodes.id}
label={
<FormControlLabel
control={
<Checkbox
checked={selected.some(item => item === nodes.id)}
onChange={event =>
getOnChange(event.currentTarget.checked, nodes)
}
onClick={e => e.stopPropagation()}
/>
}
label={<>{nodes.name}</>}
key={nodes.id}
/>
}
>
{Array.isArray(nodes.children)
? nodes.children.map(node => renderTree(node))
: null}
</TreeItem>
);
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpanded={["0", "3", "4"]}
defaultExpandIcon={<ChevronRightIcon />}
>
{renderTree(data)}
</TreeView>
);
}
幾つか修正
App.tsx
import { useState } from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { TreeItem, TreeView } from '@mui/x-tree-view';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
export const RecursiveTreeView = () => {
const [selected, setSelected] = useState<string[]>([]);
// idから再起的にnodeを問い合わせて取得して返す
const getNodeById = (nodes: RenderTree, id: string) => {
if (nodes.id === id) {
return nodes;
} else if (Array.isArray(nodes.children)) {
let result = null;
nodes.children.forEach((node) => {
if (getNodeById(node, id)) {
result = getNodeById(node, id);
}
});
return result;
}
return null;
};
// idの値をidにもつ自身と子のidを再起的に取り出した配列を返す
const getChildById = (node: RenderTree, id: string) => {
let array: string[] = [];
// 自身と子のidを再起的に取り出した配列を返す
function getAllChild(nodes: RenderTree | null) {
if (nodes === null) return [];
array.push(nodes.id);
if (Array.isArray(nodes.children)) {
const childrenIdList = nodes.children.flatMap((child) =>
getAllChild(child),
);
const uniqueChildrenIdList = [...new Set(childrenIdList)];
array.push(...uniqueChildrenIdList);
}
return array;
}
return getAllChild(getNodeById(node, id));
};
//checkされたidのリストの更新をする
const getOnChange = (checked: boolean, nodes: RenderTree) => {
const allNode: string[] = getChildById(data, nodes.id);
const newSelectedList = checked
? [...selected, ...allNode]
: selected.filter((value) => !allNode.includes(value));
const uniqueNewSelectedList = [...new Set(newSelectedList)];
setSelected(uniqueNewSelectedList);
};
const renderTree = (nodes: RenderTree) => (
<TreeItem
key={nodes.id}
nodeId={nodes.id}
label={
<FormControlLabel
control={
<Checkbox
checked={selected.includes(nodes.id)}
onChange={(event) =>
getOnChange(event.currentTarget.checked, nodes)
}
onClick={(e) => e.stopPropagation()}
/>
}
label={<>{nodes.name}</>}
key={nodes.id}
/>
}
>
{Array.isArray(nodes.children)
? nodes.children.map((node) => renderTree(node))
: null}
</TreeItem>
);
return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpanded={['0', '3', '4']}
defaultExpandIcon={<ChevronRightIcon />}
>
{renderTree(data)}
</TreeView>
);
};
変更点
package
-
@material-ui/icon
から@mui/icons-material
に書き換え
ライブラリのメンテ頻度を考慮 -
@material-ui/lab
から@mui/x-tree-view
に書き換え
@material-ui/labがdeprecatdになっていました
mui-xもまだalpha版のようなのでどちらにするべきなのでしょうか。
-
@material-ui/core
から@mui/material
に書き換え
@material-ui/core
がdeprecatdになっていました
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { TreeItem, TreeView } from '@mui/x-tree-view';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
その他、ソースを部分的にわかりやすく書き換えました。
最後に
- TreeView、TreeItemとCheckboxでCascaderを作れることがわかりました
- 業務で市区町村データをElement-UIのCascaderで作っているのでそれをMUIで書き換えるのを次回のブログに書きます