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

## はじめに

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 つ指定して登録
* 保存するとタイトル・本文・タグをまとめた文字列を表示

### 新規登録画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/222030/4df4d4f2-d8e4-c765-7a06-eda969637166.png)

### 一覧画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/222030/02110989-3ea4-4dd4-db39-2f4479d71aef.png)

## サンプルの前提

* model は `Post`、`Tag` の 2 種類
* `Post` has many `Tag` という関係
* `Post` は `ApplicationRecord` を継承
* `Tag` は `ApplicationRecord` を継承せず、`ActiveModel::Model` を include した model

## 基本方針

[Rails API ドキュメント の fields_for](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-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つのメソッドが不可欠ですので、こちらをもとに実装しました。

## ソースコード

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

https://github.com/gotchane/rails-form-sandbox

### model 

#### `Post`

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

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

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

```ruby: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` メソッドで定義しただけです。

```ruby: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` を指定できるようになっています。

```ruby: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` メソッドを使っています。

```ruby: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` で扱えるようになるということを知っているだけでも、実装の幅が広がるなと思います。

## 参考

* [ActionView::Helpers::FormBuilder](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-fields_for)
* [ActiveModel::Model](https://api.rubyonrails.org/classes/ActiveModel/Model.html)
* [【Rails】Form Object を作って Fat Model 解消に近づく - yujiro's blog](http://bamboo-yujiro.hatenablog.com/entry/2017/07/26/012316)