Multi Vitamin & Mineral

Multi Vitamin & Mineral です。プログラムに関することを書いております。

React with TypeScript プロジェクトの Material-UI でレイアウトを作成する

前回は Material-UI のデザインの変更を行いました。今回はその続きとして Theme をカスタマイズする元となるレイアウト作成を行います。ソースコードは前回の記事を踏まえていますのでその点ご了承ください。(といって、踏まえないでも読めてしまうように工夫はしておきます。)

それぞれの章は以下の流れで書いています。

  1. 全体のソースの掲載
  2. 部分部分の説明

「全体のソース」は長くなりがちですが、さっと流して読み進めていただければと思います。

シリーズ一覧

  1. React + TypeScript の環境構築
  2. Material-UI の導入
  3. Material-UI のデザインをカスタマイズ
  4. Material-UI のレイアウトを作成

初期状態

前回やったこと、今回やること

前回は makeStyles を利用して、独自 CSS を追加しました。また、勝手にデフォルトの Theme が使わることになるにも触れました。

multimineral-tech.com

今回は、管理画面(ダッシュボード)のレイアウトを構築します。 Theme のカスタマイズはこの次に。(カスタマイズする元画面を作るのを先にしました。)

変更前のソース

前回まで作った DashboardTemplate.tsx から説明用の余計なモノを削り、シンプルな形に変更しておきました。

// /src/templates/DashboardTemplate.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import { makeStyles, Theme } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Container from '@material-ui/core/Container';

const useStyles = makeStyles((theme: Theme) => ({
  link: {
    margin: theme.spacing(1, 1.5),
  },
}));

export interface DashboardTemplateProps {
  children: React.ReactNode;
  title: string;
}

const DashboardTemplate: React.FC<DashboardTemplateProps> = ({
  children,
  title,
}) => {
  const classes = useStyles();
  return (
    <div>
      <AppBar>
        <Toolbar>
          <nav>
            <Link to="/" className={classes.link}>
              Top
            </Link>
            <Link to="/about" className={classes.link}>
              About
            </Link>
          </nav>
        </Toolbar>
      </AppBar>
      <Container>
        <h1>{title}</h1>
        <div>{children}</div>
      </Container>
    </div>
  );
};

export default DashboardTemplate;

見た目は以下です。 <Container> 内の <h1> が、 <AppBar> の下に潜って見えなくなっちゃってますが、こちらは追々修正します。

変更前
変更前

サイドメニューの追加

左サイドにメニューを用意し、 Top と About のリンクをこちらに移動させます。

使うのは <Drawer> 。これが左サイドメニューになります。(実際には上下左右どこにでも配置可能です。)

そして、その中に縦にリンクを配置するのに <List> とその仲間( <ListItem> , <ListItemIcon> , <ListItemText> )を使います。

この時点で <Link> タグを削っています。一時的にコメントアウトしています。 Top と About の切り替えはまた後で追加します。まずはデザイン面をやっつけます。

// /src/templates/DashboardTemplate.tsx
import React from 'react';
// import { Link } from 'react-router-dom';
// import { makeStyles, Theme } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Container from '@material-ui/core/Container';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import HomeIcon from '@material-ui/icons/Home';
import InfoIcon from '@material-ui/icons/Info';

// const useStyles = makeStyles((theme: Theme) => ({
//   link: {
//     margin: theme.spacing(1, 1.5),
//   },
// }));

export interface DashboardTemplateProps {
  children: React.ReactNode;
  title: string;
}

const DashboardTemplate: React.FC<DashboardTemplateProps> = ({
  children,
  title,
}) => {
  // const classes = useStyles();
  return (
    <div>
      <AppBar>
        <Toolbar>
          {/* ココにあった <nav> を削除 */}
        </Toolbar>
      </AppBar>
      {/* ココから追加 */}
      <Drawer variant="permanent">
        <List>
          <ListItem>
            <ListItemIcon>
              <HomeIcon />
            </ListItemIcon>
            <ListItemText primary="Home" />
          </ListItem>
          <ListItem>
            <ListItemIcon>
              <InfoIcon />
            </ListItemIcon>
            <ListItemText primary="About" secondary="hogehoge" />
          </ListItem>
        </List>
      </Drawer>
      {/* ココまで追加 */}
      <Container>
        <h1>{title}</h1>
        <div>{children}</div>
      </Container>
    </div>
  );
};

export default DashboardTemplate;

以下のような表示になりました。

コンポーネントの追加
コンポーネントの追加

Drawer の追加

上下左右に配置できる領域です。本来は、引き出しのようにスライドして表示・非表示が切り替わります。(下記リンク先の「Temporary drawer」の項の「LEFT RIGHT TOP BOTTOM」の4つのボタンを押すとどんなものか分かると思います。)

material-ui.com

表示しっ放しにすることも可能です。すると単なるサイドバーになります。

      <Drawer variant="permanent">
        {/* 中略 */}
      </Drawer>

<Drawer> には variant という属性があります。属性値は以下のようになってます。(公式の説明は私は一読では分からんかったので、私に分かるように書いてみた。。。)

  • permanent : ずっと表示されたまま。
  • persistent : 表示・非表示の切り替えができる。表示と非表示の機能(ボタン等)はそれぞれ自作する必要がある。
  • temporary : 表示・非表示の切り替えができる。基本は非表示なのが特徴。表示機能(ボタン等)は自作が必要だが、 <Drawer> の領域外をクリックすると閉じるので、閉じる機能は作らなくてもOK。

persistenttemporary の違いが分かりにくいんですよね。 persistent は一度表示したら表示しっぱなし(永続的)、 temporary は別のところをクリックすると引っ込む(一時的)ということです。

今回は permanent なので、非表示になることがありません。

material-ui.com

List の追加

縦方向に並べる索引が Lists です。

material-ui.com

        <List>
          <ListItem>
            <ListItemIcon>
              <HomeIcon />
            </ListItemIcon>
            <ListItemText primary="Home" />
          </ListItem>
          <ListItem>
            <ListItemIcon>
              <InfoIcon />
            </ListItemIcon>
            <ListItemText primary="About" secondary="hogehoge" />
          </ListItem>
        </List>

典型的な構造は以下のようになります。(モチロン他のパターンもある。)

  • <List>
    • <ListItem>
      • <ListItemIcon>
      • <ListItemText>
    • <ListItem>
      • ...
    • <ListItem>
      • ...
    • ...

HTML でいうと、 <List><ul><ListItem><li> にあたります。実際にそう変換されてブラウザに出力されます。

その <li> にあたる <ListItem> の中には、 <ListItemIcon><ListItemText> といった専用の部品を入れる前提で考えると良さそうです。先のリンク先には他の部品も使った例がありますので、好みで利用すると良いです。

ListItemText

<ListItemText> の属性に primary="Home" とあります。 <li> のメインテキストを指します。 secondary<li> のサブテキストになります。2種類の文字を設定できる仕掛けになってます。

例えば、 <ListItemText primary="About" secondary="hogehoge" /> とすると以下のような見た目になります。(あくまで例です。今回は secondary は使いません。)

ListItemText の設定
ListItemText の設定

ちなみに、私は最初に <ListItemText primary="hoge"> のように primary だけ使われているサンプルコードを見て意味が理解できませんでした。メインとサブのテキストが設定できる仕組みになってて、そのメインだけ設定しているという意味だったんですね。詳しくは以下にあります。

material-ui.com

ListItemIcon

Material-UI には Material Icons というものがあります。

material-ui.com

任意で好きなアイコンが使えます。使いたいアイコンをクリックすると、使うべきコードも表示されます。

Material Icons の選択
Material Icons の選択

<ListItemIcon> はコンテントとして Material Icons が使われる想定になっています。 <ListItemIcon><HomeIcon /></ListItemIcon> のように書くだけなので、これは直感的で分かりやすいですよね。

Drawer の開閉機能の設置

続いて <Drawer> の開閉機能を設置します。

開く機能を持ったボタン(いわゆるハンバーガーメニューというヤツ)を <Toolbar> に付けるようにします。 <IconButton> というボタンです。

閉じる機能は <Drawer> 自身のモノを使います。 variant="temporary" にして領域外をクリックしたら閉じるというヤツです。

開閉の状態は React Hooks で管理します。 drawerOpen という変数( state )を用意しています。

import React from 'react';
// import { Link } from 'react-router-dom';
// import { makeStyles, Theme } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Container from '@material-ui/core/Container';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import HomeIcon from '@material-ui/icons/Home';
import InfoIcon from '@material-ui/icons/Info';
import MenuIcon from '@material-ui/icons/Menu';
import IconButton from '@material-ui/core/IconButton'; // 追加

// const useStyles = makeStyles((theme: Theme) => ({
//   link: {
//     margin: theme.spacing(1, 1.5),
//   },
// }));

export interface DashboardTemplateProps {
  children: React.ReactNode;
  title: string;
}

const DashboardTemplate: React.FC<DashboardTemplateProps> = ({
  children,
  title,
}) => {
  // const classes = useStyles();
  // 追加: Drawer の開閉状態(フックを利用)
  const [drawerOpen, setDrawerOpen] = React.useState(false);
  // 追加: Drawer の開閉
  const handleDrawerToggle = () => {
    setDrawerOpen(!drawerOpen); // Drawer の開閉状態を反転
  };
  return (
    <div>
      <AppBar>
        <Toolbar>
          {/* 追加 */}
          <IconButton color="inherit" onClick={handleDrawerToggle}>
            <MenuIcon />
          </IconButton>
        </Toolbar>
      </AppBar>
      {/* 属性を変更 */}
      <Drawer
        variant="temporary"
        open={drawerOpen}
        onClose={handleDrawerToggle}
      >
        <List>
          <ListItem>
            <ListItemIcon>
              <HomeIcon />
            </ListItemIcon>
            <ListItemText primary="Home" />
          </ListItem>
          <ListItem>
            <ListItemIcon>
              <InfoIcon />
            </ListItemIcon>
            <ListItemText primary="About" />
          </ListItem>
        </List>
      </Drawer>
      <Container>
        <h1>{title}</h1>
        <div>{children}</div>
      </Container>
    </div>
  );
};

export default DashboardTemplate;

以下のような表示になりました。

Drawer の開閉機能の設置
Drawer の開閉機能の設置

React Hooks による開閉状態の保持

以下のコードを追加しました。 drawerOpen という変数(以降 state と呼ぶ)に開閉状態を保持します。 handleDrawerToggledrawerOpentrue/false を反転させています。

  // 追加: Drawer の開閉状態(フックを利用)
  const [drawerOpen, setDrawerOpen] = React.useState(false);
  // 追加: Drawer の開閉
  const handleDrawerToggle = () => {
    setDrawerOpen(!drawerOpen); // Drawer の開閉状態を反転
  };

以下、 React Hooks を簡単に説明しておきます。

React.useState(状態の初期値); という関数は、「状態(の変数)」と「状態をセットする関数」を配列で返却してくれます。これをフック (Hook) と呼びます。 const [状態, 状態をセットする関数] = React.useState(状態の初期値); のように書いて使います。

「状態」である drawerOpen は、最初は false(状態の初期値) になります。また false をセットしているので drawerOpen の型は boolean であると決まりました。

「状態をセットする関数」である setDrawerOpen は、 setDrawerOpen(true)setDrawerOpen(false) のようにして使います。これで drawerOpen の値を変更します。( drawerOpen = true; のような書き方はできません!) setDrawerOpen(!drawerOpen);drawerOpen の逆(! は boolean の反転)をセットしていますから true/false が反転することを意味します。

この反転を行う処理を handleDrawerToggle という関数で用意しています。

React Hooks は公式サイトでも分かりやすく説明されています。

ja.reactjs.org

Drawer の属性を変更

<Drawer> の属性を変更しました。 <Drawer> が開閉する temporary に変更し、閉じた時の state の変更処理を加えます。

      {/* 属性を変更 */}
      <Drawer
        variant="temporary"
        open={drawerOpen}
        onClose={handleDrawerToggle}
      >
  • variant : temporary にして表示・非表示の切り替えができるようにします。領域外をクリックすると閉じます。
  • open : true で開いて false で閉じます。
  • onClose : 閉じた時に呼び出される関数です。

temporary にたので領域外をクリックすると閉じます。このときに onClose で指定した関数を呼び出します。

drawerOpentrue であった場合に領域外をクリックすると、以下のような動きになります。

  1. drawerOpentrue
  2. open 属性が true(=drawerOpen) であるため <Drawer> は開いている。
  3. 領域外をクリックする。
  4. onClose 属性で定義した handleDrawerToggle 関数が呼び出される。
  5. handleDrawerToggle 関数は drawerOpen を反転させるので drawerOpenfalse になる。
  6. open 属性が false になるので <Drawer> が閉じる。

IconButton の追加

<Toolbar><IconButton> を追加しました。ボタンのアイコンは <MenuIcon /> を使います。

      <AppBar>
        <Toolbar>
          {/* 追加 */}
          <IconButton color="inherit" onClick={handleDrawerToggle}>
            <MenuIcon />
          </IconButton>
        </Toolbar>
      </AppBar>

onClick={handleDrawerToggle} を見て分かると思いますが、クリックすると handleDrawerToggle 関数が起動して drawerOpen が反転します。その結果 <Drawer> が開くという仕掛けになります。

次は Drawer をレスポンシブにしたいと思います。