4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Choices.js + fetchAPIでフィルタ付き動的セレクトボックス [脱jQuery]

Last updated at Posted at 2020-02-08

概要

件名のモノが必要になった時
ググって出てきたのはajaxやcoffee script、select2にchosenと
内容が古かったり環境制約で使えないものだったりで苦しめられたので
rails6とpure javascriptで動くサンプルを遺します↓


親カテゴリの選択に応じて動的に子の選択肢をセットする
Screen Shot 2020-02-09 at 2.18.54.png


フィルタ検索機能
Screen Shot 2020-02-09 at 4.01.08.png


Sample app source code

  • Ruby 2.7.0
  • Rails 6.0.2.1
  • Choices.js
  • Fetch API

Choices.js

素のjavascriptで書かれた軽量custom select box
jQueryに依存せずにselect2やchosenのようなフィルタ機能を実現します
option設定やメソッドの詳細はGitHubで確認出来ます
公式動作デモ


Fetch API

Webブラウザの新しめの標準APIで、非同期通信処理をカンタンに書けます
従来のjQuery.ajax、XHR(XMLHttpRequest)を代替するとかしないとか。


How does it work?

大まかな処理の流れは以下になります

  1. ユーザが親select boxからカテゴリを選択する
  2. 親select boxのchangeイベントが発火
  3. javascriptがfetchでserverのtile一覧apiを叩く
  4. serverがtile一覧をjsonで返す
  5. javascriptがjsonを受け取る
  6. javascriptがchoicesメソッドを経由して子select boxにjsonの内容をセットする

以下具体的なコードです


javascript

  • /javascript/tiles.js
document.addEventListener('DOMContentLoaded', function() {
  /* apply choices to select boxes */
  const choicesOptions = {
    shouldSort: false,
    removeItemButton: true,
    searchResultLimit: 9,   // default: 4
    searchFields: ['label'] // default: ['label', 'value']
  };
  const selects = document.querySelectorAll('select');
  selects.forEach(function(select) {
    select.choices = new Choices(select, choicesOptions);
  });

  /* add event listener to first select */
  const firstSelect = document.getElementById('first_select')
  if (firstSelect != null) {
    firstSelect.addEventListener('change', setSecondChoices); // type: 'input' is not effective.
  }

  /* get tiles json by fetch api */
  async function getTiles(tile_category_id) {
    const params = new URLSearchParams();
    params.set('tile_category_id', tile_category_id);
    const apiURL = `/api/tiles?${params}`;
    const response = await fetch(apiURL);
    const json = await response.json();
    return json;
  }

  /* set values to second select */
  function setSecondChoices() {
    const secondSelect = document.getElementById('second_select');
    const choices = secondSelect.choices;
    /* clear current values & selected item */
    choices.clearStore();
    const tile_category_id = firstSelect.value;
    getTiles(tile_category_id).then(json => {
      // setChoices(choices, value, label, replaceChoices);
      choices.setChoices(json, 'index', 'display_name', true);
    });
  }
});

要点を抜粋解説します

序盤の以下の部分で素のselect要素に対してchoicesを適用しています

const selects = document.querySelectorAll('select');
selects.forEach(function(select) {
  select.choices = new Choices(select, choicesOptions);
});

choices適用後はカスタムタグが挿入されて見た目が変わり、フィルタが使えるようになります
select boxに対する項目の追加や削除はchoicesメソッドを通す必要があるので注意して下さい


続くaddEventListenerで親select boxのchangeイベントを拾い、setSecondChoices()を呼びます

firstSelect.addEventListener('change', setSecondChoices);

setSecondChoices()内
choices適用時にselect.choices = new Choices...という風に書いたので
以下のようにselect boxのchoicesにアクセス出来ます

const choices = secondSelect.choices;

親カテゴリが変更された時、変更前に選んだ子が残っていると宜しくないのでクリアします

choices.clearStore();

getTiles()は非同期処理で/api/tiles?を叩きます
async/await+fetchAPIを使用します

/* get tiles json by fetch api */
async function getTiles(tile_category_id) {
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const apiURL = `/api/tiles?${params}`;
  const response = await fetch(apiURL);
  const json = await response.json();
  return json;
}

ES6縛りでasync/await記法が使えない場合は以下をお使い下さい

/* set values to second select */
function setSecondChoices() {
  const secondSelect = document.getElementById('second_select');
  const choices = secondSelect.choices;
  /* clear current values & selected item */
  choices.clearStore();
  const tile_category_id = firstSelect.value;
  const params = new URLSearchParams();
  params.set('tile_category_id', tile_category_id);
  const url = `/api/tiles?${params}`;
  /* get tiles JSON by fetch api */
  fetch(url).then(function(response) {
    return response.json();
  }).then(function(json) {
    // setChoices(choices, value, label, replaceChoices);
    choices.setChoices(json, 'index', 'display_name', true);
  });
}

apiを叩いて返ってくるjsonの構造は以下のような形式です

[
  {"display_name":"","index":41},
  {"display_name":"","index":42},
  {"display_name":"","index":43}
]

setChoices()の第二, 第三引数で
choicesのvalueとlabelに対応するjson中のhashのkeyを指定します(value, labelの順)

// setChoices(choices, value, label, replaceChoices);
choices.setChoices(json, 'index', 'display_name', true);

これで子select boxの選択肢が書き換わります


  • /javascript/packs/application.js
    webpackerにpackしてもらう為、↑のtiles.jsをimportしてるだけ
import '../tiles.js'

routes.rb

  • select boxesを表示: / (root)
  • カテゴリ毎のtile一覧を取得するapi: /api/tiles?tile_category_id=xxx
Rails.application.routes.draw do
  root controller: :top_page, action: :show
  namespace :api do
    resources :tiles, only: [:index]
  end
end

model

Model定義と一覧取得は今回の本筋ではないのでhashでベタ書きしました (DB不要です)
RailsユーザならActiveRecordに置き換えるのは造作も無い事でしょう
indexは各牌識別用の通し番号です
rubyのsymbolって漢字使えたんですね…:scream_cat:


  • /models/concerns/tiles_identifiable.rb
module TilesIdentifiable
  extend ActiveSupport::Concern

  included do
    def get_tiles(tile_category_id:)
      tile_category = tile_categories.key(tile_category_id)
      case tile_category
      when :萬子 then characters
      when :筒子 then dots
      when :索子 then bamboos
      when :風牌 then winds
      when :三元牌 then dragons
      else all_tiles
      end
    end

    def tile_categories
      {
        萬子: 0,
        筒子: 1,
        索子: 2,
        風牌: 3,
        三元牌: 4,
      }
    end

    def numbers
      1..9
    end

    def chinese_numerals
      ['一', '二', '三', '四', '五', '六', '七', '八', '九']
    end

    def to_chinese_numerals(number)
      chinese_numerals[number - 1]
    end

    # tile: { display_name:, index: }
    def characters(base_index: 10 * 0)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}萬", index: base_index + number } }
    end

    def dots(base_index: 10 * 1)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}筒", index: base_index + number } }
    end

    def bamboos(base_index: 10 * 2)
      numbers.map { | number | { display_name: "#{to_chinese_numerals(number)}索", index: base_index + number } }
    end

    def winds(base_index: 10 * 3)
      ['東', '南', '西', '北'].map.with_index(1) {
        | wind, index | { display_name: wind, index: base_index + index } }
    end

    def dragons(base_index: 10 * 4)
      ['白', '發', '中'].map.with_index(1) {
        | dragon, index | { display_name: dragon, index: base_index + index } }
    end

    def all_tiles
      characters | dots | bamboos | winds | dragons
    end
  end
end

controller

  • controllers/top_page_controller.rb
class TopPageController < ApplicationController
  include TilesIdentifiable
  before_action :set_values

  private
    def set_values
      @first_choices = tile_categories.to_a
    end
end

  • controllers/api/tiles_controller.rb
    指定されたカテゴリに対応する牌一覧をjsonで返すapi
class Api::TilesController < ApplicationController
  include TilesIdentifiable

  def index
    tile_category_id = params[:tile_category_id]
    if tile_category_id.blank?
      render json: all_tiles
    else
      render json: get_tiles(tile_category_id: tile_category_id.to_i)
    end
  end
end

view

素のerbです

  • views/top_page/show.html.erb
<div class='form_wrapper'>
  <%= form_with do | form | %>
    <%= form.label 'tile categories' %>
    <%= form.select :first_select, @first_choices, { include_blank: true }, {} %>
    <%= form.label 'tiles' %>
    <%= form.select :second_select, [], { include_blank: true }, {} %>
  <% end %>
</div>

  • views/layouts/application.html.erb
    choicesのjsとcss、whatwg-fetchのjsをCDNでお手軽導入
<head>
  ...
  <!-- Include Choices CSS -->
  <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css'/>
  <!-- Include Choices JavaScript (latest) -->
  <script src='https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js'></script>
  <!-- Include whatwg-fetch -->
  <script src='https://cdn.jsdelivr.net/npm/whatwg-fetch@3.0.0/dist/fetch.umd.min.js'></script>
</head>

最後に

誰かの何らかの助けになれば幸いです
フィルタの日本語動作の確認で漢字を使う麻雀牌(Tile)にしてみたけど
牌一覧用意するのが無駄に面倒でもうやらないと思います:dizzy_face:

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?