• בלוג
  • מדריך: איך לבנות שתי תיבות בחירה מתואמות ב Vue

מדריך: איך לבנות שתי תיבות בחירה מתואמות ב Vue

11/11/2021

קומפוננטה מאוד נפוצה ב Web שעדיין לא קיימת מובנית בדפדפן היא בחירה של מספר ערכים הקשורים אחד לשני - לדוגמה בחירת מדינה ואז בחירת עיר מתוך הערים באותה מדינה. בפוסט זה נכתוב קומפוננטה כזאת ב Vue ונלמד משהו על ריאקטיביות ב Vue.

1. מה אנחנו בונים

אני רוצה להציג שתי תיבות select על המסך, בתיבה הראשונה משתמש בוחר ארץ מתוך רשימה של ארצות ובתיבה השניה משתמש בוחר עיר מתוך רשימה של ערים באותה ארץ. כשמחליפים ארץ גם רשימת הערים מתחלפת.

את המידע עבור הארצות והערים אפשר לייצג באוביקט JSON הבא:

const db = {
  Israel: [ 'Jerusalem', 'Tel Aviv', 'Ashdod' ],
  England: [ 'London', 'Manchester' ],
};

אתם יכולים להוסיף בזמנכם הפנוי עוד ערים ומדינות.

2. יצירת התיבה עבור ארץ

נפתח קומפוננטה חדשה בפרויקט אני קורא לה Selects ולכן בקובץ Selects.vue אני רושם את התוכן הבא:

<script setup>
const db = {
  Israel: [ 'Jerusalem', 'Tel Aviv', 'Ashdod' ],
  England: [ 'London', 'Manchester' ],
};

</script>

<template>
</template>

<style scoped>
</style>

דבר ראשון אני רוצה להציג את התיבה לבחירת ארץ ובשביל זה אני צריך לולאה על רשימת הארצות. אני יכול לבנות את הרשימה בתוך הטמפלייט אבל מעדיף לבנות אותה בבלוק ה setup. בגלל שרשימת הארצות היא קבועה אני עדיין לא צריך להשתמש ביכולות הריאקטיביות של Vue ומוסיף פשוט:

const countries = Object.keys(db);

מה שכן אני צריך ריאקטיבי זה משתנה שיחזיק את הארץ שמשתמש בחר. בשביל זה אני יוצר ref ולכן קוד האתחול שלי הופך ל:

import { ref } from 'vue';

const db = {
  Israel: [ 'Jerusalem', 'Tel Aviv', 'Ashdod' ],
  England: [ 'London', 'Manchester' ],
};

const countries = Object.keys(db);
const selectedCountry = ref("");

בתוך הטמפלייט אני אוסיף את הקוד הבא שיציג תיבה לבחירת ארץ מתוך רשימת הארצות ועל הדרך יעדכן את המשתנה selectedCountry כל פעם שבחירת המשתמש מתעדכנת:

<template>
<div>
  <select v-model="selectedCountry">
    <option value="" disabled="true">Please select a country</option>
    <option v-for="country in countries" :value="country">{{country}}</option>
  </select>

  <p>Selected country = {{selectedCountry}}</p>
</div>
</template>

תיבה ראשונה עובדת! נמשיך לתיבה השניה שתציג את רשימת הערים.

3. בחירת עיר

בשביל לבחור עיר אני רוצה לחשב את רשימת הערים. בניגוד לרשימת המדינות, רשימת הערים משתנה כל פעם שמשתמש מעדכן את המדינה הנוכחית - שכן תיבת בחירת עיר צריכה להציג רק את הערים במדינה הנוכחית.

אם אנסה להוסיף רק את הקוד הבא לבלוק האתחול שלי זה לא יעבוד:

// WARNING DOES NOT WORK
const cities = db[selectedCountry.value] || [];

הקוד יחושב פעם אחת ואחרי זה לא ירוץ שוב בעקבות עדכונים למשתנה selectedCountry. אנחנו רוצים לבנות מנגנון ריאקטיבי ולכן נשתמש בפונקציה computed של vue:

const cities = computed(() => db[selectedCountry.value] || []);

קוד כזה אוטומטית מוסיף Watcher על המשתנה selectedCountry ועכשיו כל פעם שמשתנה זה יתעדכן גם החישוב ירוץ פעם נוספת.

עכשיו אפשר להמשיך ולהציג את רשימת הערים. אני מוסיף לטמפלייט את הבלוק הבא:

<select v-model="selectedCity">
  <option value="" disabled="true">Please select city</option>
  <option v-for="city in cities" :value="city">{{city}}</option>
</select>

<p>You selected country = {{selectedCountry}} and city = {{selectedCity}}</p>

וזה עובד! כמעט. כי כל פעם שאני בוחר ארץ ועיר ואז מחליף את הארץ ה"עיר" הקודמת נשארת בחורה וזה יוצר אפקט מוזר. הדבר האחרון שאני צריך כאן הוא לבנות את הקשר בין הארץ לעיר, כלומר להגיד ל Vue שכשמשנים את הארץ אוטומטית צריך לאפס גם את העיר.

הדרך הכי קלה לעשות את זה היא להוסיף אפקט שיוודא שהעיר הנוכחית נמצאת בתוך הארץ הנוכחית, ואם לא יאפס את העיר:

watchEffect(() => {
  if (!cities.value.includes(selectedCity.value)) {
    selectedCity.value = '';
  }
});

קוד הקומפוננטה המלא שגם עובד הוא:

<script setup>
import { ref, computed, watchEffect } from 'vue';

const db = {
  Israel: [ 'Jerusalem', 'Tel Aviv', 'Ashdod' ],
  England: [ 'London', 'Manchester' ],
};

const countries = Object.keys(db);
const selectedCountry = ref("");
const cities = computed(() => db[selectedCountry.value] || []);
const selectedCity = ref("");

watchEffect(() => {
  if (!cities.value.includes(selectedCity.value)) {
    selectedCity.value = '';
  }
});

</script>

<template>
<div>
  <select v-model="selectedCountry">
    <option value="" disabled="true">Please select a country</option>
    <option v-for="country in countries" :value="country">{{country}}</option>
  </select>

  <p>Selected country = {{selectedCountry}}</p>
  <pre>Cities = {{cities}}</pre>

  <select v-model="selectedCity">
    <option value="" disabled="true">Please select city</option>
    <option v-for="city in cities" :value="city">{{city}}</option>
  </select>
  <p>You selected country = {{selectedCountry}} and city = {{selectedCity}}</p>
</div>
</template>

<style scoped>
</style>