防止 Select2 選擇後自動排序

Photo by Ferdinand Stöhr on Unsplash
Photo by Ferdinand Stöhr on Unsplash

Select2 可以將原本 HTML 的 Select 增加更多豐富的功能,只要透過 CSS 選擇器將目標的 Select 替換掉就可以:

<!-- HTML 部分 -->
<select id="sample-select" multiple="multiple">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
// JavaScript 部分
$('#sample-select').select();

Select2 雖然功能很完善,但是有些看似方便的功能實際上可能是所不需要的。

How to prevent Select2 auto sorting

HTML 5 原生的 Select 提供多個選項的功能,只要加上 multiple 的屬性,在 Select2 中也可以設定 multiple(參考 Configuration API),讓你可以選擇多個選項,但是使用後發現,選擇出來的選項會按照字母或是數字來排序,這或許對有些人來說是很方便的,但是因為某些需求上,這個功能對我造成了另一種困擾。

延伸上面的範例,假設今天我先選了 C,再選了 A,這個時候 A 會自動被排序到最前面去,但或許你可能不希望這個順序被更動。

Select2 的文件中,也沒有相關的設定選項可以讓你去停止自動排序這件事情,這個時候只能靠 workaround 來解決了!🙀

$('#sample-select').on('select2:select', function (event) {
const element = event.params.data.element;
const $element = $(element);
$element.detach();
$(this).append($element).trigger('change');
});

這段 JavaScript 非常的簡單,執行說明如下:

  1. 先綁定 select2 的事件 select2:select
  2. 接著當事件被觸發時,取得該目標元素
  3. 複製該元素
  4. 移除元素
  5. 把複製的元素在 append 到 select2 上,再 trigger change 事件

這樣在選擇選項的時候,排序就不會一直亂跳了。

Server Side Rendering Problem

如果先從後端將資料 query 好後,再送到前端進行 render,以 Laravel 的 Blade 為範例:

<select id="sample-select" multiple="multiple">
@foreach ($options as $option)
<option
value="{{ $option->id }}"
@if (something...)
selected="selected"
@endif
>
{{ $option->name }}
</option>
@endforeach
</select>

在某些條件下,option 是符合條件的,這時候它會被選擇,但是你發現 Select2 還是會自動幫你做排序 😥,這時候該怎麼解決?

我們先將它拆為兩個部分:

  • 確定會被選擇的選項
  • 尚未被選擇的選項

尚未被選擇的選項沒有順序的要求,而確定會被選擇的選項則需要,這時候要就考慮各自 render 的時間點,必須將 query 出來的資料按照上面的部分分成兩組。

$selectedOptions = [/* ... */];
$unSelectedOptions = [/* ... */];
return view(
'something',
[
'selectedOptions' => $selectedOptions,
'unSelectedOptions' => $unSelectedOptions,
]
);

尚未被選擇的選項可以直接在 Server Side 的時候直接 render 出來,而確定會被選擇的選項則是透過前端 JavaScript 載入完成後,再更新 Select 的選項。

尚未被選擇的選項

<select id="sample-select" multiple="multiple">
@foreach ($unSelectedOptions as $option)
<option
value="{{ $option->id }}"
>
{{ $option->name }}
</option>
@endforeach
</select>

確定會被選擇的選項

// ?

在上面描述中,事先在後端處理好兩種選擇的狀況,並分成兩組陣列,所以這時候把確定會被選擇的選項放到 data-* attribute 中:

<select
id="sample-select"
data-selected-options="{{ json_encode($selectedOptions) }}"
multiple="multiple"
>
<!-- ... -->
</select>

這時候在前端可以藉由 data-selected-options 取得被選擇的選項:

const selectedOptions = $('#sample-select').data('selected-options') || [];

selectedOptions 內容大概如下:

[
{ id: 1, text: 'Hello' },
{ id: 2, text: 'World' }
]

idtext 是 Select2 所要求的格式

接著把它們都轉成為 HTMLOptionElement

const options = selectedOptions.map(o => {
const option = new Option(o.text, o.id, true, true);
// 如果你想為 option 加上其他額外的 attribute,可以透過 `setAttribute` 完成
// 例如:`option.setAttribute('data-say-hi', 'Hi')`
// 結果會是:<option data-say-hi="Hi">...</option>
return option;
});

轉換成 HTMLOptionElement 後,得到一個新的 options,接著將這些 options append 到 select,並 trigger change 事件:

$('#select-sample').append(...options);
$('#select-sample').trigger('change');

這時候網頁剛載入進來的時候,你可以發現 select 的區塊都是一開始什麼都沒有,直到 JavaScript 整個載入完成後,會將這些 option 給 append 上去。

Reference