next.jsで作ってみたブログにamp-sidebarを導入する
TL;DR
モバイルページでもサイドバーはやはりほしい。そして最近のはやりはfloating buttonみたいなやつを押すとサイドバーが開く、というものである...気がする。もちろん、onClick
やらを使えばかんたんに実装できるのだが、ampに対応しているとonClick
は許されていない。
そういうときに使えるのがamp-sidebarである。しかし、Reactやnext.jsでamp-sidebarを導入している事例は少なく、material-uiやtypescriptと一緒にやっている例は見つからなかった。一応実装できたので、参考になる人がいることを祈って記事に残しておく。
amp-sidebar
普段は隠れていて、ボタンを押すと表示され、サイドバー以外の部分を押すと閉じる、という機能がデフォルトで実装されている。 とりあえず公式の例を見てみる。
<amp-sidebar id="sidebar1" layout="nodisplay" side="right">
<ul>
<li>Nav item 1</li>
<li><a href="#idTwo" on="tap:idTwo.scrollTo">Nav item 2</a></li>
<li>Nav item 3</li>
<li><a href="#idFour" on="tap:idFour.scrollTo">Nav item 4</a></li>
<li>Nav item 5</li>
<li>Nav item 6</li>
</ul>
</amp-sidebar>
<button class="hamburger" on='tap:sidebar1.toggle'></button>
基本的には、amp-sidebar
でid
を指定し、buttonのon
にtap:{id}.toggle
をつければ、そのボタンで開閉ができるようになる。このtoggle
の部分は他にも可能で
action | desc |
---|---|
open (default) | サイドバーを開く |
close | サイドバーを閉じる |
toggle | サイドバーを開閉する |
の3つが使える。基本的にtoggleでいい気がする。
なので、
<amp-sidebar id="sidebar1">{children}</amp-sidebar>
<button on="tap:sidebar1.toggle">toggle</button>
のようなjsxを書けばいいことがわかる。
しかし、buttonにon属性はないので、Typescriptを使う場合はon
を型定義する必要があることに注意が必要(ts-ignoreでもいいかもしれないが...)。
Float Button
こちらは簡単で@material-ui/core/Fabを使えばOK。ただ、このままだと場所が固定されておらず、onが定義されていないのでそのへんを定義する必要がある。
まず型定義は基本的に同じところからexportされているxxxProps
というものを使う。今回の場合はFabProps
をFab
と一緒にimportする。このbuttonはon
を必ず使う用途だと考えているのでdefaultpropsの拡張は行っていない。
AmpFab.tsx
import React from "react";
import Fab, { FabProps } from "@material-ui/core/Fab";
interface AmpOnProps {
on: string;
}
type Props = FabProps & AmpOnProps;
const AmpFab: React.FC<Props> = (props) => {
return <Fab {...props} />;
};
export default AmpFab;
場所の定義はここでやってしまってもいいが、amp-sidebar
とAmpFab
をあわせてAmpSidebar
コンポーネントを作成したかったので、そこで定義することにした。
AmpSidebar(amp-sidebar+float button)
画面が大きいときは固定したサイドバーを表示するので、固定したサイドバーが表示されなくなったときにFab
が表示されるように設定してある。右下に固定するのに必要な部分は以下のcss部分。
margin: 0;
top: "auto";
right: 20;
bottom: 20;
left: "auto";
position: "fixed";
注意が必要なのは、amp-sidebar
は<body>
の直下にないとだめなので、<div>
などで囲ってしまうと、Warningが表示される。なので、Fragmentで囲う必要がある。
AmpSidebar.tsx
import React from "react";
import AmpFab from "./AmpFab";
import { createStyles, Theme, makeStyles } from "@material-ui/core/styles";
import NavigationIcon from "@material-ui/icons/Navigation";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
fab: {
display: "block",
[theme.breakpoints.up("sm")]: {
display: "none",
},
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
position: "fixed",
},
})
);
const AmpSidebar = ({ children }) => {
const classes = useStyles();
return (
<>
<AmpFab
on="tap:ampsidebar.toggle"
variant="extended"
aria-label="amp-fab"
className={classes.fab}
>
<NavigationIcon>Navigation</NavigationIcon>
</AmpFab>
<amp-sidebar id="ampsidebar" className="ampsidebar" layout="nodisplay">
{children}
</amp-sidebar>
</>
);
};
AmpSidebar.defaultProps = {
children: <></>,
};
export default AmpSidebar;
Layoutにimportする
デフォルトの_document.js
は以下である。このサイトでは、_document.js
にampsidebar
を直接入れる必要がある、とされている。しかし、このpagesの中身が入る部分である<Main>
はfragmentで囲われたものなので、この中にamp-sidebar
を入れてもWarningは表示されない。ただし、material-uiのContainer
やもっと単純にdiv
などで囲ってしまうとWarningが表示されるので、できるだけ上の方のコンポーネントにamp-sidebar
を入れる必要がある。
_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
例えば、以下のようにする。これを標準レイアウトにすればWarningは表示されない。
Layout.tsx
import Header from "./Header";
import Footer from "./Footer";
import AmpSidebar from "./AmpSidebar"
const Layout = ({ children }) => {
return (
<>
<Header />
{children}
<AmpSidebar />
<Footer />
</>
);
};
export default Layout;