🥜

【Rails & JavaScript】ドロップダウンメニュー作成 / イベントハンドラ / 無名関数

2023/07/25に公開

チーム開発終了!各々プレッシャーとの戦いで大変だったけど、終わってホッとしています!笑
俺らチーム目標は次の2つでした。

  1. 3日前倒しで提出すること
  2. チームの作品に自信と愛着を持つこと

結果として4日前倒し、自分たちの作品が大好きなので目標達成!
もちろんこだわればもっとできることもあったと思うけど、限られた時間の中で本当にみんなでよくやったと思うし、みんなで足りない部分を補い合い本当に良いチームだった!

今回は、JavaScriptやjQueryの実装も少し盛り込んだので複数回に分けて紹介できればと思います。

  • 作成したトップページ
    以下のようなデザインにした。
    メイン画像は4枚の画像がフェードで切り替わるようにしている。
    新着商品はjQueryとslickを使いスライドショーにしている。

ドロップダウンメニュー作成

今回の紹介は、イベントハンドラを追加し、カーソルを合わせると発火する以下のようなドロップダウンメニューを作る。

イベントハンドラとは

イベントハンドラ(event handler)は、ウェブページ上で発生するさまざなイベント(例:ボタンのクリック、マウスカーソルの異動、キーボードの入力など)に対して、特定の処理を実行するための仕組み。JavaScriptを使って設定する。

例えば、ボタンをクリックしたときに特定のアクションを実行したい場合、イベントハンドラを使って次の処理を記述する。

  • HTML:
html
<button id="myButton">クリックしてください</button>
  • JavaScript:
js
// ボタンの要素を取得
const myButton = document.getElementById('myButton');

// クリックイベントハンドラを設定
myButton.addEventListener('click', function() {
  // ここに実行したいアクションを記述
  alert('ボタンがクリックされました!');
});

このコードでは、addEventListenerメソッドを使ってclickイベント(ボタンがクリックされたときのイベント)に対して、無名関数を登録している。ボタンがクリックされた際に、指定した無名関数が実行され、"ボタンがクリックされました!"というアラートが表示される。

無名関数とは

無名関数とは、その名の通り名前を持たない関数のこと。通常の関数は、関数名を定義してから処理を記述するが、無名関数には名前がなく、その場で定義と処理を同時に行う。

説明すると上述のような感じなんだけど、なんかイメージが湧かないのでメタファーにします笑

無名関数のメタファー

新幹線などの乗り物で、体調を崩した人のために駆け付けてくれるドクターみたいなもの。
例えば旅行中に体調を崩してしまったとき、新幹線の中で偶然にも運よく旅行していたドクターが、他の乗客の中から無名の存在(名乗らず)で助けてくれることがある。そのドクターは当然新幹線に常駐しているわけではなく、一時的な特別な存在として助けてくれる。

無名関数も同じように、プログラムの中で名前を持たない関数として活躍する。必要な場所に駆け付けて特別な処理をし、プログラムをサポートしている。

具体例

具体例は以下の通り。通常の関数では次のように名前を持っている。

js
function add(a, b) {
  return a + b;
}

無名関数の場合は次のように書く。

js
const add = function(a, b) {
  return a + b;
};

無名関数の主な使い方は、以下の2つ。

1. 他の関数の引数として渡す

他の関数に特定の処理を行わせることができる。

js
function doOperation(operationFunc) {
  const result = operationFunc(5, 3);
  console.log("結果:" + result);
}

この関数は、与えられた操作関数を実行し、その結果を表示するもの。
以下のような2つの無名関数を用意する。

js
const add = function(a, b) {
  return a + b;
};

const subtract = function(a, b) {
  return a - b;
};

これらの無名関数はそれぞれ足し算と引き算を行う関数。
ここで、doOperation関数に無名関数を引数として渡す。

js
doOperation(add); // 出力結果:結果:8
doOperation(subtract); // 出力結果:結果:2

doOperation関数が受け取った無名関数(addsubtract)が、それぞれの処理を行い、結果をだす。

2. 変数に代入する

変数に代入した無名関数は、その変数を関数として使用できる。

js
const greet = function(name) {
  console.log(`こんにちは、${name}さん!`);
};

greet("太郎"); // 出力結果:こんにちは、太郎さん!

無名関数の使い所は、特定の場面で一度だけ使われるような処理に適している。

ドロップダウンメニュー作成においてのポイント

  1. HTMLの構造を理解する
    ドロップダウンメニューは、ボタン(例: <button>)をクリックまたはホバーしたときに表示されるリスト(例: <ul>)のこと。ボタンとドロップダウンメニューは親子関係にある。

  2. ドロップダウンメニューの初期状態を非表示にする
    ドロップダウンメニューは初めは表示されないようにする。CSSのスタイルを用いて、ドロップダウンメニューの初期状態を display: none; として非表示にする。

  3. マウスイベントを利用して表示・非表示を切り替える
    JavaScriptを使って、ボタンにマウスを持ってくるとドロップダウンメニューを表示し、外すと非表示にする。例えば、マウスオーバー(mouseover)イベントとマウスアウト(mouseout)イベントで対応する処理を行うようにする。

  4. CSSでスタイルを整える
    ドロップダウンメニューが表示される際の背景色や文字色等のスタイルをCSSで設定する。

  5. 他の要素との干渉に注意する
    ドロップダウンメニューを実装する際には、他の要素との干渉に注意する。例えば、ドロップダウンメニューが他の要素を覆い隠さないようにZインデックス(z-index)を調整したり、ドロップダウンメニューがボタンの下に表示されるように位置を設定したりする。

実装準備編

実装するためにrailsの設定をしていく。

ファイル作成

app/javascriptフォルダ内にファイルを作成する。
今回はscript.jsという名で作成した。

ファイルのimport

app/javascript/packs/application.js
:

import "script.js"

:

実装編

ヘッダーフッターでも同じメニューを作成したが、どちらも実装方法は同じなので、今回はヘッダーのマイページボタンの実装方法を記載する。

HTML

html
:
:
  <li>
    <div class="mypage-btn-container" id="mypageBtnContainer">
      <button class="btn btn-lg mx-3" style="background-color:#FF9494; color: white;"><%= current_customer.full_name %>様</button>
      <div class="mypage-dropdown" id="mypageDropdown">
	<ul>
	  <li><%= link_to 'マイページ', customers_mypage_path %></li>
	  <li><%= link_to '配送先', addresses_path %></li>
	  <li><%= link_to '注文履歴', orders_path %></li>
	  <li><%= link_to 'ログアウト', destroy_customer_session_path, method: :delete %></li>
	</ul>
      </div>
    </div>
  </li>
:
:
解説

必須項目は以下の2つ。

  1. id="mypageBtnContainer"
    マイページボタン(<button>)を包む要素として設定する。この要素にはマウスイベントを追加し、マウスを乗せたり外したりしたときに、ドロップダウンメニューの表示・非表示を制御するために使う。

  2. id="mypageDropdown"
    ドロップダウンメニューを表示するための要素として設定する。最初は非表示に設定するが、JavaScriptのコードで、ボタンにマウスを乗せると表示され、外すと非表示になるようにする。

CSS

application.css
.mypage-dropdown {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background-color: #fff;
  padding: 26px;
  border: 1px solid #ccc;
  z-index: 999;
}

.mypage-btn-container {
  position: relative;
}

.mypage-btn-container ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.mypage-dropdown a {
  color: #5c3e26;
  text-decoration: none;
}

.mypage-dropdown a:hover {
  color: #8b5c38;
}

.category-container:hover {
  cursor: pointer;
}
解説
  1. .mypage-dropdown: マイページのドロップダウンメニュー全体に適用。

    • display: none;: 初期状態でメニューを非表示にする。
    • position: absolute;: 絶対位置指定により、ボタンの下に配置する。
    • top: 100%; left: 0;: 今回はメニューがボタンの下に配置するように指定。
    • background-color: #fff;: メニューの背景色。
    • padding: 26px;: 内部の余白。
    • border: 1px solid #ccc;: メニューに境界線を追加。
    • z-index: 999;: Zインデックスを指定して、他の要素よりも前面に表示されるようにする。
  2. .mypage-btn-container: マイページボタンを含むコンテナに適用。

    • position: relative;: 相対位置指定を行い、その中で絶対位置のドロップダウンメニューを配置できるようにする。
  3. .mypage-btn-container ul: ドロップダウンメニュー内のリストに適用。

    • list-style: none;: リストのマーカー(箇条書き)を削除。
    • padding: 0; margin: 0;: 内部の余白と外側の余白をゼロに設定。
  4. .mypage-dropdown a: ドロップダウンメニュー内のリンクに適用。

    • color: #5c3e26;: リンクのテキストカラーを指定。
    • text-decoration: none;: リンクの下線を削除。
  5. .mypage-dropdown a:hover: マイページドロップダウンメニュー内のリンクにマウスホバーしたときのスタイル。

    • color: #8b5c38;: マウスホバー時のテキストカラーを変更。

JavaScript

script.js
document.addEventListener('turbolinks:load', function() {
 const mypageBtnContainer = document.getElementById('mypageBtnContainer');
  const mypageDropdown = document.getElementById('mypageDropdown');

  if (mypageBtnContainer) {
    mypageBtnContainer.addEventListener('mouseover', function() {
      mypageDropdown.style.display = 'block';
    });

    mypageBtnContainer.addEventListener('mouseout', function() {
      mypageDropdown.style.display = 'none';
    });
  }
解説
  1. document.addEventListener('turbolinks:load', function() { ... });
  • document はHTML文書全体を表すオブジェクトで、addEventListener メソッドを使って特定のイベントリスナーを登録している。このコードでは、turbolinks:load というイベントが発生した際に、指定された関数が実行されるように設定。
  1. const mypageBtnContainer = document.getElementById('mypageBtnContainer');

    • document.getElementById('mypageBtnContainer') は、HTML内の特定の要素を取得している。この要素は、マイページボタンを包む親要素であり、この要素にマウスイベントを追加することで、ボタンへのマウスオーバーやマウスアウトを検知できるようになる。
  2. const mypageDropdown = document.getElementById('mypageDropdown');

    • 同様に、document.getElementById('mypageDropdown') という部分は、ドロップダウンメニューを表示するための要素を取得している。この要素は、ドロップダウンメニューのリストやリンクが含まれる要素。
  3. if (mypageBtnContainer) { ... }

    • if文は条件分岐の一部で、この場合は mypageBtnContainer が存在するかどうかをチェックしている。存在する場合のみ、マウスイベントを追加する処理が行われる。これは、mypageBtnContainer が存在しないページではエラーが起きないようにするため。
  4. mypageBtnContainer.addEventListener('mouseover', function() { ... });

    • この部分は、マウスをボタン上に乗せたときのイベント(マウスオーバー)を検知している。マウスオーバーが検知されると、ドロップダウンメニューを表示するために、mypageDropdown.style.display = 'block'; というスタイルを適用。
  5. mypageBtnContainer.addEventListener('mouseout', function() { ... });

    • この部分は、マウスがボタンから外れたときのイベント(マウスアウト)を検知。マウスアウトが検知されると、ドロップダウンメニューを非表示にするために、mypageDropdown.style.display = 'none'; というスタイルを適用。

実装で苦しんだポイント

チーム開発期間中はJSは質問できなく、JS実装解説の以下2点が苦しんだ、、、💦

  1. document.addEventListener('turbolinks:load', function() { ... });
  2. if (mypageBtnContainer) { ... }

詳細は次の通り。

  • document.addEventListener('turbolinks:load', function() { ... });
    最初はdocument.addEventListener('DOMContentLoaded', function() {と記載していた。そうすると、別ページに遷移した時にイベントが発火しないエラーが発生。
    1ページのみの実装であれば今回の表記のようなただのイベントリスナーでもOK。ページ遷移が発生(複数ページで実装したい)するときはターボリンクを考慮した実装をする必要がある。

https://zenn.dev/ganmo3/articles/d234583bdc876f

  • if (mypageBtnContainer) { ... }
    最初は条件付けの設定をしていなかった。mypageのボタンは顧客ログイン後に現れるボタンなので、ログイン前や管理者ログイン時は現れない。if文で存在の有無を条件付けしてあげないと、ボタンがないときでもID要素を探しにいってしまい、nillを返すエラーが発生していた。そうすると別のドロップダウンメニューのJSコードに干渉し、どちらのイベントも発火されないというエラーが起きた。

チーム期間フェーズはJavaScriptの質問が対応外であることから、参考になれば幸いです。
エラ―を解決できたときは嬉しかったし、実装し形になっていくのもすごい楽しかった!

Discussion