#はじめに
ポートフォリオとして作っていたグループチャットアプリで、画像を複数投稿できるようにしました!
プログラミングを学習し始めて2カ月ほどの初学者ですので、間違っていることもあるかと思います。
ご参考程度に見ていただけると幸いです。
(もっとかっこいいコードの書き方をご存知の方、教えていただけると嬉しいです!)
- 画像のアップロードには
gem
のCarrierWave
とMiniMagick
を使用。 - 画像は複数枚アップロードできる。
- テーブル同士の関係は以下の通り。(実際のアプリからは簡略化しています)
##開発環境
ruby 2.5.1
Rails 5.2.4.2
Haml 5.1.2
#画像を複数投稿する方法
まず、どのような方法があるか調べました。3つありました。(他にもあるかもです)
-
gem
のCarrierWave
が用意している"Multiple file uploads"
を使う。 -
form object
を使う。 **accepts_nested_attributes_forメソッドを使う。**
結論、3を使いました。1と2を使わなかった理由を簡単に説明します。
-
postsテーブル
にfilesカラム
を作って、そこに配列の形で画像が複数格納される仕様です。画像を1枚ずつ取り出したりするのには向かないかな?と感じたため、他の方法をとることにしました。また、保存した画像がなかなか表示できませんでした。(技術不足です・・・)
【参考】https://github.com/carrierwaveuploader/carrierwave -
純粋に実装できませんでした。なぜ
form object
を使ってみようと思ったかというと、3のaccepts_nested_attributes_for
はあまり評判の良くないメソッドであると、いくつかのサイトに書いてあったからです。
【参考】accepts_nested_attributes_forを使わず、複数の子レコードを保存する
画像を1枚保存するところまではできましたが複数は実装できず、期日も迫っていたので、3の方法をとることにしました。
Railsの生みの親が"kill"したいらしいaccepts_nested_attributes_for
メソッドですが、Railsガイド
にバッチリ載ってます!
【参考】公式ドキュメント:Active Record Nested Attributes(日本語版はこちら)
これも正攻法だ!!と思い直し、このメソッドを使い実装していきますっ
#いざ実装
CarrierWave
の導入はこちらをご参考ください▶︎【Rails入門】CarrierWaveを使って画像のアップロードに挑戦!
他にも調べればいっぱいサイトが出てきます。
##1. まずはアソシエーションから
早速accepts_nested_attributes_for
メソッドの登場です。このメソッドで、postモデル
を親モデル、post_fileモデル
を子モデルとしたアソシエーションを組むことができます。
class Post < ApplicationRecord
belongs_to :group
belongs_to :user
# dependent: :destroyをつけることで、親のレコードが削除された場合に、関連付いている子のレコードも一緒に削除されます。
has_many :post_files, dependent: :destroy
accepts_nested_attributes_for :post_files
validates :content, presence: true
end
class PostFile < ApplicationRecord
belongs_to :post
# 画像アップロードのための記述
mount_uploader :file, ImageUploader
end
##2. ビューに関するコントローラの記述をしていきましょう
###ルーティング
グループチャットなので、投稿機能であるposts
をgroups
にネストしたルーティングになります。
Rails.application.routes.draw do
devise_for :users
root "users#index"
resources :groups, except: :show do
resources :posts, only: [:index, :create]
end
end
###コントローラ
post
はgroup
に紐づいているので、before_action
でgroup_id
を取得しています。
また、今回は投稿を表示するページと新規投稿をするページが同一であるため、newアクション
ではなくindexアクション
でPostクラス
のインスタンスを作成します。
class PostsController < ApplicationController
before_action :set_group
def index
@post = Post.new
@post_file = @post.post_files.build
@posts = @group.posts.includes(:user).order(created_at: "DESC")
end
private
def set_group
@group = Group.find(params[:group_id])
end
end
注目は、indexアクション
の2行目。
@post.post_files.build
という記述により、Postクラス
のインスタンスに関連づけられたPost_fileクラス
のインスタンスを作成することができます。
##3. 投稿フォームを作っていきましょう
ゴール動画にあるようなフォームを作っていきます。
.post-form
= image_tag current_user.avater.url, id: 'avater'
.input-box
= form_for [@group, @post] do |f|
= f.text_area :content, id: 'textarea', class: 'input-box__content', placeholder: '投稿文を入力してください'
.input-box__bottom
.input-box__bottom__files
= f.fields_for :post_files do |i|
= i.file_field :file, multiple: true, name: "post_files[file][]"
= f.submit '投稿', class: 'submit-btn'
// 後述のpostの部分テンプレートの呼び出し。groupのpostを全て表示します。
.posts
= render @posts
ポイントはこの部分!
= f.fields_for :post_files do |i|
= i.file_field :file, multiple: true, name: "post_files[file][]"
1行目のfields_for
は、file_field
などと同様にinput要素を生成するフォームヘルパーです。1つのモデル(post)に紐づいた、複数の別のモデル(post_file)を同時に保存したい時に利用できます。
2行目のmultiple: true
で複数画像を選択できるようになります。
##4. フォームで入力した値を保存
いよいよデータを保存するための記述です。createアクション
を以下のようにします。
class PostsController < ApplicationController
before_action :set_group
def index
@post = Post.new
@post_file = @post.post_files.build
@posts = @group.posts.includes(:user).order(created_at: "DESC")
end
def create
@post = @group.posts.new(post_params)
# 投稿が成功した場合
if @post.save
# 画像が投稿されていないパターンもあるので条件分岐
if params[:post_files].present?
# フォームで入力されたファイルを一つずつレコードに格納していく
params[:post_files][:file].each do |a|
@post_file = @post.post_files.create!(file: a, post_id: @post.id)
end
end
redirect_to group_posts_path(@group)
# 投稿が失敗した場合
else
@posts = @group.posts.includes(:user).order(created_at: "DESC")
render :index
end
end
private
def post_params
params.require(:post).permit(:content, post_files_attributes: [:file]).merge(user_id: current_user.id)
end
def set_group
@group = Group.find(params[:group_id])
end
end
ポイントはストロングパラメータ
private
def post_params
params.require(:post).permit(:content, post_files_attributes: [:file]).merge(user_id: current_user.id)
end
fields_for
を使ったフォームから送信される値は、post_files_attributes: [:file]
のような形でparamsに入ります。
##5. 保存はできた!最後に表示だ!!
なぜこんなにも意気込んでいるかというと、ここでハマったからです笑
保存したデータを表示しましょう。
メッセージは保存しているだけ表示するので、部分テンプレートで切り出します。
(ゴール動画では、コメントするやら見ましたやらついていますが、以下コードではわかりやすさのため省略してます。)
.post
.top
= image_tag post.user.avater.url, id: 'avater'
.top__right
.top__userinfo
.top__userinfo--user-name
= post.user.name
.top__userinfo--datetime
= l post.created_at, format: :datetime
.middle
.post-content
// text_areaで入力された改行をそのまま表示するために、simple_formatを使用
= simple_format(post.content)
// 画像があれば表示
- if post.post_files.present?
.post-files
// postに紐ずくpost_filesをeachで一つずつ取り出して表示
- post.post_files.each do |file|
// objectタグで書くことでpdfとかも表示できます。(画像だけの場合はimage_tagを使えば良いと思います。)
%object{data: file.file.url, class: 'post-files__file'}
middleクラスの部分だけcssも載っけておきます。
.middle {
padding: 10px 10px 10px 50px;
.post-content {
margin-bottom: 10px;
}
.post-files {
// 横並び
display: flex;
// この2行で枠の幅の中で要素を折り返し表示
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 10px;
&__file {
width: 150px;
height: 150px;
margin: 5px;
border: 1px solid #ccc;
// 画像の縦横比率を変えずに表示
object-fit: contain;
}
}
}
ハマったのはもちろんここ。
- if post.post_files.present?
.post-files
- post.post_files.each do |file|
%object{data: file.file.url, class: 'post-files__file'} <---ここ!!
最初にどう記述していたかといいますと、以下です。
%object{data: file.url, class: 'post-files__file'}
この記述だと、
NoMethodError
undefined method 'url' for #<PostFile:0x00007fd979263950>
ってなります。
原因は、each
で一つずつ取り出したpost_files
はカラム名までしっかり指定してあげる必要があるからです。
- post_files(postと紐ずくファイルたち)
- file (eachで一つずつ取り出したfile)
- file(カラム名) <-----ここに画像データが保存されている!
- post_id (カラム名)
- file
- file
- post_id
という感じになっているので、file.file.url
と記述する必要があります。
これで画像複数投稿機能の完成です!
画像を複数投稿後の表示に関わるコードは載っていないサイトも多く、ここまで表示に情熱を注いだブログもないのではないかと・・・(ゆえに調べてもなかなか解決せず苦労しました。)
form object
でも実装できるようになりたいです!
次は、この複数投稿をajaxで表示させる方法を投稿したいと思います。
最後までお読みいただきありがとうございました!
#参考サイト