Hotwireを使って正規表現チェッカーを作ろう

[@z_ohnami](https://twitter.com/z_ohnami)です。こんにちは。
railsの7から導入されたHotwireを使って、JavaScriptをできる限り書かずに正規表現チェッカーを作ってみました。

正規表現チェッカーといえば、[Rublar](https://rubular.com/)というサイトが以前から存在していて私は日頃からお世話になっていたのですが、見た目はそちらを参考にして作りました。

名前は「RegExpress」にしました。
[https://tools.bigwave.biz/regular_expressions](https://tools.bigwave.biz/regular_expressions?utm_source=qiita)
![ogp_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10138/2e00e299-d7b5-c4c4-108c-07d87ff33583.png)
以下のように、正規表現のパターンとテスト用の文字列を入力した時点で結果が返ってきます。

![69168d9afa163a0a1218155b7df228fb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10138/bc8f6fb3-8fdf-ba1a-4507-57cf512ac8a1.gif)


Hotwireを使わずにrailsで作った場合はformのsubmitをしてもらわないと結果を返すことができません。

また、テキストエリアをポンポンと追加していますが、これもRailsのみで実現しています。

こうした動的かつ部分的な画面の変更自体は特に珍しいものではりませんが、
これをrailsのみで実現できているところがHotwireのGood pointになります。

# Hotwireを使ってみよう
さて、ここからは自分でゼロから試してみたい方向けにハンズオン的な解説をしていこうと思います。

ここでは冗長なCSSやHTMLを省いたソースコードを例にHotwireの使い方を一部お伝えします。
今回はHotwireが提供している機能のひとつ、**Turbo Frame**を使います。

Turbo Frameを使うと、ユーザーからの何らかのリクエストに応じて画面を部分的に書き換えることが可能となります。

サンプルのソースコードはこちらです。
https://github.com/z-ohnami/hotwire-regexp

完成するとこんな感じに仕上がります。
![d79c0a2392ca98222ad62bce96d9e2d6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10138/5e834c46-c016-a283-eea0-500dfe46b929.gif)


## セットアップ
rails newするところからやっていきましょう。
コマンドを実行していきます。
```shell-session
$ cd {アプリのディレクトリへ}
$ echo 3.1.2 > .ruby-version
$ bundle init
$ bundle install # Gemfile中のgem "rails"をコメントを解除して実行
$ rails new . --css=sass --javascript=esbuild --skip-jbuilder --skip-action-mailbox --skip-action-mailer --skip-test --skip-active-storage --skip-action-text
$ bin/dev
```

これでとりあえず、http://localhost:3000 にアクセスするとrailsのデフォルト画面が表示されるはずです。

## 設定ファイルなど
rails newをした後に、以下のように設定ファイルを更新、追加しています。
これらはお好みで適宜整えてください。

viewのテンプレートにはslimを使っています。
もし、同様にslimで作る場合は、Gemfileを更新後にbundle installをお願いします。

### 更新
[Gemfile](https://github.com/z-ohnami/hotwire-regexp/blob/main/Gemfile#L48)
[config/application.rb](https://github.com/z-ohnami/hotwire-regexp/blob/main/config/application.rb#L36)
[config/routes.rb](https://github.com/z-ohnami/hotwire-regexp/blob/main/config/routes.rb#L6)
[app/assets/stylesheets/application.sass.scss](https://github.com/z-ohnami/hotwire-regexp/blob/main/app/assets/stylesheets/application.sass.scss#L2)

### 追加
[config/locales/ja.yml](https://github.com/z-ohnami/hotwire-regexp/blob/main/config/locales/ja.yml)
[config/locales/model.ja.yml](https://github.com/z-ohnami/hotwire-regexp/blob/main/config/locales/model.ja.yml)


## modelを作る
`RegularExpression`というmodelを作り、ここで正規表現の成否を判定します。
今回はデータベースを使いませんが、ActiveModelをincludeしてバリデーションなどは使えるようにしています。

[app/models/regular_expression.rb](https://github.com/z-ohnami/hotwire-regexp/blob/main/app/models/regular_expression.rb)

```Ruby
class RegularExpression
  include ActiveModel::Model

  # 正規表現のパターン文字列
  attr_reader :expression

  # 正規表現をかける対象となる文字列
  attr_reader :test_string

  validate :check_expression

  def initialize(expression: '', test_string: '')
    @expression = expression
    @test_string = test_string
  end

  # 画面が初期表示、未入力の状態であるかを判定するのに使用。
  def unready?
    @expression.blank? || @test_string.blank?
  end

  private

  def regexp
    @regexp ||= Regexp.new(expression)
  end

  # この結果を正規表現の判定結果として表示している
  def check_expression
    return if regexp.match?(test_string)

    errors.add(:base, :invalid, message: '一致しませんでした...')
  end
end

```

## controllerを作る
controllerは基本的にHotwireを使わないときに作るものと変わらないですね。

[app/controllers/regular_expressions_controller.rb](https://github.com/z-ohnami/hotwire-regexp/blob/main/app/controllers/regular_expressions_controller.rb)

```Ruby
class RegularExpressionsController < ApplicationController
  def index
    @regular_expression = RegularExpression.new
  end

  def create
    @regular_expression = RegularExpression.new(expression: regexp_params[:expression], test_string: regexp_params[:test_string])

    render :index
  end

  private

  def regexp_params
    params
      .require(:regular_expression)
      .permit(:expression, :test_string)
  end
end
```

## viewを作る
1枚っぺらのviewファイルを作ります。
submitのボタンがないのがポイントです。

テキストフィールドに文字列が打ち込まれるたびにsubmitが実行され、正規表現のチェック結果が返ってきます。
(先ほどのmodelで定義したバリデーションが実行される)

[app/views/regular_expressions/index.html.slim](https://github.com/z-ohnami/hotwire-regexp/blob/main/app/views/regular_expressions/index.html.slim)
```slim
.container
  h1 正規表現チェッカー

  = form_for @regular_expression,
      url: regular_expressions_path,
      html: { data: { turbo_frame: 'regexp', controller: 'regexp', action: 'input->regexp#submit' } } do |f|

    .field
      = f.label :expression
      = f.text_field :expression, size: 80, placeholder: ''
    .field
      = f.label :test_string
      = f.text_area :test_string, size: '60x6', placeholder: '', class: ''

  / [ポイント]
  / 動的かつ部分的に変わる部分をturbo_frame_tagで囲む。
  / Turbo Frameは1ヶ所しか指定できない。
  / 複数箇所を同時に差し替えたいときはTurbo Streamを使う
  ul.result_message
    = turbo_frame_tag 'regexp' do
      - if @regular_expression.unready?
        li 結果はここに出力されます。
      - elsif @regular_expression.valid?
        li.result_message__success マッチしました！！
      - else
        - @regular_expression.errors.full_messages.each do |message|
          li.result_message__fail = message
```


## inputを検知してsubmitを実行する
submitボタンを実行してもらう代わりに、ユーザーの打ち込みをアプリ側で検知してsubmitをしなければなりません。

Hotwireではそうしたインプットイベントなどを検知するときにはStimulusを使用することがおすすめされているようです。
以下のようにコマンドを実行することでStimulusの雛形を生成することができます。
今回はregexpという名前を付けています。

```shell-session
$ rails g stimulus regexp
```

あとは、以下のように任意の処理を書き込んでいけばOKです。
submitという関数を書き、そこでformのsubmitを実行しています。

[app/javascript/controllers/regexp_controller.js](https://github.com/z-ohnami/hotwire-regexp/blob/main/app/javascript/controllers/regexp_controller.js)

```javascript
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="regexp"
export default class extends Controller {
  submit() {
    clearTimeout(this.timeout)

    this.timeout = setTimeout(() => {
      this.element.requestSubmit()
    }, 200)
  }
}
```

submit関数の中で単純に`this.element.requestSubmit()`を実行するように書くとinputの中身が変わるたびに高頻度でバックエンドにPOSTが飛んでしまいます。
そこで実行回数を適当に減らすためにdebounceをしています。

debounceについてはこちらの記事が大変参考になりました。
(というか、そのまま利用させていただいております)
- [Debounceの実装](https://zenn.dev/shita1112/books/cat-hotwire-turbo/viewer/tutorial-3#debounceの実装)
- [throttleとdebounce](https://aloerina01.github.io/blog/2017-08-03-1)

そして、このjsはviewファイルに書いた以下の設定で紐つけられています。
inputイベントが発火するときにregexp_controller.jsのsubmit関数が実行されるようにしています。
ほかにも、clickとかいくつか検知できるイベントがあり、同様に紐つけが可能ですね。

```ruby
data: { turbo_frame: 'regexp', controller: 'regexp', action: 'input->regexp#submit' }
```

以上で最低限の正規表現チェッカーは動作すると思います！

# Hotwireをキメると気持ちいい
Hotwireで得られる開発体験は自分にとってかなり快適に感じました。

ぶっちゃけた話、Railsでアプリを作るときはJavaSccriptを書きたくない(!)というのが本音で、そうしたストレスが大幅に減少されるのはかなり気持ちがいいです。
実戦レベルのプロダクトではjsをゼロにはできないと思いますが、Stimulusのルールに則り、子気味よく書けるのも自分にはありがたく感じました。

viewファイルが一箇所に同じ書き方でまとまりやすいのもいいところですね。
ReactとかVueを入れると、それらのコンポーネントはrailsの`app/views`とは別になってしまいますが、そうしたviewファイルの分離が発生しません。

また、既存のRailsのプロジェクトにHotwireを部分的に導入して試す、といったことが容易にできるのもいいところですね。

もし、Hotwireをまだ使ったことがないようでしたら、この素晴らしき開発体験をあなたも是非！！

# 参考文献
- [猫でもわかるHotwire入門 Turbo編](https://zenn.dev/shita1112/books/cat-hotwire-turbo)
- [throttleとdebounce](https://aloerina01.github.io/blog/2017-08-03-1)
- [Rublar](https://rubular.com/)