この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
Ruby on Rails でフォームなんかを作る際、テキスト入力部分に「文字数カウント機能」を付けたいと思うことが多々あります。 テキストエリアの右下とかに「0/30」とか付いてて、テキストエリアに入力するとリアルタイムにカウントしてくれるアレです。 JavaScriptで作れるのですが、テキストエリアがたくさんあるフォーム画面などで、 「こっちのフォームは300文字までだけど、その下のフォームは500文字まで入力できる」 などがあると、いちいち作っていると結構ごちゃごちゃしてきてしまいます。 この文字数カウント生成処理を、railsのhelperに全部入れると、けっこう分かりやすかったのでメモがわりの覚え書きです。
設置する文字数カウントの概要
* 「xx(現在の入力文字数) / xx(最大入力文字数)」の形で表示
* 入力文字数が最大文字数を超えた時、入力文字数の色を赤くする
* 各フォームで最大文字数の値はそれぞれ違う
まずviewを見てみます。
例えば「title」「text」という二つのカラムを持つColumnというモデルについて、
それぞれの値を入力するためのフォームを必要最低限の物だけ設置してみます。
<%= form_for(@column, url: url_path) do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :text %>
<%= f.text_area :text %>
<% end %>
これだとラベルとそれぞれのテキスト入力エリアしかありません。
文字数カウントを設置したい・・ので、helperに作ってもらうことにしましょう。
def countable_field(model_name, model, attribute, maxlength)
sanitized_model_name = model_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, '_').sub(/_$/, '')
content_tag(:div, class: 'text_count') do
len = model.send(attribute).to_s.length
style = 'color: red;' if len > maxlength
concat content_tag(:span, len, id: "#{sanitized_model_name}_#{attribute}_len", style: style)
concat ' / '
concat content_tag(:span, maxlength, id: "#{sanitized_model_name}_#{attribute}_max")
end
end
ちょっとごちゃごちゃしていますが、
countable_field という関数に「モデル名」「モデルインスタンス」「カラム名」「最大長」を渡すと、HTMLソースを作ってくれるようになりました。
現在の入力文字数部分は「モデルカラム_len」、最大文字数部分は「モデルカラム_max」のidが付くように統一してます。
form_forで出来る入力フォームのidにそれぞれ「len」「max」が付く形です。JSで使います。
初期値がある場合を想定して、テキストエリアの内容が既に最大文字数超えていた場合は、赤くするようにしています。
そして、JSで文字数カウント機能を作っておきます。
CoffeeScriptで作ってみたのが以下です。
$ ->
$('.countable').on 'click keyup blur change paste input onload', ->
countLength($(this))
countLength = ($field) ->
len = $field.val().length
$("##{$field.attr('id')}_len").html(len)
countMax = Number($("#" + ($field.attr('id')) + "_max").html())
countDown = countMax - len
if countDown < 0
$("##{$field.attr('id')}_len").css
color: ‘red’
else
$("##{$field.attr('id')}_len").css
color: 'black’
やっていることは「.countable」というクラスが付いたテキスト入力エリアに変更があったら、
「id : モデルカラム_len」のHTMLの中身に入力された文字数の数値を入れて、
「id : モデルカラム_max」のHTMLの中身の数値より大きければ、文字色を赤くしています。
あとはビューでcountable_fieldを呼び出してあげればいい・・・のですが。
引数が面倒くさい
渡さなければならないのは「モデル名」「モデルインスタンス」「カラム名」「最大長」。
わりといろいろ渡してあげなければいけません。ちょっと面倒です。出来れば二つぐらいに減らしたい。
form_forの中で使いたいので、ActionView::Helpers::FormBuilderのサブクラスを作ってみましょう。
module ActionView
module Helpers
class FormBuilder
def countable_field(method, maxlength)
@template.countable_field(@object_name, @object, method, maxlength)
end
end
end
end
先程つくったcountable_fieldメソッドを呼ぶだけの関数ですが、引数として渡すのは「カラム名」「最大文字長」だけになりました。
これをviewに仕込むとこんな感じになります。
<%= form_for(@column, url: url_path) do |f| %>
<%= f.label :title %>
<%= f.text_field :title, class: 'countable' %>
<%= f.countable_field(:title, 100)%>
<%= f.label :text %>
<%= f.text_area :text, class: 'countable' %>
<%= f.countable_field(:text, 500)%>
<% end %>
viewに足したのは各入力エリアにcountableクラスを追加したのと、countable_fieldを設置しただけですが、これで文字数をカウントしてくれます。
ActionView::Helpers::FormBuilderのサブクラスを作ることの利点としては、引数を減らすことに加え、
他のモデルについても「カラム名」「最大文字長」だけを渡せば流用できます。
また、form_forで出来る入力フォームのidをもとにjsが補足するので、
nested_formなどの動的にフォームが増えたりするものでも、揃えるようにすれば対応が出来ました。