問題03の解答と解説

2016/03/21

解答

先月(2016年2月)の演習問題「その他」用のテキスト入力欄を持つ選択式入力欄の出題者(黒田)による解答例です。

7 回に分けて GitHub のリポジトリにコミットしてありますので、ソースコードの差分をご覧ください。

  • 0e1140b Bump Rails to 4.2.6
  • 7442c4a 職業関連のカラムを追加
  • a086af2 バリデーション、定数定義
  • 5399799 HTMLテンプレートに職業欄追加
  • 15f1ef9 フォーム制御JavaScript作成
  • f0f6c73 職業「その他」ではない場合はoccupation_nameをnilに
  • a3723f0 HTMLテンプレートの改善

このページは「Rails 演習問題」という連載の一部です。詳しくは、はじめにをお読みください。

解説

今回も応募者は1名(rinpopoyoさん)でした。毎度、ありがとうございます。

ソースコードの差分は以下の通り:


まずは、データベースのマイグレーションスクリプトから見て行きましょう。

rinpopoyoさんはこう書きました。

class ModifyMembers2 < ActiveRecord::Migration
  def change
    add_column :members, :job, :integer
    add_column :members, :other_job, :string
  end
end

カラム名こそ異なるものの、私のコードと本質的には同じです。

class ModifyMembers2 < ActiveRecord::Migration
  def change
    add_column :members, :occupation_number, :integer
    add_column :members, :occupation_name, :string
  end
end

ちょっと細かいことを言えば、"job" という英単語はソフトウェア開発でよく使われるので、紛らわしさを避けるために "occupation" を使ったほうがいいかもしれません。


次に、HTMLフォームのERBテンプレートを比較してみましょう。

rinpopoyoさん:

  <tr>
    <th><%= form.label "職業" %></th>
    <td>
      <%= form.radio_button :job, "会社員・会社役員" %>
      <%= form.label "会社員・会社役員" %>
      <br>
      <%= form.radio_button :job, "自営業・自由業" %>
      <%= form.label "自営業・自由業" %>
      <br>
      <%= form.radio_button :job, "公務員" %>
      <%= form.label "公務員" %>
      <br>
      <%= form.radio_button :job, "学生" %>
      <%= form.label "学生" %>
      <br>
      <%= form.radio_button :job, "無職" %>
      <%= form.label "無職" %>
      <br>
      <%= form.radio_button :job, "その他" %>
      <%= form.label "その他" %>
      <p class="other_job_text"><%= form.text_field :other_job %></p>
    </td>
  </tr>

私:

  <tr>
    <th><%= Member.human_attribute_name(:occupation_number) %></th>
    <td>
      <% Member::OCCUPATIONS.each_key do |n| %>
      <div>
        <label>
          <%= form.radio_button :occupation_number, n %>
          <%= Member::OCCUPATIONS[n] %>
        </label>
      </div>
      <% end %>
      <div id="occupation-name-form-group">
        <label>
          具体的に
          <%= form.text_field :occupation_name %>
        </label>
      </div>
    </td>
  </tr>

この部分はかなり違いますね。

私の方は Member モデルで OCCUPATIONS という定数を定義してラジオボタンにラベルを付けるために使っています。

  OCCUPATIONS = {
    1 => '会社員・会社役員', 2 => '自営業・自由業',
    3 => '公務員', 4 => '学生', 5 => '無職',
    0 => 'その他' }

rinpopoyoさんは、Rails 4.1 で導入された比較的新しい(といっても、2年前ですが) ActiveRecord::Enum の機能を利用しています。拙著『改訂3版基礎Ruby on Rails』では紹介しませんでした。

この機能についての解説を書き始めると長くなるので、省略します。ネットで「activerecord enum」を検索すれば、日本語の解説記事が見つかります。

rinpopoyoさんは、Member モデルに次のコードを追加しています(実際のコードは1行で書いてありますが、長いので適宜折り曲げました)。

  enum job: {
    "会社員・会社役員" => 0,
    "自営業・自由業" => 1,
    "公務員" => 2,
    "学生" => 3,
    "無職" => 4,
    "その他" => 5
  }

その結果、整数型の job カラムの値を次のような書き方で扱えるようになります。

@member.job = '学生'
p @member.job # => "学生"

ちなみに、整数の値を使うこともできます:

@member.job = 3
p @member.job # => "学生"
p @member[:job] # => 3

さて、ActiveRecord::Enum を利用するというアイデアはとてもよいと思いますが、3点ほど改善案があります。

まず、「その他」の値を 0 にします。将来的に職業の選択肢が増えることを見越しての措置です。

  enum job: {
    "会社員・会社役員" => 1,
    "自営業・自由業" => 2,
    "公務員" => 3,
    "学生" => 4,
    "無職" => 5,
    "その他" => 0
  }

次に、選択肢リストをループで生成します。

  <tr>
    <th><%= form.label "職業" %></th>
    <td>
      <% Member.jobs.each_key do |name| %>
      <div>
        <%= form.radio_button :job, name %>
        <%= form.label name %>
      </div>
      <% end %>
      <div class="other_job_text"><%= form.text_field :other_job %></div>
    </td>
  </tr>

本当は、次のように修正したいところです:

  <tr>
    <th><%= form.label "職業" %></th>
    <td>
      <% Member.jobs.each do |name, value| %>
      <div>
        <%= form.radio_button :job, value %>
        <%= form.label name %>
      </div>
      <% end %>
      <div class="other_job_text"><%= form.text_field :other_job %></div>
    </td>
  </tr>

修正前のコードは各ラジオボタンの <input> タグの id 属性の値がすべて "member_job_" になってしまい気持ち悪い(HTML のルール違反)です。しかし、この修正を行うと、JavaScriptのコードが動かなくなってしまうので、そのままにしておきましょう。

3番目の改善点は、<input> タグと <label> タグを同じレベルで並べる代わりに、<label> タグで<input> タグを囲むことです。

  <tr>
    <th><%= form.label "職業" %></th>
    <td>
      <% Member.jobs.each_key do |name| %>
      <div>
        <label>
          <%= form.radio_button :job, name %> <%= name %>
        </label>
      </div>
      <% end %>
      <div class="other_job_text"><%= form.text_field :other_job %></div>
    </td>
  </tr>

こうすれば、ラジオボタンそのものだけでなく、横のラベルテキストをクリックしても選択・非選択を切り替えられるようになります。このテクニックは意外に知られていませんが、使い勝手を左右するポイントのひとつです。


続いて、バリデーション。「職業として「その他」を選んだ時、それを具体的にテキストで10文字以内で入力できるようにし、何も入力しない場合はバリデーションエラーとする」という仕様をどのように実装すればいいでしょうか。

rinpopoyoさんは、app/models/member.rb にこう書いています。

  validates :other_job, length: { maximum: 10 }, presence: true, if: :otherjob?

  def otherjob?
    job == "その他"
  end

これでも OK ですが、otherjob? メソッドはここでしか使っていないので、ちょっと冗長な気がします。私なら、次のように書きます。

  validates :other_job, length: { maximum: 10 }, presence: true,
    if: -> { |obj| obj.job == "その他" }

それから、仕様の説明には書いてありませんが、obj.job の値が「その他」ではない場合、other_job カラムの値は NULL にしておくべきでしょう。最終形は次のようになります。

  before_validation do
    self.other_job = nil unless job == "その他"
  end

  validates :other_job, length: { maximum: 10 }, presence: true,
    if: -> { |obj| obj.job == "その他" }

最後に、JavaScriptコードのレビューをして終わりにしましょう。

rinpopoyoさんのコードはこうなっています。

$(function(){
  $(".other_job_text").hide();
});

$( 'input[id="member_job_"]:radio' ).change( function() {
  $(".other_job_text").hide("slow");
});

$( 'input[value="その他"]:radio' ).change( function() {
  $(".other_job_text").show("slow");
});

ここは、私なら次のように書きます。

$(function(){
  $(".other_job_text").hide();
});

$('input[name="member[job]"]').change( function() {
  if ($(this).val() === 'その他')
    $(".other_job_text").show("slow");
  else
    $(".other_job_text").hide("slow");
});

次回の出題をお楽しみに!