اذهب إلى المحتوى

المزيد حول المكونات في Vue.js


حسام برهان

سنتعلّم في هذا الدرس:

  • بناء تطبيق نموذجي (مشاريب حسوب).
  • إضافة وسائل تنقيح متطوّرة لتطبيقات Vue.js.
  • المكوّنات المتداخلة.
  • تسجيل المكوّنات محليًّا وتسجيلها على المستوى العام.
  • تحديد المكوّن الذي اختاره المستخدم.
  • التخاطب بين المكوّنات باستخدام أحداث مخصّصة.

سنتعلّم في هذا الدرس المزيد عن المكوّنات، حيث سنتعرّف على المزيد من المزايا المتعلّقة بها، والتي ستساعدنا على بناء مكوّنات عملية ومفيدة. سنبدأ هذا الدرس ببناء تطبيق نموذجي سيكون الهيكل الأساسي الذي سنستخدمه لتعلّم المزايا الجديدة حول المكوّنات، ثم سنتعرّف على كيفية استخدام أدوات تنقيح متطوّرة مكتوبة خصيصًا لـ Vue.js، ثم نتعرّف على المكوّنات المتداخلة وكيفية استخدامها، ثم سنتعلّم كيفية تسجيل المكوّنات محليًا وعلى المستوى العام في التطبيق، ونختم بتعلّم كيفية التخاطب بين المكوّنات المتداخلة.

بناء تطبيق نموذجي (مشاريب حسوب)

سنبني من أجل هذا الدرس تطبيق نموذجي بسيط لكي نطبق عليه الأفكار التي سنتاولها هنا. سيكون هذا التطبيق عبارة عن واجهة بسيطة لمحل افتراضي بيع مشروبات متنوعة. سيعبّر المكوّن في هذه المرة عن مشروب معيّن من قائمة المشروبات المتاحة.

انظر إلى الشكل النهائي المقترح:

1.png

كما ترى فإنّني أستخدم اللغة العربية في التطبيق هذه المرّة! أنشئ مجلّدًا جديدًا سمّه hsoub-drinks سنستخدمه لوضع ملفات المشروع ضمنه.

يحتوي هذا التطبيق على مكوّن وحيد حاليًا أسميته drink أي مشروب ما. لهذا المكوّن خاصيّة وحيدة اسمها name تعبّر عن اسم هذا المشروب. انظر إلى شيفرة Vue.js التي سأضعها ضمن الملف app.js الذي سيكون موجودًا ضمن المجلّد hsoub-drinks الذي أنشأته توًّا:

Vue.component('drink', {
    template: '#drink-template',
    props: {
      name: {
        type: String
      }
    }
  });


  new Vue({
    el: '#app',
    data: {
        drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"]
    }
  })

لاحظ كم هو بسيط هذا التطبيق: مكوّن عادي معرّف في الأعلى، وكائن Vue.js بسيط في الأسفل يحتوي على البيانات التي نريد عرضها على الشاشة. هذه البيانات موجودة ضمن المصفوفة drinks كما هو واضح.

لننتقل الآن إلى شيفرة HTML التي سأضعها ضمن ملف سأسمّه index.htmlوسيكون موجودًا أيضًا ضمن المجلّد hsoub-drinks:

<html lang="ar">

<body>
    <div class="header">
        <span id="logo">مشاريب حسوب</span>
    </div>

    <div id="app" class="container">
        <div class="content">
            <h1 class="title">المشروبات المتوفرة</h1>

            <div class="drinks">
                <drink v-for="drink in drinks" v-bind:name="drink"></drink>
            </div>
        </div>
    </div>

    <script type="text/x-template" id="drink-template">
        <div class="drink">
            <div class="description">
            <span class="title">
                {{name}}
            </span>
            </div>
        </div>
    </script>

    <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="app.css" />

</body>

</html>

كما مرّ معنا فيما سبق نستخدم حلقة v-for في المرور على عناصر المصفوفة drinks وبالتالي توليد عنصر المكوّن drink كما هو واضح من الشيفرة السابقة. لاحظ أيضًا أنّ العناصر المولَّدة والتي تمثّل المشروبات المتوفرة ستكون موجودة ضمن عنصر div يحمل الصنف drinks. أرجو أن لا يختلط عليك الأمر بين اسم المكوّن drink وبين صنف التنسيق drink لأنّ كليهما يحملان نفس الاسم، وهذا أمر جائز تمامًا.

بقيت أخيرًا تنسيقات CSS الذي سأضعها ضمن الملف app.css ضعه أيضًا ضمن المجلد hsoub-drinks:

@import url(//fonts.googleapis.com/earlyaccess/notonaskharabic.css);
body {
    height: 100vh;
    -webkit-font-smoothing: auto;
    -moz-osx-font-smoothing: auto;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-family: 'Noto Naskh Arabic', serif;
    background-color: #ccdcdc;
    background-repeat: no-repeat;
    background-position: 100% 100%
}

span#logo {
    font-weight: 700;
    color: #eee;
    font-size: larger;
    letter-spacing: .05em;
    padding-left: .5rem;
    padding-right: .5rem;
    padding-bottom: 1rem;
    float: right;
    padding-top: 6px;
    margin-right: 20px;
}

.header{
    background-color:slategray;
    width: 80% ;
    height: 50px;
    margin-left: auto;
    margin-right: auto;
}

h1.title {
    text-align: center;
    font-size: 1.875rem;
    font-weight: 500;
    color: #2d3336
}

h2.subtitle {
    margin: 8px auto;
    font-size: x-large;
    text-align: center;
    line-height: 1.5;
    max-width: 500px;
    color: #5c6162
}

.content {
    margin-left: auto;
    margin-right: auto;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    width: 620px
}

.drinks {
    padding: 0 40px;
    margin-bottom: 40px
}

.drinks .drink {
    background-color: #fff;
    -webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
    margin-top: 1rem;
    margin-bottom: 1rem;
    border-radius: .25rem;
    -webkit-box-pack: justify;
    -ms-flex-pack: justify;
    justify-content: space-between;
    cursor: pointer;
    position: relative;
    -webkit-transition: all .3s ease;
    transition: all .3s ease
}

.drinks .drink, .drinks .drink>.weight {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex
}

.drinks .drink>.description {
    width: 100%;
    padding: 1rem;
}

.drinks .drink>.description .title {
    color: #3d4852;
    display: block;
    font-weight: 700;
    margin-bottom: .25rem;
    float: right;
}

.drinks .drink>.description .description {
    font-size: .875rem;
    font-weight: 500;
    color: #8795a1;
    line-height: 1.5
}

.drinks .drink>.price {
    width: 20%;
    color: #09848d;
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    padding-top: 1.5rem;
    font-family: Crimson Text, serif;
    font-weight: 600
}

.drinks .drink>.price .dollar-sign {
    font-size: 24px;
    font-weight: 700
}

.drinks .drink>.price .number {
    font-size: 72px;
    line-height: .5
}

.drinks .active-drink, .drinks .drink:hover {
    -webkit-box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .11), 0 5px 15px 0 rgba(0, 0, 0, .08);
    box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .11), 0 5px 15px 0 rgba(0, 0, 0, .08)
}

.drinks .active-drink:after, .drinks .drink:hover:after {
    border-width: 2px;
    border-color: #7dacaf;
    border-radius: .25rem;
    content: "";
    position: absolute;
    display: block;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0
}

أصبح التطبيق النموذجي جاهزًا. نستطيع الآن المتابعة مع موضوعات هذا الدرس.

إضافة وسائل تنقيح متطوّرة لتطبيقات Vue.js

يوفّر مطوروا Vue.js أدوات تنقيح متطوّره مخصصة لتطبيقات Vue.js بحيث تسهّل حياة المبرمج إلى حدّ كبير. تظهر هذه الأدوات ضمن أدوات المطوّر التي يمكن الوصول إليها بضغط المفتاح F12. يمكنك زيارة هذه الصفحة للاطلاع على كيفية تثبيت هذه الأدوات. بعد أن تثبت الأدوات السابقة، يمكنك الوصول إليها بضغط المفتاح F12 كما أشرنا، ثم تبحث عن لسان التبويب Vue.js (قد تحتاج إلى نقر زر عرض المزيد في حال لم تجد لسان التبويب هذا). انظر الشكل التالي لترى كيف تبدو.

2.png

المكونات المتداخلة

نحتاج في بعض الأحيان إلى جعل المكونات متداخلة فيما بينها، كأن يكون هناك مكوّن أساسي (مكوّن أب) يستخدم مكوّنات أصغر (مكوّنات أبناء) ضمنه. سنوظف هذا المفهوم ضمن التطبيق النموذجي السابق.

سأضيف مكوّنًا جديدًا ضمن الملف app.js وسأسمّه drink-selector. سيلعب هذا المكوّن دور المكوّن الأساسي الذي يحوي المكوّن drink الذي سيكون ضمنه بشكل متداخل. انظر إلى محتويات الملف app.js بعد إضافة المكوّن الجديد إليه:

Vue.component('drink', {
    template: '#drink-template',
    props: {
        name: {
            type: String
        }
    }
})

Vue.component('drink-selector', {
    template: '#drink-selector-template',
    data() {
        return {
            drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"]
        }
    }
})

new Vue({
    el: '#app'
})

لاحظ معي كيف نقلت البيانات الخاصة بالتطبيق من كائن Vue.js إلى المكوّن drink-selector. لهذا الأمر فائدة سنراها لاحقًا إن شاء الله.

بالنسبة لشيفرة HTML فالأمر بسيط أيضًا. سننشئ القالب drink-selector-template المعبّر عن مكوّننا الجديد. وسننقل إليه الشيفرة الخاصة بتوليد قائمة المشاريب. انظر محتوى القالب:

<script type="text/x-template" id="drink-selector-template">
        <div class="drinks">
            <drink v-for="drink in drinks" v-bind:name="drink"></drink>
        </div>
    </script>

بدلًا من الشيفرة التي نقلناها توًا يمكنك استخدام العنصر <drink-selector></drink-selector>. انظر كيف ستصبح الشيفرة بعد التعديل الأخيرة على الملف index.html:

<html lang="ar">

<body>
    <div class="header">
        <span id="logo">مشاريب حسوب</span>
    </div>

    <div id="app" class="container">
        <div class="content">
            <h1 class="title">المشروبات المتوفرة</h1>
            <drink-selector></drink-selector>

        </div>
    </div>

    <script type="text/x-template" id="drink-selector-template">
        <div class="drinks">
            <drink v-for="drink in drinks" :name="drink"></drink>
        </div>
    </script>

    <script type="text/x-template" id="drink-template">
        <div class="drink">
            <div class="description">
            <span class="title">
                {{name}}
            </span>
            </div>
        </div>
    </script>

    <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
    <script src="app.js"></script>
    <link rel="stylesheet" href="app.css" />

</body>

</html>

إذًا استطعنا استخدام مكوّن ضمن مكوّن آخر بشكل متداخل. يمكنك الآن أن تنشأ عناصر drink-selector جديدة بنسخ ولصق السطر التالي بشكل متكرر:

<drink-selector></drink-selector>

تسجيل المكونات محليًّا وتسجيلها على المستوى العام

يمكن تسجيل المكوّنات ضمن تطبيق Vue.js بأسلوبين مختلفين. الأسلوب الأوّل هو الأسلوب العام، وهو الأسلوب الذي استخدمناه حتى هذه اللحظة باستخدام التابع Vue.component. والأسلوب الآخر هو الأسلوب المحلي والذي سنتعرّف عليه في هذه الفقرة.

في الحقيقة لا يُعتبر تسجيل المكوّنات على المستوى العام أمرًا جيّدًا، لأنّه عندما ستكبر تطبيقاتك وتبدأ باستخدام أساليب متقدمة في بناء التطبيقات (كما سيمر معنا في الدروس اللاحقة)، سيتم بناء المكوّنات التي سجلتها على المستوى العام وذلك في التطبيق النهائي، حتى ولو لم تستخدمها في ذلك التطبيق. وهذا يعني زيادة في حجم شيفرة JavaScript التي على المستخدمين تحميلها من الانترنت دون فائدة. هذا فضلًا عن مشاكل من الناحية التصميمية للتطبيق. ففي تطبيقنا الأخير مثلًا، لن نستخدم المكوّن drink خارج المكوّن drink-selector، وبالتالي لا حاجة لتسجيل المكوّن drink على المستوى العام.

لحل هذه المشكلة، وبالتالي تسجيل المكوّنات بشكل محلي إذا اقتضى الأمر ذلك، فيمكننا بكل بساطة تعريف المكوّن على شكل كائن JavaScript وإسناده إلى متغيّر عادي بدون استخدام التابع Vue.Component. ثم تسجيله في المكان الذي سنستخدمه فيه فقط. دعنا نطبّق هذه الطريقة على المكوّن drink الذي سيصبح تعريفه على النحو التالي:

let drink_component = {
    template: '#drink-template',
    props: {
        name: {
            type: String
        }
    }
}

بما أنّ هذا المكوّن سيُستخدم ضمن المكون drink-selector لذلك سننشئ قسمًا جديدًا ضمن المكون drink-selector اسمه components والذي يسمح بتسجيل أي مكوّنات داخلية سيستخدمها هذا المكون. انظر إلى تعريف المكون drink-selector بعد التعديل:

Vue.component('drink-selector', {
    template: '#drink-selector-template',
    components:{
        drink: drink_component
    },
    data() {
        return {
            drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"]
        }
    }
})

وبذلك نكون قد سجّلنا المكوّن drink محليّا ضمن المكوّن drink-selector ولا يمكن بعد ذلك استخدام المكون drink في أي مكان آخر غير المكون drink-selector.

كما يمكن بطبيعة الحال فعل الأمر ذاته مع المكوّن drink-selector أي تسجيله محليا بدون استخدم التابع Vue.component في حال أردنا استخدام هذا المكوّن فقط ضمن صفحة محدّدة ضمن تطبيق الويب. سنجر الآن تعديلًا مماثلًا على المكوّن drink-selector:

let drink_selector_component = {
    template: '#drink-selector-template',
    components:{
        drink: drink_component
    },
    data() {
        return {
            drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"]
        }
    }
}

الآن، أين تعتقد أنّه يجب تسجيل المكوّن drink-selector؟

الجواب بكل بساطة، وبما أنّه لا يوجد مكوّن رئيسي يمكن أن يُعرَّف المكوّن drink-selector ضمنه، فسنسجله ضمن كائن Vue.js الخاص بالتطبيق باستخدام القسم components أيضًا:

new Vue({
    el: '#app',
    components:{
        'drink-selector': drink_selector_component
    }
})

لاحظ معي أنّني قد عرفت اسم المكوّن هذه المرة على شكل نص: drink-selector بشكل مختلف عن تعريف المكوّن drink. السبب في ذلك أنّني أرغب بالاستمرار باستخدام الرمز - ضمن اسم المكون drink-selector وهذا جائز تمامًا بالطبع. إذا أردت الاطلاع على الشكل النهائي للملف app.js بعد التعديلات الأخيرة، انظر إلى الشيفرة التالية:

let drink_component = {
    template: '#drink-template',
    props: {
        name: {
            type: String
        }
    }
}

let drink_selector_component = {
    template: '#drink-selector-template',
    components:{
        drink: drink_component
    },
    data() {
        return {
            drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"]
        }
    }
}

new Vue({
    el: '#app',
    components:{
        'drink-selector': drink_selector_component
    }
})

تحديد المكون الذي اختاره المستخدم

لنمضي قدمًا في تطبيق المشاريب، حيث سنعمل الآن على إضافة ميزة تحديد المشروب الذي اختاره المستخدم عن طريق النقر عليه بالفأرة. تحتاج هذه الميزة إلى بعض المتطلّبات البسيطة، حيث سنُكسِب المكوّن drink حقلًا جديدًا اسمه is_selected سيُعرّف ضمن القسم data كما نعلم، لتحديد فيما إذا كان المستخدم قد اختار هذا المشروب أم لا، بالإضافة إلى تزويده بتابع جديد يسمح باختيار المشروب ولنسمّه select والذي سيحتوي على تعليمة برمجية واحدة تجعل قيمة الحقل is_selected مساوية للقيمة true، أي تعبّر عن عملية الاختيار. انظر كيف سيبدو شكل المكون drink بعد إضافة التعديلين السابقين عليه:

let drink_component = {
    template: '#drink-template',
    props: {
        name: {
            type: String
        }
    },
    data() {
        return {
            is_selected: false
        }
    },
    methods: {
        select() {
            this.is_selected = true;
        }
    }
}

لنجر الآن التعديلات اللازمة ضمن الملف index.html حيث سنضيف الموجّه v-on:click ضمن قالب المكوّن drink للاستجابة لحدث النقر. انظر التعديلات التالية على القالب drink-template:

<script type="text/x-template" id="drink-template">
        <div v-on:click="select" class="drink">
            <div class="description">
                <span class="title">
                    {{name}}
                </span>
            </div>
        </div>
    </script>

عد إلى المتصفّح لمعاينة التغييرات (تذكّر أنّنا نستعرض الملف index.html ضمن الخادوم Live Server). جرب أن تنقر على المشروبين: "شاي" و "شاي أخضر"، ثم انتقل إلى نافذة أدوات المطوّر بضغط F12، ومن هناك انتقل إلى لسان التبويب Vue. انشر العقد في حال احتجت ذلك، ستحصل على شكل شبيه بما يلي بالنسبة للمشروب الأول "شاي":

3.png

جرب أن تعاين حالة المشروب التالي "قهوة" ستجد أن قيمة الحقل is_selected له تساوي false لأنّنا لم ننقر على القهوة. جرب الآن أن تعاين حالة المشروب التالي "شاي أخضر"، ستلاحظ أنّ قيمة is_selected في هذه الحالة هي true. أي أنّ التطبيق حاليًا يعمل كما هو متوقّع منه حتى الآن. بقي أن نضيف بعض التأثيرات البصرية البسيطة للإشارة إلى أنّ المشروب قد اختير من قبل المستخدم. سأستخدم الموجّه v-bind:class لإضافة تنسيق CSS وذلك ضمن القالب drink-template على النحو التالي:

<script type="text/x-template" id="drink-template">
        <div v-on:click="select" class="drink" v-bind:class="{'active-drink': is_selected}">
            <div class="description">
                <span class="title">
                    {{name}}
                </span>
            </div>
        </div>
    </script>

سيُطبّق تنسيق CSS المسمى 'active-drink' فقط عندما تكون قيمة الحقل is_selected تساوي true أي عندما يختار المستخدم المشروب الحالي. ولاحاجة بطبيعة الحال لاستخدام علامة الاقتباس المفردة في حال لم يحتوي اسم الصنف على رمز مثل -. بقي أن نضيف تنسيق CSS نفسه إلى ملف التنسيقات app.css. أضف الصنفين التاليين إلى ذلك الملف:

.drinks .active-drink, .drinks .drink:hover {
    background-color: lightgray;
    -webkit-box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .11), 0 5px 15px 0 rgba(0, 0, 0, .08);
    box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .11), 0 5px 15px 0 rgba(0, 0, 0, .08)
}

.drinks .active-drink:after, .drinks .drink:hover:after {
    border-width: 2px;
    border-color: #7dacaf;
    border-radius: .25rem;
    content: "";
    position: absolute;
    display: block;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0
}

عد إلى المتصفح واختر المشروب الأول والثالث مرة أخرى لتحصل على شكل شبيه بما يلي:

4.png

لقد تمّت المهمة بنجاح! ولكن، ماذا لو أردنا اختيار مشروب واحد فقط من القائمة؟ سنحل هذه المشكلة في الفقرة التالية حيث سنتعلّم كيفية التخاطب بين المكوّنات المختلفة باستخدام أحداث مخصّصة.

التخاطب بين المكونات باستخدام أحداث مخصصة

برزت الحاجة في الفقرة السابقة إلى اختيار مشروب واحد فقط من قائمة المشروبات المتاحة. أي أنّنا بحاجة إلى آلية معيّنة تسمح باختيار مشروب واحد فقط، بحيث تعمل هذه الآلية على إلغاء أي اختيار سابق لمشروب ما (في حال وجوده) ضمن القائمة، والإبقاء على المشروب الذي اختاره المستخدم أخيرًا.

يمكن تحقيق هذا الأمر بالتخاطب بين المكوّن الابن drink والمكوّن الأب drink-selector حيث سنستخدم المكوّن الأب للمساعدة في هذا الأمر.

يمكن التخاطب بين المكونات باستخدام الأحداث المخصّصة التي سنتحدث عنها بعد قليل، حيث سنبلغ المكوّن الأب drink-selector بأن مشروبًا ما قد اختاره المستخدم، فيعمل عندئذ المكون الأب على إلغاء اختيار أي مشروب سابق يمكن أن يكون قد اختير من قبل، باستثناء المشروب الحالي.

قبل المتابعة، توقف عن القراءة قليلًا، حضر كوبًا من الشاي (أو القهوة)، ثم عد إلى هنا من جديد، لأنّ الشرح التالي يحتاج إلى المزيد من التركيز.

سنبدأ من الملف app.js حيث سنجري بعض التعديلات على المكونين drink و drink-selector. بالنسبة للمكون drink سأجري التعديلات التالية:

استخدمنا في المثال في الفقرة السابقة الحقل is_selected للإشارة إلى كون المشروب الحالي قد اختير أم لا. سأحول هذا الحقل إلى خاصية محسوبة (Computed Property) بالإضافة إلى أنّني سأحذف قسم data بشكل كامل، وسنرى سبب ذلك بالإضافة بعد قليل. كما سأستخدم لأوّل مرة التابع المضمّن ‎ $emit ضمن التابع select (بدلًا من التعليمة this.is_selected = true) لكي نُصدر الحدث drink_selected_event وذلك لكي نبلّغ المكون الأب بأن المشروب الحالي قد اختير، وذلك بأن أمرّر اسم هذا المشروب كوسيط ثانٍ للتابع $emit. سأضيف أيضًا خاصية جديدة اسمها selectedDrink إلى المكون drink ضمن القسم props وذلك لكي أسمح للمكون الأب فيما بعد، بتمرير اسم المشروب (مكون drink) الذي اختاره المستخدم حاليًا إلى هذا المكون.

تأمّل الشيفرة البرمجية الجديدة الخاصة بالمكون drink:

let drink_component = {
    template: '#drink-template',
    props: {
        name: {
            type: String
        },
        selectedDrink:{
            type: String
        }
    },
    computed:{
        is_selected(){
            return this.selectedDrink === this.name;
        }
    },
    methods: {
        select() {
            this.$emit('drink_selected_event', this.name)
        }
    }
}

أمّا بالنسبة للمكوّن drink-selector فسأجري عليه أيضًا التعديلات التالية:

  • وضعت ضمنه القسم methods لكي أستطيع وضع التابع drink_selected_handler والذي سيكون معالجًا للحدث drink_selected_event الذي سأصدره (باستخدام التابع المضمّن ‎ $emit) ضمن التابع select في المكوّن drink كما رأينا قبل قليل.
  • أضفت حقلًا جديدًا أسميته current_drink ضمن القسم data وهو الذي سيحتفظ باسم المكوّن (المشروب) الذي اختاره المستخدم توًّا.

إليك الشيفرة البرمجية الجديدة الخاصة بالمكون drink-selector:

let drink_selector_component = {
    template: '#drink-selector-template',
    components: {
        drink: drink_component
    },
    data() {
        return {
            drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"],
            current_drink: null
        }
    },
    methods:{
        drink_selected_handler(drink_name){
            this.current_drink = drink_name;
        }
    }
}

إليك الآن التعديلات التي حدثت ضمن قالب المكوّن drink-selector:

<script type="text/x-template" id="drink-selector-template">
        <div class="drinks">
            <drink v-for="drink in drinks" v-bind:name="drink" 
                v-on:drink_selected_event="drink_selected_handler" 
                v-bind:selectedDrink="current_drink"></drink>
        </div>
    </script>

أريد هنا التركيز على أمرين هما في صلب الموضوع:

  • الأمر الأوّل هو استخدامي للموجّه v-on:drink_selected_event. هذا الموجّه ليس مبيّتًا بالأصل. إنّما هو موجّه جديد مُحدَث بسبب استخدامي للتابع ‎$emit وتمريري للقيمة 'drink_selected_event' له. إذًا يمكن إنشاء موجّهات مخصّصة باستخدام التابع المبيّت ‎ $emit.
  • الأمر الثاني الذي أريد التركيز عليه هو الموجّه v-bind:selectedDrink="current_drink"‎ الذي يربط قيمة الحقل current_drink من الموجّه الأب، مع الخاصية selectedDrink من الموجّه الابن.

فالذي يحدث بكل بساطة، هو أنّه عندما يختار المستخدم مشروبًا ما من القائمة، فسيُستدعى في البداية التابع select من المكون الابن. ضمن هذا التابع لتنفّذ التعليمة البرمجية:

this.$emit('drink_selected_event', this.name)

التي تعمل على استدعاء حدث مخصص للتخاطب مع المكوّن الأب، حيث يقول المكوّن الابن للمكوّن الأب: "انظر لقد تمّ اختياري!"، علمًا أنّ اسم المكوّن الابن (اسم المشروب) سيمرّّر ضمن الوسيط الثاني من التابع ‎ $emitكما أوضحنا قبل قليل.

بسبب وجود الموجّه v-on:drink_selected_event="drink_selected_handler" ضمن قالب المكوّن الأب، سيُلتَقط هذا الحدث، وسيُستدعى التابع drink_selected_handler من المكوّن الأب، حيث تعمل تعليمة بسيطة ضمنه على إسناد اسم المشروب (المكون الابن) الذي اختاره المستخدم ضمن الحقل current_drink وبهذه الطريقة يعرف المكوّن الأب من هو المشروب الحالي الذي اختاره المستخدم.

ستؤدي هذه المعرفة، وبسبب طبيعة Vue.js إلى تحديث قالب المكوّن بكامله، وبالتالي ستنفّذ حلقة v-for مرة أخرى، ولكن في هذه الحالة سيمرّر اسم المكوّن (المشروب) الذي اختاره المستخدم إلى جميع المشروبات الموجودة ضمن المكوّن الأب عن طريق الخاصية selectedDrink وبالتالي سيعرف كل مكوّن ابن من المكوّن الابن "المحظوظ" الذي اختاره المستخدم حاليًا. وبسبب وجود الخاصية المحسوبة is_selected في المكون الابن، ستنفّذ التعليمة البرمجية الموجودة ضمنها:

return this.selectedDrink === this.name;

هذه التعليمة بسيطة للغاية، وهي تُرجع قيمة من النوع المنطقي ture إذا كان اسم المكوّن الحالي يطابق اسم المكوّن "المحظوظ" الذي اختاره المستخدم. أو تُرجع القيمة false إذا لم تتحقق هذه المطابقة.

زبدة القول، فإنّ القيمة التي سترجعها الخاصية المحسوبة is_selected هي التي ستُسند إلى الموجّه v-bind:class="{'active-drink': is_selected}‎"‎ وبالتالي يظهر هذا المشروب كما لو أنّه تم اختياره بتطبيق تنسيق CSS المناسب أو يظهر بشكل عادي.

ملاحظة نستنتج مما سبق، أنّه يمكن للمكوّن الأب أن يرسل رسائل إلى المكوّن الابن الموجود ضمنه، عن طريق الخصائص التي نعرفها ضمن القسم props في المكون الابن، التي تُعتبر مستقبلات للمكون الابن تسمح له بالتقاط الرسائل من خارجه بشكل عام. أمّا عندما يريد المكون الابن مخاطبة المكون الأب، فيمكن ذلك عن طريق أحداث مخصّصة باستخدام التابع ‎ $emit كما أوضحنا في المثال الأخير.

جرب التطبيق بعد التعديلات الأخيرة. لاحظ كيف أصبح يسمح باختيار مشروب واحد فقط.

ختامًا

لقد تناولنا في هذا الدرس الكثير من الموضوعات المهمة حول المكوّنات، وأعتقد أنّه أصبح من الضروري الانتقال إلى مستوى أعلى، عن طريق بناء تطبيقات بأساليب احترافية توفرها Vue.js لنا. سنعمل في الدرس القادم إن شاء الله على الاطلاع على كيفية فصل المكوّنات ضمن ملفات مستقلة تحتوي الواحدة منها على جميع التفاصيل المتعلّقة بالمكوّن مما يساهم بتنظيم الشيفرة البرمجية إلى حدّ كبير. بالإضافة إلى الاطلاع على تقنية بناء تطبيقات Vue.js بشكل مختلف عن طريق Vue.js CLI. حتى الدرس القادم أرجو أن تكونوا بأفضل حال!

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...