Railsのフォームヘルパーを深掘りしたら得るものが多かった

これは「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」8日目の記事です。

https://adventar.org/calendars/9309

adventar.org

昨日は、Yuki Watanabeさんの『認可を設定するときは「タイミング」を気にしよう』でした。 yukiwatanabe.hatenablog.com

パート1はこちらです。

https://adventar.org/calendars/9142

adventar.org

この記事では、初学者の私が普段何気なく使っていたRailsのヘルパーメソッドを深掘りすることで得られた気づきをご紹介できればと思います😃

開発環境

Railsの開発環境と、エディタ・検証用ブラウザは以下のとおりです。

Rails 7.0.6
ruby 3.2.2

エディタ:
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 with options. 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メソッドを呼び出し、引数にkeyrequired)を渡す。

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の仕様を知ることができただけでなく、以下の学びを得ることができました🙌🏻

RailsAPIドキュメントの読み方がなんとなくわかった

  • 英語の説明も、翻訳ツールを使いながら(なんとか)読むことができる
  • メソッドの説明文にはリンクが添えられているので、ソースコードへアクセスしやすい
  • 目的のメソッドだけでなく、その前後のメソッドの説明文にも目を通す

Railsソースコードを読むハードルが下がった

  • ファイル数は膨大だけど、実行される箇所だけデバッグで確認していけばよい
  • 1ファイル数百行もあるが、コメントアウトが結構多い

デバッグ超重要❗

  • デバッガー(debug.gem)の基本的な使い方を覚える
  • 地道にステップインして、メソッドの処理と出力される値を1つずつ確認していく

深掘りして調べるのは、予想よりも時間と労力がかかる⌚️

  • 課題の実装と調査〜学習のバランスを考える
    • この記事を書くため、調査〜執筆まで6時間以上かかった😳
  • @jnchitoさん より
    • 時間がかかるのは自身の成長に必要な時間だと前向きに捉える
    • 公式ドキュメントやマニュアルを確認する習慣をつける

実はこれまでFBCの課題を進めることを優先し、なかなかブログを書くことができませんでした😅

今回、ようやく重い腰を上げて記事を書いてみましたが、Railsの知識がより理解が深まったと感じています💪🏻


この記事が、私と同じようにブログを書こうかと悩んでいる方の参考になれば幸いです📝

参考リンク