はじめに
現在、社内の有志で気になる gem をランチを食べながらコードリーディングする、ということを週1回のペースで続けています。
その中で、Rails で form を簡単に実装できる gem である simple_form において、f.input
でどんな処理がされているかコードリーディングした記録をまとめようと思います。
前提
想定読者
Rails で simple_form を使ったことがある
simple_form のバージョン
現時点で最新の simple_form v4.1.0 を対象に読みます。
メソッドの記号表記について
コードを読み進めるにあたり、メソッドの記号表記は、ruby 2.5.0 ドキュメントのヘルプ を参考にします。
記号 | 意味 | 例 |
---|---|---|
.# |
モジュール関数 | Kernel.#require |
# |
インスタンスメソッド | String#size |
. |
クラスメソッド | Dir.chdir |
読むテーマについて
今回は、以下サンプルコードにおいて、simple_form_for
メソッド内の f.input
でどんな処理をしているのかを追っていきます。
= simple_form_for @user do |f|
= f.input :username
= f.input :password
= f.button :submit
おさらい:simple_form の The Wrappers API について
実際にコードリーディングを始める前に、今回のコードリーディングに関わる前提知識として simple_form の The Wrappers API についておさらいしておきます。
simple_form の The Wrappers API では、フォームのコンポーネントをどのように render するかを設定、カスタマイズすることができます。
例えば simple_form で以下のような簡単なフォームを作った時を考えます。
= simple_form_for @user do |f|
= f.input :username
= f.input :password
= f.button :submit
上記において、f.input
や f.button
といった要素をフォームのコンポーネントと呼びます。
こちらが render されると、若干省いているところもありますが、ざっくり以下のような html が生成されます。
<form novalidate="novalidate" class="simple_form new_user" id="new_user" action="/users/sign_in" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="xxx">
<div class="input email optional user_email">
<label class="email optional" for="user_email">メールアドレス</label>
<input class="string email optional" type="email" value="" name="user[email]" id="user_email">
</div>
<div class="input password optional user_password">
<label class="password optional" for="user_password">パスワード</label>
<input class="password optional" type="password" name="user[password]" id="user_password">
</div>
<input type="submit" name="commit" class="btn">
</form>
それぞれのコンポーネントにおいて、単純な input
タグだけではなく、div
タグで囲われていたり、label
タグが追加されていたりと、簡単な記述で様々な補完を加えた上で生成してくれているのがわかると思います。
このそれぞれのコンポーネントにおいて、どのような形で html タグを生成するかを指定するのが、simple_form の The Wrappers API です。
The Wrappers APIは、config.wrappers
を使うことで設定できます。
simple_form をインストールする時に、 rails generate simple_form:install
と実行すると思いますが、その際 config/initializers/simple_form.rb
という初期設定ファイルが生成されますよね。
以下に生成されるファイルをコメントを省いて記載します。
SimpleForm.setup do |config|
config.wrappers :default, class: :input,
hint_class: :field_with_hint, error_class: :field_with_errors do |b|
b.use :html5
b.use :placeholder
b.optional :maxlength
b.optional :pattern
b.optional :min_max
b.optional :readonly
b.use :label_input
b.use :hint, wrap_with: { tag: :span, class: :hint }
b.use :error, wrap_with: { tag: :span, class: :error }
end
config.default_wrapper = :default
# 以下略
end
こちらに記載されている config.wrappers
ブロック内の設定をいじることで、生成される html をカスタマイズすることができます。
詳細な利用方法は README に書いてありますので、ご参照ください。
STEP①: simple_form_for
メソッドの定義場所を確認する
それではコードを読み進めていきます。
まずサンプルコードで定義した simple_form_for
メソッドをどこで定義しているかから確認していきます。
-
simple_form_for
メソッドは、SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for
メソッドから実行される - 色々
options
の指定がされているが、最終的にform_for
メソッドが実行されている -
form_for
は、ActionView::Helpers::FormHelper#form_for メソッドのこと -
ActionView::Helpers::FormHelper
のメソッドがSimpleForm::ActionViewExtensions::FormHelper
内で使えるのは、ActiveSupport
を使ってinclude
しているから -
ActionView::Helpers::FormHelper#form_for
のbuilder
オプションとして、SimpleForm::FormBuilder
が指定されている
STEP②: SimpleForm::FormBuilder
の概要を確認する
ここまでで、SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for
メソッドで、ActionView::Helpers::FormHelper#form_for
の builder
として SimpleForm::FormBuilder
クラスのオブジェクトが使われていることがわかりました。
次は、SimpleForm::FormBuilder
の概要をさらっていきます。
-
SimpleForm::FormBuilder
は ActionView::Helpers::FormBuilder を継承したもの -
f.input
やf.button
といったメソッドはSimpleForm::FormBuilder
で定義されている。例えば以下-
f.input
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L117 -
f.input_field
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L164 -
f.association
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L206 -
f.button
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L236
-
- ここから、
simple_form_for
のブロック引数f
は、SimpleForm::FormBuilder
クラスのオブジェクトであることがわかる
STEP③: SimpleForm::FormBuilder#input
の処理を確認する
ここまでで、f.input
の処理は、SimpleForm::FormBuilder#input
メソッドが実行されていることがつかめました。
次は、SimpleForm::FormBuilder#input
の処理内容を読んでいきます。
-
input
をfind_input
、wrapper
をfind_wrapper
でそれぞれ検索・指定しつつ、wrapper.render input
を実行している -
SimpleForm::FormBuilder#find_input
メソッド では、メソッドの引数input_type
に応じて、SimpleForm::Inputs::TextInput
やSimpleForm::Inputs::BooleanInput
といったクラスのオブジェクトを戻り値として返している -
SimpleForm::FormBuilder#find_wrapper
メソッドでは、option
の有無やinput_type
に応じて、render
メソッドに応じるオブジェクトか、SimpleForm.#wrapper
メソッドの戻り値か、SimpleForm::FormBuilder#wrapper
メソッドの戻り値かを出し分けしている
STEP④: SimpleForm::FormBuilder#wrapper
メソッドの戻り値を確認する
ここまでで、SimpleForm::FormBuilder#input
メソッドでは、SimpleForm::Inputs::TextInput
などのオブジェクトを引数に、wrapper.render
を実行することがわかりましたが、wrapper
のオブジェクトの実体がまだはっきりしていません。
そのため次は、SimpleForm::FormBuilder#wrapper
メソッドの戻り値をたどって、wrapper
の実体を確認します。
-
SimpleForm::FormBuilder#wrapper
メソッドはattr_reader
を使って定義されている -
SimpleForm::FormBuilder#wrapper
の実体となるインスタンス変数@wrapper
は、initialize
時に値が代入されるが、その値はSimpleForm.#wrapper
メソッドの実行結果を代入している -
SimpleForm.#wrapper
メソッドの記述場所は以下。@@wrappers[name.to_s]
が処理の内容 - しかし
@@wrappers
の初期値は空の Hash として定義されている - 実際に
@@wrappers
に値を代入している処理をしているのは、SimpleForm.#wrappers
メソッド -
SimpleForm
モジュール内でSimpleForm.#wrappers
メソッド実行する処理が書かれていて、ここで@@wrappers
に代入している -
SimpleForm.#wrappers
メソッドの内部処理ではSimpleForm.#build
メソッドが実行され、SimpleForm::Wrappers::Builder
の処理を実行し、その結果をSimpleForm::Wrappers::Root
クラスのインスタンス変数に含めつつ戻り値として返している - つまり、
SimpleForm::FormBuilder#wrapper
メソッドの戻り値はSimpleForm::Wrappers::Root
クラスのオブジェクト
STEP⑤: SimpleForm::Wrappers::Builder
の処理概要を確認する
ここまでで、wrapper
の実体が SimpleForm::Wrappers::Root
クラスのオブジェクトであることがわかりました。
しかし、SimpleForm::Wrappers::Root
のインスタンス変数に含めているSimpleForm::Wrappers::Builder
の処理は何をしているのでしょうか?少し確認してみます。
-
SimpleForm::Wrappers::Builder
クラスには、config.wrappers
の中で使われているb.use
やb.optional
のようなメソッドが定義されている-
b.use
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L49 -
b.optional
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L57 -
b.wrapper
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L62
-
-
SimpleForm::Wrappers::Builder#use
メソッドでは、optionに:wrap_with
があればSimpleForm::Wrappers::Single
クラス、なければSimpleForm::Wrappers::Leaf
クラスのオブジェクトを@components
に配列の要素として代入している -
SimpleForm::Wrappers::Root
オブジェクト作成時、インスタンス変数にbuilder.to_a
を渡している。つまり、SimpleForm::Wrappers::Single
やSimpleForm::Wrappers::Leaf
オブジェクトの配列を渡している -
builder.to_a
を渡されたSimpleForm::Wrappers::Root
のインスタンス変数は、super(:wrapper, *args)
でクラス継承元のSimpleForm::Wrappers::Many
クラスのインスタンス変数を一部使っている -
super(:wrapper, *args)
と記載されているので、@namespace
に:wrapper
が入り、その次は@components
、 それ以降は@defaults
にまとめて順に代入される - つまり
SimpleForm::Wrappers::Root
クラスのinitialize
時に渡されたbuilder.to_a
は、インスタンス変数@components
となる
STEP⑥: wrapper.render input
の処理内容を確認する
ここまでで、wrapper
の実体が SimpleForm::Wrappers::Root
クラスのオブジェクトであり、SimpleForm::Wrappers::Root
クラスのインスタンス変数 @components
には、SimpleForm::Wrappers::Single
や SimpleForm::Wrappers::Leaf
オブジェクトの配列が格納されていることがわかりました。
今まで読んできた内容を踏まえ、一旦SimpleForm::FormBuilder#input
メソッドに戻り、wrapper.render input
の処理を追っていきます。
-
wrapper.render input
はSimpleForm::Wrappers::Root#render
が呼ばれている - メソッド内部で
super
しているため、SimpleForm::Wrappers::Root
クラス継承元のSimpleForm::Wrappers::Many#render
メソッドが呼ばれる -
SimpleForm::Wrappers::Many#render
メソッド では、空文字列をhtml_safe
したcontent
に、components
ごとにcomponent.render
の処理結果を文字列に変換したものをどんどん連結させている - 最後に
SimpleForm::Wrappers::Many#wrap
メソッドで処理結果を html として整形しつつ返している
STEP⑦: component.render
の処理内容を確認する
ここまでで、wrapper.render input
が実行されると、整形された html が返ってくるというところまでわかりました。(ようやくhtml変換のところまで来ましたね。。)
あとモヤッとしているところが component.render
の処理内容だったので、こちらを確認していきます。
-
component.render
のcomponent
は、SimpleForm::Wrappers::Single
やSimpleForm::Wrappers::Leaf
オブジェクト -
SimpleForm::Wrappers::Single#render
メソッドの処理内容は以下 -
SimpleForm::Wrappers::Single
のインスタンス変数@component
も、実体はSimpleForm::Wrappers::Leaf
クラスのオブジェクトの模様 -
SimpleForm::Wrappers::Leaf#render
メソッドを確認すると、メソッド引数input
において、input.method(namespace)
を実行している -
method
メソッドについては ruby のObject#method
メソッドについてを参照 -
input
の実体は STEP③ で確認したとおり、SimpleForm::Inputs::TextInput
やSimpleForm::Inputs::BooleanInput
クラスのオブジェクト - この
@namespace
に入ってくる値は、b.use :html5
やb.use :label_input
でいうと:html5
や:label_input
のこと -
SimpleForm::Inputs::TextInput
やSimpleForm::Inputs::BooleanInput
クラス継承元のSimpleForm::Inputs::Base
クラスを確認すると、SimpleForm::Components::HTML5
やSimpleForm::Components::LabelInput
といったモジュールをinclude
している - include されたそれぞれのモジュールで、
html5
メソッドやlabel_input
メソッドを定義してある-
SimpleForm::Components::HTML5.#html5
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/components/html5.rb#L9-L18 -
SimpleForm::Components::LabelInput#label_input
: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/components/label_input.rb
-
-
SimpleForm::Components::LabelInput.#label_input
メソッド内で、label
の有無を判断しつつ、input
をnamespace
に指定してmethod
をcall
している - 上記によって、
SimpleForm::Inputs::TextInput
などのクラスで定義してあるinput
メソッドが実行され、内部処理で対象の html が生成される -
SimpleForm::Inputs
以下のクラスは全てSimpleForm::Inputs::Base
クラスを継承しており、input
メソッドを実装している。実装していないとNotImplementedError
となる - つまり、
component.render
ではconfig.wrappers
で定義されたオプションを一つ一つ処理し、html として生成している
まとめ
ここまで非常に長くなりましたが、simple_form_for
メソッド内の f.input
メソッドが実行されるまでの処理をまとめました。
-
SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for
メソッドで、ActionView::Helpers::FormHelper#form_for
のbuilder
としてSimpleForm::FormBuilder
クラスのオブジェクトが使われている -
f.input
の処理は、SimpleForm::FormBuilder#input
メソッドが実行されている -
SimpleForm::FormBuilder#input
メソッドでは、SimpleForm::Inputs::TextInput
クラスなどのオブジェクトを引数に、wrapper.render
を実行する -
wrapper
オブジェクトの実体はSimpleForm::Wrappers::Root
クラスのオブジェクト -
SimpleForm::Wrappers::Root
クラスのインスタンス変数@components
には、SimpleForm::Wrappers::Single
やSimpleForm::Wrappers::Leaf
オブジェクトの配列が格納されている -
wrapper.render input
が実行されると、整形された html が返ってくる -
wrapper.render
内の処理component.render
では、config.wrappers
で定義されたオプションを一つ一つ処理し、html として生成している
終わりに
今回は f.input
の処理だけをコードで追ってみましたが、それだけでも simple_form のコードを全体的に読むことになり、良い経験になりました。
一気に読み進めたので拙い部分もあると思いますが、もし間違いなどあればコメントいただけるとありがたいです。