何をしたか
インスタグラムを模したアプリを作っています。
下記のように、ユーザーに紐づく投稿があり、それがさらに複数のimageを持っている構造になっています。
こちらの構造で、form object
を使って投稿の新規作成フォームを作ったのはよかったのですが、
▼こちらの記事で実装しています
form objectを使ってネストしたフォームにcarrierwaveで画像を保存した話。
そこからさらに、update
のフォームを作成するのにかなり苦労をしてしまったので、書いたコードをまとめておこうと思います。
なお、実行環境は下記の通りです。
Rails 5.2.3
Ruby 2.6.0
また、画像の投稿にはcarrier wave
を使用しています。
書いたコード
実際に書いたコードを紹介して、そこに説明を加えていこうと思います。
controller
form object
で生成したインスタンスを、@post_formの形でビューに渡しています。
class PostsController < ApplicationController
before_action :require_login, only: %i(new edit)
before_action :set_post, only: %i(edit update)
def new
@post_form = PostForm.new(current_user)
end
def create
@post_form = PostForm.new(current_user, post_params, post: Post.new)
if @post_form.save!
redirect_to root_path
flash[:success] = "投稿しました"
else
flash.now[:danger] = "投稿に失敗しました"
render :new
end
end
def edit
@post_form = PostForm.new(current_user, post: @post)
end
def update
@post_form = PostForm.new(current_user, post_params, post: @post)
if @post_form.save!
redirect_to root_path
flash[:success] = "投稿を編集しました"
else
flash.now[:danger] = "投稿の編集に失敗しました"
render :edit
end
end
private
def post_params
params.require(:post).permit(:body, photoes: [])
end
def set_post
@post = Post.find(params[:id])
end
end
view
ビューでは、model: @post_form
の形でインスタンスを受け取ります。(実際にはnew
とedit
で同じパーシャルを使いまわしていたため、@post_form
はform
というローカル変数にしました。)
= form_with model: @post_form, local: true do |f|
.form-group
= f.label :photoes, t('activerecord.models.images.photo'), class: 'bmd-label-floating"'
= f.file_field :photoes, multiple: true, class: 'form-control mb-1'
.form-group
= f.label :body, t('activerecord.models.posts.body'), class: 'bmd-label-floating"'
= f.text_field :body, class: 'form-control'
= f.submit '登録する', class: 'btn btn-raised btn-success'
form object
class PostForm
include ActiveModel::Model
attr_accessor :body, :photoes, :user
validates :body, presence: true
def initialize(user, params = {}, post: '')
@post ||= Post.new
@post.assign_attributes({user: user, body: params[:body]})
super(params)
end
def to_model # 解説します
@post
end
def save!
return false if invalid?
if photoes
photoes.each do |photo|
@post.images.build(photo: photo).save!
end
end
@post.save! ? true : false
end
end
ポイントは、to_model
でform object
にモデルのような振る舞いをさせているところです。どういうことかというと、form object
はActiveRecordのモデルとは異なる「ただのクラス」であるため、Railsが新規作成・編集フォームでデフォルトで推測してくれている、ルーティングのパスがうまく適用されません。
そのためto_model
でform object
があたかもモデルのように思い込ませることで、Railsのデフォルトのルーティングがform object
でも使える様にします。
この辺りの実装は、この記事を大変参考にさせていただきました。
Rails: Form Objectと#to_model
を使ってバリデーションをモデルから分離する(翻訳)
[未解決]updateアクションに起こっていた課題。
ところが、この実装をしてもupdate
アクションに関しては、POST 'posts/:id'
にアクセスしようとしてしまい、うまくルーティングをたどれませんでした。
それに対し、ネットでは以下の2つの解決策を見つけることができました。
私の場合、前者はコードが冗長になりすぎるのと、後者はうまく動作させることができなかったので、仕方なしですがroutes.rb
を下記のように書き換えて対応しました。
post '/posts/:id', to: 'posts#update'
モンキーパッチ感が否めないですが、今回はこれで良しとします...。もう少し実力が上がったらリファクタリングしてみたいです。
感想など
仕事の実装では、form object
のupdateメソッドは実装の時間が取れず諦めていたのですが、これで実装に向けた糸口が見つかりました
社内でも共有できるぐらい知見を高めていきたいです