tl;dr
Rails界隈でダウンロード数が増えているらしいtrailblazerというgemだが
既存の記法とはかなり違った印象を受けてしまう。
しかし、Trailblazerに含まれるReformというgemだけで使うなら
そこまで既存の記法から離れることはない。
また、Reformの仕組みを知ることで、trailblazerの動きの理解の助けにもなる。
ただ、Rails,Reform,Trailblazerはかなり多機能なので色々飛ばしているところがあるが勘弁してほしい。
Reformとは
Reformはいわゆる__Form Object__用のGemである。簡単に言えば、2つの役割がある
- わかりやすいデータ構造でhtmlのformを作成することができる。
- 保存時のパラメータの検証・Modelへの受け渡しを容易にする。
環境
- Rails5(おおよそバージョンに関係なく動くはず)
- trailblazer(ver.2)
セットアップ
Gemfileに以下の記述を追加して、bundle install
gem 'trailblazer'
gem 'trailblazer-rails'
本来ならreformだけでいいのだが、trailblazerに含まれる他のライブラリへの依存が怖いので全部入れておく。
Model
説明用に適当な親子構造を持ったModelを作成
Albumが親で、AlbumとArtistが1対1、AlbumとSongが1対多になるように作成。
has_manyやnested_attributesも設定しておく
rails g model Album album_title:string
rails g model Artist full_name:string birthday:datetime album:references
rails g model Song title:string album:references
Reformクラス作成
app/forms/album_form.rb
を作り編集する。
class AlbumForm < Reform::Form
property :album_title
# has_oneならproperty
property :artist do
property :title
# birthdayは入れない
end
# has_manyならcollection
collection :songs, populate_if_empty: Song do
property :title
validates :title, unique: true
end
end
ControllerにReformインスタンス作成
Controllerを作ってReformのインスタンスを作ってみる。
class AlbumController < ApplicationController
def show
album = Album.includes(:artist, :song).find(params[:id])
@form = AlbumForm.new(album)
end
end
この時大事なのは、Reformオブジェクト作成時に入れる引数は、先程作ったReformクラスのpropertyやcollectionの名前でメソッドとして呼べることで形式でないといけないということである。
雰囲気でHashが渡せる気がするが、そんなことはない。
Modelならもちろん問題ないし、OpenStructやStructでも引数に渡すことができる。
# 駄目な例
album = {album_title: 'アルバム', artist: {full_name: '歌手の名前'}}
# 大丈夫な例
# Model
album = Album.new
@form = AlbumForm.new(album)
# StructやOpenStruct
album = OpenStruct.new(album_title: 'アルバム', artist: OpenStruct.new(full_name: '歌手の名前'))
@form = AlbumForm.new(album)
HTMLでFormタグと中身を書く
HTMLを書いてみる
<%= form_for @form do |f|%>
<%= f.text_field :album_title %>
<%= fields_for @form.artist do |artist| %>
<%= f.text_field :full_name %>
<% end %>
<% f.submit '送信' %>
<% end %>
普通のform_forと変わらない。
ただ、Reformクラスのおかげで、どういう構造になっているのかがわかりやすい。
ActiveRecordだけだとModelにカラムが書いてないので、どういう構造なのか分かりづらかったりする。
FormObjectを作れるGemには他にもActiveModelやVirtusがあるが、こちらのほうが見やすい。
Reformでデータを保存する
updateアクションに前述のFormをsubmitして保存する。
class ArtistController < ApplicationController
...
def update
# 乱暴だが、全パラメータを許可する
params.permit!
# 更新する対象のデータを入れる。
album = Album.includes(:artists, :song).find(params[:id])
album_form = AlbumForm.new(album)
# バリデーション
if album_form.validate params[:album]
# 保存
album_form.save
else
album_form.errors
end
end
end
Reformクラスのデータの持ち方・保存に関して
実は、Reformはデータの保持する領域は2つある。
- album_form直下のpropertyやcollectionで指定されたキー。初期化時に使われた変数からコピーされる。
- album_form.model以下。ここには初期化時に使われた変数がまるごとそのまま入る。保存時にも使われる。
コードで表すと以下のようになる
album_form = AlbumForm.new(album)
# アクセスできる
album_form.album_title
album.artist.full_name
# propertyにないからアクセスできない
album_form.artist.birthday
# modelにはalbumがそのまま代入される
album_form.model.album_tile
# modelにはそのまま入っているので、birthdayにもアクセスできる
album_form.model.artist.birthday
album_form.validates params[:artist]
を実行すると
1の領域にparams内の値が入る。
album_form.save
を実行すると、まず内部でalbum_form.sync
が実行される。
これは1の領域にあるデータを2の領域にコピーする。
そのあと、各Modelのsaveメソッドが走り、データが保存される。
params[:album] = {title: タイトル}
alubm = Album.new(title: '仮')
album_form = AlbumForm.new(album)
# album.title => 仮
# album_form.model.title => 仮
# album_form.title => 仮
album_form.validates params[:album]
# album.title => 仮
# album_form.model.title => 仮
# album_form.title => タイトル
# save時に自動でsycnが呼ばれる。ここでは明示的に呼び出している。
album_form.sync
# album.title => タイトル
# album_form.model.title => タイトル
# album_form.title => タイトル
album.save
このようにreformでは、スムーズにModelへとデータをコピーできる。
デフォルトのrailsだと単体のModelならparamsの値をModelへ反映させるのは簡単だが、
入れ子状になってたりして複雑なデータ構造の場合は難しい。
nested_attributesを使うと多少ましになるが、paramsを加工する場合、子Modelのカラム名指定時にattributesをつけ忘れたりして大変だったりする。
それに比べると、validates, saveとするだけでデータ保存されるのはかなり楽だ。
Validationについて
特定のページにだけ必要なバリデーションというのはよくある話だ。
こういうものはModelに書くと冗長になりがちだ。
Reformは、それぞれのReformクラスで特有のバリデーションを持てるようになっている。
validates :title, unique: true
というのがそれに該当する。
これはRailsのModelのバリデーションと同じ構文が書ける。
ただしcollectionに関しては、property_if_empty: Class
を指定するのを忘れると、バリデーションが動かない。(確か前述の1のデータ領域にデータを入れるときに何のクラスを元にデータを入れればいいかわからなくてエラーを起こしたはず。)
また、album_form.validates params[:artist]
でバリデーションエラーがある場合は、
form.errors
にエラー内容が入る。
railsと同様に扱えるため、form.errors.full_messages
でエラーメッセージ全文を取得できる。
終わりに
今回はreformについてざっくり紹介した。
大体のイメージを掴んでもらえたらうれしい。
ただし、あくまでざっくりなので、掴んだイメージを元に公式のドキュメント見ていたらければと思う
http://trailblazer.to/gems/reform/
Form Objectは近年機運が高めってきているので、なにか良さげなものを感じたらぜひ使ってみてほしい。