<input type="number">
では物足りない
form.number_field
でレンダリングされる input タグでは次のような不満があります。
- 3桁区切りの表示をして欲しい(ことがある)
- うっかり全角数字で入力しても、入力がなかったことにしないで欲しい1
HTML の仕様(規格)の範疇だから自分でなんとかするしかない。なので設計方針はつぎのとおりです。
- input 欄に入力中 (focusin) は数値のみで、カンマは入力しない
- 入力は全角でも可能(使用者にできるだけ負担をかけない)
- 欄外に移ったら (focusout) 3桁区切りのカンマが入った表示にする
- POST(PATCH、PUT) の送信時 (submit) は数値を渡す
Stimulus についての概要は把握していることを前提としてます。
検証は、ruby 3.2、Rails 7.1 の環境です。
実装
サンプル作成
rails new sample
cd sample
rails g scaffold fruit price:float
rails db:create ; rails db:migrate
パーシャルテンプレートを変更します。comma_field
が今回の目的のヘルパーです。
- <%= form.number_field :price %>
+ <%= form.comma_field :price %>
FormBuilder に comma_field を追加する
表示を担うダミーの input エレメントと値を担う input エレメントをレンダリングするようにします。そのため、ActionView::Helpers::FormBuilder にcomma_field
というメソッドを追加します。
module ActionView
module Helpers
class FormBuilder
include TagHelper
def comma_field(method, options = {})
actions = token_list(%w[focusin->comma#focusin focusout->comma#focusout], options.dig(:data, :action))
controllers = token_list(options.dig(:data, :controller))
options.merge!({data: {comma_target: "dummy", action: actions}, value: @object[method]&.to_fs(:delimited), id: nil, name: nil})
options[:data].merge!({controller: controllers}) if controllers.present?
@template.content_tag(:span,
@template.text_field(nil, nil, objectify_options(options)) +
@template.hidden_field(@object_name, method, objectify_options({data: {comma_target: "numeric"}})),
data: {controller: "comma"}
)
end
end
end
end
上記コードの中心は次の部分です
@template.content_tag(:span,
@template.text_field(...) +
@template.hidden_field(...)
)
これにより *.html.erb 中の form.comma_field(..)
が<span><input type="text" ... /><input type="hidden" ... /></span>
へとレンダリングされるようになります。text_field
がダミーのエレメント、hidden_field
が数値のエレメント。それを Stimulus (後述) で扱うために <span>で囲っています。
Stimulus 用の data 属性を付加する
actions =
以下の4行は Stimulus のコントローラー、アクション、ターゲットを指示するためのものです。
- Stimulus のコントローラー名は comma とする(comma_controller.js)
-
token_list
は便利なヘルパー - ダミーのエレメントは
dummy
、数値のエレメントはnumeric
というターゲットとする - class 属性や他の data 属性などもオプションで渡せるようにする
- POST(PATCH、PUT)時に、数値フィールドだけ submit したいため、ダミーフィールドの name と id は指定しない(name: nil, id: nil)
レンダリング結果はこうなります
<span data-controller="comma">
<input data-comma-target="dummy" data-action="focusin->comma#focusin focusout->comma#focusout" type="text" />
<input data-comma-target="numeric" autocomplete="off" type="hidden" name="fruit[price]" id="fruit_price" />
</span>
他の実装方法
ここでは新たなメソッドを追加して ActionView::Helpers::FormBuilder を拡張しています。そのため、form_builder.rb
ファイルの配置はconfig/initializers/
にしてあります。なので、アプリ全体で使えることになります。
他方、FormBuilder を承継したクラスを利用する方法もあります。例えばヘルパーを作成して
class CustomFormBuilder < ActionView::Helpers::FormBuilder
...
end
- <%= form_with(model: fruit) do |form| %>
+ <%= form_with(model: fruit, builder: CustomFormBuilder) do |form| %>
のように、form_with
ごとに柔軟に指定する、など
Stimulus を使ったフロント部分
入力まわりは、Javascript を使わざるを得ないので、Stimulus で対応します。
import { Controller } from "@hotwired/stimulus"
/*** 数字を桁区切りにする処理 ***/
const numberWithComma = new Intl.NumberFormat()
export default class extends Controller {
static targets = ["dummy", "numeric"]
focusin() {
if (this.dummyTarget.readOnly) { return false }
this.dummyTarget.value = this.numericTarget.value
this.dummyTarget.select()
}
focusout() {
let val = this.dummyTarget.value
val = val.replace(/[A-Za-z0-9]/g, (s) => {
return String.fromCharCode(s.charCodeAt(0) - 65248)
}).replace(/、|。|・/g, ".").replace(/˗|ᅳ|᭸|‐|‑|‒|–|—|―|⁃|⁻|−|▬|─|━|➖|ㅡ|﹘|﹣|-|𐄐|𐆑|ー/, "-").replace(/[^0-9.-]/g, "")
this.numericTarget.value = val
this.dummyTarget.value = numberWithComma.format(val)
}
}
focusin()
フォーカスが入ったら、数値エレメント(<input type="hidden" data-comma-target="numeric">)の値を表示エレメント(<input type="text" data-comma-target="dummy">)に代入してます。
事実上カンマをなくして数値にしているだけです。
focusout()
フォーカスが外れたら、まず、入力値を正規化する
- アルファベット・数字の全角を半角に変換する(アルファベットは不要ですが、この辺りはいろんなシーンでコピペしてきたものだからそのまま)
-
、。・
の読点、句点、中黒は小数点.
(U+002E)と見做す - 長音やハイフンなどなどマイナスもどきは、マイナス
-
(U+002D)と見做す - 最後に、数字、マイナス記号、小数点以外を除外する
正規化後の値の処理
- 数値になったので、数値エレメントに代入する
-
Intl.NumberFormat()
で数値をカンマ付きに変換してダミーエレメントに代入する
雑感・所感
いちど作ってしまえば使い回しができるので便利です。
参考にしてみて回った中に Don't overdo it とあったが、これくらいなら許容範囲でしょう。
-
ウェブ入力欄で、「全角で(半角で)入力してください」とか、「ひらがなで(カタカナで)入力してください」とかに出会うが、お前が正規化しろよと思っている。 ↩