在前兩天我們成功打造了 BaseButton
元件,為我們的設計系統踏出了堅實的第一步。
今天,我們將乘勝追擊,挑戰另一個在表單中無處不在的元素,下拉選單 (<select>
)。
在我們的登入頁面中,有一個「門市選擇」的下拉選單。它看起來很棒,功能也正常。
但如果未來我們有十個頁面都需要風格一致的下拉選單,我們真的要複製貼上十次那段夾雜著 HTML 和 CSS 的程式碼嗎?
這樣的話,任何微小的樣式調整,都意味著我們要在十個地方進行修改。
為了解決這個問題,我們將再次把這個下拉選單封裝成一個可複用、可維護、語意清晰的 BaseSelect
元件!
今天的目標非常明確:
BaseSelect.vue
的新元件。v-model
實現雙向綁定。options
陣列來動態生成選項。<select>
。首先,我們在 src/components
目錄下建立 BaseSelect.vue
,並寫下它的基本結構。
// BaseSelect.vue
<script setup>
</script>
<template>
<div>
<label></label>
<select></select>
</div>
</template>
<style scoped>
</style>
props
一個好的元件,它的 props
就是它的 API 文件。我們的 BaseSelect
需要什麼資訊才能運作呢?
label
: 標籤告訴使用者這是什麼。options
: 選項的來源,這將是一個陣列。// BaseSelect.vue - <script setup>
import { computed } from 'vue'
const props = defineProps({
// 標籤文字
label: {
type: String,
required: true,
},
// 接收一個選項陣列
options: {
type: Array,
required: true,
default: () => [],
},
// ... 其他 props
})
v-model
的靈魂 (defineModel
)在 Vue3 中,實現 v-model
只需要一行程式碼:
// BaseSelect.vue - <script setup>
// ...
const model = defineModel()
// ...
defineModel()
會自動幫我們註冊 modelValue
prop 和 update:modelValue
事件。不需要再使用過去那一大串 computed
的寫法。
接著,我們在 <template>
中使用 v-for
來遍歷 options
prop,並將其渲染成一個個的 <option>
。
<!-- BaseSelect.vue - <template> -->
<select v-model="model">
<option v-for="option in options"
:key="option.value"
:value="option.value"
:disabled="option.disabled"
>
{{ option.text }}
</option>
</select>
我們期望傳入的 options
陣列中,每個物件都包含 value
和 text
。並加上 :disabled
綁定,讓選項可以被禁用。
我們希望元件的樣式是獨立的。因此,我們將所有需要的 CSS 都寫在 BaseSelect.vue
的 <style scoped>
區塊內。
// BaseSelect.vue - 完整程式碼
<script setup>
import { computed } from 'vue'
const model = defineModel()
const props = defineProps({
label: { type: String, required: true },
options: { type: Array, required: true, default: () => [] },
id: { type: String, default: null },
})
// 為了可訪問性,確保 label 和 select 的 id 匹配
const selectId = computed(() => props.id || `base-select-${Math.random().toString(36).substring(2, 9)}`);
</script>
<template>
<div class="w-full">
<label :for="selectId" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
</label>
<select :id="selectId" v-model="model" class="base-select">
<option
v-for="option in options"
:key="option.value"
:value="option.value"
:disabled="option.disabled"
>
{{ option.text }}
</option>
</select>
</div>
</template>
<style scoped> ... </style>
透過 <style scoped>
,這些 CSS 只會作用於 BaseSelect
元件內部,不怕與外部樣式衝突了。
現在,我們回到 LoginView.vue
。
Before:
<!-- LoginView.vue -->
<div class="input-group">
<label for="store-select">門市選擇</label>
<select id="store-select" v-model="storeSelect" class="store-select">
<option value="taipei">臺北門市</option>
...
</select>
</div>
After:
首先,在 <script setup>
中準備好 options
陣列並引入元件。
// LoginView.vue - <script setup>
import { ref } from 'vue';
import BaseSelect from '@/components/BaseSelect.vue';
const storeSelect = ref('');
const storeOptions = [
{ value: '', text: '請選擇您的門市...', disabled: true },
{ value: 'taipei', text: '臺北門市' },
// ... 其他門市
];
然後,在 <template>
中,用一行程式碼取代原本那一大塊!
<!-- LoginView.vue - <template> -->
<div class="input-group">
<BaseSelect label="門市選擇" v-model="storeSelect" :options="storeOptions"/>
</div>
OK,LoginView
現在變得極度乾淨,
它只負責提供資料,而不用再關心下拉選單到底長什麼樣子、怎麼實現。這就是元件化的好處。
今天,我們透過打造 BaseSelect
元件,再次體會到了元件化的強大威力。我們學到了:
props
設計一個清晰的元件 API。defineModel
實現 v-model
。scoped CSS
) 的重要性,以及如何打造一個真正獨立可複用的元件。
我們的設計系統又多了一塊堅實的積木。
明天,我們會再次對我打造的元件進行測試,Day 11:[Componentの呼吸・肆之型] 測試Select - 確保輸入元件正常運作。心を燃やせ 🔥!