はじめに
以下の記事では、サイドメニューの4つの項目を出力する処理が冗長だったのでコンポーネント化しました。
コンポーネント化したことで身動きがとりやすくなったので、もう1段階レベルアップさせたいと思います。
今回は、サイドメニューの項目を動的に出力するようにしてみました。
実物はこちら
リポジトリはこちら
概要
動的出力するにあたりアイコンの指定の仕方を変更したので、見た目が若干異なっています。
以下のケースを想定して作成しています。
- ロゴと4つの項目があり、今後増えたり減ったりする可能性がある
- 項目を選択すると、対応する画面が右エリアに表示される
実装
先にコード全容です。
'use client'
import {Box, Icon, Typography} from "@mui/material";
//import {usePathname} from "next/navigation";
import React, {MouseEventHandler, useState} from "react";
const Logo = () => (
<Box
p={2}
sx={{
backgroundColor: "#6573c3",
textWrap: 'nowrap',
display: 'flex',
alignItems: 'center',
}}
>
<Icon
sx={{
fontSize: '2rem',
color: 'white',
marginRight: "0.5rem",
}}
>
local_convenience_store
</Icon>
<Typography
variant="h5"
component="span"
sx={{
color: "white"
}}
>
Shop System
</Typography>
</Box>
)
const menuItems = [
{
url: '/product',
itemName: 'Product',
icon: 'home_repair_service'
},
{
url: '/work-schedule',
itemName: 'Work Schedule',
icon: 'calendar_month'
},
{
url: '/staff',
itemName: 'Staff',
icon: 'people'
},
{
url: '/system-setting',
itemName: 'System Setting',
icon: 'build'
},
]
const MenuIcon = ({iconName}: { iconName: string }) => {
const iconStyle = {
fontSize: '1.8rem',
color: "white",
marginRight: "0.5rem",
}
return (
<Icon sx={{...iconStyle}}>{iconName}</Icon>
)
}
const MenuItem = ({itemName, onClick, iconName, isHighlight}: {
itemName: string,
onClick: MouseEventHandler<HTMLLIElement>,
iconName: string,
isHighlight: boolean
}) => {
return (
<Box
component="li"
pl={5}
py={2}
sx={{
borderBottom: '1px solid white',
'&:hover': {
opacity: 0.7
},
backgroundColor: isHighlight ? '#afaffa' : 'transparent',
display: 'flex',
alignItems: 'center',
}}
onClick={onClick}
>
<MenuIcon iconName={iconName}/>
<Typography
variant="subtitle1"
component="span"
sx={{
color: "white",
}}
>
{itemName}
</Typography>
</Box>
)
}
export default function SideMenu() {
//const pathname = usePathname()
const [pathname, setPathname] = useState<string>('/');
function getPageName() {
switch (pathname) {
case '/product':
return 'Product'
case '/work-schedule':
return 'Work Schedule'
case '/staff':
return 'Staff'
case '/system-setting':
return 'System Setting'
default:
return ''
}
}
return (
<Box display="flex">
<Box
component="nav"
height="100vh"
bgcolor="primary"
style={{
backgroundColor: "#7c88cc"
}}
>
<Logo/>
<ul>
{
menuItems.map((menuItem, index) => (
<MenuItem
key={index}
itemName={menuItem.itemName}
onClick={() => setPathname(menuItem.url)}
iconName={menuItem.icon}
isHighlight={pathname === menuItem.url}
/>
))
}
</ul>
</Box>
<Box
component="main"
sx={{
backgroundColor: "#fffafa"
}}
width="100%"
m={2}
>
<Typography variant="h3" sx={{color: "#6573c3"}}>
{
getPageName()
}
</Typography>
</Box>
</Box>
)
}
各部分の解説
動的出力のため、サイドメニューの項目をオブジェクトで定義する
サイドメニューの項目を定義するオブジェクトを作り、これを使って動的に出力します。
const menuItems = [
{
url: '/product',
itemName: 'Product',
icon: 'home_repair_service'
},
{
url: '/work-schedule',
itemName: 'Work Schedule',
icon: 'calendar_month'
},
{
url: '/staff',
itemName: 'Staff',
icon: 'people'
},
{
url: '/system-setting',
itemName: 'System Setting',
icon: 'build'
},
]
url
itemName
icon
の3つのプロパティがあることを確認してください。
icon
は名前のとおり、どのアイコンを表示するかを定義しています。
前回は、コンポーネントを props で渡す形をとっていましたが、文字列で指定できるように変更しています(後述)。
MenuItemコンポーネントとMenuIconコンポーネント
サイドメニューの各項目は MenuItemコンポーネントとして抽出しています。
さらにアイコン部分はスタイル指定を共通化したかったので MenuIconコンポーネントとして抽出しています。
作りは前回と大きく変わりませんが、MenuIconコンポーネントの引数が iconName
という文字列を受け取る形になっていることを確認してください。
const MenuIcon = ({iconName}: { iconName: string }) => {
const iconStyle = {
fontSize: '1.8rem',
color: "white",
marginRight: "0.5rem",
}
return (
<Icon sx={{...iconStyle}}>{iconName}</Icon>
)
}
const MenuItem = ({itemName, onClick, iconName, isHighlight}: {
itemName: string,
onClick: MouseEventHandler<HTMLLIElement>,
iconName: string,
isHighlight: boolean
}) => {
return (
<Box
component="li"
pl={5}
py={2}
sx={{
borderBottom: '1px solid white',
'&:hover': {
opacity: 0.7
},
backgroundColor: isHighlight ? '#afaffa' : 'transparent',
display: 'flex',
alignItems: 'center',
}}
onClick={onClick}
>
<MenuIcon iconName={iconName}/>
<Typography
variant="subtitle1"
component="span"
sx={{
color: "white",
}}
>
{itemName}
</Typography>
</Box>
)
}
アイコンを名前で指定する
これまでは、使いたいアイコンのコンポーネントをインポートして使っていました。
以下のように名前でアイコンを指定する方法もあります。
マニュアルに記載されているように、Google Web Font を読み込んでおく必要があります。
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
</head>
少し横道にそれますが、Next.js の v13 以降では、head
タグへの出力は Metadata API で行うようになりました。
export const metadata: Metadata = {
title: "React Recipe",
description: "Reactアプリのレシピ集"
};
export default function RootLayout({
しかし、rel="stylesheet"
は Unsupported とのことなのでこのように直接記載しています。
サイドメニューの項目を動的に出力する
以上でサイドメニューの項目を動的に出力する準備が整いました。
あとは map
で出力すれば完成です。
<ul>
{
menuItems.map((menuItem, index) => (
<MenuItem
key={index}
itemName={menuItem.itemName}
onClick={() => setPathname(menuItem.url)}
iconName={menuItem.icon}
isHighlight={pathname === menuItem.url}
/>
))
}
</ul>
おわりに
いかがだったでしょうか?
今回はサイドメニューの項目を動的に出力してみました。
サイドメニューの項目をオブジェクトで定義することで、柔軟に設定ができるようになります。
以下は、ログインユーザーの権限によって表示を変える設定を追加した例です。
const menuItems = [
{
url: '/product',
itemName: 'Product',
icon: 'home_repair_service',
isShowAdmin: true, // Admin のときのみ表示する
isDisabledUser: true, // User のときは非活性
},
「こんなこともできるかも!」と閃いたそこのあなた。
忘れないうちにぜひ形にしてみてくださいね。
ハコラトリは、Reactを習得したい新米エンジニアを応援しています。
Reactレシピでは、これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント