• בלוג
  • יומי
  • דוגמת Vue: מספר מונים וצביעת הגדול ביותר

דוגמת Vue: מספר מונים וצביעת הגדול ביותר

15/11/2024

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

1. פיתרון 1 - שמירת כל המידע בקומפוננטה העליונה

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

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

הקוד לקומפוננטה העוטפת הוא בסך הכל:

<script setup lang="ts">
import {ref, computed} from 'vue';
import Counter from './Counter.vue';

const counts = ref([0, 0, 0, 0]);
const maxValue = computed(() => Math.max(...counts.value));
const maxIndex = computed(() => counts.value.findIndex(el => el === maxValue.value));

function onClick(counterIndex) {
  counts.value[counterIndex]++;
}
</script>

<template>
  <p>max value = {{ maxValue }}</p>
  <p>max index = {{ maxIndex }}</p>
  <Counter @click="onClick(0)" :isMax="maxIndex == 0" :count="counts[0]"/>
  <Counter @click="onClick(1)" :isMax="maxIndex == 1" :count="counts[1]"/>
  <Counter @click="onClick(2)" :isMax="maxIndex == 2" :count="counts[2]"/>
  <Counter @click="onClick(3)" :isMax="maxIndex == 3" :count="counts[3]"/>
</template>

ובגלל שכל המידע מנוהל בקומפוננטה זו קומפוננטות המונים יכולות להתמקד בהצגת המונה וב CSS לקביעת הצבע, והקוד של קומפוננטת המונה נראה כך:

<script setup lang="ts">
import { ref, computed } from 'vue'

const emit = defineEmits(['click']);
const {isMax, count} = defineProps<{isMax?: boolean, count: number}>();
const background = computed(() => isMax ? "green" : "yellow");

function onClick() {
  emit('click');
}
</script>

<template>
  <div>
    <p>Value = {{ count }}</p>
    <button @click="onClick">+1</button>
  </div>
</template>

<style scoped>
  div {
    background-color: v-bind(background);
    padding: 20px;
  }

  button {
    color: white;
  }

  p {
    color: black;
  }
</style>

2. פיתרון 2 - שמירת המונים בכל קומפוננטה וערך המקסימום בקומפוננטה העליונה

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

בשביל הפיתרון אצטרך לספר לכם משהו נוסף לגבי אירועים - אירועים ב vue יכולים להישלח יחד עם פרמטר, והפרמטר מגיע לקוד הטיפול באירוע. מבחינת קוד זה די פשוט, הפונקציה emit יכולה לקבל פרמטר נוסף אחרי שם האירוע:

emit('change', 10);

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

<Counter @change="(newValue) => doSomethingWith(newValue)" />

עכשיו שאנחנו יודעים את זה נוכל לכתוב את הקומפוננטה העוטפת ולקבל:

<script setup lang="ts">
import {ref} from 'vue';
import Counter from './Counter.vue';

const maxValue = ref<null|number>(null);
const maxIndex = ref<null|number>(null);

function onNewValue(counterIndex, value) {    
  if (value > maxValue.value) {        
    maxIndex.value = counterIndex;
    maxValue.value = value;
  }
}
</script>

<template>
  <Counter @change="(newValue) => onNewValue(0, newValue)" :isMax="maxIndex == 0" />
  <Counter @change="(newValue) => onNewValue(1, newValue)" :isMax="maxIndex == 1" />
  <Counter @change="(newValue) => onNewValue(2, newValue)" :isMax="maxIndex == 2" />
  <Counter @change="(newValue) => onNewValue(3, newValue)" :isMax="maxIndex == 3" />
</template>

ואת קומפוננטת המונה באופן הבא:

<script setup lang="ts">
import { ref, computed } from 'vue'

const count = ref(0)
const emit = defineEmits(['change']);
const {isMax} = defineProps<{isMax?: boolean}>();
const background = computed(() => isMax ? "green" : "yellow");

function onClick() {
  count.value++;
  emit('change', count.value);
}
</script>

<template>
  <div>
    <p>Value = {{ count }}</p>
    <button @click="onClick">+1</button>
  </div>
</template>

<style scoped>
  div {
    background-color: v-bind(background);
    padding: 20px;
  }

  button {
    color: white;
  }

  p {
    color: black;
  }
</style>

3. יתרונות וחסרונות בשתי הגישות

נשים לב להבדלים וליתרונות והחסרונות בין שתי הגישות:

  1. הגישה הראשונה היתה יותר פשוטה מנקודת מבט של כל קומפוננטת מונה וגם מנקודת המבט של הקומפוננטה העוטפת. כל המידע של כל המונים נשמר במערך אחד וכל פעם שהוא משתנה בודקים מי הערך המקסימלי ומעדכנים את כל המונים.

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

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

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

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

4. עכשיו אתם

  1. עדכנו את שני הפיתרונות כך שישתמשו ב v-for במקום לשכפל את יצירת המונה 4 פעמים.

  2. שימו לב ששני הפיתרונות לא מתנהגים אותו דבר. כשיש מספר מונים עם אותו ערך מקסימלי בפיתרון השני הירוק לא משתנה אבל בפיתרון הראשון הצבע הירוק יינתן למונה העליון ביותר שמציג את הערך המקסימלי. עדכנו את הקוד של שני הפיתרונות כך שאם למספר מונים יש את אותו ערך מקסימלי אז אף מונה לא ייצבע בירוק.