iT邦幫忙

2025 iThome 鐵人賽

DAY 10
1
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 10

Day 10:[Componentの呼吸・參之型] Select打造 - 表單元件開發

  • 分享至 

  • xImage
  •  

在前兩天我們成功打造了 BaseButton 元件,為我們的設計系統踏出了堅實的第一步。
今天,我們將乘勝追擊,挑戰另一個在表單中無處不在的元素,下拉選單 (<select>)

在我們的登入頁面中,有一個「門市選擇」的下拉選單。它看起來很棒,功能也正常。

但如果未來我們有十個頁面都需要風格一致的下拉選單,我們真的要複製貼上十次那段夾雜著 HTML 和 CSS 的程式碼嗎?

這樣的話,任何微小的樣式調整,都意味著我們要在十個地方進行修改。

為了解決這個問題,我們將再次把這個下拉選單封裝成一個可複用、可維護、語意清晰的 BaseSelect 元件

我們的目標

今天的目標非常明確:

  1. 建立一個名為 BaseSelect.vue 的新元件。
  2. 它必須能夠透過 v-model 實現雙向綁定。
  3. 它必須能夠接收一個 options 陣列來動態生成選項。
  4. 它的樣式必須是自給自足、與世隔絕的,不依賴任何外部 CSS。
  5. 最終,我們將用這個新元件來取代登入頁面中原本的 <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 需要什麼資訊才能運作呢?

  1. label: 標籤告訴使用者這是什麼。
  2. 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 陣列中,每個物件都包含 valuetext。並加上 :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。
  • 如何使用 Vue 3 的 defineModel 實現 v-model
  • 樣式封裝 (scoped CSS) 的重要性,以及如何打造一個真正獨立可複用的元件。

我們的設計系統又多了一塊堅實的積木。

明天,我們會再次對我打造的元件進行測試,Day 11:[Componentの呼吸・肆之型] 測試Select - 確保輸入元件正常運作。心を燃やせ 🔥!


上一篇
Day 9:[Componentの呼吸・貳之型] 測試Button - 學習Component Testing
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言