🦅

【Rails & JavaScript】プレビュー機能

2023/09/24に公開

プレビュー機能は、記事によってアプローチが違うので少しでも参考になったらいいなと思います。
記載ミスや漏れ等がありましたら指摘いただければありがたいです。

  • 完成イメージ
    以下のようにpreviewボタンを押したらモーダルウィンドウが開き、プレビュー表示できるようにします。

流れ

  1. ルーティングの設定
  2. アクション実装(コントローラ)
  3. プレビューボタン & モーダルウィンドウ作成
  4. ビュー作成

テーブル

Postテーブルは参考までに。Postの新規投稿のときにプレビュー機能をつけます!

ルーティング設定

routes.rb
  :
  scope module: :public do
    resources :posts do
      collection do
        post :preview
      end
  :

解説:
プレビュー機能は、フォームを表示するだけで、データの実際の保存は行わないことが多いです。そのため、新たなHTTPメソッドとしてpostを使い、:previewというカスタムのアクション(コントローラ内のメソッド)を実行するためのルートを追加しています。
これにより、/posts/previewというURLに対してHTTP POSTリクエストを送信できます。

アクション実装(コントローラ)

posts_controller.rb
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
 :

解説:

  1. @preview_post = Post.new(post_params):
    post_params メソッドから送信されたフォームデータを使って、新しい投稿を作成します。

  2. @preview_post.user = current_user:
    プレビュー投稿にユーザー情報を関連付けます。

  3. @preview_tags = @preview_post.tag_list.clone:
    プレビュー用にタグのリストをコピーすることで、プレビュー表示にタグを含めることができます。

  4. @preview_post.tag_list = []:
    @preview_post という変数に関連づけられたタグ情報をすべて削除します。削除する理由は、プレビューの際に追加したタグを実際の投稿に反映させないようにするために、既存のタグ情報をクリアにしています。

  5. render partial: 'preview', locals: { post: @preview_post, preview_tags: @preview_tags }:
    preview という名前の部分テンプレートを表示します。そのビューに post という変数に @preview_post の値、preview_tags という変数に @preview_tags の値を渡してあげます。これにより、プレビュー表示をできるようにします。

プレビューボタン & モーダルウィンドウ作成

プレビューボタンをクリックするとモーダルウィンドウが開くようにしていきます!

https://getbootstrap.jp/docs/5.0/components/modal/

html
<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>
application.css
/* モーダルの背景 */
.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; } /* 透明に */
}

ビュー実装

部分テンプレートにプレビューで表示したい要素を記載します。
コードは参考までに。

ビューファイル
_preview.html.erb
<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>

こういう表示もつけるとそれっぽくなります!

html
<div class="preview-tag">Preview</div>
css
.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