概要
件名のモノが必要になった時
ググって出てきたのはajaxやcoffee script、select2にchosenと
内容が古かったり環境制約で使えないものだったりで苦しめられたので
rails6とpure javascriptで動くサンプルを遺します↓
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?
大まかな処理の流れは以下になります
- ユーザが親select boxからカテゴリを選択する
- 親select boxのchangeイベントが発火
- javascriptがfetchでserverのtile一覧apiを叩く
- serverがtile一覧をjsonで返す
- javascriptがjsonを受け取る
- 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って漢字使えたんですね…
/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)にしてみたけど
牌一覧用意するのが無駄に面倒でもうやらないと思います