はじめに
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_for
で fields_for
メソッドを使えるよう、必要なメソッドを model に定義すれば実現可能
サンプル
以下のような簡単なサンプルを Rails の scaffold を使って作りました。
- Post としてタイトル・本文が入力可能
- Tag を 3 つ指定して登録
- 保存するとタイトル・本文・タグをまとめた文字列を表示
新規登録画面
一覧画面
サンプルの前提
- model は
Post
、Tag
の 2 種類 -
Post
has manyTag
という関係 -
Post
はApplicationRecord
を継承 -
Tag
はApplicationRecord
を継承せず、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
カラムに保存できるようにしました。
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
メソッドで定義しただけです。
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
を指定できるようになっています。
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
メソッドを使っています。
= 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
で扱えるようになるということを知っているだけでも、実装の幅が広がるなと思います。