素のRailsとJavascriptでのスライドショーの作り方
前回書いた通話機能は難しすぎたので
https://qiita.com/Reo-lab/items/90a037f5c18c027342d1
今回は、比較的簡単に作れるかっこいい(自分的に)
スライドショー機能を紹介していきたいと思います!
完成形
左右の画像をクリックすると画像が移り変わり、真ん中の画像をクリックすると拡大表示されます。
ER図
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">×</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で配列が循環するようにしています
気になる方はすぐ出てくるの調べてみてください
- images.lengthで負の値にならないように、
- slideIndex = (slideIndex - 1 + 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;
}
最後に
今回は、私のアプリを使った説明で少し不要な機能も多かったので、
最低限の実装コードも機会があったら作成していきたいなと思っています。
かっこいいスライドショーを作っていきましょう!