[Hotwire] 数字を自動フォーマット

# はじめに

桁数が多い数字は桁数が合っているか判断しづらいです。判断しやすくするために、３桁区切りでカンマを入れます。自ら３桁区切りでカンマを入れることも可能ですが、それはやりたくないでしょう。

そこで、HotwireのStimulusを使って、数字が自動フォーマットされるようにしました。

![Sep-18-2023 22-34-39.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/ed28fa26-b220-9704-2373-6079432bf2e5.gif)


## 環境

- Rails 7.0.7
- Ruby 3.2.2

# 実装

ただ、数字をフォーマットすればいいだけに思いますが、実際は違います。データベースに保存する時にはカンマを取り除く必要があります。カンマがついた状態では、データベースで数値として扱えないからです。数値として扱えないと、年収が1,000,000円以上のユーザーを検索するのも難しくなります。数値として扱うために、データベースに保存する前に、数字からカンマを取り除く必要があります。

つまり、以下のことができればいいということです。

1. 入力時に数字にカンマをつける
2. 登録するボタンを押した時に数字からカンマを取り除く

## 1. 入力時に数字にカンマをつける

数字を入力する`<input>`要素に入力があるたびに、数字にカンマがつくようにします。

数字にカンマをつけるためのstimulus controllerを作成します。

```js:app/javascript/controllers/numeral_controller.js
import { Controller } from '@hotwired/stimulus'
import numeral from 'numeral'

// Connects to data-controller="numeral"
export default class extends Controller {
  format(event) {
    event.target.value = numeral(event.target.value).format();
  }
}
```
numeral により、数字をフォーマットしたり、フォーマットしたものを数字に戻すことができます。

https://www.npmjs.com/package/numeral

`format`メソッドが実行されると、そのトリガーとなったHTML要素の`value`をフォーマットします。つまり、３桁区切りでカンマがつきます。

以下のように`data-controller`と`data-action`をHTML要素の属性として持たせれば、入力のたびに数字がフォーマットされます。

```haml:app/views/users/_form.html.haml
= simple_form_for user, html: { 'data-controller': :numeral } do |f|
  = f.input :name
  = f.input :annual_income, as: :string, input_html: { 'data-action': 'numeral#format' }
  = f.input :phone_number
  = f.submit class: 'btn btn-primary'
```

## 2. 登録するボタンを押した時に数字からカンマを取り除く

このままだと、数字にカンマが入ったままになっているので、数値としてデータベースに保存できません。なので、以下のようにして、登録するボタンを押した時に数字からカンマが取り除かれるようにします。

```js:app/javascript/controllers/numeral_controller.js
import { Controller } from '@hotwired/stimulus'
import numeral from 'numeral'

// Connects to data-controller="numeral"
export default class extends Controller {
  static targets = ['input'];

  format(event) {
    event.target.value = numeral(event.target.value).format();
  }

  submit() {
    this.inputTargets.forEach(input => input.value = numeral(input.value).value());
  }
}
```

```haml:app/views/users/_form.html.haml
= simple_form_for user, html: { 'data-controller': :numeral } do |f|
  = f.input :name
  = f.input :annual_income, as: :string, input_html: { 'data-action': 'numeral#format', 'data-numeral-target': 'input' }
  = f.input :phone_number
  = f.submit class: 'btn btn-primary', 'data-action': 'numeral#submit'
```

`submit`メソッドをすると、数字からカンマが取り除かれます。`<button>`要素に`'data-action': 'numeral#submit'`属性を持たせて、登録するボタンを押した時に`submit`メソッドが実行されるようにしています。

数字からカンマを取り除く`<input>`要素は、Stimulusのtargetsを使って参照できるようにしています。`<input>`要素に`'data-numeral-target': 'input'`属性を持たせて、stimulus controllerで`static targets = ['input']`を定義することで、`<input>`要素をstimulus controller内で参照できます。詳しくは[リファレンス](https://stimulus.hotwired.dev/reference/targets)を参照ください。

`submit`メソッドでは`this.inputTargets`で、`'data-numeral-target': 'input'`属性を持ったHTML要素の配列を参照しています。この配列の各要素の`value`からカンマを取り除いています。


# まとめ

HotwireのStimulusを使って、数字が自動フォーマットされるようにしました。

![Sep-18-2023 22-34-39.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/ed28fa26-b220-9704-2373-6079432bf2e5.gif)

Stimulusのtargetsを利用して、数字からカンマを取り除きました。targetsは、stimulus controller内でHTML要素を参照したいときに便利です。targetsとなり得るHTML要素は、`data-controller`属性を持ったHTML要素内に絞れるところも良いところだと思います。

# メンテナブルな改善

@aki77 さんから以下の指摘を受けて、numeral_controller を以下のように改善してみました。

- ブラウザの標準機能でやった方がメンテナブルだと思う
- numeral は、６年間更新がない

```js:app/javascript/controllers/numeral_controller.js
import { Controller } from '@hotwired/stimulus'

// Connects to data-controller="numeral"
export default class extends Controller {
  static targets = ['input'];

  connect() {
    this.numberFormat = new Intl.NumberFormat();
  }

  inputTargetConnected(element) {
    element.value = this.numberFormat.format(element.value);
  }

  format(event) {
    event.target.value = this.numberFormat.format(event.target.value);
  }

  submit() {
    this.inputTargets.forEach(input => input.value = this._unformat(input.value));
  }

  _unformat(formattedNumber) {
    return formattedNumber.replace(/,/g, '');
  }
}
```