Select
是一個下拉選擇器。觸發時能夠彈出一個菜單讓用戶選擇操作。
這個元件我們底層就能夠使用我們上一篇所提到的 Dropdown 來實作。
選項
options 是我們選單的 list ,選單中每一個選項我們用 { label, value }
來表示,為何需要兩個值來表達一個欄位呢?原因是因為,我們可能會遇到顯示的 label 跟選取所需要的 value 不一樣的狀況,舉例來說,假設今天我們有一個評論管理列表,透過 Select 我們可以篩選不同星等的留言,從一星留言到五星留言,我們的選項當然可以用 1, 2, 3, 4, 5
來表示,但是假設今天設計師與 PM 討論出來的選項是 1, 2, 3, 4, 5, 全部星等
,你絕對沒有看錯,所有都是數字類別的選項今天在最後面突然出現一個非數字選項,遇到這樣的狀況,你的畫面應該如何處理?而且你打 API 的時候應該送什麼資料給後端?
除了這個特別的狀況之外,還有一些比較常見的例子,例如說我要透過 Select 篩選商品的類別,有 3C 商品
, 彩妝
, 運動
, 保健
, 親子
...等等類別,我們要顯示給使用者看的資料,跟實際上儲存在資料庫裡面或拿來做運算的資料,通常不會是我們所看到的這些中文字。另一方面,若又考慮到多國語言的處理,我們勢必又有更強烈的理由將一個選項切分為 label 以及 value。
value
用來指定當前被選中的項目。
特別提一下這個屬性,因為在設計哪個選項被選中的資料表示法,有看過有人設計成在每個選項裡面加一個 isSelected
的 boolean,所以 option 的內容變成 { label, value, isSelected }
。但其實我自己是不太偏好這樣的設計,因為這樣會讓每一個 option 都多一個參數,讓 option 複雜化。用 <Select value={value} options={options} />
其實就充分可以做到同樣的事,即使可能想要表達一次多個選項被選擇,也只要讓 value 可以支援 array 就可以了。
屬性 | 說明 | 類型 | 默認值 |
---|---|---|---|
options | 選項內容 | { label, value }[] |
|
isLoading | 資料是否正在載入中 | boolean | flase |
isDisable | 是否禁用下拉選單 | boolean | flase |
value | 用來指定當前被選中的項目 | string | |
placeholder | 未選擇任何選項時顯示的 title | string | |
onSelect | 當選項被選中時會被調用 | (value) => void |
以下就是我最終期待的 Select 使用起來的樣子,我們只需要給定 選項
、選中的值
、placeholder
、onSelect
就可以了:
const options = [
{
label: '我全都要',
value: 'all'
},
{
label: 'AZ 疫苗',
value: 'AZ'
},
{
label: 'BNT 疫苗',
value: 'BNT'
},
{
label: '莫德納疫苗',
value: 'Moderna'
},
{
label: '高端疫苗',
value: 'Vaccine'
}
];
const [selectedValue, setSelectedValue] = useState('');
<Select
value={selectedValue}
options={options}
placeholder="請選擇預約疫苗"
onSelect={(value) => setSelectedValue(value)}
/>
那我們來看一下內部是怎麼做的,按照上一篇所預告的,這個內部我就直接用上一篇做好的 Dropdown 來實現,這時候就能夠來驗證一下我們 Dropdown 到底好不好用啦!
Select 選單
我們先來看一下 Dropdown 的 overlay 這個 props,因為這裡我們要迭代出我們的選單:
<Dropdown
{...省略其他props}
overlay={(
<Menu>
{
options.map((option) => (
<MenuItem
key={option.value}
role="presentation"
$isSelected={option.value === value}
onClick={() => {
onSelect(option.value);
setIsOpen(false);
}}
>
{option.label}
</MenuItem>
))}
</Menu>
)}
>
...
</Dropdown>
這個選單其實也蠻單純的,就是判斷 isSelected
的時候是用選項 value
跟選中的 value
來做比較:
$isSelected={option.value === value}
然後點擊的時候,我們做兩件事,一件事是呼叫 onSelect 這個 callback,讓呼叫他的人可以拿到 option.value 這個值,以便更新哪個選項是被選中的。
第二件事是選中之後,我們就把選單關閉 setIsOpen(false)
,當然你不想要關閉也是可以的,就看使用的情境。
Select Box
Select Box 來顯示我們選中的內容以及選中的狀態,下面是我的 Select Box 的長相:
const foundOption = options.find((option) => option.value === value) || {};
<SelectBox $isDisable={isDisable || isLoading}>
<span>{foundOption.label || placeholder}</span>
{
isLoading ? (
<StyledCircularProgress
$color="#00000040"
size={16}
/>) : (
<ArrowDown $isOpen={isOpen}>
<KeyboardArrowDown />
</ArrowDown>
)
}
</SelectBox>
主要的顯示選中內容在這裡:
<span>{foundOption.label || placeholder}</span>
如果有選項被選中,我們就顯示他的 label,如果沒有東西被選中,我們就顯示 placeholder。
再來第二個部分就是 Icon,我們有兩種 Icon,一個是一般狀態的 ArrowIcon,用來表示目前選單是展開還是收合,第二個 Icon 是 Loading Icon,用來表示資料現在正在載入中。
比較特別的是我把 ArrowIcon 加上一個旋轉動畫,讓他看起來比較生動一點,主要是使用 css transition 搭配 transform rotate 來實現:
const ArrowDown = styled.div`
height: 24px;
width: 24px;
transform: rotate(${(props) => (props.$isOpen ? 180 : 0)}deg);
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
`;
展示一下效果:
如果他是載入中的話,我們就把 ArrowIcon 換成 Loading Icon:
Disable
我們可以看到上面的 Loading 狀態還加上了 disable 狀態的樣式,因為我們不希望資料還沒被載入完全的狀態下就被展開。
Disable 也是很單純,主要處理兩個部分,一個是樣式,一個是事件。
事件的部分,我們就是用 isDisable 這個 boolean 讓觸發事件無效化就可以了:
onClick={() => ((isDisable || isLoading) ? null : setIsOpen(true))}
樣式的部分,我們一樣把 enable 和 disable 兩個樣式分別獨立出來,然後也是用 boolean 來判斷就可以:
const selectBoxEnable = css`
color: #333;
&:hover {
border: 1px solid #222;
}
`;
const selectBoxDisable = css`
background: #f5f5f5;
color: #00000040;
`;
const SelectBox = styled.div`
// ...(省略其他樣式)
${(props) => (props.$isDisable ? selectBoxDisable : selectBoxEnable)}
`;
在 Dropdown 的幫助之下,我們的 Select 元件就順利搞定啦!
Select 元件原始碼:
Source code
Storybook:
Select