點燈坊

失くすものさえない今が強くなるチャンスよ

實現 Reactive Select

Sam Xiao's Avatar 2024-02-02

多個 Select 連動也是實務上常見需求,傳統 jQuery 必須搭配複雜的 Event 與重組 HTML 才能達成,使用 Vue 的 Reactivity API 將可輕鬆達成需求。

Data

data.json

[
  {
    "name": "基隆市",
    "areas": [
      {
        "name": "仁愛區",
        "zip": "200"
      },
      {
        "name": "信義區",
        "zip": "201"
      },
      {
        "name": "中正區",
        "zip": "202"
      },
      {
        "name": "中山區",
        "zip": "203"
      },
      {
        "name": "安樂區",
        "zip": "204"
      },
      {
        "name": "暖暖區",
        "zip": "205"
      },
      {
        "name": "七堵區",
        "zip": "206"
      }
    ]
  },
  {
    "name": "台北市",
    "areas": [
      {
        "name": "中正區",
        "zip": "300"
      },
      {
        "name": "大同區",
        "zip": "301"
      },
      {
        "name": "中山區",
        "zip": "302"
      },
      {
        "name": "松山區",
        "zip": "303"
      },
      {
        "name": "大安區",
        "zip": "304"
      },
      {
        "name": "萬華區",
        "zip": "305"
      },
      {
        "name": "信義區",
        "zip": "306"
      },
      {
        "name": "士林區",
        "zip": "307"
      },
      {
        "name": "北投區",
        "zip": "308"
      },
      {
        "name": "內湖區",
        "zip": "309"
      },
      {
        "name": "南港區",
        "zip": "310"
      },
      {
        "name": "文山區",
        "zip": "311"
      }
    ]
  },
  {
    "name": "新竹市",
    "areas": [
      {
        "name": "香山區",
        "zip": "400"
      }
    ]
  }
]
  • 儲存所有 都市行政區郵遞區號
  • 將 data 儲存在 .json 並放在 assets 目錄下
<template>
  <div>
    <span>Cities:</span>
    <select v-model="cityIndex">
      <option disabled value="">Please select one</option>
      <option v-for="(item, i) in cities" :value="i" :key="i">
        {{ item.name }}
      </option>
    </select>
  </div>
  <div>
    <span>Areas: </span>
    <select v-model="areaIndex">
      <option disabled value="">Please select one</option>
      <option v-for="(item, i) in areas" :value="i" :key="i">
        {{ item.name }}
      </option>
    </select>
  </div>
  <div>
    <span>Zip: </span>
    <span>{{ zip }}</span>
  </div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import source from './assets/data.json'

// Cities
let cities = ref([])
let cityIndex = ref('')
onMounted(() => (cities.value = source))
watch(cityIndex, () => (areaIndex.value = 0))

// Areas
let areaIndex = ref('')
let areas = computed(() => (cityIndex.value === '' ? [] : cities.value[cityIndex.value].areas))

// Zip
let zip = computed(() => (areaIndex.value === '' ? '' : areas.value[areaIndex.value].zip))
</script>

Line 27

import source from './assets/data.json'
  • import:從 assets 目錄 import 進 JSON 檔

Line 30

let cities = ref([])
let cityIndex = ref('')
  • cities state:提供 <option> 所需資料
  • cityIndex state:提供 <select>v-model 綁定資料

Line 33

onMounted(() => (cities.value = source))
  • onMounted():在 mounted hook 將 JSON 檔寫入 options state

Line 34

watch(cityIndex, () => (areaIndex.value = 0))
  • watch():追蹤 cityIndex state,當所選的 都市 有變化時,立即 reset 地區areaIndex,避免 新竹市 出現 bug

Line 3

<span>Cities:</span>
<select v-model="cityIndex">
  <option disabled value="">Please select one</option>
  <option v-for="(item, i) in cities" :value="i" :key="i">
  {{ item.name }}
  </option>
</select>
  • 顯示所有 都市
  • cityIndex state:為 <select>v-model
  • cities state:使用 v-for 列舉所有 都市

Line 37

let areaIndex = ref('')
  • areaIndex state:提供 <select>v-model 綁定資料

Line 38

let areas = computed(() => (cityIndex.value === '' ? [] : cities.value[cityIndex.value].areas))
  • areas computed:
    • cityIndex state 為 empty string 時,回傳 empty array
    • cityIndex state 非 empty string 時,值將以其值取得所有 行政區

Line 12

<span>Areas: </span>
<select v-model="areaIndex">
  <option disabled value="">Please select one</option>
  <option v-for="(item, i) in areas" :value="i" :key="i">
    {{ item.name }}
  </option>
</select>
  • 顯示所有 行政區
  • areaIndex state:為 <select>v-model
  • areas computed:使用 v-for 列舉所有 行政區

Line 41

let zip = computed(() => (areaIndex.value === '' ? '' : areas.value[areaIndex.value].zip))
  • zip computed:
    • areaIndex state 為 empty string 時,回傳 empty string
    • areaIndex state 非 empty string 時,值將以其值取得 郵遞區號

Line 21

<span>Zip: </span>
<span>{{ zip }}</span>
  • zip state:顯示該 行政區郵遞區號

Conclusion

  • 透過 Computed,只要 state 有變動,其對應 Computed 也會變動,讓我們不必再使用 Event 動態產生 HTML
  • Watch 適合處理一些特別的 edge case,主要還是盡量以 Computed 實現