Formオブジェクト~1つのフォームで複数モデルとやりとりをする画期的なヤツ~

[1.アプリの骨格を作る](#1-アプリの骨格を作る)
[2.formオブジェクト着手](#2-formオブジェクト着手)

理解するのになかなか苦労しましたがタイトルでも書いてあるとおり、formオブジェクトは複数モデルにまとめて情報の保存・更新などを行う際に用いられるものです。

まずはイメージ図です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629333/8c74267e-2a83-f57c-714b-8ed26f99372b.png)



このように複数モデルを介す必要のあるフォーマットを実装するのにformオブジェクトを使います。

では具体的にformオブジェクト内にはどのような記述が必要か考えていきましょう。

・formオブジェクトを使用するモデルに設定していたバリデーションの移行
・コントローラでformオブジェクトを使えるようにする記述
・モデルを介すための記述


大まかにこの3つが必要になりますね！
では今回はユーザー情報と住所情報を同時に登録するフォーマットを作成していきます。
※deviseを使用しないため、簡易的になっておりますのでご了承を

#1-アプリの骨格を作る
<h3>1. アプリの骨格を作る</h3>
以下コマンドを実行

````
% rails _6.0.0_ new format_app -d mysql
% cd format_app
% rails db:create
````

その後、ローカルホストに接続してrails初期画面に接続できるかチェックしておきましょう

<h3>MVCなどの設定・作成</h3>
<h5>コントローラの作成とルーティングの設定</h5>
コントローラの作成

````
 % rails g controller formats index
````

ルーティングの設定

````ruby
Rails.application.routes.draw do
  root 'formats#index'
  resources :formats, only: [:index, :new, :create]
end
````

コントローラのアクション設定
まずはユーザー登録・閲覧だけできるようにします

````ruby
class FormatsController < ApplicationController
  def index
    @formats = User.all
  end

  def new
    @format = User.new
  end

  def create
    @format = User.create(format_params)
    redirect_to root_path
  end

  private
  def format_params
    params.require(:user).permit(:name, :name_kana, :nickname)
  end
end
````
<h5>モデル・マイグレーションの作成</h5>

````
% rails g model user
% rails g model address
````

マイグレーションファイルの設定

XXXXXXXXXX_create_user.rb

````ruby
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name,           null: false
      t.string :name_kana,   null: false
      t.string :nickname,       null: false
      t.timestamps
    end
  end
end

````

XXXXXXXXXX_create_address.rb

````ruby
class CreateAddresses < ActiveRecord::Migration[6.0]
  def change
    create_table :addresses do |t|
      t.string :postal_code,    default: "",  null: false
      t.integer :prefecture_id,                  null: false
      t.string :city,           default: ""
      t.string :house_number,   default: ""
      t.string :building_name,  default: ""
      t.references :user,                     null: false,  foreign_key: true
      t.timestamps
    end
  end
end

````


モデルの設定

user.rb
・アソシエーションの設定
　-> userモデル親にして、addressモデルを子にする

・バリデーションの設定
　-> 値が空になるのを防ぐ
　-> 名前は漢字・ひらがな・カタカナのみ
　-> フリガナはカタカナのみ
　-> ニックネームは半角英数字のみ

````ruby
class User < ApplicationRecord
  has_one :address
  with_options presence: true do
    validates :name,      format: {with:/\A[ぁ-んァ-ン一-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[ァ-ヶー－]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Please input half-width characters."}
  end
end
````

address.rb
・アソシエーションの設定
　-> userモデル親にして、addressモデルを子にする

````ruby
class Address < ApplicationRecord
  belongs_to :user
end
````

<h5>ビューファイルの作成</h5>
まずはユーザ情報だけ登録・閲覧できるようにします。


````formats/index.html.erb
<div class="wrapper">
  <div class="btn">
    <%= link_to '登録する', new_format_path %>
  </div>
  <div class="formats">
    <% @formats.each do |format| %>
      <div class="format">
        <div class="format-name"><%= format.name %>/<%= format.name_kana %>/<%= format.nickname %></div>
      </div>
    <% end %>
  </div>
</div>
````


ユーザー情報だけ登録できるようなフォーマットを作成する

````formats/new.html.erb
<%= form_with(model: @format, url: formats_path, local: true) do |form| %>
  <h1>ユーザー名を入力</h1>
  <div class="field">
    <%= form.label :name, "名前（全角）" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_kana, "フリカナ（全角カタカナ）" %>
    <%= form.text_field :name_kana %>
  </div>
  <div class="field">
    <%= form.label :nickname, "ニックネーム（半角英数）" %>
    <%= form.text_field :nickname %>
  </div>
  <div class="actions">
    <%= form.submit "登録する" %>
  </div>
<% end %>

````


<h3>2. formオブジェクト着手</h3>
#2-formオブジェクト着手
さて本題はここからですね。まずは必要なファイルなどの作成をしていきましょう。
app内にformsディレクトリーを作り、その中にuser_address.rbを生成します

````
% mkdir app/forms
% touch app/forms/user_address.rb
````

その後ファイル名と同じクラス名を中に定義します

````user_address.rb
class UserAddress
end
````


formオブジェクトを利用する際は、上記で生成したファイル内にActiveModel::Modelという記述を組み込みます。これにより、user_address.rbがモデルのような働きをしてくれるようになります。以下参照。


````user_address.rb
class UserAddress
  include ActiveModel::Model
end
````
これでこのファイルが擬似的なモデルとなりました。

続いてこのファイルで参照・更新がかけられる値を設定していきます。
先ほどのユーザー登録時に必要だった値は

:name, :name_kana, :nickname でした。

そして、さらに住所登録も行いたいため以下の値を追加します。
郵便番号(:postal_code), 都道府県(:prefecture_id), 市町村(:city), 番地(:house_number), 建物名(:building_name)

ということでattr_accessorを使ってインスタンス変数の中身を参照・変更できるような記述をします。

````user_address.rb
class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
end
````

続いてはformオブジェクト(user_address.rb)内にユーザーモデルと住所モデルのバリデーションをまとめてしまいます。これを行う理由は管理のしやすさや可読性を上げるためです。

````models/user.rb

class User < ApplicationRecord
  has_one :address
# ---------- ここから　-------------
  with_options presence: true do
    validates :name,      format: {with:/\A[ぁ-んァ-ン一-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[ァ-ヶー－]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Input half-width characters."}
  end
# ------- ここまで切り取り ----------
end

````


````forms/user_address.rb

class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
  
 # ----- 切り取った部分の貼り付け ここから -----
  with_options presence: true do
    validates :name,      format: {with:/\A[ぁ-んァ-ン一-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[ァ-ヶー－]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Input half-width characters."}
  end
# ----- 切り取った部分の貼り付け ここまで -----  
end

````

そしてさらに住所モデルのバリデーションを追記していきます。
今回の注意点は
・ 郵便番号は数字のみ、３桁、ハイフン、4桁の順でのみ登録ができる仕様にする
・ 都道府県はactive_hashを使って、都道府県を選択しないと登録できないようにする

active_hashについてはあまり触れないので、他の記事など参考ください。


````forms/user_address.rb
class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
  
  with_options presence: true do
    validates :name,      format:   {with:/\A[ぁ-んァ-ン一-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format:   {with:/\A[ァ-ヶー－]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format:   {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Please input half-width characters."}
    # ----- ここから　住所モデルのバリデーション -----
    validates :postal_code, format: {with: /\A[0-9]+\z/, message: "is invalid. Please input half-width characters."}
    validates :prefecture_id, numericality: { other_than: 0, message: "can't be blank" }
    # ----- ここまで　住所モデルのバリデーション -----
  end
end

````

これでユーザーモデルと住所モデルのバリデーションの統合は完了したので、次はコントローラの仕組みを編集していきます。

コントローラで行いたいことはまず値の登録です。
そのため、formオブジェクト内に2つのモデルを介して値が登録できるメソッドを生み出す必要があります。今回は分かりやすくsaveとしましょう。

```` forms/user_address.rb

class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name

   # ----- バリデーションの記述は割愛 -----

  def save
    # ここにモデル2つを介して値を保存する記述を施す
  end
end

````

今回介したいモデルはUser.rbとAddress.rbですので以下のようになります。

````forms/user_address.rb
 
class UserAddress.rb
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name

   # ----- バリデーションの記述は割愛 -----

  def save
     user = User.create(name: name, name_kana: name_kana, nickname: nickname)
     Address.create(postal_code: postal_code, prefecture_id: prefecture_id, city: city, house_number: house_number, building_name: building_name, user_id: user.id)
  end
end

````

これによりコントローラの記述もformオブジェクトを経由させる記述に変更します。


````formats_controller.rb
class FormatsController < ApplicationController
  def index
    @formats = User.includes(:address) # ここを変更
  end

  def new
    # @format = User.new           ここを削除 
    @format = UserAddress.new    # ここを追記
  end

  def create
    # @format = User.create(format_params)     ここを削除
    @format = UserAddress.new(format_params) # ここを追記
    @format.save  # user_addres.rbで定義したメソッドをここで使用する
    redirect_to root_path
  end

  private
  def format_params 
    # permit内に住所登録に必要な値を追加する
    params.require(:user_address).permit(:name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name)
  end
end
````

ビューファイルを編集して、住所項目登録欄を作ります。

Gemfileにactive_hashを追加

````
gem 'active_hash'
````

以下のファイルを作成して記述を行う

````models/prefecture.rb
class Prefecture < ActiveHash::Base

  self.data = [
               {id: 0, name: '--'}, {id: 1, name: '北海道'}, {id: 2, name: '青森県'}, 
               {id: 3, name: '岩手県'}, {id: 4, name: '宮城県'}, {id: 5, name: '秋田県'}, 
               {id: 6, name: '山形県'}, {id: 7, name: '福島県'}, {id: 8, name: '茨城県'}, 
               {id: 9, name: '栃木県'}, {id: 10, name: '群馬県'}, {id: 11, name: '埼玉県'}, 
               {id: 12, name: '千葉県'}, {id: 13, name: '東京都'}, {id: 14, name: '神奈川県'}, 
               {id: 15, name: '新潟県'}, {id: 16, name: '富山県'}, {id: 17, name: '石川県'}, 
               {id: 18, name: '福井県'}, {id: 19, name: '山梨県'}, {id: 20, name: '長野県'}, 
               {id: 21, name: '岐阜県'}, {id: 22, name: '静岡県'}, {id: 23, name: '愛知県'}, 
               {id: 24, name: '三重県'}, {id: 25, name: '滋賀県'}, {id: 26, name: '京都府'}, 
               {id: 27, name: '大阪府'}, {id: 28, name: '兵庫県'}, {id: 29, name: '奈良県'}, 
               {id: 30, name: '和歌山県'}, {id: 31, name: '鳥取県'}, {id: 32, name: '島根県'}, 
               {id: 33, name: '岡山県'}, {id: 34, name: '広島県'}, {id: 35, name: '山口県'}, 
               {id: 36, name: '徳島県'}, {id: 37, name: '香川県'}, {id: 38, name: '愛媛県'}, 
               {id: 39, name: '高知県'}, {id: 40, name: '福岡県'}, {id: 41, name: '佐賀県'}, 
               {id: 42, name: '長崎県'}, {id: 43, name: '熊本県'}, {id: 44, name: '大分県'}, 
               {id: 45, name: '宮崎県'}, {id: 46, name: '鹿児島県'}, {id: 47, name: '沖縄県'}
              ]
end
````


モデルにアソシエーションを追加

````models/address.rb
class Address < ApplicationRecord
  belongs_to :user
  extend ActiveHash::Associations::ActiveRecordExtensions
    belongs_to_active_hash :prefecture
end
````

ビューを以下のように編集

```` views/formats/new.html.erb
<%= form_with(model: @format, url: formats_path, local: true) do |form| %>
  <h1>ユーザー名を入力</h1>
  <div class="field">
    <%= form.label :name, "名前（全角）" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_kana, "フリカナ（全角カタカナ）" %>
    <%= form.text_field :name_kana %>
  </div>
  <div class="field">
    <%= form.label :nickname, "ニックネーム（半角英数）" %>
    <%= form.text_field :nickname %>
  </div>
  <%# ここから　追加 %>
  <h1>住所を入力</h1>
  <div class="field">
    <%= form.label :postal_code, "郵便番号（ハイフンを含む）" %>
    <%= form.text_field :postal_code %>
  </div>
  <div class="field">
     <%= form.label :prefecture_id, "都道府県" %>
     <%= form.collection_select :prefecture_id, Prefecture.all, :id, :name, {} %>
  </div>
  <div class="field">
    <%= form.label :city, "市町村（任意）" %>
    <%= form.text_field :city %>
  </div>
  <div class="field">
    <%= form.label :house_number, "番地（任意）" %>
    <%= form.text_field :house_number %>
  </div>
  <div class="field">
    <%= form.label :building_name, "建物名（任意）" %>
    <%= form.text_field :building_name %>
  </div>
  <%# ここまで　追加 %>
  <div class="actions">
    <%= form.submit "登録する" %>
  </div>
<% end %>
````

登録できた値を表示させてみましょう。

````views/formats/index.html.erb
<div class="wrapper">
  <div class="btn">
    <%= link_to '登録する', new_format_path %>
  </div>
  <div class="formats">
    <% @formats.each do |format| %>
      <div class="format">
        <div class="format-name">ユーザー情報：<%= format.name %>/<%= format.name_kana %>/<%= format.nickname %></div>
        <div class="format-name">住所情報：<%= format.address.postal_code%>/<%= format.address.prefecture.name%>/<%= format.address.city%>/<%= format.address.house_number%>/<%= format.address.building_name%></div>
      </div>
    <% end %>
  </div>
</div>
````


これで完成です！