Choices.jsでリッチな複数選択リストを実装する

スマートな操作性と、モダンなUIを提供してくれる「[Choices](https://joshuajohnson.co.uk/Choices/)」をRailsアプリに組み込む方法です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/7a549c5f-0ec8-54c8-7bdc-885ccaa7d3b0.png)

今回のサンプルでは「タスクに複数のメンバーをアサインする」部分を例に解説してみたいと思います。

# 動作イメージ

![choices.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/e4a300f2-111e-a7b2-b533-0a01b243c97b.gif)


# 環境とgem

- ruby 2.7.1
- rails 6.0.3
- webpackerは有効
- turbolinksは無効
- hamlit-rails
- simple_form

# 手順

1. モデルの作成
2. Choices.jsのセットアップ
3. select要素とChoicesの紐付け
4. おまけ:RSpecでシステムテスト
5. サンプルレポジトリ
6. 追記：2020/09時点のメンテナンス状況

# 1. モデルの作成

```console
> rails g scaffold task title content
> rails g scaffold member name
> rails g model assignment task:references member:references
> rails db:migrate
```


```ruby:task.rb
class Task < ApplicationRecord
  has_many :assignments, dependent: :destroy
  has_many :members, through: :assignments
end
```

```ruby:member.rb
class Member < ApplicationRecord
  has_many :assignments, dependent: :destroy
  has_many :tasks, through: :assignments
end
```

```ruby:assignment.rb
class Assignment < ApplicationRecord
  belongs_to :member
  belongs_to :task
end
```

# 2. Choices.jsのセットアップ

```console
> yarn add choices.js
```


# 3. select要素とChoicesの紐付け

## js側の設定

```js:app/javascript/packs/choices/init.js
import * as Choices from 'choices.js'

document.addEventListener('DOMContentLoaded', function () {

  new Choices('#choices-multiple-remove-button', {
    delimiter: ',',
    maxItemCount: 5,
    removeItemButton: true,
    noChoicesText: '選択されていません',
    itemSelectText: '選択してください',
    maxItemText: (maxItemCount) => {
      return `タスクにアサインできるのは ${maxItemCount} 人までです`;
    },
  });
});
```

## js側の設定（スタイルシート）

```scss:app/javascript/src/choices.scss
@import '~choices.js/src/styles/choices'
```

## ファイルのimport

ふたつのファイルをimportしておきます。

```js:app/javascript/packs/application.js
import './choices/init.js';
import '../src/choices.scss'
```

## view側の設定

```haml:tasks/_form.html.haml
= simple_form_for @task do |f|
  = f.error_notification
  = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present?

  .field
    = f.input :title, id: 'task_title'
  .field
    = f.input :content, as: :text

  .field
    = f.association :members,
      required: true,
      as: :select,
      collection: Member.pluck(:name, :id),
      input_html: { id:'choices-multiple-remove-button', multiple: true }

  .actions
    = f.button :submit
```

Choices.jsがマッピングするために、idをあわせておきます。
`init.js`内の`new Choices('#choices-multiple-remove-button`で指定したidを設定します。複数選択なので`multiple: true`もわすれずに。

```haml:抜粋
= f.association :members,
  required: true,
  as: :select,
  collection: Member.pluck(:name, :id),
  input_html: { id:'choices-multiple-remove-button', multiple: true }
```

選択肢の部分は、`collection: Member.pluck(:name, :id)`とすれば、あとはChoicesが面倒みてくれます。「編集時は`choices.setValue`して初期値を設定」なんてこともしなくてよいです。
`selected`な項目が選ばれた状態になってくれます。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/1a10c5ca-c04d-7c8b-9d78-ac4fdadeb6bb.png)

```html:レンダリングされたHTML
<select multiple="multiple" class="select required" required="required" aria-required="true" name="task[member_ids][]"
  id="task_member_ids">
  <option value="1">Alice</option>
  <option selected="selected" value="2">Bob</option>
  <option selected="selected" value="3">Charlie</option>
</select>
```

## TasksControllerのStrongParameterを修正

選択した項目の値を配列で受け取れるように、以下のように修正します。

```ruby:tasks_controller.rb
def task_params
  params.require(:task).permit(:title, :content, member_ids: [])
end
```

変更はこれだけで完了です👍

メッセージの日本語化や、表示をカスタマイズできるオプションもありますので、色々できそうですね。
[jshjohnson/Choices: A vanilla JS customisable select box/text input plugin ⚡️](https://github.com/jshjohnson/Choices#setup)

# 4. おまけ:RSpecでシステムテスト
```ruby:tasks_spec.rb
require 'rails_helper'

RSpec.describe 'Tasks', type: :system do
  let!(:board) { create(:board) }
  let!(:member) { create(:member, name: 'チョイ・スー') }

  it 'タスクを追加できる', js: true do
    visit new_board_task_path(board)

    fill_in 'Title', with: 'Qiita記事を書く'
    fill_in 'Content', with: 'Choices.jsの使い方を紹介します'

    # リストを展開
    first('.choices').click
    # リストアイテムを選択
    first('.choices__item').click
    # リストを閉じるためEsc送信
    find('#task_title').send_keys :escape

    expect do
      click_on 'Create Task'
    end.to change { Task.count }.by(1)
  end
end
```

※Rspecや、FactoryBotをつかっていますが詳細は割愛します。動かなかった場合はコメントいただければお答えできるかも。

# 5. サンプルレポジトリ
:octocat: [hirocueki/kanban\-for\-choices\-js](https://github.com/hirocueki/kanban-for-choices-js)


# 6. 追記：2020/09時点のメンテナンス状況

公式レポジトリに、以下の文面がありました。
要約すると、「このライブラリをサポートする時間がなくてメンテナーを探しているので連絡ください」ということです。利用の際には、この点も含めて考慮してください :pray: 
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2883/cc37ae21-15e8-3166-bcfc-5f5870d9c489.png)