この記事は、以下のような方を想定しています。
- 複数のフィルタUIを1画面に共存させたい方
- 「ボタンが効かなくなる」系のJSバグを経験した方
- レスポンシブ対応でテーブルのレイアウト崩れに悩んでいる方
前回の記事では、バックエンド側で新規・継続・変化・解除を正しく 判定する差分検出ロジックを紹介しました。今回はその結果をWebサイト上で表示するフロントエンド側の話です。 特に「フィルタの設計」と「スマホ対応」で発生した不具合と、その直し方を中心にまとめます。
3系統のフィルタをどう設計するか
このサイトには、データを絞り込むためのUIが3種類あります。
| UI | 役割 |
|---|---|
| KPIカード | 現在表示中のCSVデータ内を、状態(新規/継続/変化/解除)で絞り込む |
| タブ | 表示するCSVファイル自体を切り替える(ランキング/全銘柄/新規 など) |
| フィルタチップ | 規制フラグ(売禁/増担/注意喚起)で絞り込む |
最初の失敗:フィルタが連動して混乱する
最初に実装したときは、KPIカードの「新規」をクリックすると同時にタブも「全銘柄」に切り替え、 フィルタチップも自動でオンにする、という「親切すぎる」連動をさせていました。
しかし、これは使ってみると逆に分かりにくいという指摘をもらいました。 「ランキングタブを見ながら新規だけ確認したい」というシンプルな操作ができず、 意図しないタブ遷移が起きてしまうためです。
設計方針の転換
最終的には「KPIカードは現在表示中のタブの中だけで絞り込む」「タブ切り替えはフィルタ状態に 一切影響しない」「フィルタチップも他のUIと独立して動く」という、お互いに干渉しない3本立ての設計に 落ち着きました。それぞれの状態をJavaScript側で別々の変数として持ち、表示時にAND条件で重ね合わせる 形にすることで、利用者が「今どのフィルタが効いているか」を把握しやすくなりました。
addEventListener重複で「ボタンが効かない」バグ
タブ切り替え機能を実装したとき、フィルタボタンにaddEventListenerでクリック処理を
登録していました。ところが運用しているうちに「新規・継続・削除ボタンを何度かクリックしていると、
突然反応しなくなる」という現象が発生しました。
原因は、タブを切り替えるたびに呼ばれる関数の中で、毎回同じボタンに対して
addEventListenerを実行していたことでした。タブを3回切り替えると、ボタンには
3つのクリックリスナーが積み重なります。1回のクリックで状態の反転処理が3回実行されることになり、
奇数回切り替えていれば変化が見えますが、偶数回切り替えた後にクリックすると変化が相殺されて
「見た目は何も起きていない」という結果になっていました。
修正方法
addEventListenerによる動的登録をやめる- HTML側のボタンに
onclick="toggleFilter('new', this)"のように直接記述する - トグル処理は1つの関数として独立させ、ページ内で1度だけ定義する
これにより、何度タブを切り替えてもイベントが重複登録される心配がなくなりました。 画面更新のたびにイベントを再登録する設計は便利に見えますが、要素を作り直さずに使い回す場合は 積み重なりのリスクがあることを学びました。
ヘッダーとデータ部が重なる問題
テーブルのヘッダー行(th要素)を画面上部に固定して見やすくしたいと考え、
position: stickyを設定していました。ところがページ上部に固定表示している
サイトヘッダーや、上部ページネーション、注意書きバーなど複数の固定要素が積み重なる構成だったため、
topの値の計算が想定通りにいかず、ヘッダー行とデータ行が重なって表示される不具合が
発生しました。
固定要素が1つだけならsticky位置の計算は簡単ですが、複数の要素が縦に重なるレイアウトでは 正確なオフセット計算が複雑になります。最終的には、テーブルヘッダーのstickyを諦め、 代わりにテーブルの上下にページネーションを設置することで、長いテーブルでも操作しやすい構成に 変更しました。
スマホでテーブル幅が安定しない問題
スマホ表示で、ページを切り替えるたびにテーブルの横幅が変わってしまう現象もありました。
原因はtable-layoutのデフォルト値であるautoのままにしていたことです。
この設定では、行ごとのテキスト量に応じて列幅が自動的に再計算されるため、ページごとに
テーブル全体の見た目がガタつきます。
| 対処 | 内容 |
|---|---|
table-layout: fixed |
列幅をコンテンツに依存させず固定する |
<colgroup> |
各列の幅をピクセル単位で明示的に指定する |
white-space: nowrap |
大半のセルはテキストを折り返さず、横スクロールで対応する |
word-break: break-all |
銘柄名のように長くなりやすい列だけ折り返しを許可する |
テーブル全体にはoverflow-x: autoを持つラッパー要素を被せ、画面幅に収まらない場合は
横スクロールで見せる方針に統一しました。これにより、スマホでもPCと同じ情報量を保ったまま、
レイアウトが崩れない表示が実現できています。
行クリックでYahoo Financeへ遷移する導線
細かい部分ですが、利便性を上げるために、テーブルの行をクリックすると該当銘柄のYahoo Financeページが 新しいタブで開く仕組みを入れています。
実装時に気をつけたのは、銘柄コードや銘柄名のリンク自体をクリックした場合と、行のその他の場所を
クリックした場合の挙動です。当初、コード列・銘柄名列のtd要素自体に
event.stopPropagation()を設定していたため、その列をクリックすると行全体のクリックイベントが
キャンセルされ、リンクのonclick自体も発火しないという不具合がありました。
stopPropagation()は内部のaタグ側だけに残し、td要素からは外すことで、
どこをクリックしても自然にYahoo Financeへ遷移するようになりました。
今後の展望
現在は、データの更新やCSV生成までは自動化が完了していますが、収集したデータをもとに 「規制が解除されやすい曜日や日数の傾向分析」のような、もう一段踏み込んだ機能も検討しています。 地道な開発ですが、自分が欲しい機能から優先して実装していく方針は今後も変えずに続けていく予定です。
FAQ
複数のフィルタUIを組み合わせるときに気をつけることは何ですか?
1つのボタンを押すと別のフィルタ状態まで自動的に変わってしまう設計は、利用者にとって分かりにくくなります。 各フィルタの役割を明確に分け、状態をそれぞれ独立した変数で管理することが重要です。
ボタンのクリックイベントが効かなくなる現象の原因は何ですか?
画面更新のたびにaddEventListenerでイベントを登録し直していると、同じ要素に複数のリスナーが
積み重なり、奇数回・偶数回のクリックで結果が相殺されることがあります。
onclick属性で直接呼び出す方式に変更すると解決しやすいです。
スマホでテーブルの横幅が安定しないのはなぜですか?
table-layoutがautoのままだと、表示内容によって列幅が再計算されてしまいます。
table-layout: fixedとcolgroupで列幅を固定し、長いテキストは
overflowやword-breakで制御するのが効果的です。