共通レイアウトを作る2

概要

MUI(Material UI)を使って、共通レイアウトを作っていく。 MUIとはデザイナーやアプリ開発者向けに作られたUIコンポーネントのライブラリである。Reactを用いてUIを構築する際にかっこいいモノができる。

実装方法

インストール

まずはreactプロジェクトにMUIをインストールする。

npm install @mui/material @emotion/react @emotion/styled

参考: Installation - Material UI

Drawerコンポーネントのコピー

Drawerと呼ばれる、開閉可能なNavigationバーのようなものを共通レイアウトにする。
下記サイトをスクロールしていくと「Responsive Drawer」というものがある。そこで「show code」(TypeScriptの方)を開き、コピーしてAppLayout.tsxに張り付ける。 mui.com この状態でnpm startするとサンプルが表示される。

コードの解説

全文

import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import InboxIcon from '@mui/icons-material/MoveToInbox';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MailIcon from '@mui/icons-material/Mail';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';

const drawerWidth = 240;

interface Props {
  /**
   * Injected by the documentation to work in an iframe.
   * Remove this when copying and pasting into your project.
   */
  window?: () => Window;
}

export default function ResponsiveDrawer(props: Props) {
  const { window } = props;
  const [mobileOpen, setMobileOpen] = React.useState(false);
  const [isClosing, setIsClosing] = React.useState(false);

  const handleDrawerClose = () => {
    setIsClosing(true);
    setMobileOpen(false);
  };

  const handleDrawerTransitionEnd = () => {
    setIsClosing(false);
  };

  const handleDrawerToggle = () => {
    if (!isClosing) {
      setMobileOpen(!mobileOpen);
    }
  };

  const drawer = (
    <div>
      <Toolbar />
      <Divider />
      <List>
        {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
          <ListItem key={text} disablePadding>
            <ListItemButton>
              <ListItemIcon>
                {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
              </ListItemIcon>
              <ListItemText primary={text} />
            </ListItemButton>
          </ListItem>
        ))}
      </List>
      <Divider />
      <List>
        {['All mail', 'Trash', 'Spam'].map((text, index) => (
          <ListItem key={text} disablePadding>
            <ListItemButton>
              <ListItemIcon>
                {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
              </ListItemIcon>
              <ListItemText primary={text} />
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </div>
  );

  // Remove this const when copying and pasting into your project.
  const container = window !== undefined ? () => window().document.body : undefined;

  return (
    <Box sx={{ display: 'flex' }}>
      <CssBaseline />
      <AppBar
        position="fixed"
        sx={{
          width: { sm: `calc(100% - ${drawerWidth}px)` },
          ml: { sm: `${drawerWidth}px` },
        }}
      >
        <Toolbar>
          <IconButton
            color="inherit"
            aria-label="open drawer"
            edge="start"
            onClick={handleDrawerToggle}
            sx={{ mr: 2, display: { sm: 'none' } }}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" noWrap component="div">
            Responsive drawer
          </Typography>
        </Toolbar>
      </AppBar>
      <Box
        component="nav"
        sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
        aria-label="mailbox folders"
      >
        {/* The implementation can be swapped with js to avoid SEO duplication of links. */}
        <Drawer
          container={container}
          variant="temporary"
          open={mobileOpen}
          onTransitionEnd={handleDrawerTransitionEnd}
          onClose={handleDrawerClose}
          ModalProps={{
            keepMounted: true, // Better open performance on mobile.
          }}
          sx={{
            display: { xs: 'block', sm: 'none' },
            '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
          }}
        >
          {drawer}
        </Drawer>
        <Drawer
          variant="permanent"
          sx={{
            display: { xs: 'none', sm: 'block' },
            '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
          }}
          open
        >
          {drawer}
        </Drawer>
      </Box>
      <Box
        component="main"
        sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
      >
        <Toolbar />
        <Typography sx={{ marginBottom: 2 }}>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
          tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
          enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
          imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
          Convallis convallis tellus id interdum velit laoreet id donec ultrices.
          Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
          adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
          nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
          leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
          feugiat vivamus at augue. At augue eget arcu dictum varius duis at
          consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
          sapien faucibus et molestie ac.
        </Typography>
        <Typography sx={{ marginBottom: 2 }}>
          Consequat mauris nunc congue nisi vitae suscipit. Fringilla est ullamcorper
          eget nulla facilisi etiam dignissim diam. Pulvinar elementum integer enim
          neque volutpat ac tincidunt. Ornare suspendisse sed nisi lacus sed viverra
          tellus. Purus sit amet volutpat consequat mauris. Elementum eu facilisis
          sed odio morbi. Euismod lacinia at quis risus sed vulputate odio. Morbi
          tincidunt ornare massa eget egestas purus viverra accumsan in. In hendrerit
          gravida rutrum quisque non tellus orci ac. Pellentesque nec nam aliquam sem
          et tortor. Habitant morbi tristique senectus et. Adipiscing elit duis
          tristique sollicitudin nibh sit. Ornare aenean euismod elementum nisi quis
          eleifend. Commodo viverra maecenas accumsan lacus vel facilisis. Nulla
          posuere sollicitudin aliquam ultrices sagittis orci a.
        </Typography>
      </Box>
    </Box>
  );
}

要らない部分の削除

(1) interface Props {
  /**
   * Injected by the documentation to work in an iframe.
   * Remove this when copying and pasting into your project.
   */
  window?: () => Window;
}

(2) props: Props   ※export default function ResponsiveDrawer()のところ

(3) const { window } = props;

(4) // Remove this const when copying and pasting into your project.
  const container = window !== undefined ? () => window().document.body : undefined;

(5) container={container}  ※{/* The implementation can be swapped with js to avoid SEO duplication of links. */}直下のところ

構造

全文を上から虱潰しに読んでいくと辛いので、構造から把握していく。

import * as React from 'react';
    インポート文の塊

export default function ResponsiveDrawer(props: Props) {
    モバイル画面が開いてるかどうかのstate
    ドロワーが閉じる動作中かのstate
    
    const handleDrawerClose = () => {
        ドロワーが閉じる処理
    };
    const handleDrawerTransitionEnd = () => {
        ドロワーの閉じるアニメーションが終了したときの処理
    }
    const handleDrawerToggle = () => {
        ドロワーを開閉するためのトグル処理。
    }
    const drawer = メニュー項目の定義

    return (
        <Box>
            <AppBar>ヘッダー</AppBar>
            <Box>サイドバー</Box>
            <Box>メインコンテンツ</Box>
        </Box>
    );
}

こういう構造になっている。

drawerの解説

  const drawer = (
    <div>
      <Toolbar />
      <Divider />
      <List>
        {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
          <ListItem key={text} disablePadding>
            <ListItemButton>
              <ListItemIcon>
                {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
              </ListItemIcon>
              <ListItemText primary={text} />
            </ListItemButton>
          </ListItem>
        ))}
      </List>
      <Divider />
      <List>
        {['All mail', 'Trash', 'Spam'].map((text, index) => (
          <ListItem key={text} disablePadding>
            <ListItemButton>
              <ListItemIcon>
                {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
              </ListItemIcon>
              <ListItemText primary={text} />
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </div>
  );
  • <Toolbar />はMUIのコンポーネントである。ここでは上部にスペースを設けるために使用される。通常、タイトルやメニューアイコンを置く場所になる。MUI の `<Toolbar/>` について

  • <Divider /> は線を引いて要素同士を区切るためのMUIコンポーネントである。

  • <List>...</ List> はリスト表示を簡単に作成できるMUIコンポーネントである。アイテムのリストを整理して表示するために使用され、特にモバイルアプリやウェブアプリでよく見かけるリストのスタイルを作成するのに便利。
    List→ListItem→ListItemTextの順で入れ子にして使う。```内にリストアイテムを表示する。

  • {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
    map関数により配列の要素を分解し、各項目に対してListItemを作っている。textは配列の各要素。indexは配列の要素番号。

  • <ListItem key={text} disablePadding>
    ListItemは、個々のリストアイテムを表すMUIコンポーネントである。key={text}により、各アイテムを一意に識別できるようにしている。 disablePaddingのところは、disablePadding={true}の省略記法。これを書くことでリストアイテム間のデフォルトのパディングを無効にしている。

  • <ListItemButton> ... </ListItemButton>
    これはリストアイテムをボタンのようにクリック可能にするMUIコンポーネントである。これにより、アイテムをクリックするとアクションが実行されるようになる。

  • <ListItemIcon> ... </ListItemIcon>
    これはリストアイテムの左側にアイコンを配置するためのコンポーネントである。```を使用すると、アイコンがリストアイテム内で適切にスタイルされ、アイコン部分に自動的に余白やサイズ調整が適用される。

  • {index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
    単純にリストのアイコンをを交互に並べるための記述。

  • <ListItemText primary={text} />
    リストアイテムのテキスト部分を表示するためのコンポーネントである。