【Rails & JavaScript】プレビュー機能
プレビュー機能は、記事によってアプローチが違うので少しでも参考になったらいいなと思います。
記載ミスや漏れ等がありましたら指摘いただければありがたいです。
- 完成イメージ
以下のようにpreviewボタンを押したらモーダルウィンドウが開き、プレビュー表示できるようにします。
流れ
- ルーティングの設定
- アクション実装(コントローラ)
- プレビューボタン & モーダルウィンドウ作成
- ビュー作成
テーブル
Postテーブルは参考までに。Postの新規投稿のときにプレビュー機能をつけます!
ルーティング設定
:
scope module: :public do
resources :posts do
collection do
post :preview
end
:
解説:
プレビュー機能は、フォームを表示するだけで、データの実際の保存は行わないことが多いです。そのため、新たなHTTPメソッドとしてpost
を使い、:preview
というカスタムのアクション(コントローラ内のメソッド)を実行するためのルートを追加しています。
これにより、/posts/preview
というURLに対してHTTP POSTリクエストを送信できます。
アクション実装(コントローラ)
class Public::PostsController < ApplicationController
before_action :authenticate_user!, only: [:preview]
:
def preview
@preview_post = Post.new(post_params)
@preview_post.user = current_user
@preview_tags = @preview_post.tag_list.clone
@preview_post.tag_list = []
render partial: 'preview', locals: { post: @preview_post, preview_tags: @preview_tags }
end
:
解説:
-
@preview_post = Post.new(post_params)
:
post_params
メソッドから送信されたフォームデータを使って、新しい投稿を作成します。 -
@preview_post.user = current_user
:
プレビュー投稿にユーザー情報を関連付けます。 -
@preview_tags = @preview_post.tag_list.clone
:
プレビュー用にタグのリストをコピーすることで、プレビュー表示にタグを含めることができます。 -
@preview_post.tag_list = []
:
@preview_post
という変数に関連づけられたタグ情報をすべて削除します。削除する理由は、プレビューの際に追加したタグを実際の投稿に反映させないようにするために、既存のタグ情報をクリアにしています。 -
render partial: 'preview', locals: { post: @preview_post, preview_tags: @preview_tags }
:
preview
という名前の部分テンプレートを表示します。そのビューにpost
という変数に@preview_post
の値、preview_tags
という変数に@preview_tags
の値を渡してあげます。これにより、プレビュー表示をできるようにします。
プレビューボタン & モーダルウィンドウ作成
プレビューボタンをクリックするとモーダルウィンドウが開くようにしていきます!
<button id="preview-button" type="button" class="preview-btn ml-3" data-toggle=tooltip data-placement="bottom" title="プレビューを表示">Preview</button>
<!-- プレビューのモーダルウィンドウ -->
<div id="preview-modal" class="modal">
<div class="preview-modal-content"></div>
<button id="close-preview-modal" class="close-button">×</button>
</div>
<script>
$(document).ready(function() {
// プレビューボタンがクリックされたときの処理
$('#preview-button').click(function() {
// フォームデータを収集
var formData = {
link: $('#post_link').val(),
tag_list: $('#post_tag_list').val(),
title: $('#post_title').val(),
body: $('#post_body').val()
};
// AJAXリクエストを送信してプレビューを取得
$.ajax({
url: '<%= preview_posts_path %>', // プレビューページのURL
type: 'POST',
data: { post: formData }, // フォームデータをPOSTリクエストで送信
success: function(response) {
// プレビューモーダル内のコンテンツを更新
$('#preview-modal .common-modal-content').html(response);
// プレビューモーダルを表示
$('#preview-modal').addClass('fade-in').show();
}
});
});
// プレビューモーダルがクリックされたときの処理
$('#preview-modal').on('click', function(event) {
// モーダルの背景をクリックした場合、プレビューモーダルを閉じる
if (event.target === this) {
closePreviewModal();
}
});
// プレビューモーダルの閉じるボタンがクリックされたときの処理
$('#close-preview-modal').on('click', function(event) {
event.preventDefault();
event.stopPropagation();
// プレビューモーダルを閉じる
closePreviewModal();
});
// プレビューモーダルを非表示にする関数
function closePreviewModal() {
// フェードアウトのアニメーションを追加
$('#preview-modal').addClass('fade-out');
// 一定時間後にモーダルを非表示にし、アニメーションクラスを削除
setTimeout(function() {
$('#preview-modal').hide().removeClass('fade-out');
}, 300); // アニメーションの時間に合わせて調整
}
});
</script>
/* モーダルの背景 */
.modal {
display: none; /* 初期状態では非表示 */
position: fixed;
z-index: 1000; /* モーダルを最前面に表示 */
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* 半透明な背景色 */
}
/* モーダルコンテンツ */
.preview-modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 80%;
width: 500px;
overflow-y: auto; /* コンテンツがはみ出た場合にスクロールバーを表示 */
max-height: 600px;
}
/* モーダルの閉じるボタン */
.close-button {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 50px;
color: #999;
background-color: transparent;
border: none;
transition: color 0.3s;
}
.close-button:hover {
color: #333;
}
/* モーダルのアニメーション */
.modal.fade-in {
animation: fadeIn 0.3s ease-in; /* フェードインのアニメーション */
}
.modal.fade-out {
animation: fadeOut 0.3s ease-out; /* フェードアウトのアニメーション */
}
@keyframes fadeIn {
from { opacity: 0; } /* 透明から不透明に */
to { opacity: 1; } /* 不透明に */
}
@keyframes fadeOut {
from { opacity: 1; } /* 不透明から透明に */
to { opacity: 0; } /* 透明に */
}
ビュー実装
部分テンプレートにプレビューで表示したい要素を記載します。
コードは参考までに。
ビューファイル
<div class="youtube-container-fluid">
<div class="row">
<div class="col-md-12 justify-content-center">
<% video_id = extract_youtube_video_id(@preview_post.link) %>
<% if video_id.present? %>
<div class="show-thumbnail-container">
<%= youtube_thumbnail(video_id, size: '1280x720', style: 'width: 65%; height: auto;') %>
<div class="preview-tag">Preview</div>
</div>
<% else %>
<p>表示ができません</p>
<% end %>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article class="post">
<div class="tags">
<% if preview_tags.present? %>
<div class="d-flex flex-wrap">
<% preview_tags.each do |tag| %>
<span class="badge badge-info mr-2 mb-2">
<%= "#{tag}" %>
</span>
<% end %>
</div>
<% else %>
<p>登録されているタグはありません</p>
<% end %>
</div>
<div class="show-post-title-container d-flex justify-content-between">
<h3 class="show-post-title"><%= @preview_post.title %></h3>
</div>
<div class="post-meta">
<div class="user-info d-flex justify-content-between">
<div class="d-flex align-items-center">
<%= user_icon_or_youtube(@preview_post.user, size: '20x20', class: 'rounded-circle ml-3') %>
<p class="user-nickname ml-1"><%= @preview_post.user.nickname %></p>
</div>
</div>
</div>
<div class="post-body" id="dynamicSize">
<%= @preview_post.body %>
</div>
</article>
</div>
</div>
</div>
こういう表示もつけるとそれっぽくなります!
<div class="preview-tag">Preview</div>
.preview-tag {
position: absolute; /* 要素を絶対位置指定で配置してあげる */
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 3px;
border-radius: 5px;
}
UX向上のためにも入れたい機能のひとつでした!
Discussion