LoginSignup
9
6

More than 3 years have passed since last update.

form objectでupdate機能を実装した話

Last updated at Posted at 2021-01-02

何をしたか

インスタグラムを模したアプリを作っています。
下記のように、ユーザーに紐づく投稿があり、それがさらに複数のimageを持っている構造になっています。

Image from Gyazo

こちらの構造で、form objectを使って投稿の新規作成フォームを作ったのはよかったのですが、

▼こちらの記事で実装しています
form objectを使ってネストしたフォームにcarrierwaveで画像を保存した話。

そこからさらに、updateのフォームを作成するのにかなり苦労をしてしまったので、書いたコードをまとめておこうと思います。

なお、実行環境は下記の通りです。

  • Rails 5.2.3
  • Ruby 2.6.0

また、画像の投稿にはcarrier waveを使用しています。

書いたコード

実際に書いたコードを紹介して、そこに説明を加えていこうと思います。

controller

form objectで生成したインスタンスを、@post_formの形でビューに渡しています。

controllers/posts_controller.rb
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の形でインスタンスを受け取ります。(実際にはneweditで同じパーシャルを使いまわしていたため、@post_formformというローカル変数にしました。)

views/posts/_post_form.html.slim
= 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

forms/post_form.rb
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_modelform objectにモデルのような振る舞いをさせているところです。どういうことかというと、form objectはActiveRecordのモデルとは異なる「ただのクラス」であるため、Railsが新規作成・編集フォームでデフォルトで推測してくれている、ルーティングのパスがうまく適用されません。

そのためto_modelform objectがあたかもモデルのように思い込ませることで、Railsのデフォルトのルーティングがform objectでも使える様にします。

この辺りの実装は、この記事を大変参考にさせていただきました。

Rails: Form Objectと#to_modelを使ってバリデーションをモデルから分離する(翻訳)

[未解決]updateアクションに起こっていた課題。

ところが、この実装をしてもupdateアクションに関しては、POST 'posts/:id'にアクセスしようとしてしまい、うまくルーティングをたどれませんでした。

それに対し、ネットでは以下の2つの解決策を見つけることができました。

私の場合、前者はコードが冗長になりすぎるのと、後者はうまく動作させることができなかったので、仕方なしですがroutes.rbを下記のように書き換えて対応しました。

config/routes.rb
post '/posts/:id', to: 'posts#update'

モンキーパッチ感が否めないですが、今回はこれで良しとします...。もう少し実力が上がったらリファクタリングしてみたいです。

感想など

仕事の実装では、form objectのupdateメソッドは実装の時間が取れず諦めていたのですが、これで実装に向けた糸口が見つかりました:relaxed:
社内でも共有できるぐらい知見を高めていきたいです:sparkles:

9
6
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
9
6