Next.js + MUI で簡単な管理画面テンプレートを作ってみる(その2)
前回からの続きです。
Next.jsとMaterial UIを使ってそれっぽい管理画面の見た目までできたので、今回は機能面を追加します。
サイドバーからメニューを選択したらページ遷移するようにしてみましょう。
おさらい
前回は以下のような画面を作りました。デザインはMUIのサイトにあったDrawerのサンプルコードそのままです。layoutのみ作っており、メニューをクリックしても何も起こりません。
前回の画面
ナビゲーションバーを修正する
タイトルにClipped drawer
と書かれているのでMUI 管理画面
に変更し、タイトルをクリックしたらトップページに遷移するようにします。
"use client";
import { AppBar, Toolbar, Typography, Link } from "@mui/material";
import React from "react";
const NavigationBar = () => {
return (
<AppBar
position="fixed"
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar>
<Typography variant="h6" noWrap component="div">
<Link href="/" underline="none" color="inherit">
MUI 管理画面
</Link>
</Typography>
</Toolbar>
</AppBar>
);
};
export default NavigationBar;
ついでにlayout.tsxに定義されているmetadataのtitleも変えておきましょう。
export const metadata: Metadata = {
title: "MUI 管理画面",
description: "Material-UI Admin Page Example",
};
これでナビゲーションバーのタイトルが変更され、クリックするとトップページに戻るようになりました。ついでにブラウザのタブに表示されるタイトルも変更されました。
タイトル変更
サイドバーを修正する
Page2を作る
前回Page1
というページを作りましたが、それをコピーしてPage2
を作りましょう。この2つのページをサイドバーのメニューから切り替わるようにします。
import React from "react";
const Page2 = () => {
return <h1>Page2</h1>;
};
export default Page2;
http://localhost:3000/page2
にアクセスすると以下のようになります。
ページ2
ページ遷移を実装する
ページ遷移を実装する方法はいくつかあります。現状のコードに使われている<ListItemButton>
にonClickイベントハンドラを設定し、router.push()で遷移しても良いのですが、ここは一番簡単にLinkで遷移することにします。
まず、サイドバー内には`Divider`の上下に2つのメニューがありますが、Diverから下は今回は不要なので削除してしまいます。そして上側のメニューの中で
<ListItemText primary={text} />
となっている部分を
<Link href="/page1" underline="none" color="inherit">
Page1
</Link>
に変更します。URLやメニューの表示文字がハードコーディングになっていますが、これは後で直します。全体としては以下のようなコードになります。
import {
Box,
Drawer,
Link,
List,
ListItem,
ListItemButton,
ListItemIcon,
Toolbar,
} from "@mui/material";
import InboxIcon from "@mui/icons-material/MoveToInbox";
import MailIcon from "@mui/icons-material/Mail";
import React from "react";
const drawerWidth = 240;
const SideBar = () => {
return (
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: "border-box",
},
}}
>
<Toolbar />
<Box sx={{ overflow: "auto" }}>
<List>
{["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<Link href="/page1" underline="none" color="inherit">
Page1
</Link>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
);
};
export default SideBar;
すると、以下のようにPage1が4つ表示され、どれをクリックしてもPage1に遷移するようになったはずです。
遷移先を変える
では、ハードコーディングされている遷移先を変更できるようにしていきましょう。ついでにメニューのアイコンも変更できるようにします。
sidebar.tsxの最初に以下のコードを入れ、表示するメニューの名前、リンク先、アイコンを配列で定義します。
type MenuItem = {
name: string;
url: string;
icon: React.ReactNode;
};
const menuList: MenuItem[] = [
{ name: "ページ1", url: "/page1", icon: <BeachAccessIcon /> },
{ name: "ページ2", url: "/page2", icon: <CoffeeIcon /> },
];
そして上記定義を参照して、配列分のメニューを作成します。
<List>
{menuList.map(({ name, url, icon }: MenuItem) => (
<ListItem key={name} disablePadding>
<ListItemButton>
<ListItemIcon>{icon}</ListItemIcon>
<Link href={url} underline="none" color="inherit">
{name}
</Link>
</ListItemButton>
</ListItem>
))}
</List>
全体は以下のようになります。
import {
Box,
Drawer,
Link,
List,
ListItem,
ListItemButton,
ListItemIcon,
Toolbar,
} from "@mui/material";
import BeachAccessIcon from "@mui/icons-material/BeachAccess";
import CoffeeIcon from "@mui/icons-material/Coffee";
import React from "react";
type MenuItem = {
name: string;
url: string;
icon: React.ReactNode;
};
const menuList: MenuItem[] = [
{ name: "ページ1", url: "/page1", icon: <BeachAccessIcon /> },
{ name: "ページ2", url: "/page2", icon: <CoffeeIcon /> },
];
const drawerWidth = 240;
const SideBar = () => {
return (
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: "border-box",
},
}}
>
<Toolbar />
<Box sx={{ overflow: "auto" }}>
<List>
{menuList.map(({ name, url, icon }: MenuItem) => (
<ListItem key={name} disablePadding>
<ListItemButton>
<ListItemIcon>{icon}</ListItemIcon>
<Link href={url} underline="none" color="inherit">
{name}
</Link>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
);
};
export default SideBar;
すると以下のようにページ1とページ2のメニューが表示され、クリックするとそれぞれのページへ遷移するようになりました。アイコンも変更できました。
メニュー修正後
これでmenuList[]
配列の定義を編集することでメニュー本体のコードに手を入れなくてもメニューの追加・変更ができるようになりました。
アイコンについて
メニューに表示しているアイコンはsidebar.tsxの上部で
import BeachAccessIcon from "@mui/icons-material/BeachAccess";
import CoffeeIcon from "@mui/icons-material/Coffee";
のようにインポートしています。これらのインポート文を作成する方法について説明します。以下のページでアイコンの一覧を見ることができます。
アイコンのリスト
ここで目的のアイコンを探してクリックします(かなりたくさんあるので探すのが大変です...)。すると、以下のようなダイアログが表示され、上部にインポート文が表示されるのでそれをコピーして使ってください。
メニューを選択状態にする
メニューからページ遷移ができるようになりましたが、今のままだと遷移後にメニューが非選択状態になってしまいます。メニューを選択状態に保つようにしましょう。
sidebar.tsxを編集する
メニューの選択、非選択を切り替えるには、ListItemButtonのselectedパラメータにtrueまたはfalseを渡せば良いです。
今回は以下のように、メニューのurlから選択状態を返すisSelected
というメソッドを実装することにします。
<ListItemButton selected={isSelected(url)}>
実装は以下のようになります。
const pathname = usePathname();
const isSelected = (url: string) => {
if (pathname === url || pathname.startsWith(url + "/")) {
return true;
}
return false;
};
まずusePathname()で現在表示しているページのパス名をとってきます。パス名とは、現在表示しているページがhttp://localhost:3000/page1
であれば、/page1
の部分になります。今回の実装ではパス名が/page1
でも/page1/
でも/page1/child1
でも/page1
に対応するメニューを選択する仕様になっています。
なお、usePathname()を使う場合には先頭で"use client";
を宣言しなければならないので注意してください。
sidebar.tsxの全体は以下のようになります。
"use client";
import {
Box,
Drawer,
Link,
List,
ListItem,
ListItemButton,
ListItemIcon,
Toolbar,
} from "@mui/material";
import BeachAccessIcon from "@mui/icons-material/BeachAccess";
import CoffeeIcon from "@mui/icons-material/Coffee";
import React from "react";
import { usePathname } from "next/navigation";
type MenuItem = {
name: string;
url: string;
icon: React.ReactNode;
};
const menuList: MenuItem[] = [
{ name: "ページ1", url: "/page1", icon: <BeachAccessIcon /> },
{ name: "ページ2", url: "/page2", icon: <CoffeeIcon /> },
];
const drawerWidth = 240;
const SideBar = () => {
const pathname = usePathname();
const isSelected = (url: string) => {
if (pathname === url || pathname.startsWith(url + "/")) {
return true;
}
return false;
};
return (
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: "border-box",
},
}}
>
<Toolbar />
<Box sx={{ overflow: "auto" }}>
<List>
{menuList.map(({ name, url, icon }: MenuItem) => (
<ListItem key={name} disablePadding>
<ListItemButton selected={isSelected(url)}>
<ListItemIcon>{icon}</ListItemIcon>
<Link href={url} underline="none" color="inherit">
{name}
</Link>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
);
};
export default SideBar;
動かすと以下のようになります。
ページ1を選択
ページ2を選択
無事メニューが選択状態になりました。また、メニューから遷移した場合だけでなく、ブラウザのアドレスバーに直接URLを入力して表示した場合にもちゃんとメニューが選択状態になります。
子ページで動作を確かめる
Page1の下にChild1というページを作ります。また、Page1にはChild1へのリンクを付けます。ついでに、わかりやすいようにそれぞれにパン屑リストを表示しましょう。
import { Breadcrumbs, Link, Typography } from "@mui/material";
import React from "react";
const PageOne = () => {
return (
<>
<Breadcrumbs aria-label="breadcrumb">
<Typography color="text.primary">ページ1</Typography>
</Breadcrumbs>
<h1>Page1</h1>
<p>
<Link href="/page1/child1">Child1 </Link>
</p>
</>
);
};
export default PageOne;
import { Breadcrumbs, Link, Typography } from "@mui/material";
import React from "react";
const Child1 = () => {
return (
<>
<Breadcrumbs aria-label="breadcrumb">
<Link underline="hover" color="inherit" href="/page1">
ページ1
</Link>
<Typography color="text.primary">Child1</Typography>
</Breadcrumbs>
<h1>Child1</h1>
</>
);
};
export default Child1;
表示を確かめます。
ページ1
ページ1を表示しました。メニューが選択状態になっています。child1へのリンクをクリックしてみます。
Child1
Child1ページに遷移しました。メニューはページ1が選択されたままになっています。
まとめ
MUIを使って管理画面テンプレートを簡単に作る例でした。今回のコードは以下で公開しています。
Discussion