問題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");
});
次回の出題をお楽しみに!