1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Railsでのかっこいいスライドショーを作ろう!

Posted at

素のRailsとJavascriptでのスライドショーの作り方

前回書いた通話機能は難しすぎたので
https://qiita.com/Reo-lab/items/90a037f5c18c027342d1

今回は、比較的簡単に作れるかっこいい(自分的に)
スライドショー機能を紹介していきたいと思います!

完成形

Animation.gif

左右の画像をクリックすると画像が移り変わり、真ん中の画像をクリックすると拡大表示されます。

ER図

スクリーンショット (474).png
SlideImagesにActiveStorageでpositionと一緒に画像を1枚保存します。
それをUsers_slidesに一対多で紐づけて、写真を複数枚保存していきます。

構成

  • Model
    • Users_slides
    • SlideImages
  • Controller
    • UsersSlidesController
  • View
    • users/show.html.erb
    • users_slides/edit.html.erb

今回のアプリでは、ユーザーのプロフィール画面(users/show)にスライドショーがあります。

作っていこう!

ActiveStorageを使用するので、入っていない方は
bin/rails active_storage:install
bin/rails db:migrate
で導入しましょう。

モデルの作成

migration.rb
# CreateUsersSlides
class CreateUsersSlides < ActiveRecord::Migration[7.0]
  def change
    create_table :users_slides do |t|
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
migration.rb
# CreateSlideImages
class CreateSlideImages < ActiveRecord::Migration[7.0]
  def change
    create_table :slide_images do |t|
      t.references :users_slide, null: false, foreign_key: true
      t.integer :position

      t.timestamps
    end
  end
end
  • UserSlidesではUserとの紐づけ
  • SlideImagesでusers_slideとの紐づけとpositionカラムを作成しています

モデルに関連付けを追加

model.rb
# User
class User < ApplicationRecord
  has_many :users_slides, dependent: :destroy
  has_many :slide_images, through: :users_slides
end
model.rb
# UsersSlide
class UsersSlide < ApplicationRecord
  belongs_to :user
  has_many :slide_images, dependent: :destroy
end
model.rb
# SlideImage
class SlideImage < ApplicationRecord
  belongs_to :users_slide
  has_one_attached :image
end
  • User
    • UsersSlideを通じたSlideImageとの関連付けを追加します
  • UsersSlide
    • Userモデルとの関連付け、SlideImageとの一対多の関連付けをします。
  • SlideImage
    • UsersSlideとの関連付け
    • has_one_attached :image
      • ActiveStorageを使用して、SlideImageモデルに1つの画像を添付できるようにしています。

Routes

resources :users do
  resources :users_slides
end

今回は、ユーザに紐づいてスライドがあるのでこんな感じです。

View

#users/show.html.erb
<% if @user.slide_images.exists?%>
  <div id="slideshow-container">
    <div class="slide-wrapper">
      <!-- 左側の小さい画像 -->
      <div class="small-slide left">
        <%= image_tag @user.slide_images.last.image, class: "small-icon" if @user.slide_images.any? %>
      </div>
      <!-- 中央の大きな画像 -->
      <div class="main-slide">
        <%= image_tag @user.slide_images.first.image, class: "main-icon" if @user.slide_images.any? %>
      </div>
      <!-- 右側の小さい画像 -->
      <div class="small-slide right">
        <%= image_tag @user.slide_images.second.image, class: "small-icon" if @user.slide_images.count > 1 %>
      </div>
    </div>
  </div>
  <% if @users_slide.present?  && @user == current_user %>
    <%= link_to "Edit Slide", edit_user_users_slide_path(@user, @users_slide) %>
  <% else %>
    <p></p>
  <% end %>
<% else %>
  <% if @user == current_user  %>
  <h1>スライドショーを設定しましょう!</h1>
  <%= form_with model: @users_slide, url: user_users_slides_path(@user) , local: true do |form| %>
    <% (1..3).each do |i| %>
      <div>
        <%= form.label "images_#{i}", "スライドショー画像 #{i}", for: "users_slide_images_#{i}" %>
        <%= form.file_field "users_slide[images][]", id: "users_slide_images_#{i}", multiple: false %> <!-- 画像は一つずつ選択 -->
        <%= form.label "position#{i}", "Position", for: "users_slide_positions_#{i}" %>
        <%= form.number_field "users_slide[positions][]", id: "users_slide_positions_#{i}", value: i %> <!-- デフォルトでiを設定 -->
      </div>
    <% end %> 
    <div>
      <%= form.submit "スライドショーを設定する" %>
    </div>
  <% end %>
  <% end %>
<% end %>
  • if @user.slide_images.exists? の部分がスライドショーを表示する部分。
  • else 以降がスライドショーがない場合の作成フォームです。
    <% (1..3).each do |i| %>とすることで、positionを1.2.3に分けて3つの画像フォームを作成します。
#users/show.html.erb
<!-- モーダル -->
<div id="image-modal" class="modal-slide">
  <span class="close">&times;</span>
  <img class="modal-slide-content" id="modal-image">
</div>
  • 中心画像をクリックしたときに表示されるモーダルです
  • スライドショーがあるページのどこかに追加しましょう
# users_slides/edit.html.erb
<%= form_with(model: @users_slide, url: user_users_slide_path(current_user, @users_slide), local: true) do |form| %>
  <% @users_slide.slide_images.each_with_index do |slide_image, index| %>
    <div>
      <%= slide_image.image.attached? ? image_tag(slide_image.image,class: "user-icon") : "No Image" %>
      <%= form.hidden_field "slide_images[][id]", value: slide_image.id %>
      <%= form.file_field "slide_images[][image]", id: "slide_image_#{index}" %>
      <%= form.number_field "slide_images[][position]", value: slide_image.position %>
      <%= hidden_field_tag "slide_images[][id]", slide_image.id %>
    </div>
  <% end %>
  <div>
    <%= form.submit "Update Slides" %>
  </div>
<% end %>
  • スライドショーの編集画面です。

Controller

# UsersController
class UsersController < ApplicationController
  def show
    @users_slide = @user.users_slides.first  #追加
  end
end
  • users/showで表示しているのでusers_slideを追加しています
# UsersSlidesController
class UsersSlidesController < ApplicationController
  before_action :set_user
  before_action :set_user_slides

  def create
    @users_slide = current_user.users_slides.new
    Rails.logger.debug "Params: #{params.inspect}"
    if @users_slide.save
      create_slide_images
      redirect_to user_path(current_user), notice: 'Slide show was successfully created.'
    else
      render :new
    end
  end

  def update
    Rails.logger.debug "Params: #{params.inspect}"
    slide_images_params.each do |slide_image_param|
      slide_image = SlideImage.find(slide_image_param[:id])
      slide_image.image.attach(slide_image_param[:image]) if slide_image_param[:image].present?
      slide_image.update(position: slide_image_param[:position])
    end
    redirect_to @user, notice: 'Slides updated successfully.'
  end

  def destroy
    @users_slide = @user.users_slides
    @users_slide.destroy
    redirect_to user_users_slides_path(@user), notice: 'Slide show was successfully deleted.'
  end

  private

  def set_user
    @user = current_user
  end

  def set_user_slides
    @users_slide = @user.users_slides.includes(slide_images: { image_attachment: :blob }).first
  end

  def users_slide_params
    params.require(:users_slide).permit(images: [], positions: [])
  end

  def slide_images_params
    params.require(:users_slide).permit(slide_images: %i[id image position])[:slide_images]
  end

  def create_slide_images
    (0..2).each do |index|
      position = params[:users_slide][:positions][index].to_i
      image = get_image_for_slide(index)

      if image.present?
        create_slide_image(image, position)
      else
        attach_default_image(position)
      end
    end
  end

  def get_image_for_slide(index)
    params[:users_slide][:images].present? ? params[:users_slide][:images][index] : nil
  end

  def create_slide_image(image, position)
    @users_slide.slide_images.create(image:, position:)
  end

  def attach_default_image(position)
    default_image_file = load_default_image
    @users_slide.slide_images.create(
      image: {
        io: default_image_file,
        filename: 'GameFriend.jpg',
        content_type: 'image/jpeg'
      },
      position:
    )
    default_image_file.close
  end

  def load_default_image
    default_image_path = Rails.root.join('app/assets/images/GameFriend.jpg')
    File.open(default_image_path)
  end
end

  • めちゃくちゃdefが多いなって感じますがLintチェックでコードが長すぎて警告されたので分割されまくってます。
    • 分割しないとcreateはこんな感じです
    def create
    @users_slide = current_user.users_slides.new
    Rails.logger.debug "Params: #{params.inspect}" 
    if @users_slide.save
      # スライドショー画像の配列をチェックして、選択されていない部分にデフォルトの画像を設定
      (0..2).each do |index|
        position = params[:users_slide][:positions][index].to_i    
        # 画像が提供されているか確認し、params[:users_slide][:images]が存在するか確認
        if params[:users_slide][:images].present? && params[:users_slide][:images][index].present?
          image = params[:users_slide][:images][index]
          @users_slide.slide_images.create(image: image, position: position)
        else
          # デフォルト画像を指定してActiveStorageで扱える形式に変換
          default_image_path = Rails.root.join("app/assets/images/GameFriend.jpg")
          default_image_file = File.open(default_image_path)
          # ActiveStorageで添付する場合はioとfilenameを明示する
          @users_slide.slide_images.create(
            image: {
              io: default_image_file, 
              filename: 'GameFriend.jpg',
              content_type: 'image/jpeg'  # 適切なMIMEタイプを指定
            }, 
            position: position
          )
          # ファイルを明示的に閉じる(セキュリティとパフォーマンスのため)
          default_image_file.close
        end
      end
      redirect_to user_path(current_user), notice: 'Slide show was successfully created.'
    else
      render :new
    end
    end
    
    • @users_slide = current_user.users_slides.new
      if @users_slide.save
      • ここでusers_slidesを作ってしまい、その後slide_imagesにpositionと画像を保存していくという流れです。
    • ユーザーが何も画像を指定しなかったら、defaultの画像が挿入されて作成されるようにしています

Javascript

document.addEventListener('turbo:load', function() {
  let slideIndex = 0; // 現在のスライドインデックス
  const images = <%= @user.slide_images.map { |slide| url_for(slide.image) }.to_json.html_safe %>;
  const mainSlide = document.querySelector(".main-slide img");
  const leftSlide = document.querySelector(".small-slide.left img");
  const rightSlide = document.querySelector(".small-slide.right img");

  const modal = document.getElementById("image-modal");
  const modalImage = document.getElementById("modal-image");
  const closeModal = document.querySelector(".close");

  // スライドを更新する関数
  function updateSlides() {
    mainSlide.src = images[slideIndex];
    leftSlide.src = images[(slideIndex - 1 + images.length) % images.length];
    rightSlide.src = images[(slideIndex + 1) % images.length];
  }
  // 画像をクリックするとモーダルを表示
  mainSlide.addEventListener("click", function() {
    modalImage.src = mainSlide.src; // データ属性に大きい画像URLを設定
    modal.style.display = "block";
  });
  // モーダルを閉じる
  closeModal.addEventListener("click", function() {
    modal.style.display = "none";
  });
  // モーダル外をクリックした場合も閉じる
  window.addEventListener("click", function(event) {
    if (event.target === modal) {
      modal.style.display = "none";
    }
  });
  // 左右スライドのクリックイベント
  leftSlide.addEventListener("click", function() {
    slideIndex = (slideIndex - 1 + images.length) % images.length;
    updateSlides();
  });
  rightSlide.addEventListener("click", function() {
    slideIndex = (slideIndex + 1) % images.length;
    updateSlides();
  });
  // 初期スライドの表示
  updateSlides();
});
  • 案外短いです
    • slideIndex = (slideIndex - 1 + images.length) % images.length;
      これなんだよって感じるかもしれませんが、
      • images.lengthで負の値にならないように、
        % images.lengthで配列が循環するようにしています
        気になる方はすぐ出てくるの調べてみてください

完成!!

ここまででスライドショーが動くようになるはずです
あとは見た目を好きなようにいじりましょう!
参考までに私のCSSを張っておきます

/*スライドショー*/
#slideshow-container {
  width: calc(100% - 15vh);
  height: auto;  
  position: relative;
  margin: auto;
  overflow: hidden;
  background-color: rgba(0, 0, 0, 0.6); /* 背景を半透明に */
}

.slide-wrapper {
  display: flex;
  justify-content: center; /* 中央揃え */
  align-items: center;     /* 垂直方向の中央揃え */
  gap: 10px;               /* 画像間に隙間を設定 */
}

.main-slide {
  flex: 2;                 /* メインの画像を大きく表示 */
  text-align: center;
}

.small-slide {
  flex: 1;                 /* 小さい画像のスペースを小さく */
  text-align: center;
}

.prev:hover, .next:hover {
  background-color: rgba(0,0,0,0.8);
}

.main-icon{
  width: 100%;  /* 画像の幅を全体に合わせる */ /* 画像の幅を指定 */
  height: 100%; /* 画像の高さを指定 */
  object-fit: contain;
}

.small-icon {
  width: 100%;             /* 小さい画像の幅をコンテナに合わせる */
  max-height: 150px;       /* 小さい画像の高さを設定 */
  object-fit: contain;       /* 画像の比率を保つ */
  opacity: 0.7;            /* 少し透明にして目立たないように */
  cursor: pointer;         /* マウスカーソルを変更(クリック可能に見せる) */
}

.small-icon:hover {
  opacity: 1;              /* ホバー時に画像を目立たせる */
}

.modal-slide {
  display: none; /* 非表示がデフォルト */
  position: fixed;
  z-index: 1000;
  padding-top: 60px;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgba(0, 0, 0, 0.8); /* 背景を半透明に */
}

/* モーダルの画像 */
.modal-slide-content {
  margin: auto;
  display: block;
  max-width: 80%;
  object-fit: contain;
  background-color: transparent; /* 画像の周りの背景を透明に */
}

/* 閉じるボタン */
.close {
  position: absolute;
  top: 15px;
  right: 35px;
  color: #fff;
  font-size: 40px;
  font-weight: bold;
  cursor: pointer;
}

.close:hover,
.close:focus {
  color: #bbb;
  text-decoration: none;
  cursor: pointer;
}

最後に

今回は、私のアプリを使った説明で少し不要な機能も多かったので、
最低限の実装コードも機会があったら作成していきたいなと思っています。
かっこいいスライドショーを作っていきましょう!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?