とある案件で、メーカーのリストと商品のリストを select - option タグで作りましょう的なことになりました。
メーカーと商品はマスターテーブルにあるので、そこからとってきます。
Laravel の blade なので、以下のようなコードで生成します。
option タグには、class で maker-n みたいな感じでメーカーのIDを持たせておきます。
<select name="maker" id="makers" class="form-control">
<option value="-1">メーカーを選択</option>
@foreach($makers as $m)
<option value="{{ $m->id }}">{{ $m->name }}</option>
@endforeach
</select>
<select name="items" id="items">
<option value="-1">商品名を選択</option>
@foreach($items as $item)
<option value="{{ $item->id }}" class="maker-{{ $item->maker_id }}" data-name="{{ $item->name }}">{{ $item->name }}</option>
@endforeach
</select>
メーカーを選択したら、選択されたメーカー以外の商品は非表示にしたいので、以下のようなコードを書きます。
$(function() {
$('#makers').on('change', function() {
selectionChanged();
});
});
function selectionChanged() {
let options = $('#items').children();
let makerId = $('#makers').val();
for (let i = 0; i < options.length; i++) {
if (isMatched(makerId, $(options[i]))) {
$(options[i]).show();
} else {
$(options[i]).hide();
}
}
}
function isMatched(mid, e) {
if (mid < 0) return true;
if (mid > 0) {
return e.hasClass('maker-' + mid);
}
return e.hasClass('maker-' + mid);
}
Chrome と Firefox と Edge はこれで期待通りに動作してくれます。
が、safari はすべての option を表示してしまいます。
ぐぐってみると、safari だけがそういう挙動をするみたいです。
で、解決策はどうすればいいのかというと、option を span で囲んで span に display: none を指定すればいいとかなんとか。
option を span の中に入れればいいのかと思って、bladeを修正してみました。
<select name="items" id="items">
<option value="-1">商品名を選択</option>
@foreach($items as $item)
<span>
<option value="{{ $item->id }}" class="maker-{{ $item->maker_id }}" data-name="{{ $item->name }}">{{ $item->name }}</option>
</span>
@endforeach
</select>
これで動かしてみたんですが、このソースで実行してみたとき、開発者ツールで確認すると、完全に select 内の span が無視されてます。
select の直下に option があり、 span のタグは消滅してます。
でも、JavaScriptコンソールから
$('option').eq(1).wrap('<span>');
という操作をすると、option が span タグ内に入ります。
JavaScriptコンソールで $('option').wrap('');とかすると option が span に囲まれるので、動的には可能だけど静的には無視されるっぽい感じなのがわかります。
ブラウザ依存なのかもしれません。
option を span の内側に入れるのは、blade ではできなさそうなので諦めます。
というわけで、次のアプローチとして、別の場所に option を用意しておいて、それを条件に従って select の中を削除して、条件にあったものだけを追加する方向で考えてみます。
まず、全部のリストを持つ、見えない要素を用意します。
<div id="model-source" style="display: none;">
<select name="" id="model-options">
<option value="-1">商品名を選択</option>
@foreach($models as $m)
<option value="{{ $m->id }}" class="maker-{{ $m->maker_id}} type-{{ $m->category }}"
data-name="{{ $m->name }}" data-kana="{{ $m->name_kana }}"
data-type="{{ $m->type->name }}" data-chance="{{ $m->chance }}">{{ $m->name }}</option>
@endforeach
</select>
</div>
で、メーカーが選択されたら、そのメーカーの商品に対応する option だけを実際に見えている select に追加するようにします。
function selectionChanged() {
let mid = $('#makers').val();
let cid = $('#types').val();
let models = $('#model-source').clone();
$(models).prop('id', 'cloned');
let opts = $(models).children();
let reset = false;
$('#models').empty();
$('#models').append($(opts[0]));
for (let i = 1; i < opts.length; i++) {
if (isMatched(mid, cid, $(opts[i]))) {
$('#models').append($(opts[i]));
}
}
}
あ、「let models = $('#model-source').clone();」の次に「$(models).prop('id', 'cloned');」してるのは、同一idの要素をなくすためです。
しなくても大丈夫そうですが、念のため。
これで無事に期待通りの動作をするようになりました。