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-sidebaridを指定し、buttonのontap:{id}.toggleをつければ、そのボタンで開閉ができるようになる。このtoggleの部分は他にも可能で

actiondesc
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というものを使う。今回の場合はFabPropsFabと一緒に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-sidebarAmpFabをあわせて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.jsampsidebarを直接入れる必要がある、とされている。しかし、この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;

参考

この記事に関するIssueをGithubで作成する

Read Next