🦓

[Rails]フォームヘルパー

2023/06/30に公開

はじめに

フォームヘルパーform_withについてまとめてみました。

Railsのフォームヘルパーメソッド

Railsでは、form_withform_tagform_forなどのフォームヘルパーメソッドを提供しています。
Rails 5.1でform_withが導入されるまでは、form_withの機能はform_tagform_forに分かれていました。

form_for: form_for メソッドは、モデルを基にしたフォームを作成するためのメソッドです。

<%= form_for @article do |form| %>
  <%= form.label :title %>
  <%= form.text_field :title %>
  
  <%= form.label :content %>
  <%= form.text_area :content %>
  
  <%= form.submit "Create Article" %>
<% end %>

form_tag: form_tag メソッドは、モデルに依存しない独自のフォームを作成するためのメソッドです。

<%= form_tag articles_path do %>
  <%= label_tag :title %>
  <%= text_field_tag :title %>
  
  <%= label_tag :content %>
  <%= text_area_tag :content %>
  
  <%= submit_tag "Create Article" %>
<% end %>

form_with: form_with メソッドは、Rails 5.1以降で導入された新しいフォームヘルパーメソッドです。それ以前のバージョンでは使用することができません。

# app/views/articles/new.html.erb

<%= form_with model: @article, url: articles_path do |form| %>
  <div>
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>
  
  <div>
    <%= form.label :content %>
    <%= form.text_area :content %>
  </div>
  
  <%= form.submit "Create Article" %>
<% end %>

form_with

  1. デフォルトでAjaxリクエストをサポートする: form_with メソッドは、デフォルトでAjaxリクエストをサポートします。これにより、フォームの送信時にページ全体の再読み込みを回避し、非同期でフォームの処理結果を取得することができます。一方、他のフォームヘルパーメソッドでは、明示的にAjaxリクエストを指定する必要があります。

  2. デフォルトの挙動: form_with メソッドは、フォームがモデルオブジェクトに基づいている場合、自動的に適切なHTTPメソッドとURLを設定します。また、フォームフィールドの生成やバリデーションエラーメッセージの表示など、様々な便利な機能を提供します。

  3. シンプルな記法: form_with メソッドは、シンプルな記法を持っています。フォームのオプションをハッシュ形式で指定することができます。これにより、直感的で読みやすいコードを書くことができます。

フォームをオブジェクトに結び付ける

form_withの:model引数を使うと、フォームビルダーオブジェクトをモデルオブジェクトに紐付けできるようになります。

<%= form_with model: @article do |form| %>
  <%= form.text_field :title %>
  <%= form.text_area :body, size: "100x5" %>
  <%= form.submit %>
<% end %>
<form enctype="multipart/form-data" action="/articles" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="B9xiJsqaZFqpbYIF3Ze8k8xeVkDSoyckXA-yylEkKw2xUTyC7qXtqweSRSdAeE04vs83Ra-2cTBPRCzETBiIMg" autocomplete="off">
  <div>
    <label for="article_title">タイトル</label>
    <input type="text" name="article[title]" id="article_title">
  </div>
  <div>
    <label for="article_body">本文</label>
    <textarea name="article[body]" id="article_body" cols="100" rows="5"></textarea>
  </div>
  <input type="submit" name="commit" value="登録する" data-disable-with="登録する">
</form>
  • フォームのactionには、@articleに適した値が自動入力されている。
  • フォームのフィールドには、@articleにある値が自動入力されている。
  • フォームのフィールド名は、article[...]という形でスコープされている。
  • params[:article]がすべてのフィールドの値を含むハッシュになる
  • コントローラ内でparams[:article][:title]でアクセスすると、送信された値を取り出せる

リソースを利用する

レコードをリソースとして宣言するとform_withの呼び出しがはるかに簡単になります。

resources :articles

この短いform_with呼び出しは、レコードの作成・編集のどちらでもまったく同じです。

## 新しい記事の作成
# 長いバージョン
form_with model: @article, url: articles_path)
form_with model: Post.new
# 短いバージョン(レコード識別を利用)
form_with model: @article

## 既存の記事の編集
# 長いバージョン
form_with model: @article, url: article_path(@article), method: "patch"
# 短いバージョン(レコード識別を利用)
form_with model: @article
# urlを渡す
<%= form_with url: posts_path do |form| %>
  <%= form.text_field :title %>
<% end %>
# 生成されたhtml
<form action="/posts" method="post" data-remote="true">
  <input type="text" name="title">
</form>

# スコープを指定する
<%= form_with scope: :post, url: posts_path do |form| %>
  <%= form.text_field :title %>
<% end %>
# 生成されたhtml。名前がpostスコープに入っている
<form action="/posts" method="post" data-remote="true">
  <input type="text" name="post[title]">
</form>

# Post.newという新しいPostオブジェクトを作成してフォームに関連付けする
<%= form_with model: Post.new do |form| %>
  <%= form.text_field :title %>
<% end %>
# 生成されたhtml
<form action="/posts" method="post" data-remote="true">
  <input type="text" name="post[title]">
</form>

# Post.firstで最初のpostを取得し更新する
<%= form_with model: Post.first do |form| %>
  <%= form.text_field :title %>
<% end %>
# 生成されたhtml
<form action="/posts/1" method="post" data-remote="true">
  <input type="hidden" name="_method" value="patch">
  <input type="text" name="post[title]" value="<the title of the post>">
</form>

# モデルに含まれてない属性も追加できる
<%= form_with model: Cat.new do |form| %>
  <%= form.text_field :cats_dont_have_gills %>
  <%= form.text_field :but_in_forms_they_can %>
<% end %>
# 生成されたhtml
<form action="/cats" method="post" data-remote="true">
  <input type="text" name="cat[cats_dont_have_gills]">
  <input type="text" name="cat[but_in_forms_they_can]">
</form>

よく使うパラメーター

  • model: フォームのモデルオブジェクトを指定します。例えば、model: @postとすると、@postインスタンスに関連するフォームが作成されます。
  • url: フォームデータの送信先URLを指定します。例えば、url: posts_pathとすると、posts_pathにデータが送信されます。指定しない場合はフォームを現在のページに送ります。
  • method: フォームの送信メソッドを指定します。デフォルトは自動的に適切なHTTPメソッドが選択されますが、明示的に指定することもできます。例えば、method: :patchとすると、PATCHリクエストが使用されます。
  • scope: フォームのスコープを指定します。主にネストされたリソースのフォームを作成する場合に使用します。
  • local: フォームの送信時にAjaxを使用するかどうかを指定します。デフォルトはfalseで、ページの再読み込みが行われます。remote: trueを指定すると、Ajaxリクエストが行われます。

https://api.rubyonrails.org/v6.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_with

コントローラー

コントローラ内でパラメータをモデルに渡す前に、定番のパラメータの許可リストチェックを宣言する必要があります。

def create
  @person = Person.new(person_params)
  # ...
end

private
  def person_params
    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
  end

ネストされたリソース

ネストされたリソースのフォームを作成するためmodelパラメーターにネストされたリソースの関係を表現します。
Article モデルと Comment モデルがあるとします。
Article は複数のコメントを持つ関連付けがあります。

config/routes.rb
resources :articles do
  resources :comments
end

上記のルーティング設定により、articles/:article_id/comments のURLでコメントを作成することができます。

# app/views/articles/show.html.erb
<h1><%= @article.title %></h1>

<!-- Articleの情報を表示 -->

# コメント一覧
<h2>コメント</h2>
<%= render @article.comments %>

# コメントフォーム
<%= form_with(model: [@article, @comment]) do |form| %>
  <%= form.text_area :body %>
  <%= form.submit %>
<% end %>

form_withmodel パラメーターには [@article, @comment] を指定しています。これにより、ネストされたリソースのフォームが作成されます。コメント作成フォームの送信先は articles/:article_id/comments となります。

ネストされたフォーム

ネストされたフォームを使うと、親モデルと関連する子モデルのデータを同時に送信できます。親モデルと子モデルの関連性を設定し、fields_forメソッドを使用して子モデルのフォームを作成します。
例えば、ユーザー(User)とその住所(Address)を同時に作成/更新する場合などです。ユーザーと住所のフォームを同じページに表示し、一度に入力して送信することで、関連するモデルをまとめて作成/更新します。

# app/models/post.rb
class User < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses
end

# app/models/address.rb
class Address < ApplicationRecord
  belongs_to :user
end

# app/views/users/_form.html.erb
<%= form_with model: @person do |form| %>
  Addresses:
  <ul>
    <%= form.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>

        <%= addresses_form.label :street %>
        <%= addresses_form.text_field :street %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

accepts_nested_attributes_forヘルパーが受け取るのはこのようなパラメータの名前です。たとえば、2つの住所を持つユーザーを1人作成する場合、送信されるパラメータは以下のようになります。

{
  'person' => {
    'name' => 'John Doe',
    'addresses_attributes' => {
      '0' => {
        'kind' => 'Home',
        'street' => '221b Baker Street'
      },
      '1' => {
        'kind' => 'Office',
        'street' => '31 Spooner Street'
      }
    }
  }
}

検索フォーム

検索フォームを作成するためにこちらのものが必要です。

  • GETメソッドを送信するためのフォーム要素
  • 入力するものを示すラベル
  • テキスト入力要素
  • 送信ボタン要素
    form_withとその中で生成されるフォームビルダーオブジェクトを使った検索フォームになります。
<%= form_with url: "/search", method: :get do |form| %>
  <%= form.label :query, "キーワード" %>
  <%= form.text_field :query %>
  <%= form.submit "検索する" %>
<% end %>

出力は以下のようになります。

<form action="/search" method="get" accept-charset="UTF-8" >
  <label for="query">Search for:</label>
  <input id="query" name="query" type="text" />
  <input name="commit" type="submit" value="Search" data-disable-with="Search" />
</form>

フォームコントロール

フォームコントロールはユーザーからの入力を受け付けたり、操作したりすることのできる要素です。

チェックボックス

チェックボックスはフォームコントロールの一種で、ユーザーがオプションをオンまたはオフにできるようにします。

<%= form.check_box :pet_dog %>
<%= form.label :pet_dog, "犬" %>
<%= form.check_box :pet_cat %>
<%= form.label :pet_cat, "猫" %>

出力は以下のようになります。

<input type="checkbox" id="pet_dog" name="pet_dog" value="1" />
<label for="pet_dog"></label>
<input type="checkbox" id="pet_cat" name="pet_cat" value="1" />
<label for="pet_cat"></label>

check_boxの第1パラメータはinputの名前です。第2パラメータはinput要素のvalue属性を指定します。チェックボックスをオンにすると、この値がフォームデータに含まれ、最終的にparamsに渡されます。

ラジオボタン

チェックボックスと同様、ラジオボタンも一連のオプションをユーザーが選択できますが、一度に1つの項目しか選択できない排他的な動作が特徴です。

<%= form.radio_button :age, "child" %>
<%= form.label :age_child, "21歳以下" %>
<%= form.radio_button :age, "adult" %>
<%= form.label :age_adult, "22歳以上" %>

出力は以下のようになります。

<input type="radio" id="age_child" name="age" value="child" />
<label for="age_child">21歳以下</label>
<input type="radio" id="age_adult" name="age" value="adult" />
<label for="age_adult">22歳以上</label>

check_boxヘルパーと同様、radio_buttonの第2パラメータはinput要素のvalue属性を指定します。2つのラジオボタン項目は同じ名前('age')を共有しているので、ユーザーは一方の値だけを選択できます。そしてparams[:age]の値は"child"と"adult"のいずれかになります。

チェックボックスとラジオボタンには必ずラベルを表示してください。ラベルを表示することで、そのオプションとラベルの名前が関連付けられるだけでなく、ラベルの部分もクリック可能になるのでUIが向上します。

その他のヘルパー

他にも、以下の「テキストエリア」「隠しフィールド」「パスワードフィールド」「数値フィールド」「日付時刻フィールド」など多くのフォームコントロールがあります。

<%= form.text_area :message, size: "70x5" %>
<%= form.hidden_field :parent_id, value: "foo" %>
<%= form.password_field :password %>
<%= form.number_field :price, in: 1.0..20.0, step: 0.5 %>
<%= form.range_field :discount, in: 1..100 %>
<%= form.date_field :born_on %>
<%= form.time_field :started_at %>
<%= form.datetime_local_field :graduation_day %>
<%= form.month_field :birthday_month %>
<%= form.week_field :birthday_week %>
<%= form.search_field :name %>
<%= form.email_field :address %>
<%= form.telephone_field :phone %>
<%= form.url_field :homepage %>
<%= form.color_field :favorite_color %>

出力は以下のようになります。

<textarea name="message" id="message" cols="70" rows="5"></textarea>
<input type="hidden" name="parent_id" id="parent_id" value="foo" />
<input type="password" name="password" id="password" />
<input type="number" name="price" id="price" step="0.5" min="1.0" max="20.0" />
<input type="range" name="discount" id="discount" min="1" max="100" />
<input type="date" name="born_on" id="born_on" />
<input type="time" name="started_at" id="started_at" />
<input type="datetime-local" name="graduation_day" id="graduation_day" />
<input type="month" name="birthday_month" id="birthday_month" />
<input type="week" name="birthday_week" id="birthday_week" />
<input type="search" name="name" id="name" />
<input type="email" name="address" id="address" />
<input type="tel" name="phone" id="phone" />
<input type="url" name="homepage" id="homepage" />
<input type="color" name="favorite_color" id="favorite_color" value="#000000" />

隠しinputはユーザーには表示されず、種類を問わず事前に与えられた値を保持します。隠しフィールドに含まれている値はJavaScriptで変更できます。

終わりに

form_withを理解する上で難しいと感じることは、パラメーターの使い方とオプションの意味を正確に把握することです。form_withは多くの異なるオプションを提供していますが、それぞれのオプションがどのように機能し、どのようにフォームの動作に影響を与えるのかを理解することは重要です。

https://qiita.com/snskOgata/items/44d32a06045e6a52d11c
https://railsdoc.com/form
https://pikawaka.com/rails/form_with

Discussion