3
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?

記事投稿キャンペーン 「2024年!初アウトプットをしよう」

【Rails】JavaScript ”だけ” を使ったモーダル表示

Posted at

Railsを使ったWebアプリの開発中、「モーダル表示できたら天才...」と思い、実際にモーダル機能をつけてみました。

Image from Gyazo

今回実装するモーダルの仕様 ( ≒ 要件定義)

モーダル非表示時

  • ボタンを押すと、モーダルが開く
  • 画面は、スクロール可能

モーダル表示時

  • 「×」ボタンを押すと、モーダルが閉じる
  • モーダルの外側を押しても、モーダルは閉じる
  • モーダル背景部分は、スクロール ”不可”



モーダル機能は、とてつもなく重要です。なぜなら...

  • 重要な理由①:viewファイル新規追加/ページ更新といったステップを踏むことなく、補足事項をユーザーに伝えることができるから :fire:
  • 重要な理由②:文章等の情報をモーダルに格納せずそのまま描画すると、文字情報量の多い ”文字壁” Webページ = ”ダサい” UIのWebアプリ になってしまうから :fire:

しかし、Railsでのモーダル実装にあたって、問題点がありました。それは...

  • 問題①:Railsのどのディレクトリ/ファイルに、どんな記述をすればいいのか、わかりにくい :thumbsdown_tone1:
  • 問題②:純粋なJavaScript ”だけ” を使って機能実装したいのに、解説記事はjQuery等のJavaScriptライブラリを用いた解説ばかり :thumbsdown_tone1:

そこで、「純粋なJavaScript ”だけ” を使ってモーダルを表示させる」*** ためのRails実装手順をまとめました。

*** 厳密には、「Turbo」といったデフォルトでRailsに同梱されているJavaScriptライブラリは、実質使うことになります...


JavaScriptだけを使ってモーダルを表示させる手順説明は、次の内容でまとめました。

  • 開発環境
  • 実装の準備
  • 具体的な実装内容 《本編》

「開発環境」では、本記事で解説する機能が正しく挙動したことを実際に視認できた環境を整理しました。「実装の準備」では、RailsでJavaScriptファイルを読み込むための設定手順を整理しました。「具体的な実装内容 《本編》」では、本記事のテーマとなるJavaScriptでのレスポンス(モーダル表示)処理をどうやって実現するのか、JavaScriptファイルとviewファイルへの具体的な記述内容を解説しました。


開発環境

Category Value,Version
OS MacOS Sonoma 14.1.2
Ruby 3.2.0
Ruby on Rails 7.0.8

CSS実装には「Tailwind CSS」を用いています 🙏

今回の手順解説では、純粋なCSS記述の代わりに Tailwind CSS というCSSフレームワークを用いてCSSを実装しています。

理由は、CSSファイルを用意せずHTMLファイル上に直接CSSスタイルを記述できる、というメリットがあるからです。

しかし、今までRailsアプリ開発で純粋なCSSの記法を使ってきた方にとって、次の点で不便を感じるかもしれません。

  • 純粋なCSSの記法からTailwind CSS独特の記法へ読み替え
  • rails s ではなく ./bin/dev コマンドでサーバーを起動しないと、Tailwind CSSで組んだCSSが反映されない

Tailwind CSSの導入は、モーダル実装に必須というわけではありませんが、CSSフレームワークを用いている点だけ先にご理解ください。

... RailsへTailwind CSSを導入する方法は解説済みなので、気になる方は↓をチェックしてください(ゆうて簡単)

解説:【Rails】Tailwind CSS 導入〜使えるようになるまで《3分で完了》
オススメ:「純粋なCSSの記法 → Tailwind CSS独特の記法」読替用チートシート



実装の準備

ここでは、RailsでJavaScriptを使えるようになるための方法を、次の順番で解説します。

  • モーダル用JavaScriptファイルの用意
  • ImportMapの設定
  • application.jsの編集

準備①:モーダル用JavaScriptファイルの用意

まず、app/javascript/というディレクトリに、.js というJavaScript用拡張子を使ってモーダル処理を記述するためのファイルを生成します(解説では modal.js とします)

app/javascript/modal.js(ファイル名はお好み。ただし、拡張子は ”.js”)

という階層関係になっていれば、問題ありません。

しかし、新たに生成したJavaScriptファイルは、このままだと読み込まれません。

つまり、この状態のまま、どれだけ正確にJavaScript処理やview編集をおこなえたとしても、思い通りの挙動を実現することはできません...

そこで、ここからはモーダル用JavaScriptファイルを読み込むために、いくつかのファイルを編集します(ゆうて簡単)


準備②:ImportMapの設定

次に、先ほど生成したJavaScriptファイル(modal.js)を読み込むために、ImportMap (RailsのJavaScriptファイル専用アセットパイプラインである importmap-rails というGem)の仕組みを利用します。

このGem機能はデフォルトで有効になっているので、bundle install といったコマンド操作は不要です(Gemfileの上の方を見ると、gem "importmap-rails" という1行を確認できます)


最初に、config ディレクトリから importmap.rb を見つけ、次のように追記します。

config/importmap.rb
# 〜〜〜「pin ~」という他の記述 〜〜〜

pin "modal", to: "modal.js"   # importmapファイルの "最下部" に追記


modal.jsファイルをmodalという名前でピン留めすることで、JavaScriptファイルをmodalという名前で使えるようになります。

次に、app/views/layouts/application.html.erb というレイアウトファイルの <head> 要素に <%= javascript_importmap_tags %> という記述があるか、確認しましょう(もし記述が無ければ、追加する必要があります)

app/views/layouts/application.html.erb
<head>

    <title>(アプリ名)</title>
    
    <!-- 省略 -->

    <%= javascript_importmap_tags %>   <!-- このtagがあることを確認 -->

</head>

<%= javascript_importmap_tags %> がないと、JavaScriptでつくった機能がレンダリングされません。

確認できたら、ImportMapの設定は完了です。


準備③:application.jsの編集

最後に、ImportMapで読み込み準備の完了したmodal.jsファイルをアプリケーションで動作させられるよう、application.jsを編集します。

ここで、同じ app/javascript/ というディレクトリ階層に application.js というファイルが存在することを確認してください。

そしたら、 application.js ファイルを次のように編集してください。

application.js
// 〜〜〜「import ~」という他の記述 〜〜〜

import "modal"   // application.jsファイルの "最下部" に追記


これで、modal.jsの内容が読み込まれるようになりました。


これで、準備万端です :tada:


具体的な実装内容 《本編》

ここでは、RailsでJavaScriptだけを使って実際にモーダルを表示させるところまで、次の順番で解説します。

  • 完成形のソースコード + モーダル仕様の確認
  • モーダル表示用viewファイルの作成
  • モーダル表示用JavaScriptファイルの作成

本編⓪:完成形のソースコード + モーダル仕様の確認

一番最初に、完成形となるviewファイルとJavaScriptファイルの記述内容を、それぞれ共有します。

index.html.erb
<main class="container grid place-items-center w-full h-screen">
  <button
    class="bg-[#eb6100] text-white text-[23px] py-[20px] px-[40px] border-b-8 border-b-solid border-b-[#9e4a0e] shadow-[0_3px_4px_rgba(0,0,0,0.3)] transition-all hover:mt-[5px] hover:border-b-2 hover:border-b-[#eb6100]"
    id="modalOpen"
  >
    モーダルを開く
  </button>
  <div class="hidden fixed left-0 top-0 h-full w-full bg-gray-400/50 z-10"
   id="modalSelf">
    <div class="modal-content bg-[#f4f4f4] my-[15%] mx-auto w-[500px] shadow-[0_2px_7px_rgba(0,0,0)] 
    animate-popup">
      <div class="modal-header bg-[#eb6100] py-[3px] px-[15px] flex justify-between">
        <h1 class="my-[10px]">モーダルウィンドウ</h1>
        <span class="modalClose text-xl cursor-pointer">&times;</span>
      </div>
      <div class="modal-body py-[15px] px-[25px]">
        <p>モーダル表示成功!</p>
      </div>
    </div>
  </div>
</main>
config/tailwind.config.js

// "modal-content" というclass名の入ったdiv要素に付与した、
// "animate-popup" というCSSアニメーション設定

// モーダルを颯爽と登場させるために付けたTailwind CSS式アニメーションなので、
// 最低限の挙動をつけたいだけなら別に要らないです...

const defaultTheme = require("tailwindcss/defaultTheme");

module.exports = {
  content: ["./public/*.html", "./app/helpers/**/*.rb", "./app/javascript/**/*.js", "./app/views/**/*.{erb,haml,html,slim}"],
  theme: {
    extend: {
      // ここから...
      keyframes: {
        modalPopup: {
          from: { transform: "translateY(150%)", opacity: 0 },
          to: { opacity: 1 },
        },
      },
      animation: {
        popup: "modalPopup 1.1s",
      },
      // ..ここまで編集
    },
  },
  plugins: [require("@tailwindcss/forms"), require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("@tailwindcss/container-queries")],
};

modal.js
function modalOperation() {
  const modalOpen = document.getElementById("modalOpen");
  const modalClose = document.querySelector(".modalClose");
  const modal = document.getElementById("modalSelf");

  modalOpen.addEventListener("click", () => {
    modal.style.display = "block";
    document.body.style.overflow = "hidden";
  });

  modalClose.addEventListener("click", () => {
    modal.style.display = "none";
    document.body.style.overflow = "";
  });

  document.addEventListener("click", (evt) => {
    // console.log(evt.target);
    if (evt.target == modalSelf) {
      modal.style.display = "none";
      document.body.style.overflow = "";
    }
  });
}

window.addEventListener("turbo:load", modalOperation);


正しいディレクトリ/ ファイルに↑の記述をおこなえば、以降の解説を読まなくても機能自体はつけられます(ただし、↑のようにTailwind CSS を使う場合、./bin/dev コマンド必要でサーバーを再起動する必要があります)

次に、↑のコードを正しく反映させたときのモーダルの挙動を、もう一度確認しておきます。

Image from Gyazo

今回実装するモーダルの仕様 ( ≒ 要件定義)

モーダル非表示時

  • ボタンを押すと、モーダルが開く
  • 画面は、スクロール可能

モーダル表示時

  • 「×」ボタンを押すと、モーダルが閉じる
  • モーダルの外側を押しても、モーダルは閉じる
  • モーダル背景部分は、スクロール ”不可”


本編①:モーダル表示用viewファイルの作成

ここでは、index.html.erbより、JavaScriptを記述する前に理解しておく必要がある、次のポイントに絞って解説します。

  • id, classの関係
  • モーダル本体の隠れ方

本編①-1: id, classの関係

Tailwind CSS特有の冗長なclass内記述をいったん消して、JavaScriptファイルとの紐付けに必要なidやclass(a ~ c)だけを抽出して、先ほどの完成形コードを表示します↓

index.html.erb
<main class="~">
  <button
    class="~" id="modalOpen"> <!-- a -->
    モーダルを開く
  </button>
  <div class="~" id="modalSelf"> <!-- b -->
    <div class="modal-content ~">
      <div class="modal-header ~">
        <h1 class="~">モーダルウィンドウ</h1>
        <span class="modalClose ~">&times;</span> <!-- c -->
      </div>
      <div class="modal-body ~">
        <p>モーダル表示成功!</p>
      </div>
    </div>
  </div>
</main>


aは、「このbutton要素を押したらモーダルが開く」というイベント発生に必要なid="modalOpen" です。

bは、「これがモーダル本体( = 元々隠れている部分)です」という意味を伝える id="modalSelf" です。

つまり、このidを付与したdivの開始タグと終了タグで囲った部分全体を「モーダル」として認識させています。

もっと噛み砕いていうと、 id="modalSelf"を付与したdiv要素内ではモーダルそのものを表し、インデントを一つ下げたclass="modal-content ~" を付与したdiv要素内ではモーダル内の具体的なコンテンツ内容を示せるように階層分けをしました(さらにもう一つインデントを下げてclass="modal-header ~"class="modal-body ~"に分けて、モーダル内の具体的なコンテンツ構造を確認しやすくしました)

cは、「このspan要素を押したらモーダルが閉じる」というイベント発生に必要なclass="modalClose" です。

spanタグで囲った &times; は「×」を表し、掛け算("times":(数を)~倍する という意味)記号、すなわち「バッテン」マークが表示されます。

この「×」だけidではなくclass名で指定した理由は、「同じページ内にもう1個モーダルを作成したい!」といった場合、もう一度「×」ボタンが必要になる可能性が高く、同じページ内に原則1回しか使えないidで id="modalClose" と指定してしまうと、文法的に間違った解釈になる恐れがある... と判断したからです。

本編①-2: モーダル本体の隠れ方

JavaScriptファイルで ~.style.display = block / none とモーダル表示/非表示の動きを付けるときに必要な(d)だけを抽出して、完成形コードを再表示します↓

index.html.erb
<main class="~">
  <button
    class="~" id="modalOpen">
    モーダルを開く
  </button>
  <div class="hidden ~" id="modalSelf"> <!-- d -->
    <div class="modal-content ~">
      <div class="modal-header ~">
        <h1 class="~">モーダルウィンドウ</h1>
        <span class="modalClose ~">&times;</span>
      </div>
      <div class="modal-body ~">
        <p>モーダル表示成功!</p>
      </div>
    </div>
  </div>
</main>


dのclass内の "hidden" となっている部分は、標準CSS的な解釈で display: none;という意味になります。

つまり、「モーダルを見えなくする = モーダルを隠す」ために必要なスタイルになります。

本編②:モーダル表示用JavaScriptファイルの作成

ここでは、↓のmodal.jsになるよう、順番にJavaScriptファイルを完成させていきます。

  • loadイベントの発火
  • 「モーダルを開く」関数の定義
  • 「モーダルを閉じる」関数の定義:「×」ボタンを押す場合
  • 「モーダルを閉じる」関数の定義:モーダル外側を押す場合
  • モーダル背景部分の制御
modal.js

// 完成形

function modalOperation() {
  const modalOpen = document.getElementById("modalOpen");
  const modalClose = document.querySelector(".modalClose");
  const modal = document.getElementById("modalSelf");

  modalOpen.addEventListener("click", () => {
    modal.style.display = "block";
    document.body.style.overflow = "hidden";
  });

  modalClose.addEventListener("click", () => {
    modal.style.display = "none";
    document.body.style.overflow = "";
  });

  document.addEventListener("click", (e) => {
    // console.log(e.target);
    if (e.target == modalSelf) {
      modal.style.display = "none";
      document.body.style.overflow = "";
    }
  });
}

window.addEventListener("turbo:load", modalOperation);


本編②-1: loadイベントの発火

まず、windowオブジェクト(ブラウザの情報を持つ最上位のオブジェクト)へ、loadイベント(HTMLページ全体をすべて読み込む処理の要求)を、addEventListener() メソッド(第一引数:処理要求内容、第二引数:実行するJavaScriptの関数)で発火させます。

modal.js
// loadイベントの発火
window.addEventListener("turbo:load", function (){

})


一点だけ注意したいのは、Railsではloadイベントを turbo:load という書き方で表す、という点です。

この書き方が必要な理由は、Turbo というJavaScriptライブラリがRailsアプリにデフォルトで同梱されているからです(Gemfile の上側を見ると、 gem "turbo-rails" という記述があります)

具体的には、Turbo が、Railsアプリにてクライアント↔︎サーバ間でHTMLをやりとりする際のDOM(HTMLを解析してデータを作成する仕組み)更新を担っているからです。

参考:Turbo《Rails公式》
参考:turbo:load < Events《Turbo(Hotwire)公式》

本編②-2: 「モーダルを開く」関数の定義

次に、本編①で作成したviewファイルのDOMツリー(DOMによって「階層構造」として解析されたHTMLのデータ)より、「モーダルを開く」ために必要なidを、documentオブジェクト(ブラウザ上に表示されるHTMLを操作できるオブジェクト。windowオブジェクトが持つプロパティの一つ)から取得します。

modal.js
// 「モーダルを開く」ために必要なidを取得
window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen"); // button要素のid
    const modal = document.getElementById("modalSelf"); // モーダル本体のid
})

そのまま、「モーダルを開く」処理を記述します。

modal.js
// 「モーダルを開く」処理
window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block"; // "display: none;" → "display: block;"
    });
})

この処理をもう少し噛み砕くと、"display: none;" で隠れているdiv要素のclass(セレクタ)に "display: block;" を付与、つまりdisplayプロパティへblockという値を再代入してあげることで、モーダルを表示しています。

本編②-3: 「モーダルを閉じる」関数の定義:「×」ボタンを押す場合

次に、「モーダルを閉じる」ために必要なclassを、documentオブジェクトから取得します。

modal.js
// 「モーダルを閉じる」ために必要なclassを取得
window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");   // 「×」ボタンのセレクタ(class名)
    const modal = document.getElementById("modalSelf"); 

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
    });
})

一点だけ注意したいのは、セレクタ(class名)取得するための書き方が、idを取得するための書き方と異なり、.querySelector(".(クラス名)") になる、という点です。

それでは、「 × ボタンをクリック → モーダルが閉じる」処理を記述します。

modal.js

// 「モーダルを閉じる」処理(「×」ボタンを押す場合)

window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
    });

    modalClose.addEventListener("click", () => {
      modal.style.display = "none"; // "display: dispkay;" → "display: none;"
    });
})

この処理をもう少し噛み砕くと、"display: block;" で表示中のモーダル要素に "display: none;" を付与してあげることで、モーダルを格納し戻します。

本編②-4: 「モーダルを閉じる」関数の定義:モーダル外側を押す場合

ここでは、「×」ボタンを押す場合に加えて、モーダル外側を押した場合にもモーダルが閉じるように追加実装していきます。

まず、「モーダル外側の要素を押したときのHTML要素は何か?」をチェックするために、consoleオブジェクトのlogメソッドを使って確認します。

modal.js

// モーダル外側のHTML要素(オブジェクト)は何か?

window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
    });

    modalClose.addEventListener("click", () => {
      modal.style.display = "none";
    });

    document.addEventListener("click", (evt) =>{
      console.log(evt.target); // 追記
    });
})

evtはイベントオブジェクトといい、イベント発生時の情報を持ったオブジェクトです。なお、evtにはどんな文字列を指定してもOKです(慣例的な文字列は、"event" の頭文字 e

今回だと、「モーダル外側をクリックした」という情報を持ったオブジェクトということです。

そして、このオブジェクトを参照するために、target を用いてます。

参考:target プロパティ《MDN Web Docs》

つまり、console.log() の対象を evt.target とすることで、「モーダル外側をクリックした」ときの要素を参照します。

実際にブラウザのデベロッパツールからConsoleを開いて、「モーダル外側をクリックした」ときの要素 = HTMLのid を確認しましょう(以下のブラウザ:Chrome)

Image from Gyazo

モーダルの外側の要素を押したときの要素は id="modalSelf" だとわかりましたね。

それでは、「モーダル外側をクリック → モーダルが閉じる」処理を記述します。

modal.js

// 「モーダルを閉じる」処理(モーダル外側を押す場合)

window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
    });

    modalClose.addEventListener("click", () => {
      modal.style.display = "none";
    });

    document.addEventListener("click", (evt) =>{
      // console.log(evt.target);   → コメントアウト
      if (evt.target == modalSelf) {
        modal.style.display = "none";
      }
    });
})

これで、「×」ボタンを押す場合に加えて、モーダル外側を押した場合にもモーダルが閉じるようになりました。

本編②-5: モーダル背景部分の制御

最後に、モーダルの背景部分を制御します。

具体的には、スクロール操作を制御します

まず、「モーダル表示中、背景画面がスクロールされないようにする」という制御を、次のように実装します。

modal.js

// モーダル表示中、背景画面がスクロールされないようにする

window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
      document.body.style.overflow = "hidden"; // 追記
    });

    modalClose.addEventListener("click", () => {
      modal.style.display = "none";
    });

    document.addEventListener("click", (evt) =>{
      // console.log(evt.target);
      if (evt.target == modalSelf) {
        modal.style.display = "none";
      }
    });
})

documentオブジェクトのbody要素に overflow: hidden; を付与することで、モーダル背景部分のスクロールをさせないようにしています。
参考:要素のはみ出し(オーバーフロー)《MDN Web Docs》

しかし、このままだとモーダルを閉じた後も、ページが更新されない間は画面がスクロールされないままになってしまいます。

そこで、モーダルを閉じた後は元通り画面をスクロールできるようにするために「モーダル非表示の間は、画面がスクロールできる」という制御を、次のように実装します。

modal.js

// モーダル非表示の間は、画面がスクロールできる

window.addEventListener("turbo:load", function (){
    const modalOpen = document.getElementById("modalOpen");
    const modalClose = document.querySelector(".modalClose");
    const modal = document.getElementById("modalSelf");

    modalOpen.addEventListener("click", () => {
      modal.style.display = "block";
      document.body.style.overflow = "hidden";
    });

    modalClose.addEventListener("click", () => {
      modal.style.display = "none";
      document.body.style.overflow = ""   // 追記:""の代わりに"scroll"でもOK
    });

    document.addEventListener("click", (evt) =>{
      // console.log(evt.target);
      if (evt.target == modalSelf) {
        modal.style.display = "none";
        document.body.style.overflow = ""   // 追記:""の代わりに"scroll"でもOK
      }
    });
})

これで、、、

  • モーダル表示中:背景のスクロール不可
  • モーダル非表示中:画面のスクロール可

になりました。


最後に、コードを整えたら「モーダル表示用JavaScriptファイルの作成」作業はすべて終わりです。

modal.js

// 完成形

function modalOperation() {
  const modalOpen = document.getElementById("modalOpen");
  const modalClose = document.querySelector(".modalClose");
  const modal = document.getElementById("modalSelf");

  modalOpen.addEventListener("click", () => {
    modal.style.display = "block";
    document.body.style.overflow = "hidden";
  });

  modalClose.addEventListener("click", () => {
    modal.style.display = "none";
    document.body.style.overflow = "";
  });

  document.addEventListener("click", (evt) => {
    // console.log(evt.target);
    if (evt.target == modalSelf) {
      modal.style.display = "none";
      document.body.style.overflow = "";
    }
  });
}

window.addEventListener("turbo:load", modalOperation);

以上で、実装はすべて完了です :tada: :tada: :tada:

おつかれさまでした。


プログラミング学習のアウトプット、以上になります。
ご指摘あれば、お気軽にコメントください。

参考 + 感謝:モダールウィンドウをHTMLとCSSとJavascriptで実装してみよう

3
1
1

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
3
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?