React + material-ui(MUI)でDrawerコンポーネントをつかった左メニュー作成

データ管理

ReactでMarkdownアプリを作っています。今回はDrawerコンポーネントをつかったサイドメニューを作成します。

今回作成するメニューの内容は固定です。React+material-ui(MUI)を使ったアプリを作成されている方の参考になればと思います。

前回の内容は以下を参照してください。

React + material-ui(MUI)でCardコンポーネントを使ってみる

今回の手順は以下のとおりです。

サイドナビゲーションの作成

サイドナビゲーションを作成します。

作成する場所は、「components/uilibs/organisms」の配下です。

ファイル名は、「LeftNaviagtion.tsx」とします。

    
project/
 ┣ front/
 ┃ ┣ Dockerfile
 ┃ ┗ src/
 ┃   ┗ markdown-app/
 ┃     ┗ src/
 ┃       ┣ components/
 ┃       ┃ ┗ uilibs/
 ┃       ┃   ┗ organisms/
 ┃       ┃     ┗ LeftNaviagtion.tsx
・・・略・・・
    
  

ファイルの内容です。

    
import { FC } from "react";

import {
  Box,
  Divider,
  Drawer,
  List,
  ListItemButton,
  ListItemIcon,
  ListItemText,
  Toolbar,
} from "@mui/material";

import MenuBookIcon from '@mui/icons-material/MenuBook';
import CodeIcon from '@mui/icons-material/Code';
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
import { useNavigate } from "react-router-dom";

interface Props {
  setUrlLocation: React.Dispatch<React.SetStateAction<string>>;
}

export const LeftNaviagtion: FC<Props> = (props) => {

  const { setUrlLocation } = props;

  const drawerWidth: number = 240;

  const navigate = useNavigate();

  const onClickItem1 = () => { setUrlLocation('勉強'); navigate('勉強') };
  const onClickItem2 = () => { setUrlLocation('プログラミング'); navigate('プログラミング') };
  const onClickItem3 = () => { setUrlLocation('体調管理'); navigate('体調管理') };

  return (
    <Drawer variant="permanent" sx={{ width: drawerWidth, flexShrink: 0, [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' } }}>
      <Toolbar />
      <Box sx={{ width: drawerWidth, foverflow: 'auto', backgroundColor: "secondary.main", height: "100vh" }}>
        <List component="nav">
          <ListItemButton onClick={onClickItem1} >
            <ListItemIcon sx={{ color: (theme) => theme.palette.primary.contrastText }} >
              <MenuBookIcon />
            </ListItemIcon>
            <ListItemText sx={{ color: (theme) => theme.palette.primary.contrastText }} primary="勉強" />
          </ListItemButton>
          <ListItemButton onClick={onClickItem2}>
            <ListItemIcon sx={{ color: (theme) => theme.palette.primary.contrastText }}>
              <CodeIcon />
            </ListItemIcon>
            <ListItemText sx={{ color: (theme) => theme.palette.primary.contrastText }} primary="プログラミング" />
          </ListItemButton>
          <ListItemButton onClick={onClickItem3}>
            <ListItemIcon sx={{ color: (theme) => theme.palette.primary.contrastText }}>
              <HealthAndSafetyIcon />
            </ListItemIcon>
            <ListItemText sx={{ color: (theme) => theme.palette.primary.contrastText }} primary="体調管理" />
          </ListItemButton>
        </List>
        <Divider />
      </Box>
    </Drawer>
  );
};
    
  

「Drawer」コンポーネントの直下に「Toolbar」のコンポーネントを配置します。

後述するヘッダナビゲーションの「AppBar」の「position」を"relative"から"fixed"に変更します。

変更することで、ヘッダーナビゲーションが固定されます。固定されることで高さがズレます。

ズレた高さは「Toolbar」をつかって調整します。

「Box」コンポーネント以下がメニューの内容になります。

ヘッダーナビゲーションの変更

ヘッダーの下の左側にサイドナビゲーションを作成します。

最初にヘッダナビゲーションを修正します。

ヘッダナビゲーション(HeaderNavigation.tsx)の内容です。

    
import { FC } from "react";

import {
  AppBar,
  Toolbar,
  Typography
} from "@mui/material";

import { HomeNavigation } from "../molecules/HomeNavi";

export const HeaderNavigation: FC = () => {

  const StyleToolBar = { display: "flex", justifyContent: "space-between" };

  return (
    <AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
      <Toolbar sx={StyleToolBar}>
        <Typography>markdown</Typography>
        <HomeNavigation />
      </Toolbar>
    </AppBar>
  );
};
    
  

ヘッダナビゲーションの「AppBar」の「position」を"relative"から"fixed"に変更します。

「z-index」を「Drawer」より+1にします。これによりサイドバーの上にヘッダーナビゲーションが上に表示されます。

ヘッダナビゲーションをfixedに変更することで呼び出している各テンプレートも改修が必要になります。

Markdown表示用テンプレートの変更

Markdown表示用テンプレートの改修します。

ファイル名は、「ViewTemplate.tsx」とします。

ファイルの内容です。

    
import { FC, useState } from "react";

import remarkGfm from "remark-gfm";
import ReactMarkdown from "react-markdown";

import { Box, Container, ThemeProvider, Toolbar, CssBaseline } from "@mui/material";
import { MarkdownType } from "../../types/MarkdownType.type";

import { Theme } from "../uilibs/theme/Theme";

import { SubHeaderNavigation } from "../uilibs/organisms/SubHeaderNavigation";
import { HeaderNavigation } from "../uilibs/organisms/HeaderNavigation";
import { LeftNaviagtion } from "../uilibs/organisms/LeftNaviagtion";

interface Props {
  markdown: MarkdownType;
  setMarkdown: React.Dispatch<React.SetStateAction<MarkdownType>>;
  editMode: boolean;
  insertMode: boolean;
  setInsertMode: React.Dispatch<React.SetStateAction<boolean>>;
  onClickSwitchMode: (event: React.MouseEvent<HTMLButtonElement>) => void;
  setUrlLocation: React.Dispatch<React.SetStateAction<string>>;
};

export const ViewTemplate: FC<Props> = (props) => {
  const { markdown, setMarkdown, editMode, insertMode, setInsertMode, onClickSwitchMode, setUrlLocation } = props;

  return (
    <ThemeProvider theme={Theme}>
      <Box sx={{ display: "flex" }}>
        <CssBaseline />
        <HeaderNavigation />
        <LeftNaviagtion setUrlLocation={setUrlLocation} />
        <Box component="main" sx={{ flexGrow: 1 }}>
          <Toolbar />
          <SubHeaderNavigation markdown={markdown} setMarkdown={setMarkdown} editMode={editMode} insertMode={insertMode} setInsertMode={setInsertMode} onClickSwitchMode={onClickSwitchMode} />
          <Container className="markdown-view" maxWidth="xl">
            <ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown.body}</ReactMarkdown>
          </Container>
        </Box>
      </Box>
    </ThemeProvider>
  );
};
    
  

前述した「LeftNaviagtion」コンポーネントを配置します。

パラメータで「setUrlLocation」を渡します。

「main」の「Box」コンポーネントの直下に「ToolBar」を配置します。

ヘッダーナビゲーションを「Fixed」に変更したことによる高さの調整のために配置します。

Markdown編集用テンプレートの変更

Markdown表示用テンプレートの改修します。

ファイル名は、「EditTemplate.tsx」とします。

ファイルの内容です。

    
import { ThemeProvider } from "@emotion/react";
import { Container, Toolbar } from "@mui/material";
import { Box } from "@mui/system";
import { FC, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { MarkdownType } from "../../types/MarkdownType.type";
import { MarkdownEditTextFiled } from "../uilibs/atoms/MarkdownEditTextFiled";
import { HeaderNavigation } from "../uilibs/organisms/HeaderNavigation";
import { SubHeaderNavigation } from "../uilibs/organisms/SubHeaderNavigation";
import { Theme } from "../uilibs/theme/Theme";

interface Props {
  markdown: MarkdownType;
  setMarkdown: React.Dispatch<React.SetStateAction<MarkdownType>>;
  editMode: boolean;
  insertMode: boolean;
  setInsertMode: React.Dispatch<React.SetStateAction<boolean>>;
  onClickSwitchMode: (event: React.MouseEvent<HTMLButtonElement>) => void;
};

export const EditTemplate: FC<Props> = (props) => {

  const { markdown, setMarkdown, editMode, insertMode, setInsertMode, onClickSwitchMode } = props;

  const StyleTemplateBox = { display: "flex", flexDirection: "column", height: "100vh" };
  const StyleMainBox = { display: "flex", flexDirection: "row", height: "100vh", width: '100%' };
  const StyleLeftBox = { height: "100vh", width: '50%', backgroundColor: Theme.palette.primary.main };
  const StyleRightBox = { height: "100vh", width: '50%' };

  const onChangeValue = (event: React.ChangeEvent<HTMLTextAreaElement>) => { setMarkdown({ url: markdown.url, title: markdown.title, body: event.target.value }); };

  useEffect(() => {
  }, [markdown.body]);

  return (
    <ThemeProvider theme={Theme}>
      <Box sx={StyleTemplateBox}>
        <HeaderNavigation />
        <Toolbar />
        <SubHeaderNavigation markdown={markdown} setMarkdown={setMarkdown} editMode={editMode} insertMode={insertMode} setInsertMode={setInsertMode} onClickSwitchMode={onClickSwitchMode} />
        <Box sx={StyleMainBox}>
          <Box sx={StyleLeftBox}>
            <Container>
              <MarkdownEditTextFiled markdown={markdown} onChangeValue={onChangeValue} />
            </Container >
          </Box>
          <Box sx={StyleRightBox}>
            <Container className="markdown-view" >
              <ReactMarkdown remarkPlugins={[remarkGfm]}>{markdown.body}</ReactMarkdown>
            </Container >
          </Box>
        </Box>
      </Box>
    </ThemeProvider>
  );
};
    
  

「HeaderNavigation」コンポーネントの直下に「ToolBar」を配置します。

ヘッダーナビゲーションを「Fixed」に変更したことによる高さの調整のために配置します。

トップテンプレートの変更

トップテンプレートの改修します。

ファイル名は、「EditTemplate.tsx」とします。

ファイルの内容です。

    
import { FC } from "react"

import { Container, ThemeProvider, Toolbar } from "@mui/material";
import { Box } from "@mui/material";

import { Theme } from "../uilibs/theme/Theme";
import { MarkdownType } from "../../types/MarkdownType.type"
import { HeaderNavigation } from "../uilibs/organisms/HeaderNavigation";
import { MarkdownCardList } from "../uilibs/organisms/MarkdownCardList";

interface Props {
  markdowns: Array<MarkdownType>
};

export const TopTemplate: FC<Props> = (props) => {

  const { markdowns } = props;
  const theme = Theme;
  const styleContainer = { display: "flex", flexDirection: "row", paddingTop: "10px" };

  return (
    <ThemeProvider theme={theme}>
      <Box>
        <HeaderNavigation />
        <Container maxWidth="xl" sx={styleContainer}>
          <Toolbar />
          <MarkdownCardList markdowns={markdowns} />
        </Container>
      </Box>
    </ThemeProvider>
  );
};
    
  

「Container」コンポーネントの直下に「ToolBar」を配置します。

ヘッダーナビゲーションを「Fixed」に変更したことによる高さの調整のために配置します。

Markdownページの変更

Markdownページの改修します。

ファイル名は、「Markdown.tsx」とします。

ファイルの内容です。

    
import { FC, useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import axios from "axios";

import { MarkdownType } from "../../types/MarkdownType.type";

import { EditTemplate } from "../templates/EditTemplate";
import { ViewTemplate } from "../templates/ViewTemplate";

export const Markdown: FC = () => {

  const [editMode, setEditMode] = useState<boolean>(false);
  const [markdown, setMarkdown] = useState<MarkdownType>({ url: '', title: '', body: '' });
  const [insertMode, setInsertMode] = useState<boolean>(false);
  const [urlLocation, setUrlLocation] = useState<string>("");

  const location = useLocation();

  useEffect(() => {
    axios.get<MarkdownType>("http://localhost:5000/markdown" + location.pathname)
      .then(res => {
        if (!Object.keys(res.data).length) {
          setMarkdown({ url: decodeURI(location.pathname), title: '', body: '' });
          setEditMode(true);
          setInsertMode(true);
        } else {
          setMarkdown(res.data);
          setUrlLocation(res.data.url);
        }
      })
  }, [urlLocation])

  const onClickSwitchMode = () => { setEditMode(!editMode) }

  return (
    <>
      {editMode ?
        <EditTemplate markdown={markdown} setMarkdown={setMarkdown} editMode={editMode} insertMode={insertMode} setInsertMode={setInsertMode} onClickSwitchMode={onClickSwitchMode} /> :
        <ViewTemplate markdown={markdown} setMarkdown={setMarkdown} editMode={editMode} insertMode={insertMode} setInsertMode={setInsertMode} onClickSwitchMode={onClickSwitchMode} setUrlLocation={setUrlLocation} />
      }
    </>
  );
};
    
  

「urlLocation」の「useState」を追加します。

これまでは、他ページからの遷移を考えていました。

メニューを追加することで、URLは異なりますがテンプレートは同じテンプレートを使うことになります。

useEffectで初期表示のときのみレンダリングする調整を行っていたため、画面がレンダリングされませんでした。

urlLocationを追加することでurlLocationで値が変更されたときにレンダリングする対応を行いました。

画面確認

docker-compose upを使って各コンテナを起動します。

docker-compose.ymlファイルがある場所で以下のコマンドを実行します。

    
docker-compose up
    
  

起動したら以下のURLをブラウザに入力します。

http://localhost:3000

以下ブラウザの表示です。

markdownページの表示モードです。

markdownページの表示

まとめ

今回はDrawerコンポーネントをつかったサイドメニューを作成しました。

メニューは固定になっていますが、今後管理画面を作った管理についても検討します。

コメント

0 件のコメント:

コメントを投稿

コメントをお待ちしています。