LoginSignup
0
0

MUIでCascaderを作ってみる

Last updated at Posted at 2024-02-11

背景

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で書き換えるのを次回のブログに書きます

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0