Railsのフォームヘルパーを深掘りしたら得るものが多かった
これは「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」8日目の記事です。
https://adventar.org/calendars/9309
昨日は、Yuki Watanabeさんの『認可を設定するときは「タイミング」を気にしよう』でした。 yukiwatanabe.hatenablog.com
パート1はこちらです。
https://adventar.org/calendars/9142
この記事では、初学者の私が普段何気なく使っていたRailsのヘルパーメソッドを深掘りすることで得られた気づきをご紹介できればと思います😃
開発環境
Railsの開発環境と、エディタ・検証用ブラウザは以下のとおりです。
エディタ:
VSCode:1.84.2
VSCode rdbg Ruby Debugger:v0.2.1
ブラウザ:
GoogleChrome Ver120
はじめに
フィヨルドブートキャンプでは、様々なプラクティスが用意されています。受講生はそのプラクティスごとに課題を提出し、メンターの皆様からレビューを受けながら学習を進めています。
学習期間中は毎日『日報』を提出する必要があり、メンターの皆様がチェックしてくれます。
日報の書き方は人ぞれぞれですが、学習の中で学んだことや悩みなどを投稿します📝
チェックしてくれたメンターから感想のコメントを頂くこともありますし、悩みや疑問点があれば直接回答やアドバイスを頂けます。
今回の記事は、Railsアプリを開発する課題に取り組んでいた際に浮かんだ疑問を、メンターに相談したことから始まります。
コメント投稿に制限をかける
Railsでは、フォームを作成するための、フォームヘルパーが用意されています。
例えば、掲示板のように、ユーザーがコメントを投稿するためのフォームを作成したいとします。
Railsでは、コメントを投稿するテンプレートファイルに以下のようなコードを書くと、
# app/views/comments/_form.html.erb <%= form_with(model: commentable) do |form| %> <%= form.text_area :content %> <%= form.submit %> <% end %>
HTMLのフォームタグが出力されます。便利ですね😃
<form action="/books/2" accept-charset="UTF-8" method="post"> <input type="hidden" name="_method" value="patch" autocomplete="off"><input type="hidden" name="authenticity_token" value="~~" autocomplete="off"> <textarea name="book[content]" id="book_content"></textarea> <input type="submit" name="commit" value="更新する" data-disable-with="更新する"> </form>
ブラウザ確認すると、このようにコメントを入力するテキストエリアと「更新する」ボタンが表示されます。
このままコメントを投稿することもできるのですが、テキストエリアに何も入力していない状態では投稿させたくありません🤔
テキストエリアに何も入力されていない場合は、コメントを投稿できないようにしてみましょう。
text_area
メソッドにrequired: true
を追加します。
# app/views/comments/_form.html.erb <%= form_with(model: commentable) do |form| %> # `required: true` を追加します <%= form.text_area :content, required: true %> <%= form.submit %> <% end %>
出力されたHTMLはこのようになりました。textarea
タグ内にrequired
属性が確認できますね💡
<form action="/books/2" accept-charset="UTF-8" method="post"> <input type="hidden" name="_method" value="patch" autocomplete="off"> <input type="hidden" name="authenticity_token" value="~~" autocomplete="off"> <!-- required="required" が追加されました --> <textarea required="required" name="book[content]" id="book_content"></textarea> <input type="submit" name="commit" value="更新する" data-disable-with="更新する"> </form>
textarea
タグにrequired
属性が追加されたので、テキストエリアに入力がない状態で更新ボタンをクリックしても投稿はできず、入力を促すメッセージが表示されるようになります。(GoogleChrome Ver120で検証)
これで、コメント投稿に制限をかける実装ができました🙌🏻
require: true
の仕組みがわからなかった
text_area
メソッドにrequired: true
を追加すると、出力されたtextarea
タグにrequired
属性が追加されます。これで未入力のコメント投稿を制限できることができました。
この仕組みを覚えておけば、input
タグなどにも未入力の制限を実装できますね❗
でも、・・・なぜrequired: true
からrequired="required"
が追加されるのだろう?🤔
いくつかの記事を検索したところ、required: true
を追加すると投稿を制限できるようになることは説明されていましたが、(自分が検索した範囲では)その"理由"を見つけることができませんでした。
Ruby on Railsの公式ドキュメントを読んでみる
こういう時は公式ドキュメントを覗いてみるほうが良さそうです。
FBCメンターの@jnchitoさんがZennで公開されている「付録:Ruby on Railsの公式リファレンスについて|Rubyの公式リファレンスが読めるようになる本」を参考に、Railsの公式APIドキュメントを確認してみます。
APIドキュメントの「ActionView::Helpers::FormHelper」に、text_area
メソッドの使用例が記載されていました。
サンプルコードには、required: true
の説明はありませんが、disabled
要素の出力方法が記載されていました。
# text_areaのExamples text_area(:entry, :body, size: "20x20", disabled: 'disabled') # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled"> # #{@entry.body} # </textarea>
このdisabled
要素を出力する書き方を参考に、text_area
メソッドにrequired: 'required'
を渡します。
# app/views/comments/_form.html.erb <%= form_with(model: commentable) do |form| %> # `required: required` に修正 <%= form.text_area :content, required: 'required' %> <%= form.submit %> <% end %>
出力されたHTMLコードがこちら。required: 'required'
に変更しても、textareaタグにはrequired
属性が追加されていました。
<form action="/books/2" accept-charset="UTF-8" method="post"> <input type="hidden" name="_method" value="patch" autocomplete="off"> <input type="hidden" name="authenticity_token" value="~~~" autocomplete="off"> <!-- required="required" が追加された --> <textarea required="required" name="book[content]" id="book_content"></textarea> <input type="submit" name="commit" value="更新する" data-disable-with="更新する"> </form>
よって、required: true
またはrequired: 'required'
のどちらの記述でも、textarea
タグにはrequired
属性が追加されることがわかりました。
とりあえず、ここまでの検証作業を日報にまとめて提出したところ、翌日メンターの@jnchitoさんからtext_field
メソッドの説明文にヒントが書かれていることを教えていただきました。
text_field(object_name, method, options = {})
Additional options on the input tag can be passed as a hash withoptions
. These options will be tagged onto the HTML as an HTML element attribute as in the example shown.
つまり、text_area
メソッドのオプション(options)にハッシュの形で渡すと、そのハッシュをHTML属性に変換して出力する仕様になっているようです。
よって、text_area
メソッドにハッシュ形式で「required: 'required'
」で渡してあげると、HTMLの属性として「required="required"
」のように出力されるというわけです💡
<%= form.text_area :content, required: 'required' %> #=> <textarea required="required" ~~></textarea>
さらに検証を行っていたところ、ハッシュのキーがrequired
であれば、値に任意の文字列を指定しても「required="required"
」で出力されることがわかりました😳
<%= form_with(model: commentable) do |form| %> # 値に文字列 hoge を指定する <%= form.text_area :content, required: 'hoge' %> <%= form.submit %> <% end %> #=><textarea required="required" name="book[content]" id="book_content"></textarea>
また、HTMLの仕様を確認したところ、required
属性は『論理属性』になるため、下記3パターンの書き方が可能であることがわかりました。
<!-- 下記3パターンのいずれかの記述で、制限がかかる --> <textarea required ~~></textarea> <textarea required="" ~~></textarea> <textarea required="required" ~~></textarea>
Railsはこの仕様に合わせて、text_area
メソッドに「require: true
」を渡すと、論理属性であるrequire
属性がtrue
とみなされ、「required="required"
」の形で出力される仕様になっているのだと推測できます💡
ここまでの検証結果を再度まとめて日報を提出しました🙌🏻
自分なりに納得でき満足していたところ、@jnchitoさんから下記コメントを頂きました😳
@jnchitoさん
これ、どこかでそういう制御をやっているはずなので、そのコードまで特定できるとさらに素晴らしいですね!(期待)
Railsのソースコードに当たる🔎
Railsのソースコードのどこからチェックしていけばいいのだろうか😨、と考えつつdebug.gemでデバッグを開始します。
メソッドを1つずつ実行していった結果、タグヘルパーのtag_options
メソッドの引数として渡されたオプション(options)が、論理属性名="論理属性名"
で出力される仕様になっていることがわかりました。
https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/tag_helper.rb#L261
# actionview/lib/action_view/helpers/tag_helper.rb def tag_options(options, escape = true) # ~省略~ end
オプション(options)に渡されたkey
(今回の場合はrequired
)が変数type
に格納されて定数BOOLEAN_ATTRIBUTES
に格納された論理属性名と一致するかをチェック。
https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/tag_helper.rb#L21
BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus autoplay checked compact controls declare default defaultchecked defaultmuted defaultselected defer disabled enabled formnovalidate hidden indeterminate inert ismap itemscope loop multiple muted nohref nomodule noresize noshade novalidate nowrap open pauseonexit playsinline readonly required reversed scoped seamless selected sortable truespeed typemustmatch visible).to_set
type
が論理属性と判定されたら、boolean_tag_option
メソッドを呼び出し、引数にkey
(required
)を渡す。
https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/tag_helper.rb#L290
def tag_options(options, escape = true) # ~省略~ elsif type == :boolean if value output << sep output << boolean_tag_option(key) end # ~省略~ end
boolean_tag_option
メソッドにて、 論理属性はkey="key"
(つまり論理属性名="論理属性名"
)に変換されることから、最終的に「required="required"
」でHTML出力されていることを突き止めました😃
https://github.com/rails/rails/blob/main/actionview/lib/action_view/helpers/tag_helper.rb#L303
def boolean_tag_option(key) %(#{key}="#{key}") end
今回学んだこと
慣れないことばかりでかなり時間がかかりましたが、今回ヘルパーメソッドの仕様を調査したことで、Railsの仕様を知ることができただけでなく、以下の学びを得ることができました🙌🏻
RailsのAPIドキュメントの読み方がなんとなくわかった
- 英語の説明も、翻訳ツールを使いながら(なんとか)読むことができる
- メソッドの説明文にはリンクが添えられているので、ソースコードへアクセスしやすい
- 目的のメソッドだけでなく、その前後のメソッドの説明文にも目を通す
Railsのソースコードを読むハードルが下がった
デバッグ超重要❗
- デバッガー(debug.gem)の基本的な使い方を覚える
- 地道にステップインして、メソッドの処理と出力される値を1つずつ確認していく
深掘りして調べるのは、予想よりも時間と労力がかかる⌚️
- 課題の実装と調査〜学習のバランスを考える
- この記事を書くため、調査〜執筆まで6時間以上かかった😳
- @jnchitoさん より
- 時間がかかるのは自身の成長に必要な時間だと前向きに捉える
- 公式ドキュメントやマニュアルを確認する習慣をつける
実はこれまでFBCの課題を進めることを優先し、なかなかブログを書くことができませんでした😅
今回、ようやく重い腰を上げて記事を書いてみましたが、Railsの知識がより理解が深まったと感じています💪🏻
この記事が、私と同じようにブログを書こうかと悩んでいる方の参考になれば幸いです📝
参考リンク
- Action View フォームヘルパー - Railsガイド
- HTML 属性: required - HTML: ハイパーテキストマークアップ言語 | MDN
- 付録:Ruby on Railsの公式リファレンスについて|Rubyの公式リファレンスが読めるようになる本
- ActionView::Helpers::FormHelper
- HTML 属性: required - HTML: ハイパーテキストマークアップ言語 | MDN
- HTML Standard 日本語訳
- HTMLの基本書式 - 開始・終了タグや属性の指定方法 | できるネット
- Ruby 3.1 の debug.gem を自慢したい - クックパッド開発者ブログ
- [Ruby基礎][Rails基礎] Visual Studio CodeでRailsアプリを開発