2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-09-01

はじめに

Webサイトによく出てくるサイドバーを作りたいと思います。
これまで作ってきたサイドバーを共通化して、複数のHTMLで利用可能なようにします。
また、ハンバーガーアイコンはこれまではdiv要素で作成してきましたが、今回はSVGを使用したいと思います。
SVGは自作のもので、第2回 SVG ハンバーガーアイコン+hoverで作ったものを利用しようと思います。

今回実施する内容

第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)で作成したモーダルサイドバーと同じですが、サイドバーに表示する「ホーム」と「マイページ」をリンクをクリックすることで、ページ移動できるようにします。
サイドバーは、第19回では、HTMLのbody要素内に直接記載していましたが、この部分をJavaScriptに置き換えて作成します。
sidebar3.gif

ソースコード(Git Hub)

環境

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

参考

第2回 SVG ハンバーガーアイコン+hover
JavaScript 第1回 Hello World!と環境構築
第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)

用語

特になし

モーダルサイドバーの共通化の実現方法

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

どうやって実現するか?なのですが、色々調べてみると

  • ファイルを読みこんで実装。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ファイルで、単にメニューアイコンという文字列だけを含む。
index.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>
mypage.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>
common.js
common.js
const commonArea = document.getElementById("commonArea");

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

index.htmlmypage.htmlの説明

index.htmlmypage.htmlはコピーして作成して修正しているだけで、ほとんど同じです。
head要素内のscript要素で、common.jsの読み込みを行うようにしています。script要素内のdeferについては、JavaScriptの記事の初期のJavaScript 第1回 Hello World!と環境構築で記載しましたが、

defer 属性の付いたスクリプトは、スクリプトが読み込まれて評価が完了するまで、DOMContentLoaded イベントの発生が抑制されます。
ということで設定しています。

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

common.jsの説明

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

image.png
image.png

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

サイドバーを共通化

以下のファイルを作っていきます。

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

index.htmlmypage.htmlの説明

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

sideBar.jsの説明

sideBar.jsの内容は、第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)で実施していることと同じです。

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

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

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

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

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

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

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

showSideBarの部分は第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)と同じです。300msでサイドバーの表示と非表示の切り替えます。

sideBar.cssの説明

第19回 JavaScript アニメーションによるモーダルサイドバー(translateX)と内容はほぼ同じです。
サイドバーの見た目を少し良くしたいため、CSSで装飾をしていますが、本質ではないため、割愛します。

サイドバーを共通化の別パターン

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

thenで実装

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"
        },
    );
};

これを以下のように最後の部分にreturn animation.finished;を追加します。

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によれば、

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

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

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

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

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

async/awaitで実装

次は、async/awaitでの実装です。
showSideBar関数の修正は、「thenで実装」と同じです。
「ホーム」や「マイページ」のaddEventListenerの部分を見直します。

ホームの場合(見直し後)
    document.getElementById("home").addEventListener("click", async () => {
        await showSideBar();
        location.href = "index.html";
    });

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

おわりに

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

おわりに2

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

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

2
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?