第20回 JavaScript モーダルサイドバーの共通化

# はじめに
Webサイトによく出てくるサイドバーを作りたいと思います。
これまで作ってきたサイドバーを共通化して、複数のHTMLで利用可能なようにします。
また、ハンバーガーアイコンはこれまでは`div`要素で作成してきましたが、今回はSVGを使用したいと思います。
SVGは自作のもので、[第2回 SVG ハンバーガーアイコン＋hover](https://qiita.com/Mount/items/7b4101afcd1c23ffb227)で作ったものを利用しようと思います。

# 今回実施する内容
[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)で作成したモーダルサイドバーと同じですが、サイドバーに表示する「ホーム」と「マイページ」をリンクをクリックすることで、ページ移動できるようにします。
サイドバーは、第19回では、HTMLの`body`要素内に直接記載していましたが、この部分をJavaScriptに置き換えて作成します。
![sidebar3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/eaf67382-65c1-a921-07bb-e6bfb96d0297.gif)

# ソースコード(Git Hub)
https://github.com/Build-Mount/JS_20_sideBar3

# 環境
OS: Windows 11 JP (64bit)
Microsoft Edge: バージョン 128.0.2739.42 (公式ビルド) (64 ビット)

# 参考
[第2回 SVG ハンバーガーアイコン＋hover](https://qiita.com/Mount/items/7b4101afcd1c23ffb227)
[JavaScript 第1回 Hello World!と環境構築](https://qiita.com/Mount/items/eb97dc6a9596c71ebbcc)
[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)

# 用語
特になし

# モーダルサイドバーの共通化の実現方法
サイドバーは、色々なページに移動するためのメニューを表示します。したがって、色々なページで同じサイドバーが必要です。
それを毎回それぞれのHTMLにソースを記載するのは色んな点で思わしくなく、どこかに１つのソースを作成してそれを参照させるほうが普通の考え方だと思います。

どうやって実現するか？なのですが、色々調べてみると
- ファイルを読みこんで実装。PHP使用
- ファイルを読み込んで実装。JavaScript使用

あたりなのかなと思いました。
で、今回は、
- JavaScriptで構築していること
- ローカルPC環境で実施

を踏まえて、**ファイルを読み込んで実装。JavaScript使用**で実装することにしました。

# FileやFileReaderはローカル環境では厳しい
最初は、FileやFileReaderでHTMLを読み込んで、それを`innerHTML`で追加しようと考えました。
しかし、FileやFileReaderを使用するには、`input`要素でファイルの設定をしないとダメっぽいんです。
セキュリティ上の制限で、ローカルファイルをHTMLにソース書いただけで選択して読み込んでいくことができないようです。
なので、これは没にして、共通化したJavaScriptを作成しておき、それを読み込むことにしました。
これなら、`script`要素でファイル名を指定して読み込めばよいだけなので、大丈夫と思いました。

# まずは共通化のHTMLを読み込めるかを確認
以下のファイルを作っていきます。

|ファイル名|内容|
|:--|:-|
|index.html|初期のホーム。|
|mypage.html|マイページ。|
|common.js|index.htmlとmypage.htmlから読み込まれるJavaScriptファイルで、単にメニューアイコンという文字列だけを含む。|

<details>
<summary>index.html</summary>

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <script src="common.js" defer></script>
</head>
<body>
    <div id="commonArea"></div>
    <div id="home"><a href="index.html">ホーム</a></div>
    <div id="mypage"><a href="mypage.html">マイページ</a></div>
</body>
</html>
```
</details>

<details>
<summary>mypage.html</summary>

```html:mypage.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mypage</title>
    <script src="common.js" defer></script>
</head>
<body>
    <div id="commonArea"></div>
    <div id="home"><a href="index.html">ホーム</a></div>
    <div id="mypage"><a href="mypage.html">マイページ</a></div>
</body>
</html>
```
</details>

<details>
<summary>common.js</summary>

```javascript:common.js
const commonArea = document.getElementById("commonArea");

window.addEventListener("DOMContentLoaded", () => {
    commonArea.innerHTML ="メニューアイコン";    
});
```
</details>

## `index.html`と`mypage.html`の説明
`index.html`と`mypage.html`はコピーして作成して修正しているだけで、ほとんど同じです。
`head`要素内の`script`要素で、`common.js`の読み込みを行うようにしています。`script`要素内の`defer`については、JavaScriptの記事の初期の[JavaScript 第1回 Hello World!と環境構築](https://qiita.com/Mount/items/eb97dc6a9596c71ebbcc)で記載しましたが、
> defer 属性の付いたスクリプトは、スクリプトが読み込まれて評価が完了するまで、DOMContentLoaded イベントの発生が抑制されます。
ということで設定しています。

`body`要素の最初では、`<div id="commonArea"></div>`を追加して、`common.js`で読み込んだHTMLを表示するエリアとして使用します。

## `common.js`の説明
ほとんど内容はありませんが、`index.html`と`mypage.html`の`commonArea`に「メニューアイコン」という文字列を表示するようにしています。
`window.addEventListener`で`DOMContentLoaded`の時に表示するようにしました。
どこでイベントを追加するのがいいのか判断ついておりませんが、思いついたもので設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/c3957ad8-d8f9-0542-c33a-3250ef83f885.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/4dfaa1fb-c6ce-5a8b-3ec2-fef17e4f893b.png)

上記のような感じで、とてもシンプルですが、`common.js`の内容が読み込まれました。

# サイドバーを共通化
以下のファイルを作っていきます。

|ファイル名|内容|
|:--|:-|
|index.html|初期のホーム。|
|mypage.html|マイページ。|
|sideBar.js|index.htmlとmypage.htmlから読み込まれるJavaScriptファイルで、メニューアイコンとサイドバーを含む。|
|sideBar.css|サイドバー用のCSS。|
|humbergerIcon.svg|ハンバーガーアイコンのSVG|

<details>
<summary>index.html</summary>

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <link rel="stylesheet" href="sideBar.css" type="text/css">
    <script src="sideBar.js" defer></script>
</head>
<body>
    <div id="menuSideBar"></div>
    <div class="title">ホーム</div>
</body>
</html>
```
</details>

<details>
<summary>mypage.html</summary>

```html:mypage.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>mypage</title>
    <link rel="stylesheet" href="sideBar.css" type="text/css">
    <script src="sideBar.js" defer></script>
</head>
<body>
    <div id="menuSideBar"></div>
    <div class="title">マイページ</div>
</body>
</html>
```
</details>

<details>
<summary>sideBar.js</summary>

```javascript:sideBar.js
let offset = -200;

window.addEventListener("DOMContentLoaded", () => {
    let strHtml = `\n`;
    strHtml += `\t<img src="humbergerIcon.svg" id="menuIcon" class="menuIcon"/>\n`;
    strHtml += `\t<div id="sideBar">\n`;
    strHtml += `\t\t<img src="humbergerIcon.svg" id="menuIcon2" class="menuIcon"/>\n`;
    strHtml += `\t\t<div id="menuList">\n`;
    strHtml += `\t\t\t<div id="home" class="menuItem">ホーム</div>\n`;
    strHtml += `\t\t\t<div id="mypage" class="menuItem">マイページ</div>\n`;
    strHtml += `\t\t</div>\n`;
    strHtml += `\t</div>\n`;
    strHtml += `\t<div id="cover" class="cover"></div>\n`;
    menuSideBar.insertAdjacentHTML("beforeend", strHtml);

    document.getElementById("menuIcon").addEventListener("click", () => {
        showSideBar();
    });

    document.getElementById("menuIcon2").addEventListener("click", () => {
        showSideBar();
    });

    document.getElementById("home").addEventListener("click", () => {
        showSideBar();
        setTimeout(() => {
            location.href = "index.html";
        }, 300);
    });

    document.getElementById("mypage").addEventListener("click", () => {
        showSideBar();
        setTimeout(() => {
            location.href = "mypage.html";
        }, 300);
    });

    document.getElementById("cover").addEventListener("click", () => {
        showSideBar();
    });

});


const showSideBar = () => {
    document.getElementById("cover").classList.toggle("coverVisible");
    offset = -offset;
    const animation = document.getElementById("sideBar").animate(
        [
            {   transform: `translateX(${offset}px)`    }
        ],
        {
            duration: 300,
            fill: "forwards"
        },
    );
};
```
</details>

<details>
<summary>sideBar.css</summary>

```css:sideBar.css
/* menu Icon用開始 */
.menuIcon {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 48px;
  height: 48px;
}

.menuIcon:hover {
  background-color: #999;
  border-radius: 50%;
}
/* menu Icon用終了*/

/* side Bar用開始 */
#sideBar {
  position: absolute;
  bottom: 0px;
  left: -200px;
  width: 200px;
  height: 100%;
  border: 0px solid gray;
  z-index: 99;
  background-color: #fff;
}

#menuList {
  position: absolute;
  top: 60px;
}

.cover {
  position: absolute;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  background-color: gray;
  visibility: hidden;
  opacity: 0.3;
  z-index: 98;
}

.coverVisible {
  visibility: visible;
}

#main {
  position: absolute;
  top: 60px;
  left: 200px;
  font-size: 2em;
}

.menuItem {
  width: 180px;
  padding-left: 10px;
  padding-right: 10px;
  padding-top: 10px;
  padding-bottom: 10px;
}

.menuItem:hover {
  background-color: #ccc;
  border-radius: 10% / 50%;  
}

.title {
  position: absolute;
  left: 100px;
}
```
</details>

## `index.html`と`mypage.html`の説明
`index.html`と`mypage.html`はコピーして作成して修正しているだけで、ほとんど同じです。「まずは共通化のHTMLを読み込めるかを確認」と記載内容もほとんど同じです。
`head`要素では、`sideBar.css`を読み込むようにしたことや、読み込むJavaScriptのファイル名を`common.js`から`sideBar.js`に変更しました。
`body`要素では、最初にサイドバーを設置する`div`要素を配置していますが、`id`を`menuSideBar`としました。
`sideBar.js`で読みこんだHTMLを表示するエリアとして使用します。

## `sideBar.js`の説明
`sideBar.js`の内容は、[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)で実施していることと同じです。

`let offset= -200;`は、サイドバーを画面の左側にオフセットしておくための設定です。

`window.addEventListener("DOMContentLoaded", () => ...});`の部分は、ウィンドウのDOMを読み込んだ時に実施する動作を記載しますが、いくつかのパートに分かれています。
`strHtml`には、ハンバーガーアイコンとサイドバーを表示するソースを記載します。ウィンドウの
内容は、[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)を参考ください。`\t`や`\n`でタブや改行を入れているため、少し見づらいかもしれません。

`menuSideBar.insertAdjacentHTML("beforeend", strHtml);`で、上記のHTMLソースを読み込んで、`div`要素にくっつけています。

`document.getElementById("menuIcon").addEventListener`と`document.getElementById("menuIcon2").addEventListener`は、サイドバーのハンバーガーアイコンをクリックした時に`showSideBar()`を実行して、サイドバーの表示と非表示を切り替えます。

`document.getElementById("home").addEventListener`と`document.getElementById("mypage").addEventListener`は、サイドバーの「ホーム」と「マイページ」をクリックした時に、`showSideBar()`を実行しつつ、`setTimeout`で`location.href`を使用してリンク先に遷移しています。`setTimeout`を使用したのは、サイドバーが左側へ移動するための時間を待つためであり、300msを設定しました。

`document.getElementById("cover").addEventListener`は、サイドバー表示時に、画面を少しグレーアウトしてカバーをかけるのですが、それをクリックした時に、`showSideBar()`を実行し、サイドバーを非表示にします。

上記の5つの`addEventListener`の`getElementById`で指定するIDは、strHtmlに記載したHTML内のIDであり、`insertAdjacentHTML`でHTMLを加えた後でなければ、IDが存在しないため、この箇所に記載をしました。

`showSideBar`の部分は[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)と同じです。300msでサイドバーの表示と非表示の切り替えます。

## `sideBar.css`の説明
[第19回 JavaScript アニメーションによるモーダルサイドバー（translateX）](https://qiita.com/Mount/items/152f6dd7631f275fdf0a)と内容はほぼ同じです。
サイドバーの見た目を少し良くしたいため、CSSで装飾をしていますが、本質ではないため、割愛します。

# サイドバーを共通化の別パターン
「ホーム」や「マイページ」をクリックした時にサイドバーを非表示に切り替えて、リンク先に遷移するのに、setTImeoutを使用していました。
この部分について、@junerさんにコメントをいただき、Promiseを使用した方法を紹介いただきましたので、それを追記します。

## thenで実装
`showSideBar()`関数はもともとこんな感じでした。
```javascript
const showSideBar = () => {
    document.getElementById("cover").classList.toggle("coverVisible");
    offset = -offset;
    const animation = document.getElementById("sideBar").animate(
        [
            {   transform: `translateX(${offset}px)`    }
        ],
        {
            duration: 300,
            fill: "forwards"
        },
    );
};
```
これを以下のように最後の部分に`return animation.finished;`を追加します。
```javascript
const showSideBar = () => {
    document.getElementById("cover").classList.toggle("coverVisible");
    offset = -offset;
    const animation = document.getElementById("sideBar").animate(
        [
            {   transform: `translateX(${offset}px)`    }
        ],
        {
            duration: 300,
            fill: "forwards"
        },
    );
    return animation.finished;
};
```
これを実施することで、animationの実行結果を応答として持つことができます。

[Animation:finished](https://developer.mozilla.org/ja/docs/Web/API/Animation/finished)によれば、

> Animation.finished は ウェブアニメーション API の読み取り専用プロパティで、アニメーションの再生が完了すると解決される Promise を返します。

ということで、アニメーションの再生完了をPromiseで応答できます。

続いて、以下の「ホーム」や「マイページ」の`addEventListener`の部分を見直します。

```javascript:ホームの場合（見直し前）
document.getElementById("home").addEventListener("click", () => {
        showSideBar();
        setTimeout(() => {
            location.href = "index.html";
        }, 300);
    });
```

```javascript:ホームの場合（見直し後）
document.getElementById("home").addEventListener("click", () => {
        showSideBar().then(() => {
            location.href = "index.html";
        });
    });
```

`showSideBar`関数にthenを記載することで、`return`のPromiseを受けた後、その後にリンク先へ遷移することができます。


## async/awaitで実装
次は、async/awaitでの実装です。
`showSideBar`関数の修正は、「thenで実装」と同じです。
「ホーム」や「マイページ」の`addEventListener`の部分を見直します。
```javascript:ホームの場合（見直し後）
    document.getElementById("home").addEventListener("click", async () => {
        await showSideBar();
        location.href = "index.html";
    });
```

「ホーム」や「マイページ」の`addEventListener`のところの無名関数をasyncを記載し、`showSideBar`関数にawaitを付与すると、`showSideBar`の`return`のPromiseを受けた後、その後にリンク先へ遷移することができます。

# おわりに
概ね期待通りのものは作れたかなと思います。
ただし、少しソースコードはもっさりしているので、もう少しすっきりさせたいなという感じがあります。
汎用性を求めるなら、もう少しソースコードはまとめたい感じはします。
例えば、サイドバーに追加するメニューは一つ一つ作成していますが、もう少しすっきりさせるならば、別にファイルを用意してメニューリストを読み込んでHTMLと`addEventListener`を追加できるようにしたほうが、汎用性が上がり、すっきりもしそうです。
ローカル環境ではファイル読み込み課題があるので、その辺はまあいいかなというのと、今回初めて作っているというのもあって、まずはお試しで始めたところもあって、どういう実装がスマートなのかも手探りな感じでした。

# おわりに2
@junerさんコメントありがとうございました。
「サイドバーを共通化の別パターン」にコメントいただいた`return animation.finished`を追加して、then、および、async/awaitで実装する内容と追記しました。
ソースコードもGit Hubに追加しました。

then、および、async/awaitの実装が、自分含め、どなたかの参考になれば幸いです。