はじめに
ついに初投稿です!
最初の投稿は、自分がRailsでとてもよく使っている form_with メソッドの仕組みについてまとめてみようと思います。
Railsを勉強していると、
-
form_withって結局なにしてるの? - URLとモデルがどう関係してるの?
- なぜ
form_with model: @xxxって書くだけで動くの?
と疑問に思うことがあると思います。(実際に自分がそうでした)
この記事では、form_withメソッドがどのような仕組みで動いているのかにフォーカスして、 実際の自分の個人開発コードを例にしながら解説していきます。
form_withメソッドとは?
form_with は、Railsが用意してくれている ビュー用のヘルパーメソッドです。
一言で言うと、
「モデルと紐づいたフォームを作るためのメソッド」
です。
「モデルに紐づくフォーム」と言われても少し分かりにくいかもしれませんが、要は特定のモデル(テーブル)を編集するためのフォームです。
これにより、
- フォームに入力された値をモデルの属性に割り当てる
- 編集、エラー時などにモデルの現在地をフォームに書き戻す(submitボタンを押した際にバリデーションエラーなどが起きても、入力した内容が消えずに残る)
などの処理が自動で行われるので、コード量を最小限に抑えることができます。
例:教材レビュー投稿フォーム
とはいえ文だけだと難しいと思うので、具体例として「使用している学習教材のレビュー(material_review)を投稿するフォーム」を見てみましょう。
レビューは MaterialReview というモデル(テーブル)に保存されます。テーブル構造は以下の通りです。
新規フォーム入力画面
まずは新しいレビュー投稿を作成する、新規フォーム入力画面を見てみましょう。今回は簡略化のため、CSSなどは除外しています。
コードは以下の通りです。
<%= form_with model: @material_review do |f| %>
<div>
<%= f.label :title, "レビュータイトル" %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :material_url, "教材URL" %>
<%= f.url_field :material_url %>
</div>
<div>
<%= f.label :score, "評価" %>
<% (1..5).each do |n| %>
<%= f.radio_button :score, n %>
<%= n %>
<% end %>
</div>
<div>
<%= f.label :description, "レビュー内容" %>
<%= f.text_area :description %>
</div>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.submit "レビューを投稿" %>
<% end %>
このように、form_with を使うことで、モデルに対応した入力フォームを簡単に作ることができます。
では具体的に form_with がどういった仕組みになっているかについて見ていきましょう。
モデルの属性に対応した入力要素を設置する
form_withブロックの配下を見ると、f.labelやf.text_field、f.radio_buttonなどのメソッドが呼ばれています。これらはform_withブロック配下で利用できるビューヘルパーで、それぞれモデルに関連付いたラベルやテキストボックス、ラジオボタンなどのフォーム要素を生成します。
たとえば、
<%= f.text_field :title %>
であれば、「MaterialReviewオブジェクトを編集するためのフォームで、このオブジェクトのtitle属性(列)に対応するテキストボックス」 を表します。つまり、ここに記入した値はMaterialReviewオブジェクトのtitle属性の値として当てはめられるということになります。
この1行から生成されるHTMLは、概ね次のようになります。
<input type="text" name="material_review[title]" id="material_review_title" />
name属性にも「material_reviewオブジェクトのtitle属性」という形式で名前がセットされていることが確認できます。他のフォームも同様のため、フォーム送信時のパラメータは最終的に
params = {
material_review: {
"title": "...",
"material_url": "...",
"score": "...",
"description": "...",
"user_id": "..."
}
}
のようなハッシュ形式でサーバーにデータが渡されます。
new/createアクションメソッド
新規フォームは2つのアクションから構成されています。
class MaterialReviewsController < ApplicationController
def new
@material_review = MaterialReview.new
end
def create
@material_review = MaterialReview.new(material_review_params)
if @material_review.save
flash[:notice] = "レビューを投稿しました!"
redirect_to material_reviews_path
else
render :new, status: :unprocessable_entity
end
end
private
def material_review_params
params.expect(material_review: [ :user_id, :title, :material_url, :score, :description ])
end
end
material_review_paramsというprivateメソッドはStrongParametersと呼ばれるものです。今回は説明を省略しますが、ここではフォームから送られてきたデータを安全に受け取るための仕組みだと考えてください。
「新規フォーム」か「編集フォーム」かで振る舞いが変わる。
ここが form_with の一番重要なポイントです。
<%= form_with model: @material_review do |f| %>
この1行から、Railsは次のようなことを自動で判断しています。
①モデルが「空」か「既存データ」か
今回の新規フォームに繋がっている new アクションでは、コントローラ側で次のように書いています。
def new
@material_review = MaterialReview.new
end
この時点での @material_review は、
- データベースにまだ保存されていない
- id を持っていない
空のモデルインスタンスです。
Railsはこれを見て、
「これは新規作成用のフォームだな」
と判断します。
②自動で決まる送信先URLとHTTPメソッド
@material_review が新規レコードの場合、
form_with model: は自動的に次の送信先を選びます。
POST /material_reviews
これは routes.rb の
resources :material_reviews
によって定義されている、createアクション用のルーティングです。
つまり、
- URLを自分で書かなくても
- methodを指定しなくても
Railsが 「新規作成用フォーム」 として正しい送信先を選んでくれます。
編集フォームのときはどうなる?
次に、既存のレビューを編集する場合を見てみましょう。
def edit
@material_review = MaterialReview.find(params[:id])
end
先程のnewアクションの部分が、レビューのidをルーティングから受け取るeditメソッドに変化したと思ってください。このときの @material_review は、
- すでにデータベースに存在している
- id を持っている
既存データのモデルインスタンスです。
editメソッドの遷移先のビュー側のコードは、新規作成時とほぼ同じです。
<%= form_with model: @material_review do |f| %>
•
•
•
<%= f.submit "更新する" %>
<% end %>
しかし、Railsは内部的に次のように判断します。
- これは新規作成ではない
- すでに
idを持っている、既存データのモデルだ - つまり「更新用フォーム」だ
その結果、フォームに入力されたデータの送信先は自動的に
PATCH /material_reviews/:id
となり、先程は create メソッドを呼び出されていたのと打って変わって、update アクションが呼ばれます。
newとeditの違いまとめ
| 状態 | 送信先URL | HTTPメソッド |
|---|---|---|
| new | /material_reviews | POST |
| edit | /material_reviews/:id | PATCH |
同じ form_with model: を使っているのに、
Railsが自動で切り替えてくれるのが分かると思います。
モデルの編集目的ではないフォームも作成できる?
結論からいうと可能です。たとえば、
- 検索フォーム
- ログインフォーム
- お問い合わせフォーム
など、特定のモデルを作成・更新しないフォームの場合です。
このようにmodelオプション(モデル)が存在しない場合、ポスト先も自動では決まらなくなるので、urlオプション(ポスト先) が必須になります。
<%# 検索フォームの例 %>
<%= form_with url: "/view/search" do |f| %>
<%= f.text_field :keyword %>
<%= f.submit "検索" %>
<% end %>
form_withメソッドは特に指定がない限りデフォルトでHTTPメソッドはPOSTに設定されているため、今回の場合だと
post 'view/search'
を探しに行きます。HTTPメソッドを適宜変更したい場合は methodオプション を付け加えてください。
<%= form_with url: "/view/keyword", method: :get do |f| %>
この場合だと、
get 'view/keyword'
というルーティングを探しに行きます。
まとめ
-
form_withはモデルを中心にフォームを組み立てる仕組み -
model: @xxxを指定すると- 送信先URL
- HTTPメソッド
- paramsの構造
をRailsが自動で決めてくれる
-
モデルが空か(new)、既存か(edit)で挙動が切り替わる
-
モデルに紐づかないフォームでは
url:を使う
「フォームはモデルの状態を見て動いている」と理解できると、form_with はとても扱いやすくなると思います!
おわりに
初めての投稿で至らない点もあったかと思いますが、ここまで読んでいただきありがとうございました!今後も記事にしたいと思ったことを投稿していくので、また読んでいただけると嬉しいです!一緒に頑張っていきましょう!
(ご指摘等ありましたら遠慮なく伝えていただけますと幸いです🙇♀️)
参考
