[Hotwire] 長押しでタスクの状態を切り替える

# はじめに

Hotwireを使って、タスクの状態を長押しで切り替えられるようにしてみました。
![Sep-08-2023 17-25-42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/908f59bd-c677-82ab-7600-26abf00c7d60.gif)

「クリックで状態の切り替えればいいではないか」と考える人もいると思います。確かにそうです。そうせずに、長押しで状態を更新できるようにしたのは以下の要望に応えるためです。

- クリックでタスク詳細画面に遷移するようにしたい
- 状態更新のために、新たな要素(例：状態更新ボタン)を追加したくない
- クリックと似たような操作感で、状態を更新したい


## 環境

- Rails 7.0.7
- Ruby 3.2.2
- Bootstrap 5.3.1

# 実装

次のことをして、長押しでの状態を切り替えられるようにします。
- タスク一覧画面が見れるように
- タスクを作成できるように
- 長押しイベントに応じて、タスクの状態が更新されるように

## タスク一覧画面が見れるように

task モデルを作ります。DB上でstateのデフォルトを`todo`にします。
```rb:app/models/task.rb
class Task < ApplicationRecord
  extend Enumerize

  enumerize :state, in: %i[todo progress done]

  validates :name, presence: true

  scope :default_order, -> { order(id: :desc) }
end
```

tasks コントローラーを作って、indexアクションを作ります。

```rb:app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @task = Task.new
    @tasks = Task.default_order
  end
end
```

次にタスク一覧画面のviewを作ります。

```haml:app/views/tasks/index.html.haml
.container.w-50.py-3.d-flex.flex-column.gap-5
  .d-flex.flex-column.gap-1
    = render partial: 'task', collection: @tasks
```

```haml:app/views/tasks/_task.html.haml
.tasks-index__task{ class: task_state_class(task:) }
  .tasks-index__task-name= task.name
  .tasks-index__task-state{ class: task_state_class(task:) }= task.state
```

```rb:app/helpers/task_helper.rb
module TaskHelper
  def task_state_class(task:)
    "is-#{task.state}"
  end
end
```

```scss:app/assets/stylesheets/_tasks-index.scss
$todo-color: rgb(35, 134, 54);
$progress-color: rgb(158, 106, 3);
$done-color: rgb(137, 87, 229);

.tasks-index__form {
  padding: 16px;
  border: 1px solid $gray-300;
  border-radius: 8px;
}

.tasks-index__task {
  padding: 12px 24px;
  cursor: pointer;
  border-radius: 8px;
  display: flex;
  justify-content: space-between;

  &.is-todo {
    border: 1px solid $todo-color;
  }

  &.is-progress {
    border: 1px solid $progress-color;
  }

  &.is-done {
    border: 1px solid $done-color;
  }
}

.tasks-index__task-name {
  font-size: 16px;
  margin-bottom: 0;
}

.tasks-index__task-state {
  font-size: 10px;
  color: #ffffff;
  padding: 4px 8px;
  border-radius: 6px;

  &.is-todo {
    background-color: $todo-color;
  }

  &.is-progress {
    background-color: $progress-color;
  }

  &.is-done {
    background-color: $done-color;
  }
}
```

## タスクを作成できるように

タスク一覧画面にフォームを追加します。

```haml:app/views/tasks/index.html.haml
.container.w-50.py-3.d-flex.flex-column.gap-5
  = render 'form', task: @task
  = turbo_frame_tag :tasks, class: 'd-flex flex-column gap-1' do
    = render partial: 'task', collection: @tasks
```

```haml:app/views/tasks/_form.html.haml
= simple_form_for task, html: { class: 'tasks-index__form' } do |f|
  = f.input :name, label: false, placeholder: 'タスク名を入力してください'
  = f.submit '作成', class: 'btn btn-primary'
```

tasks コントローラーにcreateアクションを追加します。

```rb:app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def create
    @task = Task.new(task_params)
    unless @task.save
      @tasks = Task.default_order
      render :index, status: :unprocessable_entity
    end
  end

  private

  def task_params
    params.require(:task).permit(%i[name])
  end  
end
```
現時点でタスクを作成できますが、タスク一覧画面に作成したものが表示されません。表示されるようにするために、以下のファイルを作成します。

```haml:app/views/tasks/create.turbo_stream.haml
= turbo_stream.prepend :tasks, @task
```

これで表示されるようになったはずです。HotwireのTurboのおかげです。ここでは詳しく説明しませんが、かなり大雑把に言うと、以下のことをしています。
1. `@task`を使って`_task.html.haml`をHTMLにレンダリングする。
2. 作成ボタンを押した画面に、1でレンダリングされたHTMLを追加する。
※追加先のDOMは、idで指定する。ここでは、IDが`tasks`のDOMを追加先にしている。

## 長押しイベントに応じて、タスクの状態が更新されるように

まずは、updateアクションを作ります。

```rb:app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :set_task, only: %i[update]

  def update
    @task.update_to_next_state!
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end  
end
```

```rb:app/models/task.rb
class Task < ApplicationRecord
  def update_to_next_state!
    update!(state: next_state)
  end

  private

  def next_state
    if state.todo?
      'progress'
    elsif state.progress?
      'done'
    elsif state.done?
      'todo'
    end
  end
end
```

次に、updateアクション用のformを`_task.html.haml`に作ります。

```haml:app/views/tasks/_task.html.haml
= turbo_frame_tag task do
  %form.tasks-index__task{ class: task_state_class(task:),
                           action: task_url(task),
                           method: :patch }
      .tasks-index__task-name= task.name
      .tasks-index__task-state{ class: task_state_class(task:) }= task.state
```

このformでsubmitすれば、タスクの状態が更新されます。このsubmitをstimulusを使って行います。

まず、長押しイベントを使えるようにします。これは、以下のpackageにより使えるようになります。
```sh
yarn add long-press-event
```
https://github.com/john-doherty/long-press-event

何らかのイベントに応じて、submitするstimulus controllerを作成します。

```js:app/javascript/controllers/task_state_controller.js
import { Controller } from '@hotwired/stimulus'
import 'long-press-event'

// Connects to data-controller="task-state"
export default class extends Controller {
  update_state() {
    this.element.requestSubmit();
  }
}
```

`_task.html.haml`を編集して、長押ししたら`update_state`が実行されるようにします。
```haml:app/views/tasks/_task.html.haml
= turbo_frame_tag task do
  %form.tasks-index__task{ class: task_state_class(task:),
                           action: task_url(task),
                           method: :patch,
                           data: { controller: 'task-state',
                                   action: 'long-press->task-state#update_state' } }
      .tasks-index__task-name= task.name
      .tasks-index__task-state{ class: task_state_class(task:) }= task.state
```

`controller: 'task-state'`で`task_state_controller`がコントロールする範囲をしてしいます。

`action: 'long-press->task-state#update_state'`で、長押しイベントが起きたら`task_state_controller`の`update_state`が実行されるように設定しています。`long-press`で長押しイベント、`task-state`でstimulus controller、`update_state`で実行するメソッドを指定しています。

現時点でタスクの状態を長押しで更新できますが、タスク一覧画面ではその更新を確認できません。そのために、以下のファイルを作成します。

```haml:app/views/tasks/update.turbo_stream.haml
= turbo_stream.replace @task
```

これで状態の更新を確認できるようになったはずです。これも、タスク作成について説明した時と同様に、HotwireのTurboのおかげです。かなり大雑把に言うと、以下のことをしています。
1. `@task`を使って`_task.html.haml`をHTMLにレンダリングする。
2. 長押しした画面の一部を、1でレンダリングされたHTMLに置換する。
※置換先のDOMは、`dom_id(@task)`で生成されたID


# まとめ

Hotwireを使って、タスクの状態を長押しで切り替えられるようにしてみました。こんな感じに、Hotwireを使えば、シングル・ページ・アプリケーションを割とシンプルに実装できます。
![Sep-08-2023 17-25-42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472392/908f59bd-c677-82ab-7600-26abf00c7d60.gif)

シングル・ページ・アプリケーションを作ってみたいけど、ReactとVue.jsの知識がないから難しそうと思っている人には、Hotwireはおすすめだと思います。主観的ではありますが、学習コストは結構低いと思いました。この記事が、Hotwire初学者の良い足掛かりになれば幸いです。