環境
Ruby 2.5.7
Rails 5.2.4
前提
形を問わずフラッシュメッセージの表示が確認できる状態。
turbolinksは切っています。オンの状態で動作確認はしていません。
gem
gem 'devise'
経緯
タイトルに「いい感じ」と表現しましたが、他に言葉が見つかりませんでした・・・笑
ユーザーが何かアクションを起こした時に、それをお知らせする方法としてページ内へのフラッシュメッセージの埋め込みがありますが、それをただ表示させるのは物足りないと感じて今回の形を実装してみました。(AbemaTVのアプリから発想を得ました笑)
目標
ログイン・ログアウトをメインに、アクションを起こすごとに下の方からニュッっとお知らせが一定時間出てくる。
手順
1.お知らせ窓layouts/_flash_window.html.erb
を作成する
2.cssでお知らせ窓をいい感じにする
3.コントローラーでflash[:notice]
を作成する
4.お知らせ窓専用のjsファイルassets/javascripts/flash_window.js
を作成する
1.お知らせ窓layouts/_flash_window.html.erb
を作成する
お知らせ窓views/layouts/_flash_window.html.erb
を部分テンプレートとして作成します。
今回は簡単なのでapplication.html.erb
に直接書いてもいいのですが、窓の中身が今後複雑になったり、基本的に見えていない部分なので最初から分けておいてもいいかと思います。
<div class="flash-window hidden">
<p><%= notice %><span class="flash-window--delete">X</span></p>
</div>
CSSクラス名にhiddden
を付けていますが、これがついているときは非表示、これがjs(toggle)で外れると表示される仕組みです。
2行目はnoticeメッセージ(フラッシュメッセージ)
の後ろにX(バツボタン)を表示させることでお知らせ窓が自動で閉じる前に任意で閉じることができるようにしていきます。
ここで作成した部分テンプレートをapplication.html.erb
に反映させていきます。
...
<main>
<div class="main">
<%= yield %>
<%= render partial: 'layouts/flash_window' %>
</div>
</main>
...
<main>
タグの下に<div>
を挟んでいる理由としては、お知らせ窓の位置を<main>
直下の<div>
に基点にするためです。
<main>
タグを起点にしても良かったのですが、後述するcssにmainを指定してしまうと思わぬところで依存関係ができてしまいそうでしたので、今回はお知らせ窓用に<div>
を作った形です。
また、<main>
タグの中に書くことで、どこのページでもお知らせ窓が表示されるようになります。
2.cssでお知らせ窓をいい感じにする
作成したお知らせ窓をcssでいい感じにデザインします。
これは一例なので、お好きにカスタマイズしてください。
(scssで記述していきます。)
main {
.main {
.flash-window {
// 窓の形、色、文字の形、色
height: 50px;
padding: 10px;
color: white;
background: black;
border-radius: 10px;
// 中のp要素(メッセージ)を上下左右中央揃え
display: flex;
justify-content: center;
align-items: center;
// 表示・非表示にかける時間
transition: 0.5s 0s ease;
// 窓の位置調整
position: fixed;
bottom: 40px;
left: 0;
right: 0;
margin: auto;
p {
color: white;
span {
margin-left: 1vw;
color: white;
&:hover {
cursor: pointer;
}
}
}
// 窓が非表示の時の状態
&.hidden {
opacity: 0;
bottom: -50px;
}
}
}
}
@media (min-width: PC, タブレット共通) {
main {
.main {
.flash-window {
// 大画面の時は窓の横幅は固定
width: 500px;
}
}
}
}
@media (min-width: スマホ) {
main {
.main {
.flash-window {
// 小画面の時は画面サイズの90%で可変
width: 90%;
p {
font-size: 4vw;
}
}
}
}
}
CSSセレクタのhidden
が付くと"非表示"と言いましたが、実際はbottom: -50px;
で要素が画面外に隠れているだけです。
さらにopacity: 0;
も指定しているので、hidden
の時は完全に透明になっている状態です。
なので画面内に現れる時は透明の状態から具現化し、画面外にいく(非表示になる)時は段々と透明になっていきます。
3.コントローラーでflash[:notice]
を作成する
今回はredirect後のフラッシュメッセージ(ログイン後TOPページにリダイレクトと同時にお知らせが出現など)を想定しています。
render後のフラッシュメッセージ(バリデーションエラーによるrender&エラーメッセージ)については最後に触れます。
...
def update
if current_user.update(user_params)
flash[:notice] = '会員情報を更新しました。'
redirect_to user_path(current_user)
else
render :edit
end
end
...
ここでは会員情報の更新時に会員詳細にリダイレクト&フラッシュメッセージの表示をする記述をしましたが、必要なところがあれば適宜行ってください。
基本的にはflash[:notice] = '内容'
でnoticeキーにフラッシュメッセージを格納したあとに、redirect_to
で任意のページに遷移することで、遷移先で先ほど格納したフラッシュメッセージが表示されるようになります。
ちなみに、deviseを使った新規登録・ログイン・ログアウトの処理はdevise内部で自動的にflash[:notice] = 'ログインしました。'
等が格納されるので、自分で記述する必要はありません。
4.お知らせ窓専用のjsファイルassets/javascripts/flash_window.js
を作成する
最後に専用のjsファイルを作成していきます。(既存のapplication.js
から切り分ける理由は後述します。)
addEventListener('load', ()=> {
const flashWindow = document.getElementsByClassName("flash-window")[0];
flashWindowToggle();
setTimeout(flashWindowToggle, 3000)
document.getElementsByClassName("flash-window--delete")[0].addEventListener('click', flashWindowToggle);
function flashWindowToggle() {
flashWindow.classList.toggle("hidden");
}
});
addEventListener
のイベントハンドラにload
を指定しています。
これはページの読み込みが全て完了した時点でイベントを発生させないと、ページが読み込み中(白紙)の時に先にお知らせウィンドウだけが表示されてしまうからです。
toggle
メソッドを使ったCSSクラスのhidden
の追加・削除は関数化しています。
この関数が呼び出されると、hidden
が削除されお知らせ窓が表示されます。
その後、この関数が3000ms(3秒)後に自動的に呼び出されるか、X(バツボタン)がクリックされると、hidden
が追加されます。
そして、このjsファイルはflash[:notice]
に値が入っている時だけ発火させたいので、設定を少し変えていきます。
...
//= stub flash_window #ここを追加
//= require_tree .
...
application.js
の冒頭の読み込み設定で//= stub flash_window
を書くことで、flash_window.js
がプリコンパイルで結合されるjsファイルから除外されます。
しかし、この状態ではflash_window.js
自体がプリコンパイルされないことになってしまうので、config/initializers/assets.rb
に以下を追記します。
...
# 以下を追加
Rails.application.config.assets.precompile += %w( flash_window.js )
...
これにより、flash_window.js
のプリコンパイルを明示できます。
しかし、このままではプリコンパイルはされてもapplication.js
には結合されていないので読み込まれません。
そこでapplication.html.erb
の<head>
タグ内にflash_window.js
の読み込みタグを直接追加する必要があります。
<head>
...
<%= javascript_include_tag 'application', defer: true %>
# 以下を追加
<% if flash[:notice] %>
<%= javascript_include_tag 'flash_window' %>
<% end %>
...
</head>
このようにapplication.js
からflash_window.js
を切り離すことでif文でflash[:notice]
がある時だけflash_window.js
が読み込まれるという動作を作ることができました。
defer: true
オプションを付けていない理由は、今回はflash_window.js
内のイベントがページ読み込み完了後であることをaddEventListener('load', ()=>{...});
という形で直接書いているためです。
このファイルに他のイベントを書く場合はdefer: true
が必要になるかも知れませんので、そこは適宜お願いします。
以上で、目的の動作が達成されます。
ここからは番外編です!
番外編
番外編その1.不要なフラッシュメッセージが表示される場合
deviseのcreate, update, destroyアクションにはほとんどの場合フラッシュメッセージflash[:notice]
が格納されているため、不要な場面でも窓が出てきてしまうことがあります。
その場合の対処法を書いていきます。
...
protected
...
# deviseで作成されたflashのリセット
def delete_devise_flash_messages
flash[:notice] = nil
end
application_controller.rb
でこのメソッドを定義し、不要なフラッシュメッセージが表示されるコントローラー内でbefore_action :delete_devise_flash_messages
またはafter_action :delete_devise_flash_messages
を書くとフラッシュメッセージflash[:notice]
が表示されなくなります。(:notice以外のキーは残ります。)
私の場合は新規登録の場面で利用しています。
入力内容の確認画面から認証メールの送信アクション、そして"メールを確認してください。"というページに遷移するのですが、そのページ内でメールについて詳しい説明を書いているのに、改めてフラッシュメッセージで"メールを確認してください"は重複するので不要です。
そこで下記のような記述をしています。
class Users::RegistrationsController < Devise::RegistrationsController
before_action :delete_devise_flash_messages, only: %i[email_notice]
# 入力された内容を確認するページ
def confirm
end
# 認証メールが送信されたことをお知らせするページ
def email_notice
end
# 登録が完了したことをお知らせするページ
def complete
end
end
これで認証メールの送信をお知らせするページemail_notice
アクションのみflash[:notice]
のリセットが働いてフラッシュメッセージが表示されなくなります。
番外編その2.エラーのフラッシュメッセージに対応する方法
ここまではflash[:notice]
に格納されたメッセージのみを取り扱ってきましたが、バリデーションで発生したエラーなども表示したい場合はflash[:alert]
を追加していきます。
さらにフラッシュメッセージごとに挙動を変更したい場合はflash[:error]
やflash[:success]
なんかも自作されてみるといいでしょう。
変更箇所は主に2カ所です。
...
<%= javascript_include_tag 'application', defer: true %>
<%# if文に || flash[:alert]を追加 %>
<% if flash[:notice] || flash[:alert] %>
<%= javascript_include_tag 'flash_window' %>
<% end %>
...
if文に|| flash[:alert]
キーを追加します。
これにより:noticeキーもしくは:alertキーに値があるとflash_window.js
が発火するようになります。
次に部分テンプレート内にもalertを追加をしていきます。
<div class="flash-window hidden">
<% if flash[:notice] %>
<p><%= notice %><span class="flash-window--delete">X</span></p>
<% end %>
<% if flash[:alert] %>
<p><%= alert %><span class="flash-window--delete">X</span></p>
<% end %>
</div>
部分テンプレートにもif文を書くことで表示や動作などの細かい部分を分けることができるようになります。
まとめ
今回ご紹介した物はベースとなると考えていますので、動作などはお好きなようにカスタムしていただければと思います^^
みなさまの参考になれば幸いです。
また、質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。
私のTwitterでも毎日このようなテクニックや感想・考察を発信していますので、もしご興味があれば一度覗いてみてください(´ー`)
Twitter - @Masao_Sasaki_ae
最後まで読んでいただきありがとうございました!
参考サイト
より実用的な使い方については、私のGitHubに実際に使っているファイルを公開しているのでこちらも参考にしていただければと思います!
GitHub - MasaoSasaki/matchi