画像複数投稿機能(swiperの導入)
内容
画像複数投稿機能の実装
画像複数投稿機能
画像単一投稿から複数枚投稿に編集する
1. アソシエーションを変更
has_many_attachedメソッドを用いることで、投稿と画像の間に1対多の関係を設定
app/model/post.rb
class Post < ApplicationRecord has_many_attached :images end
2. フォームを編集
複数の画像を送信できるように、フォームを編集
単一画像の投稿では:imageとなっていたが、複数の画像に対応できるように編集し、複数投稿できるようにするため、multiple: trueを使用
app/views/posts/new.html.erb
<%= form_with model: @post_address, url: posts_path, local: true do |f| %> <%= f.file_field :images, multiple: true, class:"map-image", id:"map-image" %> <% end %>
3. ストロングパラメーターを編集
送信されてきたparamsをコントローラで受け取り、こちらも画像の配列を受け取れるように変更
app/controllers/posts_controller
private def post_params params.require(:post_address).permit(:map_link, {images: []}, :distance, :course, :slope, :traffic, :crowd, :view, :comment, :postal_code, :prefecture_code, :city, :street).merge(user_id: current_user.id) end
4. 保存した画像を表示できるようにする
app/views/posts/index.html.erb
<% post.images.each do |image| %> <%= image_tag image, class: 'post-image' if post.image.attached? %> <%= image_tag 'noimage.png', class: 'post-image' unless post.image.attached? %> <% end %>
5. スライダー機能「swiper」を導入
公式サイトを参考にswiperを導入する
swiper
やり方はいくつかあるが、今回はCDNでswiperを読み込む(単体で動くのでjQuery本体を読み込む必要はない)
application.html.erb
<head> <link rel="stylesheet" href="https://unpkg.com/swiper@7/swiper-bundle.min.css"/> <script src="https://unpkg.com/swiper@7/swiper-bundle.min.js"></script> </head>
6. HTMLを編集
公式サイトを引用し、必要なものを追加(今回スクロールバーは使わず)
一覧ページと詳細ページでスワイパーを使用するため、部分テンプレートに記述する
_swiper.html.erb
<% if post.images.length > 1 %> <div class='swiper'> <div class="swiper-wrapper"> <% post.images.each do |image| %> <div class="swiper-slide"> <%= image_tag image, class: 'swiper-image' if post.images.attached? %> </div> <% end %> </div> <div class="swiper-pagination"></div> <div class="swiper-button-prev"></div> <div class="swiper-button-next"></div> </div> <% elsif post.images.length == 1 %> <div class='back-image'> <% post.images.each do |image| %> <%= image_tag image, class: 'post-image' if post.images.attached? %> <% end %> </div> <% else %> <div class='back-image'> <%= image_tag 'noimage.png', class: 'post-image' unless post.images.attached? %> </div> <% end %>
上記の条件分岐により、投稿画像が2枚以上あればスワイパー機能を使用し、1枚以下であれば画像を貼り付けるだけとした
7. CSSを編集
swiper.cssを作成し、画像をスワイプできるように実装
swiper.css
/* どんな画像でも高さが一定になるように指定し、枠内に収まるように記述 */ .swiper-slide { height: 0; overflow: hidden; padding-bottom: 75%; background-color: #f2f2f2; position: relative; } .swiper-image { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin: auto; height: 100%; width: auto; } /* 矢印ボタンをわかりやすいものに変更(ネットで調べたものを引用) */ .swiper-button-prev { background: url(https://haniwaman.com/wp/wp-content/uploads/2018/05/swiper3.png) no-repeat center center / contain; } .swiper-button-next { background: url(https://haniwaman.com/wp/wp-content/uploads/2018/05/swiper4.png) no-repeat center center / contain; } /* デフォルトの矢印ボタンを消去(!importantは、cssのどこに記述しても優先順位が先になるという記述) */ .swiper-button-prev::after, .swiper-button-next::after { content: none !important; } /* 画像が1枚以下の時は以下の記述で、背景画像とその枠内に画像を収めるようにする */ .back-image { height: 0; overflow: hidden; padding-bottom: 75%; background-color: #f2f2f2; position: relative; } .post-image { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin: auto; height: 100%; width: auto; }
画像が2枚以上の時と1枚以下の時でclass名を同じにしたらビューが崩れてしまったため、別々にclassを指定し同じcssを記述した
親要素(div)が影響している?
8. jsを編集
swiper.jsを作成し、application.jsで読み込む
swiper.jsには公式サイトで記述されていたコードを引用し、使用する動作により修正を加える
application.js
require("../swiper")
swiper.js
// 公式サイトの記述だけでは矢印ボタンを押してもスワイプできなかったので1行目を追記 window.addEventListener('DOMContentLoaded', function(){ const swiper = new Swiper('.swiper', { // 画像をループさせる loop: true, // 何枚目の画像かわかるようにし、クリックによるページ遷移を有効にする pagination: { el: '.swiper-pagination', type: 'bullets', clickable: true, }, // ページ遷移させる navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev', }, }); });
所感
swiperもしくはslickを用いてスライダー機能の実装を試みた
ネットにはかなり情報があったが、バージョンが古かったり、自分のコードとは少し異なったりで、導入するのにかなり苦戦した
動かない時はなぜ動かないのかを理解できるようにならないと、今後も同じように時間がかかってしまうと思った
日々勉強
住所検索(ransack)
内容
投稿を住所で絞り込めるように実装する
住所検索機能
1. ransackで関連するモデルのカラムを検索する
フォームタグの要素名に、関連するモデル名関連するモデルのカラム名を指定する
postモデルに紐づくaddressモデルのカラム名(postal_code)を検索条件にしたい時、f.フォームヘルパー :要素名、の要素名をaddress_postal_code_eqとする
search.html.erb
<%= search_form_for @q, url: posts_path do |f| %> <%= f.text_field :address_postal_code_eq %>
2. ビューを編集
投稿一覧/詳細ページに住所を追加する
ページネーションのデザイン変更
ページネーション機能はkaminariというgemを用いて実装しているが、デザインがいまいちなので自分でカスタマイズしてみる
1. kaminariのviewファイルを作成
viewフォルダの中にkaminariのview一覧を作成
ターミナル
rails g kaminari:views default
2. 日本語化
kaminariのgemのデフォが英語になっているので、これを日本語環境に変える
config/application.rb
config.i18n.default_locale = :ja config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
一行目は、日本語環境を適用、二行目は複数のlacaleファイル(後述)が適用されるコード
次にconfig/localesフォルダーにkaminari.ja.ymlを作成し、コード記述
config/locales/kaminari.ja.yml
ja: views: pagination: first: "« 最初" last: "最後 »" previous: "‹ 前へ" next: "次へ ›" truncate: "..."
3. 該当のcssファイルを編集
index.css
.pagination{ display: flex; justify-content: flex-start; } .pagination span{ width: 3em; background-color: #fff; text-align: center; border: solid 1px #ddd; margin-right: 0.3vw; transition: .3s; -webkit-transform: scale(1); transform: scale(1); } .pagination span.current{ background-color: limegreen; color: #fff; } .pagination span:hover{ background-color: #ddd; transition: .3s; -webkit-transform: scale(1.1); transform: scale(1.1); }
4. 投稿件数を表示したい場合
投稿何件中何件表示、という表示をさせたい時は以下のように記述する
index.html.erb
<%= @posts.total_count %>件中<%= @posts.offset_value + 1 %>〜<%= @posts.offset_value + @posts.length %>件表示
次回
画像複数投稿機能を実装
Formオブジェクトのテストコード
内容
Formオブジェクトパターンのテストコードを実装する
テストコード
1. テストのファイルを作成
ターミナル
% rails g rspec:model post_address
2. FactoryBotを生成
3. テストコードを記述
4. テストコードを実行
ターミナル
% bundle exec rspec spec/models/post_address_spec.rb
Formオブジェクトの日本語化
独自に定義したFormオブジェクトを日本語化したい時
ja.ymlにてactiverecordアトリビュート以下に書いても反映されない
ja.yml
activerecord: attributes: post_address: postal_code: 郵便番号 prefecture_code: 都道府県 city: 市区町村 street: 番地
正しくは、activemodelアトリビュート以下に書く
ja.yml
activemodel: attributes: post_address: postal_code: 郵便番号 prefecture_code: 都道府県 city: 市区町村 street: 番地
次回
投稿を住所で絞り込めるように実装する
住所自動入力機能(jpostalとjp_prefectureの導入)
内容
住所登録/検索機能を実装する
住所登録機能
投稿時に住所(都道府県、市区町村)も一緒に登録できるようにする
postsテーブルにaddressesテーブルを紐付けるため、Formオブジェクトパターンを使用して投稿時に複数のテーブルに保存できるようにする
ちなみに入力は、jpostalとjp_prefectureというgemを用いて住所自動入力を実装する
1. jp_prefectureの導入
jquery-railsとjp_prefectureを導入する
今回は既にjQueryは導入済みのため省略
Gemfile
gem 'jp_prefecture'
ターミナル
% bundle install % rails s
2. モデルを作成し、マイグレーションファイルを編集
Postモデルは作成済みのため、Addressモデルのみ作成し、マイグレーションファイルを編集する
ターミナル
% rails g model address
db/migrate/**************_create_addresses.rb
create_table :addresses do |t| t.integer :postal_code, null: false t.integer :prefecture_code, null: false t.string :city, null: false t.string :street, null: false t.references :post, null: false, foreign_key: true t.timestamps end
ターミナル
% rails db:migrate
3. アソシエーションを記述
app/models/post.rb
has_one :address, dependent: :destroy
app/models/address.rb
belongs_to :post
4. モデルを編集
公式サイトを参考にモデルを編集する
JpPrefecture
address.rb
include JpPrefecture jp_prefecture :prefecture_code def prefecture_name JpPrefecture::Prefecture.find(code: prefecture_code).try(:name) end def prefecture_name=(prefecture_name) self.prefecture_code = JpPrefecture::Prefecture.find(name: prefecture_name).code end
5. 新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義
app/modelsディレクトリ配下にpost_address.rbを作成
post_address.rb
class PostAddress include ActiveModel::Model attr_accessor :map_link, :distance, :course, :slope, :traffic, :crowd, :view, :comment, :user_id, :postal_code, :prefecture_code, :city, :street with_options presence: true do validates :map_link, format: { with: /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/, message: 'はhttpもしくはhttpsで始まるURLで入力してください' } validates :distance, numericality: { greater_than: 0 } validates :course validates :slope validates :traffic validates :crowd validates :view validates :comment, length: { maximum: 140 } validates :user_id validates :postal_code, format: { with: /\A[0-9]{7}\z/ } validates :prefecture_code validates :city validates :street end end
postsテーブルに保存されるuser_idには、本来belongs_to :userのアソシエーションにより、バリデーションが設定されている
しかし、post_addressクラスにはアソシエーションを定義することはできないため、belongs_toによるバリデーションを行うことができない
そこで、post_addressクラスでuser_idに対してバリデーションを新たに設定
6. 元々記述していたpost.rbのバリデーションを削除
7. データをテーブルに保存する処理を記述
post_address.rb
class PostAddress # 略 def save post = Post.create(map_link: map_link, distance: distance, course: course, slope: slope, traffic: traffic, crowd: crowd, view: view, comment: comment, user_id: user_id) Address.create(postal_code: postal_code, prefecture_code: prefecture_code, city: city, street: street, post_id: post.id) end end
8. コントローラーを編集
postsコントローラーを以下のように編集する
posts_controller.rb
def new @post_address = PostAddress.new end def create @post_address = PostAddress.new(post_params) if @post_address.valid? @post_address.save redirect_to posts_path else render :new end end def post_params params.require(:post_address).permit(:map_link, :distance, :course, :slope, :traffic, :crowd, :view, :comment, :image, :postal_code, :prefecture_code, :city, :street).merge(user_id: current_user.id) end
valid?メソッドを使用しているのは、PostAddressクラスがApplicationRecordを継承していないことにより、saveメソッドにはバリデーションを実行する機能がないため
9. ビューを編集(新規投稿ページ)
生成したインスタンスをnew.html.erbにおいて利用
app/views/posts/new.html.erb
# 修正前 <%= form_with model: @post, local: true do |f| %> # 修正後 <%= form_with model: @post_address, local: true do |f| %> # 下記追加 <div class="weight-bold-text"> 郵便番号(ハイフンなし) <span class="indispensable">必須</span> </div> <%= f.text_field :postal_code, autocomplete: 'postal_code', class: "form-control", id: 'postal_code' %> <div class="weight-bold-text"> 都道府県 <span class="indispensable">必須</span> </div> <%= f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :code, :name, { prompt: '選択してください' }, class: 'form-control', id: 'prefecture_code' %> <div class="weight-bold-text"> 市区町村 <span class="indispensable">必須</span> </div> <%= f.text_field :city, autocomplete: 'city', class: "form-control", id: 'city' %> <div class="weight-bold-text"> 番地 <span class="indispensable">必須</span> </div> <%= f.text_field :street, autocomplete: 'street', class: "form-control", id: 'street' %>
10. jquery.jpostal.jsを導入
公式GitHubを参考に導入
jquery.jpostal.js
設置方法は2種類あり、自分のサーバに郵便データを設置する方法としない方法がある
jpostal-1006.appspot.comで公開しているので、jquery.jpostal.jsやjson/*.jsonを設置する必要がない、サイト運営者の定期的な郵便データ更新作業も必要ない、と記載されているので今回は自分のサーバにはデータを設置しない方法で導入する(おそらくこちらの方が楽)
まず、jquery本体とjquery.jpostal.jsをインクルードする(今回はjqueryは導入済みのためインクルードの必要はなかった)
application.html.erb
<head> <script type="text/javascript" src="//jpostal-1006.appspot.com/jquery.jpostal.js"></script> </head>
app/javascript配下にaddress_autofill.jsを作成し、コードを記述
app/javascript/address_autofill.js
$(function(){ $('#postal_code').jpostal({ postcode: ['#postal_code'], address: { '#prefecture_code': '%3', '#city': '%4', '#street': '%5%6%7', } }); });
公式サイトではjQueryのreadyメソッドを使用しているが、現在は非推奨となっているため修正している
最後にapplication.jsでaddress_autofill.jsを読み込む
app/javascript/packs/address_autofill.js
require("../address_autofill")
以上で導入完了
11. Formオブジェクトのedit/update
ここは難しく説明ができないため割愛
12. Formオブジェクトのテストコード
所感
理解できていないコードを写すのはよくないと痛感。。。 でもできることも増やしていかないといけないので、やりながら理解していくしかない!
railsの日本語化
内容
投稿一覧ページとは別に、トップページを実装する
rails日本語化
1. エラーメッセージを日本語化
時刻を日本に合わせたため、ついでにエラーメッセージも日本語化する(application.rbのみ編集済み)
日本語に対応するgemを導入
Gemfile
gem 'rails-i18n' #devise日本語化 gem 'devise-i18n'
ターミナル
% bundle install % rails s % rails g devise:i18n:locale ja
config/localesディレクトリにファイルが作成され、自動的に日本語化してくれている
しかしこのままではGem 'rails-i18n'やdevise.ja.ymlを導入したことによって使える日本語しか含まれていないので、deviseのデフォルト以外の文字は英語で表示されてしまう
したがって、これらを翻訳するファイルを手動で作成
config/localesディレクトリに、ja.ymlというファイルは既に作成しているので、そこに追記していく
config/locales/ja.yml
例)以下のように日本語化されていない単語を日本語登録する ja: activerecord: attributes: user: nickname: ニックネーム post: comment: コメント comment: comment: コメント
モデルのバリデーションのメッセージも日本語にしておく
user.rb
# 例) validates :password, format: { with: /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i, message: 'は半角英数字混合で入力してください' }
2. テストコードを修正
テストコードのエラーメッセージの部分を日本語に修正
user_spec.rb
# 元 expect(@user.errors.full_messages).to include("Nickname can't be blank") # 日本語化 expect(@user.errors.full_messages).to include("ニックネームを入力してください")
修正できたらテストを実行して確かめる
ターミナル
% bundle exec rspec spec/models/user_spec.rb
コメント機能
Action Cableを用いてコメントの即時更新機能を実装する
理解に時間がかかりそうだったのと、このアプリにおいては必須ではないため、即時更新機能は保留とすることにした
トップページ機能
1. ルーティングを設定
ルートパスをindexからtopに変更する
routes.eb
root to: 'posts#top'
2. コントローラーを編集
postsコントローラーでtopアクションを定義
posts_controller.rb
before_action :authenticate_user!, except: %i[top index show search] def top end
3. ビューを編集
これまでroot_pathはindexアクションを呼び出していたが現在はtopアクションに変更されている
投稿が完了した時等の何かしらのアクションを起こした後は、indexアクションを呼び出し投稿一覧ページを表示させるようにする
root_path→posts_pathに修正
また、top.html.erbを作成し、ログインボタン等を記述
4. application_controller.rbを編集
deviseはサインアップやサインインした時、デフォルトでルートパスに遷移するように設定されている
トップページからログインすると投稿一覧ページに遷移させたいため、コントローラーを編集する
application_controller.rb
def after_sign_in_path_for(resource) posts_path end def after_sign_up_path_for(resource) posts_path end
次回
住所登録/検索機能を実装する
参考
CSSでボタンクリックの動きをつけたい時
押し込み式ボタンを作成した時、最初は以下のように記述していた
top.css
.top-btn { border-bottom: 0.2rem solid #800000; } .top-btn:active { transform: translateY(0.2rem); border-bottom: none; }
しかし、これだと前後の要素も一緒に動いてしまった
marginの相殺やらが悪さするらしいが、今回はmarginを特に指定していない
translateYを用いるとボタンの下がすべて動いてしまうのか?
いまいちわからないが以下のように編集すると問題なく動いた
top.css
.top-btn { position: relative; box-shadow: 0 0.2rem #800000; } .top-btn:active { top: 0.2rem; box-shadow: none; }
コメント機能
内容
投稿詳細ページでコメント機能を実装する
コメント機能
1. モデルを作成
Commentモデルを作成し、マイグレーションファイルを編集する
ターミナル
% rails g model comment
db/migrate/20XXXXXXXXXXXX_create_comments.rb
t.text :comment t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true
ターミナル
% rails db:migrate
続いてアソシエーションを記述
app/models/comment.rb
belongs_to :user belongs_to :post
app/models/user.rb
has_many :comments, dependent: :destroy
app/models/post.rb
has_many :comments, dependent: :destroy
2. ルーティングを設定
コメントは投稿に紐付くのでルーティングにネストを利用する
routes.rb
resources :posts, except: :index do resources :comments, only: %i[create destroy] end
※いいね機能の時はネストをresource :likeで記述してよかった
→なぜなら1件の投稿に対していいねは1回しかできないため、idを指定しなくても投稿に関連したいいねを削除できるから
※一方、コメント機能ではresources :commentsとしなければならない
→なぜなら1件の投稿に対してコメントは何回でもでき、それを削除したい場合どのコメントを消すかidを指定しなければならないから
3. コントローラーを作成
commentsコントローラーを作成し、createアクションとdestroyアクションを定義
ターミナル
% rails g controller comments
app/controllers/comments_controller.rb
before_action :post_find, only: %i[create destroy] def create @comment = @post.comments.new(comment_params) if @comment.save redirect_to post_path(@post.id) else @comments = @post.comments.includes(:user) render post_path(@post.id) end end def destroy @comment = @post.comments.find(params[:id]) @comment.destroy redirect_to post_path(@post.id) end private def post_find @post = Post.find(params[:post_id]) end def comment_params params.require(:comment).permit(:comment).merge(user_id: current_user.id) end
4. ビューを編集
投稿詳細ページにコメント投稿フォーム、およびコメント表示欄を作成
app/views/posts/show.html.erb
<%= form_with(model: [@post, @comment], local: true) do |f| %> <%= f.text_area :comment, placeholder: "コメントする", maxlength: "140" %> <%= f.submit "送信" %> <% end %> <% @comments.each do |comment| %> <p> <strong><%= link_to comment.user.nickname, user_path(comment.user.id) %>:</strong> <%= comment.comment %> <strong>[<%= link_to "削除", post_comment_path(post_id: @post.id, id: comment.id), method: :delete, data: {confirm: "コメントを削除してもよろしいですか?"} %>]</strong> </p> <% end %>
5. Postコントローラーを編集
app/controllers/posts_controller.rb
def show @comment = Comment.new @comments = @post.comments.includes(:user) end
6. ビューの微修正
コメント欄に投稿時刻等を記述
時刻の設定には、Railsのapplication.rbという設定ファイルを扱う
config/application.rb
class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 config.i18n.default_locale = :ja config.time_zone = 'Tokyo'
ja.ymlファイルを作成して、表示する時刻のフォーマットを設定
config/locales/ja.yml
ja: time: formats: default: "%Y/%m/%d %H:%M:%S"
この時刻設定を元に、lメソッドを利用して、表示する時刻へ反映
show.html.erb
<%= l comment.created_at %>
所感
もっとコードを簡潔にかけるかもしれないが、今の私にとってはこれが精一杯
次回
コメント機能の非同期化、もしくは住所登録/検索機能を実装する
ページネーション機能(kaminariの導入)
内容
コメント機能の前に、一覧ページでページネーション機能を実装する
ページネーション機能
1. kaminariの導入
railsでページネーションを実装するにはkaminariというgemが便利
gemfile
gem 'kaminari'
ターミナル
% bundle install % rails s
2. ビューでページネーションを表示
コントローラーを以下のように編集
posts_controller.rb
def index # ページネーションをつけたいデータに.page(params[:page])を追加 @posts = @q.result(distinct: true).includes(:user).page(params[:page]) end
続いてビューを以下のように編集
index.html.erb
# ページネーションを表示させたいところに<%= paginate @posts %>を追加 <%= paginate @posts %>
これで実装完了!簡単!
ページネーションが1ページに表示するレコード数はデフォルトで25件
よってレコード数が26件以上あれば、ページネーションが表示される
3. ページに表示するレコード数を変更したい時
1ページに表示するレコード数はデフォルトで25件のため、このレコード数を変更したい場合は以下のように記述
posts_controller.rb
# # .page(params[:page])の後に.per(10)を追加→数値によってレコード数を変更できる @posts = @q.result(distinct: true).includes(:user).page(params[:page]).per(10)
4. ページネーションのCSSを編集
ここではまずbootstrapを導入してからCSSを編集する
gemfile
gem 'bootstrap', '~> 5.1.0' # bootstrapはjQueryに依存するため以下も追加(既に導入してるので今回は飛ばす) gem 'jquery-rails'
sprockets-railsがv2.3.2.以上であるか確認してからインストール
% bundle show |fgrep sprockets-rails * sprockets-rails (3.2.2) % bundle install % rails s
その後、application.scssやapplication.jsを編集し、rails g kaminari:views bootstrap4(bootstrap5はなぜかなかった)を実行することでビューは編集できたのだが、ページネーション以外のビューが崩れてしまったため断念
bootstrapによるビューの修正は後回しとする
次回
投稿詳細ページでコメントのやり取りができるコメント機能の実装
参考
スクロール機能
サイドバーとメインコンテンツをdisplay: flex;で分けていて、別々にスクロールしたい場合、以下のように記述する
# 親要素にheightを指定 .contents { display: flex; height: 100vh; } # 子要素それぞれにoverflow-yを指定 .side-bar { overflow-y: auto; } .main { overflow-y: auto; }
これで別々にスクロールが可能となる