React with TypeScript プロジェクトの Material-UI でレイアウトを作成する
前回は Material-UI のデザインの変更を行いました。今回はその続きとして Theme をカスタマイズする元となるレイアウト作成を行います。ソースコードは前回の記事を踏まえていますのでその点ご了承ください。(といって、踏まえないでも読めてしまうように工夫はしておきます。)
それぞれの章は以下の流れで書いています。
- 全体のソースの掲載
- 部分部分の説明
「全体のソース」は長くなりがちですが、さっと流して読み進めていただければと思います。
シリーズ一覧
初期状態
前回やったこと、今回やること
前回は makeStyles
を利用して、独自 CSS を追加しました。また、勝手にデフォルトの Theme が使わることになるにも触れました。
今回は、管理画面(ダッシュボード)のレイアウトを構築します。 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つのボタンを押すとどんなものか分かると思います。)
表示しっ放しにすることも可能です。すると単なるサイドバーになります。
<Drawer variant="permanent"> {/* 中略 */} </Drawer>
<Drawer>
には variant
という属性があります。属性値は以下のようになってます。(公式の説明は私は一読では分からんかったので、私に分かるように書いてみた。。。)
permanent
: ずっと表示されたまま。persistent
: 表示・非表示の切り替えができる。表示と非表示の機能(ボタン等)はそれぞれ自作する必要がある。temporary
: 表示・非表示の切り替えができる。基本は非表示なのが特徴。表示機能(ボタン等)は自作が必要だが、<Drawer>
の領域外をクリックすると閉じるので、閉じる機能は作らなくてもOK。
persistent
と temporary
の違いが分かりにくいんですよね。 persistent
は一度表示したら表示しっぱなし(永続的)、 temporary
は別のところをクリックすると引っ込む(一時的)ということです。
今回は permanent
なので、非表示になることがありません。
List の追加
縦方向に並べる索引が Lists です。
<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 primary="hoge">
のように primary
だけ使われているサンプルコードを見て意味が理解できませんでした。メインとサブのテキストが設定できる仕組みになってて、そのメインだけ設定しているという意味だったんですね。詳しくは以下にあります。
ListItemIcon
Material-UI には 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;
以下のような表示になりました。
React Hooks による開閉状態の保持
以下のコードを追加しました。 drawerOpen
という変数(以降 state と呼ぶ)に開閉状態を保持します。 handleDrawerToggle
は drawerOpen
の true/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 は公式サイトでも分かりやすく説明されています。
Drawer の属性を変更
<Drawer>
の属性を変更しました。 <Drawer>
が開閉する temporary
に変更し、閉じた時の state の変更処理を加えます。
{/* 属性を変更 */} <Drawer variant="temporary" open={drawerOpen} onClose={handleDrawerToggle} >
variant
:temporary
にして表示・非表示の切り替えができるようにします。領域外をクリックすると閉じます。open
:true
で開いてfalse
で閉じます。onClose
: 閉じた時に呼び出される関数です。
temporary
にたので領域外をクリックすると閉じます。このときに onClose
で指定した関数を呼び出します。
drawerOpen
が true
であった場合に領域外をクリックすると、以下のような動きになります。
drawerOpen
はtrue
。open
属性がtrue(=drawerOpen)
であるため<Drawer>
は開いている。- 領域外をクリックする。
onClose
属性で定義したhandleDrawerToggle
関数が呼び出される。handleDrawerToggle
関数はdrawerOpen
を反転させるのでdrawerOpen
がfalse
になる。open
属性がfalse
になるので<Drawer>
が閉じる。
IconButton の追加
<Toolbar>
に <IconButton>
を追加しました。ボタンのアイコンは <MenuIcon />
を使います。
<AppBar> <Toolbar> {/* 追加 */} <IconButton color="inherit" onClick={handleDrawerToggle}> <MenuIcon /> </IconButton> </Toolbar> </AppBar>
onClick={handleDrawerToggle}
を見て分かると思いますが、クリックすると handleDrawerToggle
関数が起動して drawerOpen
が反転します。その結果 <Drawer>
が開くという仕掛けになります。
次は Drawer をレスポンシブにしたいと思います。