10
17

More than 5 years have passed since last update.

Rails で複数の ActiveModel::Model な Form オブジェクトを fields_for で使う

Posted at

はじめに

Rails で、 DB と連携していない model のオブジェクトを入力フォームで取り扱い、バリデーションなども含めたいという場合に、 ActiveModel::Model を include した model を Form オブジェクトとして取り扱う、というケースは多いと思います。

その中で、親子関係を持った Form オブジェクトを扱うときにどのように実装するか考える機会があったので、こちらの記事にまとめます。

環境前提

  • rails 5.2.2
  • ruby 2.5.1
  • enumerize 2.2.2
  • simple_form 4.0.1
  • hamlit-rails 0.2.0

tl;dr

form_forfields_for メソッドを使えるよう、必要なメソッドを model に定義すれば実現可能

サンプル

以下のような簡単なサンプルを Rails の scaffold を使って作りました。

  • Post としてタイトル・本文が入力可能
  • Tag を 3 つ指定して登録
  • 保存するとタイトル・本文・タグをまとめた文字列を表示

新規登録画面

image.png

一覧画面

image.png

サンプルの前提

  • model は PostTag の 2 種類
  • Post has many Tag という関係
  • PostApplicationRecord を継承
  • TagApplicationRecord を継承せず、ActiveModel::Model を include した model

基本方針

Rails API ドキュメント の fields_for の説明を踏まえると、fields_for メソッドで has many な子となる Tag model を扱うためには、親となる Post model に以下 2 つのメソッドを定義する必要があります。

  • tags という reader メソッドを定義
  • tags_attributes という writer メソッドを定義

どちらも ApplicationRecord なオブジェクトであれば、 accepts_nested_attributes_for を利用できたりしますが、今回は子の model が ActiveModel::Model を include した model ですので利用できません。。

今回やりたいことを実現するには、上記 2つのメソッドが不可欠ですので、こちらをもとに実装しました。

ソースコード

こちらにサンプルコードをアップしています。

model

Post

親となる Post model に必要なメソッドを定義しています。

また、新規登録時に複数 Tag を表示できるよう、build_tags メソッドと attr_writer で定義した tags の writer メソッドで、複数 Tag オブジェクトを生成できるようにしています。

あとは Form オブジェクトの値を扱う例として、set_tag_string メソッドで Tag の値を1つの文字列にまとめて、 tag_string カラムに保存できるようにしました。

app/models/post.rb
class Post < ApplicationRecord
  before_save :set_tag_string
  attr_writer :tags

  def tags
    @tags ||= []
  end

  def tags_attributes=(attributes)
    @tags = attributes.map do |i, attribute|
      Tag.new(attribute)
    end
  end

  def build_tags
    self.tags = [Tag.new] * 3
  end

  def set_tag_string
    self.tag_string = self.tags.map(&:name).join(' / ')
  end
end

Tag

Tag の方は特に難しいことはしておらず、タグのセレクトボックスに使うリストを enumerize メソッドで定義しただけです。

app/models/tag.rb
class Tag
  include ActiveModel::Model
  extend Enumerize

  attr_accessor :name
  enumerize :name, in: %i(personal business family)
end

Controller

model 側で fields_for メソッドを定義したことにより、post_params の中で、tags_attributes を指定できるようになっています。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: %i(show edit update destroy)

  def index
    @posts = Post.all
  end

  def show
  end

  def new
    @post = Post.new
    @post.build_tags
  end

  def edit
  end

  def create
    @post = Post.new(post_params)

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: 'Post was successfully created.' }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update(post_params)
        format.html { redirect_to @post, notice: 'Post was successfully updated.' }
        format.json { render :show, status: :ok, location: @post }
      else
        format.html { render :edit }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @post.destroy
    respond_to do |format|
      format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      params.require(:post).permit(
        :titie, :content, tags_attributes: [:name] 
      )
    end
end

View

simple_form でフォームを簡略化しています。
simple_form で fields_for に対応するメソッドは simple_fields_for メソッドですが、今回は fields_for メソッドの利用確認をしたかったので、fields_for メソッドを使っています。

app/views/posts/_form.html.haml
= simple_form_for @post do |f|
  = f.error_notification
  = f.input :titie
  = f.input :content
  = f.fields_for :tags do |tf|
    = tf.input :name
  = f.submit

おわりに

ApplicationRecord でない Form オブジェクトのモデルでも、fields_for メソッドを使えるようになるのは便利だなと思いました。

accepts_nested_attributes_for に頼らなくても親子関係の model を fields_for で扱えるようになるということを知っているだけでも、実装の幅が広がるなと思います。

参考

10
17
1

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
10
17