跳至主要内容

2 篇文章 含有標籤「React」

React tag description

檢視所有標籤

如何讓 shadcn/ui Select 元件 value 能夠接收空字串及其他型別

· 閱讀時間約 5 分鐘

shadcn/ui 雖然主打隨時都能修改 source code 好像很彈性,但某些元件還是依賴其他第三方的 ui library,例如 Select 元件依賴@radix-ui/react-select,導致沒辦法輕易做到修改更底層的 source code。

而最近使用 shadcn/ui 也遇到蠻多坑的,其中一個讓我覺得最麻煩,一定得想辦法處理的就是 Select 元件 value 無法接收空字串及其他型別的問題,而限制空字串這還是新版@radix-ui/react-select 的 feature...

先說說不能空字串只能字串會分別遇到什麼痛點好了

不能空字串

原本專案有個 filter search 功能,GET 能帶 status 參數,/api/orders?status="success"表示搜尋狀態為成功的訂單,/api/orders?status="error"表示搜尋狀態為失敗的訂單, 而/api/orders 表示搜尋所有狀態的訂單。

這時如果傳入物件有個 value""空字串的 key 給 axios params,我們會在 axios 的 paramsSerializer (參數序列化)或 interceptors (攔截器)過濾該物件的 key,你的 URL 還是/api/orders

但如果今天限制不能只用空字串,你就必須自定義一個常量變數用來表示全部狀態, 例如"__ALL__",然後去改變專案原本的 paramsSerializer (參數序列化)或 interceptors (攔截器),不然你就只能在 submit 的時候必須先判斷是否為"__ALL__",如果是的話就要先移除該 key 或改為空字串才能打 API,相當麻煩,況且如果遇到不是打 API,而是其他業務邏輯就是要空字串的情況怎辦?

只能字串

而只能使用字串會遇到打 POST API 或其他業務邏輯時,如果需要的參數為數字、布林值等,就必須在 submit 或處理之前將該字串手動判斷轉換為需要的型別,非常麻煩外,可讀性差又難以維護,在 react-hook-form 的 formValues 型別及驗證上也全是字串,根本沒有意義。

為了解決這問題,只能自己封裝一個 Select 元件了,而解決的思路其實也很簡單,因為不能動到底層的@radix-ui/react-select,所以我們還是傳字串valueSelectItem,只不過會先根據 value 的型別進行處理:

const valueToString = (val: any) => {
if (val === null) return "__null__"
if (val === undefined) {
console.error("option value cannot be undefined")
return "__undefined__"
}
if (val === Infinity) return "__Infinity__"
if (val === -Infinity) return "__-Infinity__"
if (Number.isNaN(val)) return "__NaN__"
if (typeof val === "symbol") return `__symbol__${val.description}`
return JSON.stringify(val)
}

然後在SelectonValueChange 事件中,將拿到的字串value再轉換回原本的型別給上層的onChange使用,

const stringToValue = (str: string) => {
switch (str) {
case "__null__":
return null
case "__undefined__":
return undefined
case "__Infinity__":
return Infinity
case "__-Infinity__":
return -Infinity
case "__NaN__":
return NaN
default:
if (str.startsWith("__symbol__")) {
return Symbol(str.slice(10))
}
return JSON.parse(str)
}
}
const Select = forwardRef<HTMLButtonElement, SelectProps>(
({ options, placeholder, onChange, value }, ref) => {
const handleValueChange = (value: string) => {
if (onChange) {
onChange(stringToValue(value))
}
}

return (
<SelectUI
// Only show placeholder when value is initially undefined
value={value !== undefined ? valueToString(value) : undefined}
onValueChange={handleValueChange}
>
<SelectTrigger ref={ref} className="w-[180px]">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map((option) => (
<SelectItem
key={valueToString(option.value)}
value={valueToString(option.value)}
disabled={option.disabled}
>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</SelectUI>
)
}
)

這樣就大功告成了,同理Radio Group也是一樣的道理,畢竟很常遇到truefalse要手動字串轉布林值的問題。

範例程式碼:

Demo

GitHub