-
المساهمات
215 -
تاريخ الانضمام
-
تاريخ آخر زيارة
-
عدد الأيام التي تصدر بها
31
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو حسام برهان
-
العفو، بالتوفيق
-
سنتعرف في هذا الدرس على طريقة التعامل مع عناصر الإدخال في بوتستراب، إذ توفّر بوتستراب دعمًا قويًا لعناصر الإدخال من خلال إضفاء لمسة جمالية واضحة عليها. سنتعرّف على العناصر الأساسية التي تُستخدم بشكل متكرّر في جميع تطبيقات الويب ثم سنعمل، كما جرت العادة، على تطبيق ما تعلّمناه على موقعنا "نبيه" الذي بنيناه من المقالات السابقة. ستحتاج إلى مراجعة المقالات السابقة في هذه السلسلة كي تتعرّف أكثر على هذا الموقع. سنتعلّم في هذا الدرس: تنسيق أشهر عناصر الإدخال في بوتستراب. إنشاء صفحة الدفع ضمن موقع نبيه. هذا الفصل جزء من سلسلة فصول عن بوتستراب 5، وإليك كامل فهرس السلسلة: مدخل إلى إطار العمل بوتستراب 5 شريط التنقل في بوتستراب 5 مخطط الصفحة في بوتستراب 5 تطبيق مخطط الصفحة في بوتستراب على صفحات الويب مكون البطاقة Card ومكون الشرائح الدوارة Carousel في بوتستراب مكون الرسائل المنبثقة Modal في بوتستراب عناصر الإدخال: إنشاء استمارة دفع في بوتستراب تنسيق أشهر عناصر الإدخال في بوتستراب تُستخدم عناصر الإدخال عادةً لاستقبال المدخلات من المستخدم، وكما نعلم توفّر HTML عناصر جيدة بهذا الصدد. تضفي بوتستراب ناحية جمالية وتناسق واضح على هذه العناصر. فيما يلي أشهر هذه العناصر التي لا غنى عنها في أي تطبيق ويب يستقبل الدخل من المستخدم. عنصر الإدخال النصي وهو من أشهر العناصر وأكثرها استخدمًا، ويمكن تنسيق هذا العنصر عن طريق استخدام الصنف التنسيقي form-control. السبب في أنّ هذا الصنف يبدأ بالكلمة form هو أنّه عادةً ما يوضع (وغيره من عناصر الإدخال) ضمن عنصر النموذج <form> رغم أنّه ليس من الضروري ذلك. الشكل العام هو: <input type="text" class="form-control" > ومن الممكن أيضًا بدلًا من استخدام الصنف form-control، استخدام الصنفين التنسيقيين form-control-lg و form-control-sm اللذين يجعلان عنصر الإدخال النصي كبيرًا أو صغيرًا على الترتيب. عنصر العنوان هو العنصر <label> وعادةً ما يُستخدم مع العديد من عناصر الإدخال لتوضيح الغاية منها. الصنف التنسيقي المستخدم هنا هو form-label والشكل العام له: <label for="DEST_ID" class="form-label">العنوان المطلوب</label> <input id="DEST_ID" type="text" class="form-control" > حيث DEST_ID هو معرّف عنصر الإدخال الذي سيكون عنصر العنوان مخصّصًا له كما موضع. ومرّة أخرى يمكن استخدام الصنفين التنسيقيين form-control-lg و form-control-sm كما في عنصر الإدخال النصي. عنصر الاختيار من الممكن أيضًا تنسيق عنصر الاختيار checkbox على الشكل التالي: <input class="form-check-input" type="checkbox" id="normal_unchecked"> لاحظ الصنف التنسيقي form-check-input. سيظهر هذا العنصر بحالته العادية دون وجود علامة الاختيار ضمنه. أمّا إذا أردت ظهوره بحالة الاختيار فأضف السمة checked فقط إلى هذا العنصر: <input class="form-check-input" type="checkbox" id="normal_checked" checked> في كلتا الحالتين السابقتين سيظهر عنصر الاختيار على شكل مربّع صغير بحالة عدم اختيار أو بحالة اختيار على الترتيب، ولكن من الضروري بالطبع إضافة عنصر عنوان توضيحي يوضّح الغاية من عنصر الاختيار، لذلك يمكن وضع عنصر عنوان بجوار عنصر الاختيار له الصنف التنسيقي form-check-label على النحو التالي: <input class="form-check-input" type="checkbox" id="normal_unchecked"> <label class="form-check-label" for="normal_unchecked">عنصر اختيار عادي</label> عادةً ما يوضّع العنصرين السابقين ضمن عنصر div له التنسيق form-check، وبهذا يصبح مثالنا البسيط على الشكل التالي: <div class="form-check"> <input class="form-check-input" type="checkbox" id="normal_unchecked"> <label class="form-check-label" for="normal_unchecked">عنصر اختيار عادي</label> </div> سيولّد ذلك شكلًا شبيها بما يلي: عنصر الانتقاء يمكن بشكل مشابه لعنصر الاختيار وضع عنصر الانتقاء radio مع عنصر العنوان الخاص به ضمن عنصر div له التنسيق form-check على الشكل التالي: <div class="form-check"> <input class="form-check-input" type="radio" name="radioGroup" id="normal_radio"> <label class="form-check-label" for="normal_radio"> عادي </label> </div> <div class="form-check"> <input class="form-check-input" type="radio" name="radioGroup" id="selected_radio" checked> <label class="form-check-label" for="selected_radio"> في وضع انتقاء </label> </div> وضعت في الشيفرة السابقة عنصري انتقاء مع ضبط السمة name لهما على النفس القيمة radioGroup لكي نستطيع انتقاء واحد منهما فقط في كل مرّة. يمكن بالطبع إضافة أي عدد من عناصر الانتقاء بحيث يكون لكل منها نفس قيمة السمة name لكي تُعامل على شكل مجموعة واحدة. ستولّد الشيفرة السابقة شكلًا شبيهًا بما يلي: إنشاء صفحة الدفع ضمن موقع "نبيه" حان الآن موعد تجهيز صفحة الدفع checkout.html الخاصة بموقع نبيه. سنجري بدايةً تعديلين صغيرين على الملفين: shopping-cart.html و styles.css. بالنسبة للملف styles.css سنضيف تنسيق بسيط لمحاذاة بيانات الدفع بشكل ملائم. أضف التنسيق التالي إلى نهاية هذا الملف: /*Checkout*/ .payment-details{ padding: 3% 5%; } بالنسبة للملف shopping-cart.html فسنضيف زر "الشراء الآن" إلى أسفل الصفحة الذي سيحولنا إلى صفحة الدفع التي سنبنيها تاليًا، لذا أضف الشيفرة التالية إلى الملف shopping-cart.html قبل القسم section id="footer" مباشرةً: <div class="row"> <div class="col-4 shopping-cart-content"><a href="checkout.html" class="btn btn-primary">الشراء الآن</a></div> </div> جاء الآن دور الملف checkout.html والتي ستكون محتوياته على النحو التالي: <!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700;800&family=Tajawal:wght@200;300;400;500;700;800;900&display=swap" rel="stylesheet"> <!-- Boostrap 5 icons - web font --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.0/font/bootstrap-icons.css"> <title>دورات نبيه | صفحة الدفع</title> </head> <body> <section id="header"> <!-- Nav Bar --> <nav class="navbar navbar-expand-lg navbar-light"> <div class="container-fluid"> <a class="navbar-brand" href="#"> <img src="images/nabih-logo.png" style="margin-left: 8px;" alt="" height="32"> نبيه </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-2 mb-2 mb-lg-0 ms-3"> <li class="nav-item"><a class="nav-link" href="#">التصنيفات</a></li> </ul> <form class="d-flex me-auto w-100"> <input class="form-control" type="search" placeholder="ابحث عن أي موضوع"> <div class="nav-link" style="position: relative;"> <div class="counter zero"></div> <img src="images/icons8-shopping-cart-32.png" /> </div> <button type="button" class="btn btn-dark nabih-buttons">دخول</button> <button type="button" class="btn btn-outline-dark nabih-buttons">تسجيل جديد</button> </form> </div> </div> </nav> </section> <div class="payment-details"> <div class="col-md-7 col-lg-8"> <h4 class="mb-3">بيانات الدفع</h4> <form> <div class="row g-3"> <!-- الموضع 1 --> <div class="col-sm-6"> <label for="firstName" class="form-label">الاسم</label> <input type="text" class="form-control" id="firstName"> </div> <div class="col-sm-6"> <label for="lastName" class="form-label">الكنية</label> <input type="text" class="form-control" id="lastName"> </div> <div class="col-12"> <label for="username" class="form-label">اسم المستخدم</label> <div class="input-group"> <!-- الموضع 2 --> <span class="input-group-text">#</span> <input type="text" class="form-control" id="username" placeholder="اسم المستخدم"> </div> </div> <div class="col-12"> <label for="email" class="form-label">البريد الإلكتروني <span class="text-muted">(اختياري)</span></label> <input type="email" class="form-control" id="email" placeholder="you@example.com"> </div> </div> <hr class="my-4"> <!-- الموضع 3 --> <h4 class="mb-3">الدفع</h4> <div class="my-3"> <div class="form-check"> <input id="credit" name="paymentMethod" type="radio" class="form-check-input"> <label class="form-check-label" for="credit">بطاقة إئتمان</label> </div> <div class="form-check"> <input id="debit" name="paymentMethod" type="radio" class="form-check-input"> <label class="form-check-label" for="debit">بطاقة عادية</label> </div> <div class="form-check"> <input id="paypal" name="paymentMethod" type="radio" class="form-check-input"> <label class="form-check-label" for="paypal">باي بال</label> </div> </div> <div class="row"> <div class="col-md-6"> <label for="cc-name" class="form-label">الاسم على البطاقة</label> <input type="text" class="form-control" id="cc-name" placeholder=""> <small class="text-muted">الاسم الكامل كما يظهر على البطاقة</small> </div> <div class="col-md-6"> <label for="cc-number" class="form-label">رقم البطاقة</label> <input type="text" class="form-control" id="cc-number" placeholder=""> </div> <div class="col-md-3"> <label for="cc-expiration" class="form-label">تاريخ الانتهاء</label> <input type="text" class="form-control" id="cc-expiration" placeholder=""> </div> <div class="col-md-3"> <label for="cc-cvv" class="form-label">CVV</label> <input type="text" class="form-control" id="cc-cvv" placeholder=""> </div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="save-card"> <label class="form-check-label" for="save-card">احفظ بيانات البطاقة</label> </div> </div> <hr class="my-4"> <button class="w-100 btn btn-primary btn-lg" type="submit">الدفع</button> <!-- الموضع 4 --> </form> </div> </div> <section id="footer"> <footer> <div class="container-fluid"> <i class="bi bi-facebook social-icon"></i> <i class="bi bi-twitter social-icon"></i> <i class="bi bi-instagram social-icon"></i> <i class="bi bi-envelope social-icon"></i> <p>© دورات نبيه 2021</p> </div> </footer> </section> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> ستحصل بعد المعاينة على شكل شبيه بما يلي: يبدو أنّ الشيفرة السابقة يجب أن تكون واضحة الآن، ولكن رغم ذلك لنستعرض بعض النقاط الواردة فيها. بالنظر إلى "الموضع 1" ستلاحظ أنّني استخدمت الصنف g-3 مع عنصر div، فتشير العائلة g-* إلى Gutters وهي عبارة عن فراغات هامشية معرّفة مسبقًا يمكن وضعها بين الأعمدة (أفقي) أو حتى بين الأسطر (عمودي). وظيفة هذه العائلة وغيرها من العائلات التي سنراها تباعًا هي توفير اختصار سريع لضبط الفراغات بين العناصر المستهدفة دون الحاجة إلى تعريف تنسيقات ضمن ملف تنسيقي منفصل مثلًا. بالنسبة للفراغات الأفقية نستخدم العائلة الفرعية gx-*، أمّا بالنسبة للفراغات العمودية فنستخدم العائلة الفرعية gy-*. أمّا إذا أردنا ضبط الفراغات الأفقية والعمودية معًا فنستخدم العائلة g-*. توجد ست قيم معرّفة مسبقًا وهي من g-0 إلى g-5 بحيث أنّ كل رقم منها (أي من 0 حتى 5) يُضرب بثابت معرّف مسبقًا في بوتستراب وهو $spacer وقيمته هي 1rem. ففي مثالنا هذا يُشير الصنف g-3 أنّنا نريد ضبط حشوات أفقية وعمودية بمقدار 3rem. بالنسبة "للموضع 2"، لاحظ الصنف input-group مع العنصر div. لهذا الصنف فائدة تنسيقية جميلة تتمثّل في إمكانية رصف نص عادي (يمكن استخدام العنصر <span>) يمثّل نصًا تعريفيًا، مع عنصر إدخال نصي عادي بحيث يبدوان وكأنّهما عنصر واحد. يمكن استخدام هذه الميزة مثلًا لكي تشير للمستخدم أن يُدخل معرّف تويتر ما، وذلك بوضع الرمز "@" قبل عنصر الإدخال النصي. في مثالنا هنا، وضعت الرمز "#" للإشارة إلى أنّنا نريد اسم المستخدم الخاص بالموقع. لكي يتم التراصف بشكل صحيح، ستحتاج أيضًا إلى استخدام التنسيق input-group-text مع عنصر <span> (انظر إلى السطر الذي يليه في الشيفرة) الذي يمثّل الحاضن للنص التعريفي، يليه عنصر الإدخال النصي الذي يحمل التنسيق form-control كما هو متوقع. لننتقل الآن إلى "الموضع 3". لاحظ كيف استخدمنا الصنف my-4. هذا الصنف يعود إلى العائلة الرئيسية m-* وهي مسؤولة عن ضبط الهوامش margins والتي يتبع لها عدة عائلات فرعية وهي: العائلة mt-* وهي لضبط الهامش العلوي margin-top. العائلة mb-* وهي لضبط الهامش السفلي margin-bottom. العائلة ms-* وهي لضبط هامش البداية (وهو margin-left في اللغات التي تتجه من اليسار LTR أو margin-right في اللغات التي تتجه من اليمين RTL). العائلة me-* وهي لضبط هامش النهاية (وهو margin-left في اللغات RTL وهو margin-right في اللغات LTR). العائلة mx-* وهي لضبط الهامشين الأيمن والأيسر بنفس الوقت (margin-right و margin-left). العائلة my-* وهي لضبط الهامشين العلوي والسفلي بنفس الوقت (margin-top و margin-bottom). وكما في العائلة g-* توجد ستة قياسات يمكن استخدامها مع العائلة m-* وهي من 0 حتى 5. استخدمنا في "الموضع 2" القياس رقم 4 لضبط الهامشين العلوي والسفلي، أي: my-4. وأخيرًا بالنظر إلى "الموضع 4"، لاحظ أنني قد استخدمت الصنف w-100، يعني ذلك أني أريد أن يشغل هذا العنصر عرضًا مقداره 100% ضمن أقرب حاوية هو موجود ضمنها، كما توجد قيم أخرى أيضًا مثل w-25 و w-50 و w-75. يمكنك تنزيل الشيفرة الكاملة لهذا الدرس من bootstrap5-tutorial.zip، كما ويمكنك الإطلاع على نسخة حيّة من هذه الصفحة من هنا. كما أنصحك بالإطلاع على صفحة التوثيق الرسمية في أكاديمية حسوب. خاتمة تعرّفنا في هذا الدرس على كيفية تنسيق بعض عناصر الإدخال الشائعة باستخدام بوتستراب، وعملنا على تطبيق ما تعلّمناه عن طريق تجهيز صفحة الدفع الخاصة بموقع "نبيه". نكون بذلك قد وصلنا إلى نهاية هذه السلسلة التي أرجو أن تكون قد حقّقت الفائدة المرجوة في التعرّف العملي على هذه المكتبة المهمة "بوتستراب" مع الإصدار الأخير لها 5.0 حتى تاريخ كتابة هذا المقال. اقرأ أيضًا بناء قائمة شجرية باستخدام البوتستراب 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap
-
سنتعامل في هذا الدرس مع مكوّن جديد ومفيد من بوتستراب، ألا وهو مكوّن الرسائل المنبثقة Modal. يُستخدم هذا المكوّن في إضفاء ميزة جمالية عندما نريد التواصل مع المستخدم، كأن نُظهر به رسالة تفيد بالانتهاء من تنفيذ إجراء معيّن، أو الإبلاغ عن حدوث خطأ ما، أو حتى عندما نريد أن نُخبر المستخدم بمعلومة معيّنة بحيث يمكنه أن يتخذ قرارًا ما بناء على ذلك. وكما جرت العادة، سنطبّق ما سنتعلّمه في هذا الدرس على موقع" نبيه" الذي بنيناه في مقالات سابقة من هذه السلسلة. سنعمل في هذا الدرس على: التعرّف على مكوّن الرسائل المنبثقة Modal في بوتستراب. إضافة المنتجات إلى سلة المشتريات في موقع نبيه. إنشاء صفحة جديدة في موقع نبيه لعرض محتويات سلة المشتريات. هذا الفصل جزء من سلسلة فصول عن بوتستراب 5، وإليك كامل فهرس السلسلة: مدخل إلى إطار العمل بوتستراب 5 شريط التنقل في بوتستراب 5 مخطط الصفحة في بوتستراب 5 تطبيق مخطط الصفحة في بوتستراب على صفحات الويب مكون البطاقة Card ومكون الشرائح الدوارة Carousel في بوتستراب مكون الرسائل المنبثقة Modal في بوتستراب عناصر الإدخال: إنشاء استمارة دفع في بوتستراب التعرف على مكون الرسائل المنبثقة Modal في بوتستراب يمكن إنشاء أشكال متنوّعة وغنيّة من مكوّن الرسائل المنبثقة. سنستعرض فيما يلي الشكل المبسّط الأساسي التالي: <div class="modal" tabindex="-1" id="exampleModal"> <!-- الموضع 1 --> <div class="modal-dialog"> <!-- الموضع 2 --> <div class="modal-content"> <!-- الموضع 3 --> <div class="modal-header"><!-- الموضع 4 --> <h5 class="modal-title">العنوان هنا</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <!-- الموضع 5 --> <p>محتويات النافذة المنبثقة هنا</p> </div> <div class="modal-footer"> <!-- الموضع 6 --> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button> <button type="button" class="btn btn-primary">حفظ التغييرات</button> </div> </div> </div> </div> من الشيفرة السابقة يبدأ تعريف النافذة المنبثقة في "الموضع 1" من خلال الصنف modal لعنصر div عادي. أما في "الموضع 2" نستخدم الصنف modal-dialog الضروري أيضًا لكي تعمل النافذة المنبثقة بشكل صحيح. تُوضع محتويات النافذة المنبثقة بشكل كامل ضمن عنصر div يحمل الصنف modal-content ويمثِّل "الموضع 3" الحاضن لمحتويات النافذة المنبثقة. يمكن أن يحتوي العنصر div.modal-content بدوره على ثلاثة أقسام: ترويسة النافذة المنبثق"الموضع 4" وهي عنصر div له الصنف modal-header. جسم النافذة المنبثقة "الموضع 5" وهي عنصر div له الصنف modal-body. تذييل النافذة المنبثقة "الموضع 6" وهي عنصر div له الصنف modal-footer. بالنسبة للترويسة، فيمكن أن نضع فيها عنوان النافذة المنبثقة (عن طريق عنصر div له التنسيق modal-title) أو بمعنى آخر عنوان الرسالة التي نريد عرضها للمستخدم، بالإضافة إلى إمكانية وضع زر صغير (له التنسيق btn-close) لنسمح للمستخدم بإغلاق هذه النافذة. أمّا جسم النافذة، فمكن أن نضع فيه أي محتوى ملائم نريد عرضه للمستخدم. وبالنسبة للتذييل، فيوضع فيه عادةً أزرار التحكّم التي يمكن من خلالها الاستجابة للرسالة أو المعلومة التي يعرضها التطبيق. لتجربة الشيفرة السابقة، ستحتاج مثلًا إلى زر منفصل لكي يعمل على تفعيل ظهور النافذة المنبثقة المعرّفة ضمن هذه الشيفرة. أقترح أن تستخدم الزر البسيط التالي: <!-- Button trigger modal --> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal"> أظهر النافذة </button> يمكن وضع هذا الزر قبل أو بعد الشيفرة السابقة ضمن القسم body من الصفحة. لاحظ أنّ السمة data-bs-target من الزر السابق لها القيمة #exampleModal وهي نفسها معرّف عنصر div الرئيسي "الموضع 1" الذي يحضن النافذة المنبثقة. يجب عليك الانتباه أيضًا أنّه لن يمكنك تجربة الشيفرة السابقة بشكل ناجح دون استيراد مكتبات بوتستراب بشكل صحيح، كما تعلّمنا ذلك مسبقًا. بعد أن تعمل الشيفرة السابقة بشكل صحيح، ستحصل على شكل شبيه بما يلي: ستلاحظ عند الضغط على الزر "إغلاق" اختفاء هذه النافذة وسبب ذلك هو وجود السمة data-bs-dismiss مع القيمة modal لها، ويمكنك أيضًا الخروج من هذه النافذة بالنقر في أي مكان خارجها، وهو السلوك الافتراضي. يمكنك منع هذا السلوك، أي أنّك تجعل إخفاء هذه النافذة يكون حصرًا بالنقر على الزر "إغلاق" بأن تضع السمة data-bs-backdrop مع القيمة static لها وذلك ضمن عنصر div الرئيسي "الموضع 1". يمكنك أيضًا الحصول على تأثير لطيف عند إظهار النافذة وإخفائها وذلك بأني تضيف التنسيق fade إلى الصنف modal ضمن عنصر div الرئيسي "الموضع 1". سيصبح عنصر div الرئيسي بعد التعديلين المقترحين على الشكل التالي: <div class="modal fade" tabindex="-1" id="exampleModal" data-bs-backdrop="static"> في الحقيقة، توجد العديد من الإضافات والتنسيقات المختلفة التي يمكنك الإطلاع عليها من خلال صفحة التوثيق الرسمية لهذا المكوّن. إضافة المنتجات إلى سلة المشتريات في موقع نبيه سنعمل في هذه الفقرة على محاكاة عملية إضافة المنتجات إلى سلّة المشتريات الخاصة بموقع نبيه (يمكنك تنزيل شيفرة الدرس الخامس السابق من الملف المرفق في الأسفل). في الحقيقة هناك العديد من الملاحظات حول هذا الموضوع، من أهمّها، أّنه من المنطقي إنشاء حسابات للمستخدمين كي يستطيع المستخدم إضافة أي منتج يرغبه إلى سلّة المشتريات الخاصة به، وأيضًا أنّنا في هذا المشروع سنعمل على حفظ المنتجات المضافة للسلة إلى التخزين المحلّي الخاص بالمتصفّخ وليس إلى قاعدة بيانات موجودة على الإنترنت. سنتغاضى في الحقيقة عن هذين المتطلّبين المهمّين والبديهيين في التطبيقات العمليّة، وذلك بسبب أنّ الهدف من هذا المشروع هو شرح مفاهيم بوتستراب 5 بصورة مبسّطة وميسرة. سنعمل على إجراء بعض التعديلات على ملف الصفحة الرئيسية index.html القديم، وذلك للسماح بإضافة المنتجات إلى سلة المشتريات، أهم هذه التعديلات هي: إضافة الشيفرة اللازمة للإظهار نافذة منبثقة عندما يريد المستخدم أن يضيف منتج ما إلى سلّة المشتريات. إضافة قسم لشيفرة جافاسكريبت تتمثّل مهمتها في إدارة عملية إضافة المنتجات التي يرغبها المستخدم إلى التخزين الداخلي ضمن المتصفّح وأيضًا تحديث أيقونة سلّة المشتريات الموجودة في أعلى الصفحة بالعدد الحالي للمنتجات التي اختارها المستخدم. في الحقيقة ستعمل شيفرة جافاسكربت السابقة على تضمين وحدة برمجية module اسمها main الموجودة ضمن الملف main.js، حيث تحتوي هذه الوحدة على بعض التوابع المفيدة التي تدير العمليات البرمجية الكاملة التي نحتاجها في هذا الدرس (سنتحدّث عن هذه الوحدة بعد قليل). ستصبح الشيفرة الخاصة بالملف index.html على الشكل التالي (الملف مرفق في نهاية المقال): <!doctype html> <html lang="ar" dir="rtl"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700;800&family=Tajawal:wght@200;300;400;500;700;800;900&display=swap" rel="stylesheet"> <!-- Boostrap 5 icons - web font --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.0/font/bootstrap-icons.css"> <title>دورات نبيه</title> </head> <body> <!-- نافذة منبثقة --> <div class="modal fade" id="addToCartConfirmation" tabindex="-1" aria-labelledby="addToCartConfirmation" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="modalLabel">نبيه</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> تمت الإضافة بنجاح </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button> <button type="button" class="btn btn-primary" onclick="window.location='shopping-cart.html'">الذهاب إلى السلة</button> </div> </div> </div> </div> <section id="header"> <!-- Nav Bar --> <nav class="navbar navbar-expand-lg navbar-light"> <div class="container-fluid"> <a class="navbar-brand" href="#"> <img src="images/nabih-logo.png" style="margin-left: 8px;" alt="" height="32"> نبيه </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-2 mb-2 mb-lg-0 ms-3"> <li class="nav-item"><a class="nav-link" href="#">التصنيفات</a></li> </ul> <form class="d-flex me-auto w-100"> <input class="form-control" type="search" placeholder="ابحث عن أي موضوع"> <div class="nav-link" style="position: relative;" onclick="goToShoppingCart()"> <div class="counter zero"></div> <a href="shopping-cart.html"><img src="images/icons8-shopping-cart-32.png" /></a> </div> <button type="button" class="btn btn-dark nabih-buttons">دخول</button> <button type="button" class="btn btn-outline-dark nabih-buttons">تسجيل جديد</button> </form> </div> </div> </nav> <!--Main View--> <div class="mainview"> <div class="row"> <div class="col-lg-6"> <h1 class="mainview-heading">دورات غنية بجودة عالية</h1> <h3 class="mainview-subheading"> طور نفسك لتنافس في سوق العمل</h3> </div> <div class="col-lg-6"> <img class="mainview-image" src="images/show-case.jpg" /> </div> </div> </div> </section> <section id="best-selling"> <div class="section-title">الدورات الأكثر مبيعًا</div> <div id="bestSellingCourses" class="carousel carousel-dark slide" data-bs-ride="carousel"> <div class="carousel-inner"> <div class="carousel-item active"> <div class="row"> <div class="col-lg-3"> <div class="card" name="productCard" id="P1"> <img src="images/python-product.jpg" class="card-img-top product-image" alt="python-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام بايثون</h5> <p class="product-text card-text">في هذه الدورة سنتعلّم مبادئ البرمجة في لغة بايثون</p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$10</div> </div> </div> </div> <div class="col-lg-3"> <div class="card" name="productCard" id="P2"> <img src="images/github-product.jpg" class="card-img-top product-image" alt="github-course"> <div class="card-body"> <h5 class="product-title card-title">دورة التعامل مع GitHub</h5> <p class="product-text card-text">تعلّم كيف تدير مشاريعك البرمجية من حيث إدارة الإصدار والمساهة في مشاريع برمجية أخرى.</p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$15</div> </div> </div> </div> <div class="col-lg-3"> <div class="card" name="productCard" id="P3"> <img src="images/python-django-product.jpg" class="card-img-top product-image" alt="django-course"> <div class="card-body"> <h5 class="product-title card-title">دورة تطوير تطبيقات ويب باستخدام بايثون مع Django</h5> <p class="product-text card-text">تعلّم كيف تبني تطبيقات ويب باستخدام لغة بايثون مع Django </p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$20</div> </div> </div> </div> <div class="col-lg-3"> <div class="card" name="productCard" id="P4"> <img src="images/javascript-product.jpg" class="card-img-top product-image" alt="javascript-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام جافاسكريبت</h5> <p class="product-text card-text"> سنتعلّم في هذه الدورة كيفية تطوير تطبيقات الواجهة الأمامية باستخدام جافاسكريبت </p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$25</div> </div> </div> </div> </div> </div> <div class="carousel-item"> <div class="row"> <div class="col-lg-3"> <div class="card" name="productCard" id="P5"> <img src="images/php-product.jpg" class="card-img-top product-image" alt="php-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام PHP</h5> <p class="product-text card-text">في هذه الدورة سنتعلّم مبادئ البرمجة في لغة PHP</p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$13</div> </div> </div> </div> <div class="col-lg-3"> <div class="card" name="productCard" id="P6"> <img src="images/arduino-product.jpg" class="card-img-top product-image" alt="arduino-course"> <div class="card-body"> <h5 class="product-title card-title">دورة التعامل مع أساسيات Arduino</h5> <p class="product-text card-text">تعلّم كيف تبني أنظمة مضمنه تتضمّن مشاريع بسيطة باستخدام Arduino</p> <a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addToCartConfirmation">أضف إلى السلة</a> <div class="price-tag">$30</div> </div> </div> </div> </div> </div> </div> <button class="carousel-control-prev" type="button" data-bs-target="#bestSellingCourses" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> </button> <button class="carousel-control-next" type="button" data-bs-target="#bestSellingCourses" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> </button> </div> </section> <section id="course-categories"> <div class="section-title">الأقسام المتاحة</div> <div class="row"> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/programming-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">البرمجة</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/marketing-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">التسويق</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/freelance-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">العمل الحر</p> </div> </div> </a> </div> </div> <div class="row"> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/cloud-computing-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">التطبيقات السحابية</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/certificates-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">الشهادات العالمية</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/apps-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">البرامج والتطبيقات</p> </div> </div> </a> </div> </div> </section> <section id="footer"> <footer> <div class="container-fluid"> <i class="bi bi-facebook social-icon"></i> <i class="bi bi-twitter social-icon"></i> <i class="bi bi-instagram social-icon"></i> <i class="bi bi-envelope social-icon"></i> <p>© دورات نبيه 2021</p> </div> </footer> </section> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <script type="module"> import {updateCartLabel, saveItem} from './js/main.js'; let products = document.getElementsByName('productCard'); for (let product of products) { const product_id = product.attributes['id'].value; const product_title = product.getElementsByClassName('product-title')[0].innerHTML; const product_price = product.getElementsByClassName('price-tag')[0].innerHTML; const addToCartButton = product.getElementsByClassName('btn-primary')[0]; addToCartButton.addEventListener('click', (e) => { const item_to_post = { id: product_id, course_name: product_title, price: product_price.substring(1) }; saveItem(item_to_post); updateCartLabel(); }); } </script> </body> </html> ستكون هناك أيضًا تعديلات طفيفة على ملف التنسيقات styles.css لكي يدعم صفحة سلة المشتريات. المحتوى الجديد للملف سيكون على النحو التالي: body{ font-family: 'Tajawal', sans-serif; font-size: 1em; } .nabih-buttons{ margin: 0 4px; padding: 6px 8px; width: 7rem; white-space: nowrap; } .container-fluid{ padding:0 5%; } .section-title{ text-align: center; font-size: xx-large; padding: 3rem 0; } /*Header Section*/ .counter{ color: white; margin: 0; position: absolute; font-size: 15px; text-align: center; right: 6px; top: 0px; width: 20px; height: 20px; background: red; border-radius: 50%; } .zero{ opacity: 0; } #header{ background-color: #eee; } .navbar-brand{ font-family: 'Almarai'; color:#393e46; font-size: 2.5rem; font-weight:900; margin: 0; } .mainview-heading { color: white; font-size: 4em; text-align: right; } .mainview-subheading{ color: white; margin: 64px 0; text-align: right; } .mainview{ background-color: #233e8b; padding: 3% 5%; } .mainview-image{ border-radius: 2%; width: 500px; float: left; } /*Best Selling Section*/ .carousel-inner{ padding: 0 5%; } .carousel-control-prev { background: linear-gradient(to right, rgba(200,200,200,0), rgba(200,200,200,1)); } .carousel-control-next { background: linear-gradient(to left, rgba(200,200,200,0), rgba(200,200,200,1)); } .product-image{ height: 12rem; } .product-title{ height: 4rem; } .product-text{ height: 6rem; } .price-tag{ float: left; border-width: 1px; border-style: solid; width: 3rem; height: 2rem; text-align: center; padding:2px; margin-top:2px; } /*Course Categories Section*/ #course-categories{ background-color: #eee; padding: 0 10% 8px; } #course-categories .row{ margin-bottom: 72px; } .category-card{ margin-left: 24px; margin-right:24px; padding: 48px 0px 32px; } .category-image{ width: 50%; height: auto; margin: 0 auto; } .category-text{ text-align: center; font-size: 1.3rem; } .category-link{ text-decoration: none; color:inherit; } /*Footer Section*/ footer{ height: 200px; } .social-icon { margin: 20px 10px; } #footer .container-fluid{ padding: 7% 15%; text-align: center; } /*Shopping Cart*/ .shopping-cart-title{ padding: 3% 5%; } .shopping-cart-content{ padding: 0 5%; } .shopping-cart-total{ padding:0 5% 3%; font-weight: bold; } .shopping-cart-item{ padding-top: 8px; padding-bottom: 8px; font-size: 1.1rem; } ستحصل على نافذة منبثقة شبيهة بما يلي: إضافة الوحدة البرمجية main إذا لم تكن قد أنشأت المجلّد js ضمن المجلّد الرئيسي للمشروع فافعل ذلك الآن، بعد ذلك أضف الملف main.js لهذا المجلّد. انسخ الشيفرة البرمجية التالية لهذا الملف: export function saveItem(item) { let key = Object.keys(localStorage) .find(e => e === String(item.id)); if(key){ let tmp = JSON.parse(localStorage.getItem(key)); tmp.push(item); localStorage.setItem(item.id, JSON.stringify(tmp)); } else{ localStorage.setItem(item.id, JSON.stringify([item])); } } export function getItem(id) { return window.localStorage.getItem(item.id); } export function getAllItems(){ const keys = Object.keys(window.localStorage); let entries = []; for(let key of keys){ entries.push(JSON.parse(localStorage.getItem(key))) } return entries; } export function updateShoppingCartList(){ let shoppingCartContent = document.getElementById('shoppingCartContent'); let counter = 1; let total_products = 0; let amount_due = 0; for (let group of getAllItems()) { const div = document.createElement('div'); div.className = 'row shopping-cart-item'; div.innerHTML = ` <div class="col-1"> <span>${counter}</span> </div> <div class="col-6"> <span>${group[0].course_name}</span> </div> <div class="col-1"> <span>${group.length}</span> </div> <div class="col-1"> <span>$${group[0].price}</span> </div> <div class="col-1"> <span>$${group.length * group[0].price}</span> </div> `; shoppingCartContent.appendChild(div); amount_due += group.length * group[0].price; counter++; total_products += group.length; } document.getElementById('amountDue').innerHTML = '$' + amount_due; } export function updateCartLabel(){ const counterElement = document.getElementsByClassName('counter')[0]; let count = 0; count = getAllItems().reduce( (acc, group) => acc + group.length, 0) if(count > 0){ counterElement.innerHTML = count; counterElement.classList.remove('zero'); } } يحتوي هذا الملف على خمسة توابع، وهي: التابع saveItem ويقبل وسيطًا وحيدًا وهو العنصر "المنتج" الذي اختاره المستخدم، حيث يحفظه ضمن التخزين الداخلي للمتصفّح. التابع getItem ويقبل وسيطًا واحيدًا هو معرّف المنتج الذي نود الحصول على بياناته من التخزين الداخلي. التابع getAllItems وهو لا يحتاج إلى أية وسائط، ويُرجع مصفوفة تحتوي على جميع المنتجات المحفوظة ضمن التخزين الداخلي. التابع updateShoppingCartList ومهمّته تحديث الصفحة الخاصة بسلّة المشتريات، بالمنتجات الموجودة ضمن التخزين الداخلي، حيث يعرضها بطريقة جدولية بسيطة، موضّحا اسم المنتج وعدد النسخ المطلوب شرائها منه، وسعر النسخة، وإجمالي المبلغ الواجب دفعه لهذا المنتج تحديدًا. وفي نهاية الجدول سيعرض أيضًا المجموع النهائي الواجب دفعه من قبل المستخدم (سنرى توضيحًا لبنية هذا الجدول بعد قليل). التابع updateCartLabel ووظيفته تحديث أيقونة سلّة المشتريات الموجودة أعلى الصفحة بعدد نسخ المنتجات المطلوب شراؤها. عرض محتويات سلة المشتريات أضف الملف shopping-cart.html إلى المشروع، وانسخ إليه المحتويات التالية: <!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet" /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700;800&family=Tajawal:wght@200;300;400;500;700;800;900&display=swap" rel="stylesheet"> <!-- Boostrap 5 icons - web font --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.0/font/bootstrap-icons.css"> <title>دورات نبيه | سلة المشتريات</title> </head> <body> <section id="header"> <!-- Nav Bar --> <nav class="navbar navbar-expand-lg navbar-light"> <div class="container-fluid"> <a class="navbar-brand" href="#"> <img src="images/nabih-logo.png" style="margin-left: 8px;" alt="" height="32"> نبيه </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-2 mb-2 mb-lg-0 ms-3"> <li class="nav-item"><a class="nav-link" href="#">التصنيفات</a></li> </ul> <form class="d-flex me-auto w-100"> <input class="form-control" type="search" placeholder="ابحث عن أي موضوع"> <div class="nav-link" style="position: relative;"> <div class="counter zero"></div> <img src="images/icons8-shopping-cart-32.png" /> </div> <button type="button" class="btn btn-dark nabih-buttons">دخول</button> <button type="button" class="btn btn-outline-dark nabih-buttons">تسجيل جديد</button> </form> </div> </div> </nav> </section> <!--Main View--> <div class="shopping-cart-title"><h2>سلة المشتريات</h2></div> <div id="shoppingCartContent" class="shopping-cart-content"> <div class="row"> <div class="col-1"> </div> <div class="col-6"> <h5>الدورة</h5> </div> <div class="col-1"> <h5>العدد</h5> </div> <div class="col-1"> <h5>السعر</h5> </div> <div class="col-1"> <h5>الإجمالي</h5> </div> </div> </div> <div class="row shopping-cart-total"> <div id="amountDue" class="offset-9 col-1" style="border-top-style: solid;"></div> </div> <div class="row"> <div class="col-4 shopping-cart-content"><a href="checkout.html" class="btn btn-primary">الشراء الآن</a></div> </div> <section id="footer"> <footer> <div class="container-fluid"> <i class="bi bi-facebook social-icon"></i> <i class="bi bi-twitter social-icon"></i> <i class="bi bi-instagram social-icon"></i> <i class="bi bi-envelope social-icon"></i> <p>© دورات نبيه 2021</p> </div> </footer> </section> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> <script type="module"> import {updateShoppingCartList, updateCartLabel} from './js/main.js'; updateShoppingCartList(); updateCartLabel(); </script> </body> </html> القسم الأول من الشيفرة السابقة مألوف بالنسبة لك، فهو مسؤول عن تعريف شريط التنقّل كما في الصفحة الرئيسية بالضبط. القسم الثاني من الشيفرة بدءًا من "الموضع 1" يعرّف بنية بسيطة تشبه الجدول مهمتها عرض محتويات سلة المشتريات. الشيفرة البرمجية المسؤولة عن تعبئة هذه البنية موجودة ضمن "الموضع 2". تستورد هذه الشيفرة المكتوبة بلغة جافا سكريبت، تابعين من الملف main.js (أضفناه قبل قليل)، ثم تعمل على استدعائهما تباعًا. في حال وجود منتجات في السلة ستحصل على شكل شبيه بما يلي: يمكنك الوصول إلى الشيفرة الكاملة لهذا الدرس من الملف bootstrap5-tutorial.zip، كما ويمكنك الإطلاع على نسخة حيّة من هذه الصفحة من هنا. خاتمة كان الدرس طويلًا بعض الشيء إلّا أنّنا أنجزنا الكثير! الميزة الجديدة التي تعرّفنا عليها في هذا الدرس هي مكوّن الرسائل المنبثقة Modal الذي يسمح بالتفاعل الفعّال مع المستخدم من خلال توفير وسيلة لعرض المعلومات للمستخدم، مع إمكانية توفير إمكانية التفاعل من خلال تلقي رد من المستخدم حول سؤال محّدد نرغب بالحصول على الإجابة عليه. سنتابع عملنا في الدرس القادم في توفير صفحة خاصّة للدفع، لشراء المنتجات التي اختارها المستخدم. اقرأ أيضًا بناء قائمة شجرية باستخدام البوتستراب 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap
-
سنتعرّف في هذا الدرس على مكوّنين جديدين من مكوّنات بوتستراب 5، وهما مكوّن البطاقة Card، ومكوّن الشرائح الدوّارة Carousel. ومن ثمّ سنطبّق ما سنتعلّمه هنا في متابعة عملنا في إكمال تصميم موقع "نبيه". سنعمل في هذا الدرس على: التعرّف على مكوّن البطاقة Card في بوتستراب. التعرّف على مكوّن الشرائح الدوّارة Carousel في بوتستراب. إنجاز قسم الدورات الأكثر مبيعًا Best Selling. إنجاز القسم الخاص بأصناف الدورات Course Categories. أيقونات Bootstrap 5 إنجاز قسم التذييل Footer. هذا الفصل جزء من سلسلة فصول عن بوتستراب 5، وإليك كامل فهرس السلسلة: مدخل إلى إطار العمل بوتستراب 5 شريط التنقل في بوتستراب 5 مخطط الصفحة في بوتستراب 5 تطبيق مخطط الصفحة في بوتستراب على صفحات الويب مكون البطاقة Card ومكون الشرائح الدوارة Carousel في بوتستراب التعرف على مكون البطاقة Card في بوتستراب لمكوّن البطاقة Card في بوتستراب استخدامات متعدّدة. فغالبًا ما يُستخدم عندما نود توضيح أو وصف شيء محدّد، كوصف منتج ما مثلًا، أو وصف صنف كامل من من المنتجات.في درسنا هذا سنستخدم مكوّن البطاقة لوصف دورة محدّدة يقدّمها موقع نبيه، وأيضًا لتصنيف مجالات الدورات التي تُقدّم في الموقع. بُنيت البطاقة في بوتستراب باستعمال التخطيط المرن Flex Box وهي لا تحوي أية هوامش بشكل افتراضي. أي بطاقة في بوتستراب يجب أن يكون لها "حاوية". هذه الحاوية عبارة عن عنصر div له الصنف التنسيقي .card. بالنسبة لمحتويات الحاوية فلدينا العديد من الإمكانيات. يمكن بشكل أساسي، أن تحتوي البطاقة على نص عادي فحسب. أمّا بالنسبة للشكل النموذجي، فيمكن أن يكون لها التنسيق التالي: صورة توضيحية في الأعلى. جسم البطاقة، والذي يحتوي على عنوان البطاقة ونص توضيحي. انظر إلى البطاقة النموذجية التالية: <div class="card" style="width: 18rem;"> <img src="..." class="card-img-top" alt="image-description"> <div class="card-body"> <h5 class="card-title">عنوان البطاقة</h5> <p class="card-text">نص بسيط لوصف المنتج أو أي شيء تود الحديث عنه</p> <a href="#" class="btn btn-primary">زر عادي</a> </div> </div> سيشكل العنصر div مع الصنف .card الحاوية الخاصة بالبطاقة ككل. داخل هذا الحاوية، لاحظ وجود عنصر الصورة img مع الصنف .card-img-top، ستلاحظ أيضًا وجود عنصر div آخر يمثّل جسم البطاقة له الصنف .card-body. يتكوّن جسم البطاقة في مثالنا هذا من قسمين: القسم الأول هو عنوان البطاقة باستخدام الصنف .card-title القسم الثاني هو نص البطاقة باستخدام الصنف .card-text . يمكن بشكل اختياري إضافة زر لهذا القسم كما فعلنا في مثالنا هذا. عند معاينة الشيفرة السابقة ستحصل على شكل شبيه بما يلي: كما أسلفت قبل قليل توجد الكثير من الإمكانيات التي توفرها البطاقة. يمكن مثلًا إضافة عنوان فرعي باستخدام الصنف .card-subtitle ويُوضع هذا العنوان الفرعي كما هو واضح أسفل العنوان الرئيسي في حال الرغبة. كما يمكن وضع روابط ضمن جسم البطاقة، بتنسيق متوافق معها باستخدام عنصر a مع الصنف .card-link يمكن أيضًا جعل البطاقة تعرض قائمة من مجموعة من العناصر . انظر إلى التعديل التالي الذي أجريته على الشيفرة السابقة: <div class="card" style="width: 18rem;"> <img src="images/tmp.png" class="card-img-top" alt="image-description"> <div class="card-body"> <h5 class="card-title">عنوان البطاقة</h5> <p class="card-text">نص بسيط لوصف المنتج أو أي شيء تود الحديث عنه</p> <a href="#" class="btn btn-primary">زر عادي</a> </div> <!--بداية التعديل --> <ul class="list-group list-group-flush"> <li class="list-group-item">عنصر 1</li> <li class="list-group-item">عنصر 2</li> <li class="list-group-item">عنصر 3</li> </ul> <!--نهاية التعديل --> </div> لاحظ الشيفرة الجديدة من المثال الأخير. وضعت عنصر قائمة غير مرتبة ul مع الصنف .list-group و الصنف .list-group-flush. الصنف الأخير وظيفته جمالية فقط، حاول إزالته وانظر الفرق. عند معاينة الشيفرة السابقة ستحصل على شكل شبيه بما يلي: يمكن أيضًا الاستغناء عن الصورة التوضيحية في الأعلى واستخدام ترويسة بدلًا منها. كما ويمكن أيضًا وضع تذييل للبطاقة. انظر إلى الشيفرة التالية: <div class="card" style="width: 18rem;"> <div class="card-header">ترويسة مناسبة</div> <div class="card-body"> <p class="card-text">نص بسيط لوصف المنتج أو أي شيء تود الحديث عنه</p> <a href="#" class="btn btn-primary">زر عادي</a> </div> <div class="card-footer text-muted">تذييل مناسب</div> </div> لاحظ اولًا أن كلًا من الترويسة والتذييل موجودان خارج جسم البطاقة ضمن عنصر يdiv منفصلين. لعنصر div الذي يحوي الترويسة الصنف .card-header أما لعنصر div الذي يحوي التذييل الصنف .card-footer. لاحظ أيضًا كيف استخدمت الصنف .text-muted لجعل نص التذييل باهتًا بعض الشيء. يمكنك بالطبع إزالة هذا التنسيق إن أحببت. ستحصل على الشكل التالي عند معاينة الشيفرة السابقة: يمكنك الإطلاع على المزيد من الخيارات المتاحة بزيارة صفحة التوثيق الرسمية لمكوّن البطاقة Card. التعرّف على مكون الشرائح الدوارة Carousel في بوتستراب هو من المكوّنات المفيدة ضمن صفحات الويب. فمن خلاله يمكن عرض شرائح للمستخدمين ذات محتوى متنوّع وقابل للدوران بشكل تلقائي، أو بالنقر على أزرار تنقّل أو بمزيج بينهما. يمكن أن يكون هذا المحتوى عبارة عن صورة مع نص وصفي، أو يمكن حتى أن يكون محتوى مخصّص للغاية كما سنفعل في مشروعنا "نبيه". ولا يمكن في بوتستراب أن تكون مكوّنات Carousel متداخلة nested فيما بينها. يمكن استخدام هذا المكوّن من خلال عنصر div مع التنسيقين .carousel و .slide. يحتوي كل مكّون Carousel على عنصر div آخر له التنسيق .carousel-inner يحتوي بدوره على الشرائح الفعلية التي ستظهر للمستخدم. كل شريحة من هذه الشرائح عبارة عن عنصر div له التنسيق .carousel-item. يجب أن يكون لإحدى هذه الشرائح تنسيق آخر هو .active للدلالة إلى الشريحة الحالية (الفعّالة) التي سُتعرض أولًا للمستخدم. انظر إلى مكوّن Carousel نموذجي ضمن الشيفرة التالية: <div id="carouselComponent" class="carousel slide" data-bs-ride="carousel"> <div class="carousel-inner"> <div class="carousel-item active"> <img src="..." class="d-block w-100" alt="..."> </div> <div class="carousel-item"> <img src="..." class="d-block w-100" alt="..."> </div> <div class="carousel-item"> <img src="..." class="d-block w-100" alt="..."> </div> </div> </div> ستظهر الشريحة التي تحمل التنسيق active أولًا أمام المستخدم. ثم ستدور الشرائح بالتناوب أمام المستخدم بشكل تلقائي ضمن فترات زمنية محّددة. يمكنك وضع صور مناسبة ضمن السمة src لكل صورة لكي ترى النتيجة بشكل فعلي. يمكن وكخيار إضافي، استخدام التنسيق .carousel-dark (يُوضع بجوار التنسيقين .carousel و .slide لعنصر div الذي يمثّل الحاوية الرئيسية لمكوّن Carousel)، وذلك لإكساب مكوّن carousel تنسيقًا داكنًا بعض الشيء. كما وسبق أن رأينا في عنصر البطاقة ، يحتوي عنصر Carousel على تشكيلة متنوّعة من الخيارات في العرض. يمكن مثلًا إضافة أزرار انتقال تسمح للمستخدم بالتنقّل اليدوي عوضًا عن التنقّل التلقائي. أضف إلى الشيفرة السابقة، الشيفرة التالية (ضعها قبل آخر وسم إغلاق </div>): <button class="carousel-control-prev" type="button" data-bs-target="#carouselComponent" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="visually-hidden">Previous</span> </button> <button class="carousel-control-next" type="button" data-bs-target="#carouselComponent" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="visually-hidden">Next</span> </button> يمكن أيضًا وضع مؤشّر يظهر أسفل مكوّن Carousel للدلالة على عدد الشرائح الموجودة بالإضافة إلى موقعك الحالي ضمن هذه الشرائح. يمكن وضع هذا المؤشّر من خلال عنصر div له التنسيق .carousel-indicators. انظر إلى الشيفرة التالية (ضعها قبل عنصر div ذي التنسيق .carousel-inner مباشرةً): <div class="carousel-indicators"> <button type="button" data-bs-target="#carouselComponent" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> <button type="button" data-bs-target="#carouselComponent" data-bs-slide-to="1" aria-label="Slide 2"></button> <button type="button" data-bs-target="#carouselComponent" data-bs-slide-to="2" aria-label="Slide 3"></button> </div> يمكنك الإطلاع على المزيد من الخيارات المتاحة بزيارة صفحة التوثيق الرسمية لمكوّن Carousel. إنجاز قسم الدورات الأكثر مبيعًا سنطبّق ما تعلّمناه في الفقرتين السابقتين في إنجاز قسم الدورات الأكثر مبيعًا Best Selling ضمن موقع نبيه. أدرج الشيفرة التالية ضمن القسم best-selling في الملف index.html: <div class="section-title">الدورات الأكثر مبيعًا</div> <div id="bestSellingCourses" class="carousel carousel-dark slide" data-bs-ride="carousel"> <div class="carousel-inner"> <div class="carousel-item active"> <!-- الموضع 1--> <div class="row"> <div class="col-lg-3"> <div class="card"> <!-- الموضع 2--> <img src="images/python-product.jpg" class="card-img-top product-image" alt="python-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام بايثون</h5> <p class="product-text card-text">في هذه الدورة سنتعلّم مبادئ البرمجة في لغة بايثون</p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> <div class="col-lg-3"> <div class="card"> <!-- الموضع 3--> <img src="images/github-product.jpg" class="card-img-top product-image" alt="github-course"> <div class="card-body"> <h5 class="product-title card-title">دورة التعامل مع GitHub</h5> <p class="product-text card-text">تعلّم كيف تدير مشاريعك البرمجية من حيث إدارة الإصدار والمساهة في مشاريع برمجية أخرى.</p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> <div class="col-lg-3"> <div class="card"> <!-- الموضع 4--> <img src="images/python-django-product.jpg" class="card-img-top product-image" alt="django-course"> <div class="card-body"> <h5 class="product-title card-title">دورة تطوير تطبيقات ويب باستخدام بايثون مع Django</h5> <p class="product-text card-text">تعلّم كيف تبني تطبيقات ويب باستخدام لغة بايثون مع Django </p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> <div class="col-lg-3"> <div class="card"> <!-- الموضع 5--> <img src="images/javascript-product.jpg" class="card-img-top product-image" alt="javascript-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام جافاسكريبت</h5> <p class="product-text card-text"> سنتعلّم في هذه الدورة كيفية تطوير تطبيقات الواجهة الأمامية باستخدام جافاسكريبت</p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> </div> </div> <div class="carousel-item"> <div class="row"> <div class="col-lg-3"> <div class="card"> <!-- الموضع 6--> <img src="images/php-product.jpg" class="card-img-top product-image" alt="php-course"> <div class="card-body"> <h5 class="product-title card-title">دورة أساسيات البرمجة باستخدام PHP</h5> <p class="product-text card-text">في هذه الدورة سنتعلّم مبادئ البرمجة في لغة PHP</p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> <div class="col-lg-3"> <div class="card"> <!-- الموضع 7--> <img src="images/arduino-product.jpg" class="card-img-top product-image" alt="arduino-course"> <div class="card-body"> <h5 class="product-title card-title">دورة التعامل مع أساسيات Arduino</h5> <p class="product-text card-text">تعلّم كيف تبني أنظمة مضمنة تتضمّن مشاريع بسيطة باستخدام Arduino</p> <a href="#" class="btn btn-primary">أضف إلى السلة</a> </div> </div> </div> </div> </div> </div> <!-- الموضع 8--> <button class="carousel-control-prev" type="button" data-bs-target="#bestSellingCourses" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> </button> <button class="carousel-control-next" type="button" data-bs-target="#bestSellingCourses" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> </button> </div> انتقل الآن الى الملف styles.css وأضف إليه التنسيق التالي قبل تنسيقات قسم الترويسة Header Section: .section-title{ text-align: center; font-size: xx-large; padding: 3rem 0; } الهدف من ذلك هو فقط تنظيم الملف styles.css. ثم بعد ذلك أضف التنسيقات التالية إلى آخر محتويات الملف: /*Best Selling Section*/ .carousel-inner{ padding: 0 5%; } .carousel-control-prev { background: linear-gradient(to right, rgba(200,200,200,0), rgba(200,200,200,1)); } .carousel-control-next { background: linear-gradient(to left, rgba(200,200,200,0), rgba(200,200,200,1)); } .product-image{ height: 12rem; } .product-title{ height: 4rem; } .product-text{ height: 6rem; } بالنسبة لشيفرة HTML، نبدأ قسم المنتجات الأكثر مبيعًا بنص توضيحي يُشير إلى بداية هذا القسم له التنسيق .section-title ثم يبدأ بعد ذلك مكوّن Carousel بتنسيق داكن .carousel-dark وبمعرّف bestSellingCourses. يبدأ بعد ذلك مباشرةً الجسم الداخلي له .carousel-inner. في الواقع سيكون مكوّن Carousel هنا بسيطًا للغاية. سيحتوي هذا المكوّن على عنصري .carousel-item وبعبارة أخرى سيحتوي على شريحتين. ستحمل الشريحة الأول الصنف .active كما هو واضح (الموضع 1). يوجد ضمن الشريحة الأولى أربعة منتجات وُضعت ضمن سطر .row ووُزّعت على أربعة أعمدة .col-lg-3. وُضِع كل منتج ضمن بطاقة card منفصلة (انظر إلى المواضع 2 و 3 و 4 و 5). أمّا الشريحة الثانية فتحتوي على منتجين فقط (انظر إلى الموضعين 6 و 7). بالنسبة إلى زري التنقّل لمكوّن الشرائح الدوّارة فيمكنك أن تجدهما اعتبارًا من الموضع 8. لاحظ أنني قد أجريت بعض التعديل على الصنفين .carousel-control-prev و .carousel-control-next ضمن الملف styles.css حيث أنّني قد أضفت تدرّجًا لونيًا لهذين الزرّين كي يسهل للمستخدم تمييزهما فورًا. بمعاينة الشيفرة السابقة ضمن المتصفّح ستحصل على شكل شبيه بما يلي: إنجاز القسم الخاص بأصناف الدورات لنبدأ الآن بتجهيز القسم الخاص بأصناف الدورات، والذي سيتكوّن في مشروعنا هذا من ستة أقسام وهي: البرمجة. التسويق. العمل الحر. التطبيقات السحابية. الشهادات العالمية. البرامج والتطبيقات. سنستخدم من أجل كل قسم مكوّن البطاقة من بوتستراب، وسنوزّع هذه البطاقات على سطرين مستقلين كما في الشكل التالي: أضف الآن شيفرة HTML التالية إلى القسم course-categories: <div class="section-title">الأقسام المتاحة</div> <div class="row"> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/programming-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">البرمجة</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/marketing-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">التسويق</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/freelance-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">العمل الحر</p> </div> </div> </a> </div> </div> <div class="row"> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/cloud-computing-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">التطبيقات السحابية</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/certificates-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">الشهادات العالمية</p> </div> </div> </a> </div> <div class="col-lg-4"> <a class="category-link" href="#"> <div class="card category-card"> <img src="images/apps-category.png" class="card-img-top category-image" alt="certifications-category"> <div class="card-body"> <p class="card-text category-text">البرامج والتطبيقات</p> </div> </div> </a> </div> </div> تتألف كل بطاقة من صورة توضيحية، بالإضافة إلى نص وصفي تحت الصورة. ستحتاج أيضًا إلى إضافة التنسيقات التالية إلى الملف styles.css أسفل التنسيقات السابقة: #course-categories{ background-color: #eee; padding: 0 10% 8px; } #course-categories .row{ margin-bottom: 72px; } .course-categories-title{ text-align: center; font-size: xx-large; padding: 3rem 0; } .category-card{ margin-left: 24px; margin-right:24px; padding: 48px 0px 32px; } .category-image{ width: 50%; height: auto; margin: 0 auto; } .category-text{ text-align: center; font-size: 1.3rem; } .category-link{ text-decoration: none; color:inherit; } أعتقد أنّه لا جديد في التنسيقات السابقة أو حتى في شيفرة HTML الخاصّة في هذا القسم. أيقونات Bootstrap 5 يأتي مع بوتستراب 5 مجموعة غنية من الأيقونات الجميلة والمتنوّعة التي يمكنك استخدامها في التطبيقات التي تُنشئها سواءً باستخدام بوتستراب 5 أو حتى بغيره. لأيقونات بوتستراب 5 نوعان: أيقونات على شكل خطوط ويب Web Fonts. أيقونات بتنسيق SVG. سنستخدم هنا النوع الأوّل: أيقونات Web Fonts. تكون هذه الأيقونات منفصلة عن بوتستراب، وقبل استخدامها، ينبغي إضافة مرجع إلى الصفحة الرئيسية الخاصة بنا. أضف الشيفرة التالية إلى قسم head ضمن الملف index.html: <!-- Boostrap 5 icons - web font --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.0/font/bootstrap-icons.css"> يمكن استخدام هذه الأيقونات بشكل مستقل كما سنفعل في الفقرة التالية. كما يمكن استخدامها مع الأزرار العادية وغيرها من العناصر. إنجاز قسم التذييل Footer وصلنا إلى الجزء الأخير في موقع "نبيه" وهو قسم التذييل. سيكون هذا القسم بسيطًا وجميلًا إذ أنّنا سنضيف إليه الأيقونات المعبّرة عن قنوات التواصل الإجتماعي بالإضافة إلى أيقونة إرسال رسالة بريد إلكتروني، ولن تكون هذه الأيقونات تُشير إلى صفحات حقيقية بطبيعة الحال. كما سندرج تحت الأيقونات السابقة، نصًا بسيطًا يُشير إلى موقع نبيه كعلامة مسجلة. أضف الشيفرة البرمجية التالية ضمن القسم footer ضمن الملف index.html: <footer> <div class="container-fluid"> <i class="bi bi-facebook social-icon"></i> <i class="bi bi-twitter social-icon"></i> <i class="bi bi-instagram social-icon"></i> <i class="bi bi-envelope social-icon"></i> <p>© دورات نبيه 2021</p> </div> </footer> التنسيقات مثل bi bi-facebook وغيرها ضرورية لاستخدام الأيقونة المطلوبة مع عنصر i ما. أضف أيضًا التنسيقات التالية إلى الملف styles.css: footer{ height: 200px; } .social-icon { margin: 20px 10px; } #footer .container-fluid{ padding: 7% 15%; text-align: center; } عند المعاينة، ستحصل على شكل شبيه بما يلي: أعتقد أنّ التنسيقات السابقة واضحة ومباشرة ولا تحتاج إلى أي تعليق إضافي. وبذلك نكون قد وصلنا إلى نهاية هذا الدرس، وبالتالي إلى نهاية تصميم الصفحة الرئيسية لموقع "نبيه". يمكنك الوصول إلى الشيفرة كاملة عن طريق زيارة هذا المستودع، ضمن المجلّد chapter05 أو من الملف المرفق في نهاية المقال، وكما ويمكنك الإطلاع على نسخة حيّة من هذه الصفحة من github.io. خاتمة تعرّفنا في هذا الدرس على مكوّنين مهمّين وهما البطاقة Card والشرائح الدوّارة Carousel، وتعرّفنا على أهم الخصائص المتاحة لهما. كما تعرّفنا على مكتبة أيقونات Font Awesome التي تضم تشكيلة رائعة من الأيقونات المميزة، والتي يمكنك استخدامها بشكل مجاني في تطبيقاتك المختلفة. كما عملنا على تطبيق ما تعلّمناه في تصميم وإنجاز باقي أقسام الصفحة الرئيسية لموقع نبيه (قسم الدورات الأكثر مبيعًا والقسم الخاص بأصناف الدورات و قسم التذييل). bootstrap5-tutorial-master.zip اقرأ أيضًا المقال السابق: تطبيق مخطط الصفحة في بوتستراب على صفحات الويب بناء قائمة شجرية باستخدام البوتستراب 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap
-
سنتعلّم كيفية تطبيق مخطّط الصفحة في بوتستراب على صفحات الويب من خلال تحديث موقع "نبيه" بآخر ما تعلّمناه في الفصول الثلاثة السابقة، فصل مدخل إلى إطار العمل بوتستراب 5 وفصل شريط التنقل في بوتستراب 5، وبالأخص الفصل الأخير "مخطط الصفحة في بوتستراب 5" من سلسلة bootstrap 5، فمن الضروري جدًا أن تكون قد اطلعت على الدروس السابقة قبل الإكمال في هذا الدرس لأنّه مرتبط بها بشكل وثيق. سنبدأ بتحديد الأقسام الأساسية في الصفحة الرئيسية، ومن ثمّ سنعمل على تنسيقها بحيث تكون الصفحة متجاوبة مع قياسات الشاشة المختلفة. سيكون هذا الموضوع موزعًا على هذا الدرس بالإضافة إلى الدروس اللاحقة. سنهتم في هذا الدرس بتعريف الأقسام الرئيسية، وتحضير أصول الموقع، بالإضافة إلى تصميم القسم الأول (الترويسة). بينما سنعمل في الدروس اللاحقة على إكمال تصميم وتنفيذ باقي أقسام الصفحة. سنعمل في هذا الدرس على: تعريف الأقسام الأساسية للصفحة الرئيسية في موقع "نبيه". تحضير أصول الموقع من خطوط وصور وألوان. تصميم قسم الترويسة. هذا الفصل جزء من سلسلة فصول عن بوتستراب 5، وإليك كامل فهرس السلسلة: مدخل إلى إطار العمل بوتستراب 5 شريط التنقل في بوتستراب 5 مخطط الصفحة في بوتستراب 5 تطبيق مخطط الصفحة في بوتستراب على صفحات الويب مكون البطاقة Card ومكون الشرائح الدوارة Carousel في بوتستراب تعريف الأقسام الأساسية للصفحة الرئيسية في موقع "نبيه" سنجعل للصفحة الرئيسية في موقع "نبيه" أربعة أقسام أساسية هي: الترويسة Header. قسم الدورات الأكثر مبيعًا Best Selling. القسم الخاص بأصناف الدورات Course Categories. قسم التذييل Footer. سأعمل على وضع كل قسم من الأقسام الأربعة السابقة ضمن عنصر <section> مع تسمية مناسبة، ليسهل فيما بعد استهداف كل قسم على حدة باستخدام CSS. بالعودة إلى الملف index.html ضمن مجلّد مشروع "نبيه" ضمن Visual Studio Code، سنعمل على إضافة الأقسام الأربعة الرئيسية. سيكون القالب الذي سننطلق منه على النحو التالي: <!doctype html> <html lang="ar" dir="rtl"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> <link href="/css/styles.css" rel="stylesheet" /> <title>دورات نبيه</title> </head> <body> <section id="header"> </section> <section id="best-selling"> </section> <section id="course-categories"> </section> <section id="footer"> </section> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> استخدمنا نسخة بوتستراب الخاصة بدعم اللغة العربية، وغيّرنا اتجاه اللغة للصفحة كاملةً لتكون من اليمين إلى اليسار (انظر إلى السمتين dir و lang ضمن الوسم <html>) - عد إلى درس شريط التنقل في بوتستراب 5. لاحظ أيضًا كيف وضعنا أربعة عناصر <section> ضمن القسم <body> وذلك للتعبير عن الأقسام الأربعة الأساسية في الصفحة الرئيسية. سنبدأ في الفقرة التالية التحدث عن أصول الموقع. تحضير أصول الموقع من خطوط وصور وألوان تُعتبر أصول الموقع من خطوط وصور وألوان بمثابة الروح لجسد الموقع! فمن خلالها يتم إكساب الموقع تلك الجاذبية التي تحفّز الزوار على التفاعل مع صفحاته. في موقعنا "نبيه" سنعمل على بناء تصميم بسيط ولكنّه جذّاب باستخدام أقل الأصول الممكنة، لأنّ الهدف من وراء هذه السلسلة هو التركيز على الاستخدام العملي لبوتستراب 5. الخطوط بالنسبة للخطوط يمكنك زيارة موقع خطوط غوغل والبحث عن الخطين التاليين: Almarai و Tajawal. سأستخدم الخط Tajawal كخط أساسي لمحتوى الموقع، أمّا الخط Almarai فسأستخدمه فقط من أجل اسم الموقع الرئيسي (اسم العلامة التجارية في قسم العنوان). بعد البحث عن خط Tajawal ضمن موقع خطوط غوغل وإيجاد هذا الخط، انقر عليه لفتح الصفحة الرئيسية له. لاحظ توفر أنماط مختلفة لهذا الخط. اختر الأنماط: 200 و 300 و 400 و 500 و 700 و 800 و 900 (انظر الوصف الخاص بكل نمط في الطرف الأيسر) وذلك بالنقر على الزر Select this style الموجود في الطرف الأيمن. ستلاحظ أنّه بمجرّد اختيارك للنمط الأوّل سيبدأ الموقع بتجهيز الرابط اللازم لتضمين هذا الخط في موقعك، كما في الشكل التالي: عُد إلى الصفحة الرئيسية للموقع، وابحث الآن مرة أخرى عن الخط Almarai. اختر الأنماط 300 و 400 و 700 و 800 بنفس الطريقة السابقة. يمكنك في أي وقت استعراض الخطوط والأنماط التي اخترتها بالنقر على زر استعراض عائلات الخطوط المختارة الموجود كأيقونة في الطرف الأيمن العلوي من الصفحة بجوار زر Download family. عند هذه النقطة انسخ الرابط الخاص بعائلات الخطوط التي اخترتها. ستحصل على رابط شبيه بما يلي: <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700;800&family=Tajawal:wght@200;300;400;500;700;800;900&display=swap" rel="stylesheet"> سنضع هذه الروابط ضمن القسم <head> من الصفحة، قبل وسم الإغلاق </head> مباشرة. الألوان والصور تتيح لنا العديد من مواقع الإنترنت استخدام تشكيلة منسجمة من الألوان لاستخدامها في تصميماتنا، وذلك لتوفير الوقت والجهد على المطوّر أو المصمّم في الموائمة بين الألوان واختيار المتجانس منها في تصميماته. بالنسبة لي، استخدمت هذا الموقع لاختيار تشكيلة ألوان موقع "نبيه". حيث عملت على استخدام ألوان بسيطة للغاية ضمنه وهي على الشكل التالي: #eee و #fff و #393e46 و #233e8b. من الأفضل دومًا اختيار أربعة ألوان على الأكثر. انظر إلى موقع ColorHunt. أمّا بالنسبة للصور، فهناك العديد من المواقع الأخرى التي توفّر صور جيدة سواءً كانت مجانية أو بمقابل مادي لاستخدامها في تصميماتك المختلفة. في حالتنا هذه، استخدمنا صور مجّانيّة بالطبع. يمكنك الحصول على مثل هذه الصور من موقع unsplash مثلًا. على أية حال يمكنك الحصول على تشكيلة الصور التي اخترتها من ملف الصور المرفق في نهاية المقال. اعمل على تنزيل الملف، ثم فك ضغطه، وبعد ذلك انقل الصور الموجودة ضمن المجلّد الناتج إلى المجلّد images الذي أنشأناه مسبقًا ضمن مجلّد مشروع "نبيه". تصميم قسم الترويسة سيحتوي قسم الترويسة بحد ذاته على شريط للتنقل بالإضافة إلى مساحة خاصة سنسميها Main View لإبراز الهدف من الموقع كما في الشكل التالي: تصميم شريط التنقل انتقل إلى الملف index.html، ثم أضف ضمن قسم الترويسة header ضمن جسم المستند <body> الشيفرة التالية: <section id="header"> <!-- Nav Bar --> <nav class="navbar navbar-expand-lg navbar-light"> <!-- الموضع 1 --> <div class="container-fluid"> <a class="navbar-brand" href="#"> <img src="/images/nabih-logo.png" style="margin-left: 8px;" alt="" height="32"> نبيه </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <!-- الموضع 2 --> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- الموضع 3 --> <ul class="navbar-nav me-2 mb-2 mb-lg-0 ms-3"> <li class="nav-item"><a class="nav-link" href="#">التصنيفات</a></li> </ul> <form class="d-flex me-auto w-100"> <!-- الموضع 4 --> <input class="form-control" type="search" placeholder="ابحث عن أي موضوع"> <img class="nav-link" src="images/icons8-shopping-cart-32.png" /> <button type="button" class="btn btn-dark nabih-buttons">دخول</button> <button type="button" class="btn btn-outline-dark nabih-buttons">تسجيل جديد</button> </form> </div> </div> </nav> </section> أود التحدث عن بعض النقاط الواردة في الشيفرة السابقة: أولًا، استخدمت في "الموضع 1" الصنف .navbar-expand-lg مع العنصر nav، إذ يسمح هذا الصنف بنشر عناصر شريط التنقّل، أو لنقل يجعلها مرئية عندما يكون عرض الشاشة كبيرًا أو فوق الكبير. أمَا في حال كان الأمر خلاف ذلك، فستُطوى "تختفي" عناصر شريط التنقل ويظهر مكانها زر قابل للنقر. انظر الشكل التالي: يسمح ذلك بإنشاء صفحات ويب متجاوبة مع الأجهزة ذات الشاشات الصغيرة. إذا نقرت الزر الذي يظهر في يسار الشكل السابق، ستظهر العناصر الخاصة بشريط التنقل. ثانيًا، يمكن إنجاز هذه الميزة بوضع زر <button> "الموضع 2" ومنحه الصنف .navbar-toggler. سيحتاج هذا الزر أيضًا إلى أن نمنحه السمة data-bs-target والتي تحدّد معرّف العنصر المراد طيّه. وضعت القيمة #navbarSupportedContent لهذه السمة. وهي معرّف العنصر <div> الموجود في ثالثًا، "الموضع 3" والذي يحتوي بطبيعة الحال على العناصر المراد طيها في حال كان عرض الشاشة أقل من الحجم "كبير" في مثالنا هذا، فستجد في "الموضع 3" عنصر <div> الذي تحدثنا عنه توًا. لاحظ كيف أسندنا له الصنفان collapse navbar-collapse لمنحه قابلية الطي (أو الاختفاء إن صح التعبير في حال كان عرض الشاشة أقل من كبير). على أية حال، عند نقر الزر الصغير، سيظهر هذا العنصر بمحتوياته (عناصر شريط التنقل) مرّة أخرى. رابعًا، يوجد في "الموضع 4" عنصر <form>، وضعت ضمنه مربع البحث بالإضافة إلى سلة التسوّق وزرين لتسجيل الدخول ولتسجيل جديد. في الحقيقة يحتوي هذا العنصر على معظم عناصر شريط التنقّل، وسنتحدث عن المزيد حول عناصر الإدخال من المستخدم لاحقًا في درس قادم. انتقل الآن إلى الملف styles.css الذي أنشأناه في الدرس الثاني، وضع فيه التنسيقات التالية: body{ font-family: 'Tajawal', sans-serif; font-size: 1em; } .nabih-buttons{ margin: 0 4px; padding: 6px 8px; width: 7rem; white-space: nowrap; } .container-fluid{ padding:0 5%; } /*Header Section*/ #header{ background-color: #eee; } .navbar-brand{ font-family: 'Almarai'; color:#393e46; font-size: 2.5rem; font-weight:900; margin: 0; } التنسيقات الثلاثة الأولى هي تنسيقات عامة يمكن استخدامها في كامل الصفحة. التنسيق الأول body مسؤول عن ضبط نوع الخط لكامل الصفحة إستنادًا للخط الذي حصلنا عليه من موقع خطوط غوغل كما مرّ معنا قبل قليل. التنسيق الثاني .nabih-buttons فمسؤول عن ضبط الشكل العام لأي زر سنستخدمه ضمن الصفحة. أمّا التنسيق الثالث .container-fluid فهو تنسيق محجوز لبوتستراب، لكننا سنجري تعديلا بسيطا على الحشوة padding الخاصة به لكي تمنحه مساحة من اليمين ومن اليسار بمقدار 5% كما هو واضح. بالنسبة لما يخص قسم الترويسة Header Section، فقد وضعت حاليًا تنسيقين متعلّقان بشريط التنقل، على الشكل التالي: استهدفت القسم header بتنسيق بسيط، حيث عملت على تغيير لون الخلفية له. التنسيق .navbar-brand هو أيضًا تنسيق محجوز لبوتستراب، ولكن أردت هنا إضافة بعض التعديلات عليه لكي يتناسب مع احتياجاتنا، وخصوصًا فيما يتعلّق بنوع خط العلامة التجارية والذي حصلنا عليه أيضًا من موقع خطوط غوغل. نكون بذلك قد انتهينا من تهيئة شريط التنقّل، ولكننا مازلنا ضمن قسم الترويسة. سننتقل الآن إلى تهيئة قسم العرض الرئيسي Main View في الموقع. تصميم قسم العرض الرئيسي Main View أضف الشيفرة التالية ضمن الملف index.html مباشرةً بعد الشيفرة الخاصة بشريط التنقّل، وقبل وسم الإغلاق < /section> : <!--Main View--> <div class="mainview"> <div class="row"> <div class="col-lg-6"> <h1 class="mainview-heading">دورات غنية بجودة عالية</h1> <h3 class="mainview-subheading"> طور نفسك لتنافس في سوق العمل</h3> </div> <div class="col-lg-6"> <img class="mainview-image" src="/images/show-case.jpg" /> </div> </div> </div> قسم العرض الرئيسي بسيط وواضح. استخدمت هنا مخطط الشبكة، وذلك بإضافة سطر عن طريق وضع الصنف التنسيقي row ضمن عنصر <div> كما هو واضح. ثم عملت على تقسيم هذا السطر إلى قسمين متساويين (لاحظ التنسيقين .col-lg-6 ). وضعت في القسم الأول نصّين وصفيين، أمّا في القسم الثاني فقد وضعت صورة تعبيرية. لكي يظهر قسم العرض الرئيسي بشكل صحيح، أضف التنسيقات التالية إلى آخر محتويات الملف styles.css: .mainview-heading { color: white; font-size: 4em; text-align: right; } .mainview-subheading{ color: white; margin: 64px 0; text-align: right; } .mainview{ background-color: #233e8b; padding: 3% 5%; } .mainview-image{ border-radius: 2%; width: 500px; float: left; } أعتقد أنّ هذه التنسيقات بسيطة وواضحة: نعمل في البداية على تنسيق النصوص التي ستظهر في الجانب الأيمن (الصنفان .mainview-heading و .mainview-subheading)، ستعمل التنسيقات على تحديد لون الخط وحجمه ومحاذاة النص نحو اليمين. نعمل أيضًا على تنسيق عنصر الصورة الذي سيظهر في الجانب الأيسر، حيث نحدّد عرض الصورة ونكسب حوافها الأربعة انحناءً جميلًا، كما نجعلها أيضًا تظهر (تطوف) في أقصى اليسار (float: left). ننسّق أيضًا قسم العرض الرئيسي بكامله (الصنف .mainview) بإكسابه لون خلفية محدّد وإضافة بعض الحشوة المناسبة إلى أطرافه الأربعة. بعد حفظ الملفين index.html و styles.css يمكنك معاينة الصفحة الآن. ستحصل على شكل شبيه بما يلي: نكون الآن قد انتهينا من قسم الترويسة. سنكمل باقي الأقسام الخاصة بالصفحة في الدروس التالية. خاتمة أنجزنا في هذا الدرس الكثير من العمل! حيث خططنا الصفحة الرئيسية وقسمناها إلى أربعة أقسام رئيسية أنجزنا منها القسم الأول (قسم الترويسة). سنتابع عملنا في الدروس التالية في التعرّف على مكوّنات جديدة في بوتستراب 5، بالإضافة إلى إكمال ما تبقى من أقسام رئيسية ضمن موقع "نبيه". ملف ch04-images-resource اقرأ أيضًا المقال التالي: مكون البطاقة Card ومكون الشرائح الدوارة Carousel في بوتستراب بناء قائمة شجرية باستخدام البوتستراب 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap
-
أنت تريد أن تحصل على بيانات من قاعدة بيانات، ثم تريد أن تمر على هذه البيانات بواسطة حلقة تكرارية for. الحقيقة هذا الموضوع منفصل وغير مغطى مع الأسف في هذه السلسلة. توجد عدة طرق للتواصل مع قاعدة بيانات في سي شارب، ويعتمد ذلك على الإصدار الذي تستخدمه من اللغة، بالإضافة إلى نوع قاعدة البيانات المراد القراءة منها. زودني بمثل هذه المعلومات كي أستطيع إعطائك مثال عن ذلك.
-
أهلا وسهلا بك أخ باسل.
-
أهلا شيخه. الحقيقة لم أفهم سؤالك بشكل جيد.
-
مرحبًا بك في الدرس الثالث من سلسلة تعليم بوتستراب 5. سنتعرّف في هذا الدرس على مخطط الشبكة Grid Layout وهو المخطط المعتمد في بوتستراب. سنمر على النقاط التالية في هذا الدرس: الحاوية Container. مخطط الشبكة Grid. استخدام الأعمدة بطريقة متجاوبة. هذا الدرس مهم وأساسي في التعرّف على بوتستراب وكيف يعمل. الحاوية Container تُعَد الحاوية حجرة البناء الأساسية في بوتستراب، وهي عبارة عن عنصر div يحمل التنسيق container أو أخواته، وذلك لكي يكسب هذا العنصر بعض التنسيقات المسبقة والمفيدة مما يسمح باحتواء بقية العناصر ضمن الشاشة المطلوبة بيسر وسهولة. يُعتبر استخدام الحاوية ضروريًا مع مخطط الشبكة ضمن بوتستراب كما سنرى بعد قليل. يوجد ثلاثة أنواع من الحاويات في بوتستراب وهي: الحاوية العادية: عبارة عن عنصر div يحمل صنف التنسيق container. الحاوية الإنسيابية: وهي عنصر div يحمل الصنف التنسيقي container-fluid ويكون عرض هذه الحاوية هو العرض الكامل للشاشة أي 100% الحاويات ذات العرض المخصص: وهي عبارة عن خمس حاويات: container-sm و container-md و container-lg و container-xl و container-xxl ومعناها على الترتيب: الحاوية ذات العرض الصغير، وذات العرض المتوسط، وذات العرض الكبير، وذات العرض الأكبر، وذات العرض الكبير جدًا. الأمر المميز في حاويات العرض المخصّص هو أنّ الصفة المرتبطة معها (صفة العرض) لا تُفعّل إلّا إذا وصل عرض الحاوية إلى عرض أصغري يمكنك أن تدعوه عتبة الاستجابة Break Point. لكي نفهم الموضوع على أكمل وجه، أنشئ ملفًا جديدًا ضمن مجلّد العمل nabih سمّ الملف test-container.html وانسخ الكود التالي إليه مع ملاحظة أنّ التنسيقات التي تحتوي على background-color وضعتها فقط لتمييز الحاويات عن بعضها وليس لها أي دور آخر: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>اختبار الحاويات</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> </head> <body> <div class="container" style="background-color: royalblue;">حاوية عادية</div> <div class="container-sm" style="background-color:sandybrown;">عرض مئة بالمئة حتى عتبة العرض الصغير</div> <div class="container-md" style="background-color: salmon;">عرض مئة بالمئة حتى عتبة العرض المتوسط</div> <div class="container-lg" style="background-color: lightpink;">عرض مئة بالمئة حتى عتبة العرض الكبير</div> <div class="container-xl" style="background-color: lightgreen;">عرض مئة بالمئة حتى عتبة العرض الأكبر</div> <div class="container-xxl" style="background-color: lightsteelblue;">عرض مئة بالمئة حتى عتبة العرض الكبير جدًا</div> <div class="container-fluid" style="background-color: lightpink;">حاوية انسيابية</div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> احفظ التغييرات ثم انقر بزر الفأرة الأيمن على هذا الملف، واختر تشغيله عن طريق الإضافة Live Server. ستظهر محتويات الملف ضمن المتصفح. جرّب الآن تصغير عرض نافذة المتصفّح إلى أصغر حد ممكن، ستلاحظ أنّ جميع الحاويات قد شغلت العرض الأعظمي الممكن 100%كما في الشكل التالي وذلك بسبب أنّ العرض الحالي هو صغير جدًا ولم يصل بعد إلى عتبة الإستجابة الأولى "صغير" : ابدأ الآن بالسحب وتوسيع عرض النافذة تدريجيًا. ستلاحظ أنّ الحاويتان العادية وذات العرض الصغير قد أخذتا منحًا مغايرًا عن باقي الحاويات الأخرى. كما في الشكل التالي: وسبب ذلك أنّ الحاوية العادية يكون عرضها دومًا مساويًا لقيمة الثابت max-width والذي تتغيّر قيمته وفقًا لاستعلامات Media Queries. أمّا الحاوية ذات العرض الصغير فقد بلغت عتبة الاستجابة الخاصة بها عندما بدأت بتوسيع عرض النافذة، وبالتالي سيصبح عرضها مساويًا أيضًا لقيمة الثابت max-width. إذا تابعت توسيع عرض النافذة بنفس الأسلوب، ستلاحظ أنّه عندما تبلغ كل عتبة ستنضم إحدى الحاويات إلى رفيقاتها السابقة في اكتساب العرض max-width حتى إذا ما وصل عرض النافذة لأقصى حد ممكن (على اعتبار أنّك تستخدم شاشة حاسوب عادية) ستحصل على شكل شبيه بما يلي: لاحظ أنّ الحاوية الإنسيابية container-fluid بقي عرضها ثابتًا بحيث يأخذ عرض الشاشة كاملًا 100%. يلخص الجدول التالي عتبات الإستجابة (قيم max-width) لكل من الحاويات السابقة: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } صغير جدا - أصغر من 576 بيكسل صغير - أكبر من أو يساوي 576 بيكسل متوسط - أكبر من أو يساوي 768 بيكسل كبير- أكبر من أو يساوي 992 بيكسل الأكبر - أكبر من أو يساوي 1200 بيكسل كبير جدًا - أكبر من أو يساوي 1400 بيكسل .container 100% 540px 720px 960px 1140px 1320px .container-sm 100% 540px 720px 960px 1140px 1320px .container-md 100% 100% 720px 960px 1140px 1320px .container-lg 100% 100% 100% 960px 1140px 1320px .container-xl 100% 100% 100% 100% 1140px 1320px .container-xxl 100% 100% 100% 100% 100% 1320px .container-fluid 100% 100% 100% 100% 100% 100% يمكنك الآن التخلص من الملف test-container.html من مجلّد المشروع إن أحببت. مخطط الشبكة Grid يمكنك أن تبني باستخدام مخطط الشبكة في بوتستراب واجهات بمختلف الأشكال والأحجام. يعتمد مخطط الشبكة على مفهوم صندوق التخطيط المرن Flex Box في بناء الواجهات، وهو يستخدم، كما سنرى بعد قليل، مزيج من الحاويات والأسطر والأعمدة لتحقيق هذا الهدف. يدعم مخطط الشبكة ست عتبات استجابة بنفس مبدأ عتبات الإستجابة التي مرّت معنا في فقرة الحاويات، وهي: sm و md و lg و xl و xxl، أمّا السادس فهو للقياس: "صغير جدًا" (أقل من 576 بيكسل) وفعليًا ليس له اختصار محدّد. يتكوّن مخطّط الشبكة من 12 عمود. تكون الأعمدة محدّدة ضمن أسطر وذلك للتحكم بعدد الأعمدة الموجودة بكل سطر على حدة. في أي سطر، يتوزّع العرض المُتاح بالتساوي على الأعمدة المتماثلة الموجودة ضمنه. انظر إلى المثال الأساسي التالي الذي يوضّح بنية بسيطة جدًا لاستخدام مخطط الشبكة: <div class="container"> <div class="row"> <div class="col" style="background-color: lightgreen;"> Row 1 - Col 1 </div> <div class="col" style="background-color: lightpink;"> Row 1 - Col 2 </div> </div> <div class="row"> <div class="col" style="background-color: lightsalmon;"> Row 1 - Col 1 </div> <div class="col" style="background-color: lightblue;"> Row 2 - Col 2 </div> <div class="col" style="background-color: lightseagreen;"> Row 3 - Col 3 </div> </div> </div> لاحظ في البداية وجود حاوية وهي ضرورية لكي يعمل مخطط الشبكة. بعد ذلك نبدأ بتعريف الأسطر باستخدام عنصر div مع الصنف التنسيقي row. في الكود السابق عرّفنا سطرين. يحتوي السطر الأوّل على عمودين. نعرّف أي عمود باستخدام العنصر div مع الصنف التنسيقي col (أو أخواته كما سنرى بعد قليل) أمّا السطر الثاني فيحتوي على ثلاثة أعمدة كما هو واضح. النصوص التي تراها في الكود السابق مثل: "Row 1 - Col 1" هي نصوص توضيحية، أيضًا هنا، جميع تنسيقات background-color هي فقط لتوضيح البنية الناتجة وليس لها أي دور في مخطّط الشبكة. لاختبار الكود السابق بسرعة يمكن تجريبه مباشرة على موقع مثل codeply.com. يمكنك فيه تجريب الكود بلا تسجيل مسبق. اختر Bootstrap 5 من القائمة الجانبية اليسرى ليظهر مختبر جاهز لتجريب الكود. احذف جميع الكود التلقائي الموجود مسبقًا في قسم HTML الموجود في الناحية اليسرى، وانسخ الكود السابق إليه ضمن هذه المنطقة. بعد ذلك ستلاحظ وجود أيقونة صغيرة أعلى الصفحة تحمل ما يشبه زر تشغيل، انقرها لترى النتائج في الأسفل. ستحصل على شكل شبيه بما يلي: يمكنك استخدام هذا الموقع دومًا في حال أردت تجريب ميزة جديدة بسرعة. كما أوضحنا توًا لدينا 12 عمود ضمن مخطّط الشبكة لكل منها العرض نفسه. ولكن في بعض الأحيان قد نحتاج إلى أن يكون أحد الأعمدة أعرض من باقي الأعمدة الأخرى، أو أن تحتاج أن يكون للأعمدة ضمن سطر ما عرض مختلف لكل منها. في الحقيقة يدعم مخطّط الشبكة هذا الأمر بصورة جميلة. لكي نفهم هذا الأمر تمامًا، تخيّل أنّ العرض المتاح ضمن الحاوية مقسّم إلى 12 حصّة متساوية فيما بينها. عندها يسمح بوتستراب بتخصيص أي عدد من هذه الحصص لأي عمود. لنفرض مثًلا أنّه يوجد لدينا ثلاثة أعمدة ضمن أحد الأسطر. يمكننا مثلًا تخصيص حصتين للعمود الأول وأربع حصص للعمود الثاني ونترك العمود الثالث في الحالة الافتراضية، أي بدون تعيين أي حصة له. أي كما في الكود التالي: <div class="container"> <div class="row"> <div class="col-2" style="background-color: lightgreen;"> Row 1 - Col 1 </div> <div class="col-4" style="background-color: lightpink;"> Row 1 - Col 2 </div> <div class="col" style="background-color: lightblue;"> Row 1 - Col 3 </div> </div> </div> لاحظ كيف عيّنت الحصص من خلال الأرقام الموجودة بجوار الصنف التنسيقي col، فالعمود الأول أصبح له الصنف التنسيقي col-2 أمّا العمود الثاني فأصبح له الصنف التنسيقي col-4 أمّا العمود الثالث فله الصنف التنسيقي col فقط بدون أي تخصيص للحصص. جرّب الكود السابق في codeply لتحصل على شكل شبيه بما يلي: لاحظ كيف أنّ عرض العمود الثاني هو ضعف العمود الأوّل وهذا أمر منطقي بالطبع، ولاحظ أيضًا أنّ العمود الثالث الذي لم نخصص له أي حصة قد شغل باقي الحصص المتاحة وهي في حالتنا هذه 6 حصص (تذكر حصة العمود الأول + حصة العمود الثاني = 6). إذًا، عندما لا نحدّد أي حصة للعمود فإنّه يشغل دومًا باقي الحصص المتاحة. ولو فرضنا وجود عمود رابع لم نحدد له أيضًا أي حصة (مثل العمود الثالث) فإنّه سيتقاسم باقي الحصص المتاحة الباقية مع العمود الثالث (سيأخذ كل واحد منها 3 حصص) وهكذا. ولا يهم بالطبع فيما إذا كان مثل هذين العمودين (أو أكثر) متجاورين أم متفرقين ضمن بقية الأعمدة الأخرى في حال وجودها. من الواضح بحسب السيناريو السابق أنّ عدد الأعمدة التي يمكننا إنشاؤها ضمن مخطّط الشبكة هي 12 عمود على الأكثر. وإذا نظرنا إلى هذا الأمر من زاوية أخرى، فيجب أن يكون مجموعة الحصص دومًا يساوي 12 حصة. استخدام الأعمدة بطريقة متجاوبة كما رأينا في الفقرة السابقة يمكننا إنشاء حتى 12 عمود ضمن مخطط الشبكة، ولكن المشكلة أنّ هذه الأعمدة ستسلك نفس السلوك عبر جميع الشاشات الممكنة. بمعنى آخر لا يمكننا بحسب المعلومات التي حصلنا عليها في الفقرة السابقة أن نغيّر مثلًا عرض أي عمود إذا تغيرت الشاشة الحالية من شاشة جوال إلى شاشة حاسوب كبيرة وهذا أمر مرغوب جدًا. ففي الحالة العامة يمكن لأي موقع أن يُعرض على شاشة حاسوب عادي، أو على شاشة جوال، ومن المفترض أن يتغير شكل الموقع بشكل يتناسب مع حجم الشاشة التي يُعرض ضمنها لكي يُعتبر هذا الموقع متجاوبًا. في الحقيقة يوجد حل أنيق يوفره لنا بوتستراب يتمثّل في إضافة بسيطة بجوار رقم الحصة التي ننوي تخصيصها للعمود وذلك على الشكل التالي: col-{sm|md|lg|xl|xxl}-n فمثًلا يمكننا أن نخبر أحد الأعمدة أننا نرغب بأن يأخذ 6 حصص في حال كانت الشاشة متوسطة العرض أو أكثر عندها نكتب col-md-6. في هذه الحالة سيأخذ العمود نصف العرض المتاح للشاشة إذا كان قياسها متوسط أو أعلى. أمّا إذا كان قياس الشاشة أقل من متوسط فإنّه سيأخذ العرض الكامل المتاح للشاشة (إذا كان العمود موجود لوحده ضمن السطر) لأننا لم نخبر العمود كيف يتصرف في حال كانت الشاشة أصغر من القياس المتوسط. يمكننا أيضًا الدمج بين أكثر من صنف تنسيقي لكي نُكسب العمود سلوكًا مختلفًا من أجل قياسات مختلفة للشاشة. انظر مثلًا إلى العمود التالي: <div class="col-6 col-md-4"></div> سيأخذ العمود السابق نصف العرض المتاح لأي شاشة يكون قياسها أقل من المتوسط بسبب وجود الصنف التنسيقي col-6. أمّا عندما يبلغ قياس الشاشة متوسط أو أكثر فعندها سيأخذ العمود أربع حصص فقط من عرض الشاشة بسبب وجود الصنف التنسيقي col-md-4. يمكن إضافة أي عدد من الأصناف التنسيقية التي تضبط عرض العمود من أجل كل قياس من القياسات الخمسة الرئيسية التي تعرفنا عليها في هذا الدرس. لنأخذ الآن مثال أكبر قليلًا يحتوي على عدة أسطر وأعمدة لكي نتأكّد من أنّنا قد استوعبنا الفكرة جيّدًا. انظر إلى الكود التالي: <div class="container"> <!-- الموضع 1 --> <div class="row"> <div class="col-md-8">.col-md-8</div> <div class="col-6 col-md-4">.col-6 .col-md-4</div> </div> <!-- الموضع 2--> <div class="row"> <div class="col-6 col-md-4">.col-6 .col-md-4</div> <div class="col-6 col-md-4">.col-6 .col-md-4</div> <div class="col-6 col-md-4">.col-6 .col-md-4</div> </div> <!-- ستأخذ الأعمدة في هذا السطر 50% من العرض الكلي المتاح بصرف النظر عن نوع الشاشة --> <div class="row"> <div class="col-6">.col-6</div> <div class="col-6">.col-6</div> </div> </div> انظر إلى الموضع 1 ضمن الكود السابق. في الشاشات الصغيرة ستتكدّس الأعمدة في هذا السطر فوق بعضها بحيث يحتل العمود الأول العرض الكلي والعمود الثاني نصف العرض الكلي. أمّا في الكود الذي يلي الموضع 2، فيشير إلى أنّه في الشاشات الصغيرة سيحتل كل عمود 50% من العرض الكلي. أمّا في الشاشات المتوسطة فأكبر، سيأخذ كل عمود ثلث العرض الكلي المتاح. بادر فورًا إلى تجربته ضمن Visual Studio Code (وليس ضمن codeply.com لكي تحصل على تجربة أدق). صغّر نافذة المتصفّح وكبّرها ولاحظ ماذا يحدث للأعمدة. يمكنك بالطبع إضافة تنسيقات لونية باستخدام background-color لكي تلاحظ التغييرات. ستحصل في حالة الشاشة الكبيرة على شكل شبيه بما يلي: يمكنك أيضًا استخدام أدوات المطوّر التي توفّر تجربة جيدة للمطورين في تجربة واختبار تطبيقات الويب الخاصة بهم. يمكنك قراءة المزيد حول هذا الموضوع من هنا. خاتمة نكون بهذا قد وصلنا إلى نهاية هذا الفصل المهم الذي سنبقى نستقي من المعلومات الواردة فيه حتى نهاية هذه السلسلة. سنعمل في الدرس التالي على تطبيق المفاهيم الواردة في هذا الدرس ضمن موقعنا الذي نبنيه تدريجيًا "نبيه"، وسنعمل في الدرس القادم على تحديث مشروعنا "نبيه" بالمعلومات الواردة في هذا الدرس بالإضافة إلى المعلومات التي سنحصل عليها في الدرس القادم. اقرأ أيضًا شريط التنقل في بوتستراب 5 مدخل إلى إطار العمل بوتستراب 5 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap
-
أهلًا بكم في الدرس الثاني من سلسلة تعليم بوتستراب 5، سنتعلم في هذا الدرس كيف ننشئ شريط تنقّل باستخدام بوتستراب من خلال موقع بسيط جدًا وظيفته توضيح هذه الفكرة، ومن ثمّ سنعمل على دعم اللغة العربية من خلال جعل جهة الموقع من اليمين إلى اليسار. سنركّز على النقاط التالية في هذا الدرس: تجهيز البنية التحتية للمشروع معاينة الموقع ضمن خادوم تجريبي لبُّ الموضوع: شريط التنقّل في بوتستراب تعديل الموقع ليدعم الاتجاه من اليمين إلى اليسار تجهيز البنية التحتية للمشروع لنعمل على تجهيز البنية التحتية لمشروعنا الخاص ببيع الدورات التعليمية "نبيه". أنشئ مجلّدًا جديدًا في المكان الذي ترغبه وسمّه nabih. انتقل بعد ذلك إلى Visual Studio Code واختر من القائمة File الأمر Open Folder ثم انتقل إلى المجلّد الذي أنشأته توًّا ثم اختر Select Folder لفتحه. أصبحت الآن جاهزًا للبدء بإضافة ملفات المشروع. ستلاحظ في الجهة اليسرى من نافذة Visual Studio ظهور المجلّد الذي اخترناه قبل قليل مع توفّر إمكانية إضافة ملفات أو مجلّدات أخرى إليه كما في الشكل التالي: أضف الملف index.html الذي سيمثّل ملف الواجهة الرئيسية في موقعنا. أضف أيضًا المجلدين التاليين: css الخاص بملفات التنسيق و images المجلّد الذي سنضع فيه الصور المستخدمة بالموقع (سنحتاج إليه في دروس لاحقة). أضف أيضًا الملف styles.css ضمن المجلّد css. سيكون هذا الملف في درسنا هذا فارغًا، ولكن سنعمل على استخدامه في دروس لاحقة. ستحصل في النهاية على شكل شبيه بالتالي: افتح الملف index.html بالنقر المزدوج عليه ثم انسخ الكود التالي إليه: <!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <title>السلام على الجميع!</title> </head> <body> <h1>Hello, world!</h1> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> احفظ التغييرات الأخيرة. وبهذا يصبح الملف index.html جاهزًا للمعاينة. معاينة الموقع ضمن خادوم تجريبي إذا كنت تذكر في المقال الأول من هذه السلسلة، فقد أضفنا الإضافة Live Server إلى Visual Studio Code. سنستخدم هذه الإضافة الآن لمعاينة الصفحة index.html ضمن المتصفّح الإفتراضي. انقر بزر الفأرة الأيمن على الملف index.html ثم اختر الأمر Open with Live Server. سيؤدي ذلك إلى فتح نافذة أو لسان من المتصفّح الافتراضي على حاسوبك، وسترى الآن الصفحة index.html وقد ظهرت محتوياتها ضمن نافذة المتصفّح. ستحصل على شكل شبيه بما يلي: يعد هذا مؤشرًا على أنّ الأمور تجري على ما يرام. لاحظ العنوان الظاهر في شريط العنوان في نافذة المتصفّح من الشكل السابق: 127.0.0.1:5501 العنوان 127.0.0.1 هو عنوان IP للجهاز المحلّي. أمّا المنفذ 5501 فهو المنفذ الذي تُصغي فيه الإضافة Live Server للطلبات الواردة من المتصفح، يمكن بطبيعة الحال أن يختلف هذا الرقم على حاسوبك. شريط التنقل في بوتستراب تمتلك معظم المواقع على الإنترنت شريطًا رئيسيًا يتموضع عادةً في الجزء العلوي من الصفحة. يحتوي هذا الشريط على روابط تساعد في التنقّل السريع بين أجزاء الموقع. من الممكن أن تكون بعض هذه الروابط على شكل أزرار. يمكن أن يؤدي النقر على بعض هذه الأزرار إلى ظهور قائمة منسدلة مثلًا تحتوي على أوامر أخرى. كما ويمكن أن يحتوي هذا الشريط على مربع نص يسمح للمستخدمين بالبحث ضمن الموقع، وغيرها من المتطلّبات الأخرى. يدعم بوتستراب شريط القائمة هذا بصورة ممتازة. فعند ضبط بعض أصناف التنسيق القليلة ستحصل على شريط تنقّل عصري يلبّي احتياجاتك المتنوّعة. شريط تنقل بسيط لنبدأ بتشكيل شريط تنقّل بسيط. الكود التالي هو نسخة معدّلة عن الكود الموجود في صفحة التوثيق الخاصة الخاصة بشريط التنقّل في بوتستراب. انسخ الكود التالي إلى الملف index.html وضعه مكان العنصر <h1> الذي يحتوي على رسالة الترحيب التي رأيناها في الفقرة السابقة: <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#">نبيه</a> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">رابط فعال</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي1</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي2</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">غير فعال</a> </li> </ul> </div> </nav> غيّر أيضًا عنوان الصفحة (ضمن الوسم title) ليصبح "دورات نبيه". بعد حفظ التغييرات ستحصل على الشكل التالي في المتصفح (إذا لم تظهر التغييرات فورًا، يمكنك تحديث الصفحة ضمن المتصفّح): شرح الكود السابق سنتحدّث بشيء من الإسهاب عن الكود السابق كونه أساسي في بناء أشرطة التنقّل في بوتستراب. إذا نظرت إلى الكود السابق سترى أنّ العنصر الأساسي المستخدم هنا هو nav وهو عنصر HTML يُستخدم عادةً عندما نريد بناء شريط تنقل في صفحات الويب. يمكن أيضًا استخدام عنصر div عادي بدلًا من العنصر nav، ولكن عندها سنحتاج إلى بعض التعديلات الطفيفة. لذلك سنبقي على العنصر nav. لاحظ أيضًا أصناف التنسيق المستخدمة مع العنصر nav التي تأتي مع بوتستراب: navbar navbar-expand-lg navbar-light bg-light سنتحدّث هنا قليلًا عن هذه الأصناف الأربعة: الصنف navbar فهو يمنح العنصر nav هيئة شريط التنقّل مباشرةً. حيث يصبح متجاوبًا مع قياسات الشاشات المختلفة، ويقبل احتواء العناصر المفيدة الأخرى مثل العلامة التجارية branding الخاصة بالموقع، وروابط التنقّل، وإمكانية ضم الأزرار ضمن قائمة مطوية واحدة تفتح بزر collapsing للحد من عرض الشريط في حال الشاشات الصغيرة كما سنرى ذلك فيما بعد. يحتاج الصنف navbar كي يعمل بطريقة صحيحة إلى الصنف navbar-expand-lg وأخواته navbar-expand-{-sm|-md|-lg|-xl|-xxl} . ووظيفة هذه الأصناف هي السماح بنشر محتويات شريط التنقّل بشكل أفقي. جرب أن تزيل هذا الصنف، لتجد أنّ محتويات شريط التنقّل قد أصبحت مكدّسة عموديًا. بالنسبة للصنف navbar-light فهو يتحكم بالنمط theme يوجد ثلاثة أنماط رئيسية في بوتستراب هي: النمط الفاتح navbar-light والنمط الغامق navbar-dark والنمط الرئيسي navbar-primary. تتحكم هذه الأصناف بلون الخط للعناصر الموجودة ضمن شريط التنقّل nav. تعمل الأصناف الثلاثة السابقة مع الصنف الذي يتحكم بلون الخلفية لشريط التنقّل وهو عبارة عن مجموعة من الأصناف مثل: bg-light و bg-dark و bg-primary و bg-success و bg-warning. لكي تحصل على أفضل النتائج استخدم الصنف bg-light مع الصنف navbar-light والصنف bg-dark مع الصنف navbar-dark. الحرفان bg من الصنف bg-light أو الصنف bg-dark أو غيرهما، يعبّران عن الكلمة background أي الخلفية. يجعل الصنف bg-light خلفية شريط التنقّل ذا لون فاتح. كما أنّ الصنف bg-dark يجعل الخلفية تبدو غامقة، وهكذا. بالنسبة للأصناف الباقية، جرّب استخدام مزيج من أصناف التنسيق مع أصناف الخلفية لكي تختار منها ما يناسبك. جرب مثلًا النمط الغامق navbar-dark مع bg-success وهكذا. يدعم شريط التنقّل في بوتستراب (ومحتوياته أيضًا) الشكل المرن أو المتدفق Fluid في التصميم. بمعنى أنّ شريط التنقّل ينتشر أفقيًا ليحاول تعبئة عرض الشاشة المتاح. يمكن استخدام أي حاوية Container تراها مناسبة للتحكّم في مدى الانتشار الأفقي. هذا ما يبرّر استخدامنا لعنصر div مع الصنفcontainer-fluid الموجود ضمن العنصر nav مباشرةً (انظر الكود السابق). في هذه الحالة استخدمنا هذه الحاوية لضبط الانتشار الأفقي لمحتويات شريط التنقّل. على العموم يمكننا استخدام نفس التقنية تمامًا مع العنصر nav نفسه. سنتحدث عن الحاويات لاحقًا في هذه السلسلة. ولكن إذا أردت معرفة فائدة وجود هذه الحاوية. أزل صنف التنسيق container-fluid من عنصر div الذي يأتي ضمن العنصر nav مباشرةً لترى كيف يؤثّر وجودها على محتويات شريط التنقّل. سنضع ضمن عنصر div (الذي يحوي الصنف container-fluid) محتويات شريط التنقّل. والتي تنحصر في مثالنا هذا على اسم الموقع، بالإضافة إلى أربعة عناصر توضّح أنواع الروابط التي يمكن وضعها ضمن شريط التنقّل. قطعة الكود التالية موجودة ضمن عنصر div ذا الصنف container-fluid من الكود السابق: <a class="navbar-brand" href="#">نبيه</a> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">رابط فعال</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي1</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي2</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">غير فعال</a> </li> </ul> نضع عادةً اسم الموقع ضمن عنصر الارتباط التشعبي a. سيكون لهذا العنصر التنسيق navbar-brand ليُنسّق على شكل علامة تجارية أو أيقونة واضحة للموقع. يمكن بالطبع إضافة صورة لاسم الموقع بإضافة عنصر صورة كما سنرى فيما بعد. بعد عنصر الارتباط التشعبي وضعنا عنصر القائمة غير المرتبة ul وهي التي ستحوي الروابط الرئيسية على شريط التنقّل. لاحظ كيف وضعنا الصنف navbar-nav لهذه القائمة كي تنسجم مع شريط التنقل الموضوعة ضمنه. ولاحظ أيضًا وجود صنف آخر هو me-auto. أول حرفين من اسم هذا الصنف يشيران إلى الكلمتين margin end، وتعنيان الهامش من النهاية. مع الكلمة -auto يعني ذلك أنّنا سنجعل الهامش من الطرف النهائي تلقائي أي أنّه سيأخد أكبر هامش ممكن من الطرف النهائي، وهذا ما سيجعل عنصر القائمة يُزاح كاملًا نحو اليسار (نحو البداية) بجوار اسم الموقع (جرب إزالة هذا الصنف وانظر ماذا سيحدث). قد تتسائل عن سبب استخدام تعبير "الهامش من النهاية" في حين أنّه كان من الممكن استخدام التعبير "الهامش من اليمين" فحسب! الجواب على ذلك بسيط. باستخدام هذا التعبير الرائع نستطيع إكساب موقعنا قابلية الاتجاه من اليمين إلى اليسار بكل بساطة. فعندما يكون اتجاه موقعنا من اليسار إلى اليمين تكون البداية من اليسار والنهاية من اليمين أمًا عندما يكون اتجاه موقعنا من اليمين إلى اليسار يحدث العكس! بمعنى آخر أنّه يمكن تعديل اتجاه موقعنا البسيط كاملًا ليصبح من اليمين إلى اليسار (بدلًا من الوضع الحالي) وذلك بتعديل بسيط جدًا في الموقع كما سنرى ذلك بعد قليل. قبل الإصدار 5 من بوتستراب، كان من الممكن الوصول إلى نفس النتيجة من خلال استخدام الصنف mr-auto وهو اختصار للكلمات: margin right auto، وتعني أنّ الهامش من الطرف الأيمن سيكون تلقائي. من الواضح أنّ هذا الصنف مفيد عندما عندما يكون اتجاه الصفحة من اليسار إلى اليمين. ولكن عندما يكون اتجاه الصفحة من اليمين إلى اليسار، سيكون استخدام هذا الصنف غير مناسب كما هو واضح. هذا ما كان يجبر المطورين على إنشاء قالبين منفصلين لصفحات الموقع في حال كان هناك حاجة لدعم لغتين باتجاهين مختلفين (مثل اللغة العربية، واللغة الانجليزية). وصلنا الآن إلى عناصر القائمة li. في الحقيقة هي عناصر بسيطة يحمل كل منها الصنف nav-item، ويحوي كل منها عنصر ارتباط تشعبي a. ولكل عنصر ارتباط تشعبي منها الصنف nav-link. العنصر الأوّل الذي يحوي النص "رابط فعال" سيكون له بالإضافة للصنف nav-link الصنف active للإشارة إلى أنّ الرابط الحالي يُشير إلى الصفحة الحالية التي تُعرض أمام المستخدم. وهذا أمر مفيد كما هو واضح. أمّا العنصران الثاني والثالث اللذان يحملان النص "عادي1" و "عادي2" على الترتيب فهما عنصران عاديان افتراضيان على شريط التنقّل. أمّا العنصر الأخير: "غير فعّال" فقد نحتاج أحيانًا إلى جعل أحد الارتباطات على شريط التنقّل غير فعّالة، وهذا يمكن الحصول عليه بتطبيق الصنف disabled كما هو واضح. هذا كل شيء! في الفقرة التالية سنحوّل الموقع السابق إلى الاتجاه الذي يناسب اللغة العربية. تعديل الموقع ليدعم الاتجاه من اليمين إلى اليسار بقي علينا إضافة بعض اللمسات الأخيرة لكي نُكسب الموقع الاتجاه المناسب لدعم اللغة العربية. في البداية أود أن أشير إلى أنّ ميزة الاتجاه من اليمين إلى اليسار RTL هي ميزة تجريبية في بوتستراب حتى وقت كتابة هذا المقال. ولكن ذلك لا يمنع بالطبع من استخدامها لأنّه من المتوقّع في الفترة القريبة القادمة أن تصبح نهائية بعد التأكّد من استقرارها. نحتاج لاستخدام الاتجاه من اليمين إلى اليسار RTL إلى إجراء ثلاثة تعديلات بسيطة ضمن الملف index.html: 1- استبدال القيمة ar بالقيمة en للسمة lang ضمن الوسم html. 2- إضافة سمة جديدة للوسم html وهي السمة dir وهي مسؤولة عن تحديد اتجاه الصفحة. سنسند القيمة rtl لها. 3- استبدال العنصر التالي بالعنصر link المسؤول عن استيراد مكتبة التنسيق الخاصة ببوتستراب: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> هذه نسخة من مكتبة بوتستراب التي تدعم (لاحظ اسمها bootstrap.rtl.min.css). يجب أن يكون الكود النهائي الموجود ضمن الملف index.html مماثلًا لما يلي: <!doctype html> <html lang="ar" dir="rtl"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.rtl.min.css" integrity="sha384-gXt9imSW0VcJVHezoNQsP+TNrjYXoGcrqBZJpry9zJt8PCQjobwmhMGaDHTASo9N" crossorigin="anonymous"> <link href="/css/styles.css" rel="stylesheet" /> <title>دورات نبيه</title> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#">نبيه</a> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">رابط فعال</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي1</a> </li> <li class="nav-item"> <a class="nav-link" href="#">عادي2</a> </li> <li class="nav-item"> <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">غير فعال</a> </li> </ul> </div> </nav> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> عاين الملف index.html باستخدام الإضافة Live Server لتحصل على شكل شبيه بما يلي: لاحظ أنّنا لم نمس كود HTML مطلقًا. وذلك بسبب استخدمنا لأصناف تنسيقية تصلح للاستخدام في طريقتي العرض يمين إلى يسار RTL أو يسار إلى يمين LTR. ففي حالتنا هذه استخدمنا الصنف me-auto المسؤول عن سلوك الهامش من النهاية، والذي أصبح في هذه الحالة من جهة اليسار بدلًا من جهة اليمين كما في الفقرة السابقة. يمكنك تنزيل شيفرة المقال من الملف hsoub-bootstrap5-ch02.zip. خاتمة أرجو أن تكون قد استمتعت بهذا الدرس! لقد تعلّمنا الكثير في هذا الدرس حول شريط التنقّل في بوتستراب. حيث بنينا شريط تنقّل بسيط، وأسهبنا في الشرح حول أجزاء هذا الشريط، وكيفية الإستفادة من التقنيات الجديدة في بوتستراب 5 لتحويل اتجاه الصفحة لتصبح من اليمين إلى اليسار. وفي الدرس التالي سنتحدّث عن بنية صفحة الويب باستخدام بوتستراب. اقرأ أيضًا 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap بناء قائمة شجرية باستخدام البوتستراب تصميم صفحة موقع باستخدام 3 Bootstrap - الجزء الأول
-
مرحبًا بك في هذه السلسلة التي سنتحدث من خلالها عن إطار العمل Bootstrap 5 ذلك الإطار الذي يهيمن على معظم عمليات التطوير التي تحدث في الواجهة الأمامية Front-End Development في مواقع الإنترنت عمومًا. لا تفترض هذه السلسلة أن يكون لديك أي فكرة مسبقة عن بوتستراب Bootstrap (ولو أنّ بعض المعرفة القليلة مرحّب بها)، في حين أنّ الإلمام بـ HTML و أساسيات CSS هو أمر ضروري للمتابعة في هذه السلسلة. رغم أنّ بوتستراب يستخدم جافاسكريبت في بعض المكوّنات إلى أنّه ليس من الضروري أن يكون لديك معرفة بها لكي تتعلّم بوتستراب، ولو أنّ الإلمام بجافاسكريبت يُعدّ بديهيًا لمطوري الواجهة الأمامية عمومًا. هذا هو المقال الأوّل في هذه السلسلة، وهو مقال تمهيدي بطبيعة الحال، وفيه سنتحدّث عن النقاط التالية: ماهو بوتستراب؟ مالجديد في بوتستراب 5؟ إعداد بوتستراب للعمل. استخدام محرر برمجي مناسب ما الذي سنبنيه في هذه السلسلة؟ ماهو بوستراب؟ بوتستراب ببساطة هو إطار عمل Framework متكامل مبني على CSS و جافاسكريبت JavaScript يُستخدم لتنسيق صفحات الويب وإكسابها نواح جمالية بدون الحاجة إلى استخدام تنسيقات معقدة من CSS. يدعم بوتستراب مبدأ تنسيق الأجهزة المحمولة أولًا Mobile First Style وهذا يعني توافقية عالية مع الأجهزة المحمولة ذات الشاشات الصغيرة. عندما يُذكر بوتستراب فأول ما يتبادر إلى الذهن هو السرعة والأناقة والسهولة والتصميم المتجاوب Responsive Design مع مختلف أنواع الشاشات. كان أول من طور بوتستراب شركة تويتر الشهيرة، وبعد عام تقريبًا، جعلته مفتوح المصدر ومتاحًا بالكامل من خلال GitHub. يستند بوتستراب كما أشرنا مسبقًا إلى CSS فهو يوفّر كمّا كبيرًا من الجهد لتنسيق المكوّنات في صفحة الويب. يهدف بوتستراب كما هو واضح إلى تبسيط عمليات التصميم التي تحدث في الواجهة الأمامية Front-End وجعلها معيارية. يسهّل هذا الأمر إلى حدّ بعيد من حياة المصممين والمطورين على حدّ سواء، ويجعل عملية انضمام مصمّم جديد إلى فريق العمل في شركة تعتمد بوتستراب أمرًا يسيرًا نسبيًا. استخدام بوتستراب سهل جدًا. فيمكن من خلال إضافة صنف Class أو أكثر إلى عنصر HTML أن تحصل على أثر فوري يحوّل هذا العنصر إلى شكل جميل وعصري. انظر مثلًا إلى شيفرة HTML التالية: <button>مرحبًا</button> سيولّد الكود السابق الزر البسيط التالي: سأضيف الآن صنفان بسيطان من بوتستراب إلى الكود السابق: <button class="btn btn-primary">مرحبًا</button> ستحصل على الشكل الجميل التالي: لاحظ كيف اكتسب هذا الزر الألوان المناسبة بالإضافة إلى حاشية Padding مناسبة أيضًا وبتلقائية حول النص الموجود ضمن الزر. وأيضًا كيف أصبحت حواف الزر منحنية. توجد العديد من التشكيلات الأخرى التي يمكن اكسابها للأزرار كما سنرى فيما بعد. مالجديد في بوتستراب 5؟ يُعَد الإصدار 5 إصدارًا رئيسيًا وهو الأحدث من بوتستراب حاليًا (وقت كتابة هذا المقال)، وكما جرت العادة، فهناك العديد من التغييرات التي طرأت على الإصدار الذي يسبقه (الإصدار 4) فمثًلا فقد أزيل المكوّن Jumbotron بالإضافة إلى إزالة الدعم عن المتصفحين IE 10 و IE 11، وأيضًا أزيل عدد من أصناف التنسيق التي كانت موجودة في الإصدار 4. هناك تغييرات أخرى قد حدثت ضمن مخطط الصفحة حيث أضيف قياس آخر جديد لم يكن موجودًا في الإصدار السابق وهو القياس xxl. و توجد تحسينات أخرى في نظام الألوان. كما أضيف دعم أيقونات SVG جميلة ومتنوّعة ومفتوحة المصدر أيضًا. يمكنك مع مكتبة الأيقونات هذه الاستغناء عن مكتبات أيقونات إضافية مثل Font Awesome. في الإصدار 5 أيضًا لم يعد هناك حاجة لاستخدام المكتبة jQuery بعد الآن (بعض المزايا الموجودة في بوتستراب تحتاج إلى جافاسكريبت)، فقد انتقل مطوّرو بوتستراب إلى استخدام جافاسكريبت فقط، مع إمكانية الإبقاء على استخدام jQuery في حال الرغبة. أود أن أركّز هنا على ميزة مهمة أضيفت إلى بوتستراب 5، طالما انتظرها المصممين والمطورين العرب! وهي دعم الاتجاه من اليمين إلى اليسار RTL مما يسهّل حياتهم إلى حدّ كبير. إعداد بوتستراب للعمل يمكن تضمين بوتستراب في صفحة الويب التي نعمل بها بإدراجه عن طريق العنصر link ضمن العنصر head في ترويسة الصفحة، ويمكن اختيار تضمين بوتستراب من مزوّد محتوى على الإنترنت CDN وهو الخيار الأفضل. ومن الممكن كذلك تنزيل نسخة من بوتستراب محليًا ومن ثمّ تضمينها ضمن صفحة الويب التي تعمل بها. سنعمل في هذه السلسلة على الخيار الأول، أي أنّنا سنستخدم مزوّد محتوى CDN. سنعتمد القالب الأساسي التالي أثناء عملنا على بوتستراب: <!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <title>Hello, world!</title> </head> <body> <h1>السلام على الجميع</h1> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script> </body> </html> لاحظ أنّه وقبل نهاية وسم الإغلاق </body> وضعنا الوسم <script> الذي يسمح بتحميل كود جافاسكريبت الذي تحتاجه بعض مكوّنات بوتستراب لكي تعمل عملًا صحيحًا. استخدام محرر برمجي مناسب توجد العديد من الخيارات المتاحة لاستخدام محرّر برمجي مناسب في سياق عملك كمطوّر واجهة أمامية، ومن المحرّرات الشهيرة Visual Studio Code و Sublime و Atom. بالنسبة لي أفضل استخدام Visual Studio Code من مايكروسوفت لما يتمتع به من مرونة ودعم كبيرين. لتثبيت محرر vs code انتقل إلى الموقع الخاص به لتنزيله. سيظهر زر التحميل في الصفحة الرئيسية. عندما تنتهي من تنزيله وتثبيته، افتحه وانتقل إلى قسم الإضافات Extensions ضمنه في الشريط الموجود على الناحية اليسرى كما في الشكل التالي: بعد ذلك وفي خانة البحث في الأعلى اكتب Live Server للبحث عن هذه الإضافة التي ستسمح لك بتشغيل خادوم مبسّط على حاسوبك الشخصي مما يسمح لنا بتجريب الشيفرة التي نكتبها مباشرةً على المتصفح الافتراضي الموجود على حاسوبنا. بعد اختيار الإضافة انقر الزر Install في الناحية الخاصة بشرح الإضافة (في الطرف الأيمن) لتثبيتها. بنفس الأسلوب السابق تمامًا ننصحك بتثبيت إضافة Auto Rename Tag، حيث تساعد هذه الإضافة على التعديل التلقائي لوسم ما عند تعديل الوسم المرافق له. ما الذي سنبنيه في هذه السلسلة؟ سنبني في هذه السلسلة موقع ويب بسيط عبارة عن صفحة واحدة يمكن أن يُعتبر كقالب يشرح مزايا بوتستراب 5 التي سنتناولها تباعًا أثناء تقدمنا في هذه السلسلة. هذا الموقع عبارة عن موقع يبيع دورات تعليمية على الإنترنت. يوفر الموقع إمكانية التسجيل الجديد للمستخدمين بالإضافة إلى تسجيل دخول للمستخدمين السابقين. كما سيعرض آخر الدورات التدريبية (المنتجات) المتوفرة، وأيضًا الدورات الأكثر مبيعًا. كما سنوفّر ميزة سلة المشتريات التي تسمح للمستخدم بالتسوّق من خلال اختيار الدورات التي يرغب بشرائها. بالإضافة إلى ما سبق سنضيف بعض الأقسام على الصفحة الرئيسية التي توفّر بعض المعلومات عن الموقع وبنفس الوقت توضّح لنا كيفية استخدام مزايا بوتستراب 5 المتنوّعة. سنعمل على بناء هذا الموقع شيئًا فشيئًا أثناء عملنا في هذه السلسلة. انظر إلى الشكل التالي الذي يعطيك شكلًا تقريبيًا لما سنحصل عليه في نهاية هذه السلسلة. مصدر الصور في آخر هذه السلسلة سنتعلّم كيف نرفع الموقع كاملًا على إحدى الإستضافات لكي يصبح بالإمكان معاينته بصورة حية. اقرأ أيضًا 10 أخطاء شائعة عند استخدام إطار العمل Bootstrap بناء قائمة شجرية باستخدام البوتستراب تصميم صفحة موقع باستخدام 3 Bootstrap - الجزء الأول
-
سنتعلّم في هذا الدرس: تجهيز التطبيق قبل النشر إنشاء موقع جديد على منصة Netlify بالإستناد إلى مستودع GitHub هذا الدرس هو الأخير ضمن سلسلة Vue.js، حيث سنتوّج هذه السلسلة بشرح كيفية نشر التطبيق الوارد في الدرس السابق، وهو عبارة عن تطبيق SPA يدعم التخاطب مع قاعدة بيانات موجودة على Firebase كما تذكر. بعد بناء التطبيق ونشره، سيتمكّن أي مستخدم حول العالم من الوصول إلى تطبيقنا هذا والتفاعل معه. سأعتمد استخدام المنصّة المجانية Netlify لهذا الغرض، ويمكنك بالطبع استخدام أي منصّة أو خادوم مخصّص ترغب به. تجهيز التطبيق قبل النشر كما ذكرت قبل قليل، سنعتمد التطبيق المبني في الدرس السابق لتشغيله على خدمة Netlify. بغية ذلك، عملت على رفع هذا التطبيق بشكل كامل على مستودع Github التالي: HsoubAcademy/vuejs-spa سبب ذلك هو أنّ Netlify يسحب التطبيقات الموجودة على خدمات Git مثل GitHub. ستحتاج بالطبع إلى أن يكون لديك حساب على GitHub. زُر المستودع السابق، وسجّل دخولك على GitHub إذا لم تكن قد فعلت ذلك مسبقًا، ومن الصفحة الرئيسية لمستودعنا، انقر الزر Fork في الزاوية اليمنى العليا لإنشاء تفريعة جديدة من الشيفرة البرمجية الحالية. سيتطلب الأمر لحظات حتى يتم إنشاء التفريعة من الشيفرة البرمجية ووضعها ضمن حسابك الخاص. إنشاء موقع جديد على منصة Netlify بالإستناد إلى مستودع GitHub أنشئ حسابًا جديدًا على Netlify عن طريق زيارة الصفحة الرئيسية له الموقع الرسمي. انقر Sign up من الزاوية اليمنى العليا لتظهر لك الصفحة الخاصة بإنشاء اشتراك جديد. تبدو هذه الصفحة حاليًا على النحو التالي: لاحظ معي وجود خيارات متعددة للتسجيل. سنستخدم في حالتنا هذه التسجيل باستخدام البريد الإلكتروني. انقر الخيار الأخير "Email". بعد كتابة البريد الإلكتروني الذي تودّ التسجيل عن طريقه، سيرسل الموقع رسالة تحقق إلى هذا البريد تحتوي على رابط لتفعيل الحساب الجديد، انقر ذلك الرابط المرسَل إلى بريدك، وبعد تسجيل الدخول إلى Netlify باستخدام حسابك الجديد ستظهر صفحة مشابهة لما يلي: هذه هي الصفحة الرئيسية والتي تحتوي على جميع عناصر التحكم الخاصة بحسابك. نحن الآن موجودون ضمن الصفحة الخاصة بمواقع الويب التي أنشأتها أنت مسبقًا، وهي فارغة الآن بطبيعة الحال. انقر الزر "New site from Git" من الطرف الأيمن في الأعلى، للإتصال بمستودع من النوع Git. ستحصل على شكل شبيه بما يلي: انقر الزر GitHub الموجود في الأسفل، سيؤدي ذلك إلى فتح نافذة جديدة قد تطلب منك تسجيل الدخول بحسابك على GitHub في حال لم تكن مسجلًا دخولك على GitHub بعد (ذلك الحساب الذي أنشأت التفريعة ضمنه): انقر الزر Authorize Netlify للسماح لـ Netlify بالوصول إلى حساب GitHub الخاص بك. بعد إكمال عملية الاستيثاق (Authorization)، ستظهر لك صفحة تسمح لك بالاختيار بين السماح الكامل لـ Netlify للوصول إلى جميع المستودعات ضمن حسابك أو السماح بالوصول إلى مستودع محدّد. بالنسبة إلي، سأسمح له بالوصول إلى المستودع vuejs-spa فقط، وهو المستودع الذي أنشأناه كتفريعة قبل قليل من الفقرة السابقة. انقر زر Install لتبدأ عملية التنصيب التي تتضمّن الانتقال إلى موقع GitHub بشكل تلقائي وذلك لإكمال عملية الربط (قد يطلب منك كلمة المرور لحسابك في GitHub مرة أخرى). بعد الإنتهاء من العملية السابقة ستحتاج إلى العودة إلى الصفحة الرئيسية لموقع Netlify وتكرار عملية إنشاء موقع من مستودع GitHub (الشكل رقم 3). هذه المرة سيظهر لنا المستودع الذي نرغب باستيراده. انظر إلى الشكل التالي: لاحظ معي كيف ظهر المستودع vuejs-spa مع اسم حساب GitHub التابع لي husamburhan. سأختار هذا المستودع بالنقر عليه، سيؤدي ذلك إلى الخطوة النهائية والتي تتمثّل بتحديد الفرع الرئيسي (Branch) التي سنستخدمه في عملية نشر التطبيق، بالإضافة إلى تحديد التعليمة الخاصة ببناء التطبيق، والمجلّد الخاص بالنشر. احرص على أن تحصل على شكل شبيه بما يلي: أخيرًا، انقر الزر Deploy site لبناء التطبيق ونشر الموقع على الانترنت. ستأخذ هذه العملية القليل من الوقت، حيث ستنتقل إلى صفحة تخبرك بأنّ عملية البناء والنشر قيد التجهيز. بعد أن تنتهي عملية البناء والنشر ستحصل على رابط حي للصفحة تستطيع مشاركته مع من ترغب. بالنسبة إلي حصلت على الرابط التالي: suspicious-mahavira-10e9ac.netlify.app لاحظ أنّ اسم الموقع يُولَّد بشكل عشوائي. يمكنك بالطبع تغيير هذا الاسم بحيث يصبح منطقيًا أكثر. كما يمكنك إضافة اسم نطاق مخصّص مثل www.example.com وربطه مع هذا الموقع إن أحببت. جميع الخيارات السابقة وأكثر يمكن التحكم بها من خلال خيارات الموقع في Netlify. ختامًا ختمنا في هذا الدرس السلسلة التعليمية الخاصة بـ Vue.js بشرح كيفية نشر التطبيق إلى خدمة مجانية ليصبح تطبيقنا متاحًا على الانترنت. توجد العديد من الخدمات التي تسمح بالنشر المجاني، ولكنّني اخترت أبسطها وهي Netlify حسب رأيي. حاولت في هذه السلسلة تغطية الأمور الأساسية فقط في Vue.js، لذلك فأمامك الكثير بكل تأكيد لتتعلّمه حول هذه التقنية الواعدة. يمكنك زيارة الموقع الرسمي لإطار العمل Vue.js والاستزادة حوله. اقرأ أيضًا المقال السابق: بناء تطبيقات ذات صفحة واحدة باستخدام التوجيه Routing في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: بناء هيكل التطبيق والتعرّف على أجزاءه الرئيسية. تحويل التطبيق الموجود في الدرس السابق إلى تطبيق SPA. سنتعلّم في هذا الدرس كيفية بناء تطبيقات تعتمد على صفحة واحدة فقط (Single Page Applications) أو اختصارًا SPA. توفّر Vue.js هذه الإمكانية من خلال مكتبة اسمها vue-router تسمح بتعريف مسارات داخلية ضمن التطبيق بهدف دعم مفهوم SPA. وتطبيقات SPA هي تطبيقات ويب عادية، لكنّها تختلف عن التطبيقات الكلاسيكية في أنّ الانتقال من صفحة إلى أخرى ضمن الموقع لا يحتاج إلى إعادة تحميل كامل الصفحة بما تحويه من ملفات شيفرة نصية وصور وغيرها من أصول الموقع. فالذي يحدث في تطبيقات SPA هو أنّ جميع الواجهات المفترض وجودها في الموقع ستكون معرّفة مسبقًا ومحمّلة إلى حاسوب المستخدم، فيعمل تطبيق SPA فقط على استبدال واجهة مكان واجهة أخرى دون الحاجة إلى تحميل الصفحة كاملةً من الخادوم. يرتبط هذا الدرس ارتباطًا وثيقًا بالدرس السابق لأنّه سيعمل على تحويل التطبيق المعتمد في الدرس السابق إلى تطبيق يدعم SPA لذلك أنصحك بمراجعة الدرس السابق قبل الاستمرار في هذا الدرس. على أية حال، أرجو أن يكون هذا الدرس ممتعًا ومفيدًا. بناء هيكل التطبيق والتعرّف على أجزاءه الرئيسية بنينا في الدرس السابق تطبيق تجريبي بسيط، هدفه التواصل مع قاعدة بيانات موجودة على الـ Firebase، ولعلّك تذكر، أنّني قد أشرت في ذلك الدرس إلى أنّ الأسلوب الذي اتبعناه في بناء ذلك التطبيق لم يكن عمليًّا بسبب أنّنا قد "حشرنا" جميع وظائف التطبيق ضمن واجهة واحدة وهذا أمر غير عملي بالطبع. حان الآن إصلاح ذلك العيب وذلك باستخدام تقنية SPA. سنحوّل جميع وظائف التطبيق السابق إلى تطبيق جديد سنبنيه في هذا الدرس. لنبدأ بإنشاء هيكل التطبيق الجديد بتنفيذ الأمر التالي ضمن موجّه الأوامر: vue create vue-spa لكي ننجز مفهوم SPA في تطبيقنا هذا، سنستخدام تقنية التوجيه (Routing) التي عن طريق المكتبة vue-router كما أشرنا قبل قليل. استخدم الأمر التالي لتنصيب المكتبة vue-router: npm install vue-router افتح مجلّد التطبيق باستخدام Visual Studio Code ثم احذف الملف HelloWorld.vue. سنحتاج إلى إنشاء مجموعة جديدة من الملفات والمجلّدات من أجل هذا التطبيق. انقر الآن بزر الفأرة الأيمن على المجلّد src واختر New Folder لإنشاء مجلّد جديد سمّه views. سنستخدم هذا المجلّد لتخزين الواجهات المختلفة للتطبيق الخاص بنا. أنشئ ضمن المجلّد views الذي أنشأناه توًّا الملفات التالية: AddUser.vue و EditUser.vue و AllUsers.vue و Home.vue. أنشئ أيضًا الملف routes.js ضمن المجلّد src. الملف routes.js سيحتوي على مسارات العناوين الداخلية التي سنستخدمها ضمن التطبيق. ستحصل بالنتيجة على البنية التالية من الملفات: تحويل التطبيق الموجود في الدرس السابق إلى تطبيق SPA لكي نبدأ باستخدام التوجيه، يجب أولًا أن نستورد المكتبة vue-router عن طريق عبارة الاستيراد التالية: import VueRouter from "vue-router"; سنضع هذه العبارة (وبشكل مختلف عما اعتدنا عليه) ضمن الملف routes.js وسأوضّح سبب ذلك بعد قليل. انظر الآن إلى محتويات الملف routes.js: import Vue from "vue"; import VueRouter from "vue-router"; import Home from "./views/Home"; import AddUser from "./views/AddUser"; import EditUser from "./views/EditUser"; import AllUsers from "./views/AllUsers"; Vue.use(VueRouter); const routes = [ { path: "/", name: "Home", component: Home }, { path: "/AddUser", name: "AddUser", component: AddUser }, { path: "/EditUser", name: "EditUser", component: EditUser }, { path: "/AllUsers", name: "AllUsers", component: AllUsers }, ]; const router = new VueRouter({ routes }); export default router; أوضحنا قبل قليل، أنّ وظيفة الملف routes.js هي تعريف مسارات العناوين الداخلية التي سنستخدمها ضمن التطبيق. لننظر الآن إلى القسم الخاص بتعليمات الاستيراد: import Vue from "vue"; import VueRouter from "vue-router"; import Home from "./views/Home"; import AddUser from "./views/AddUser"; import EditUser from "./views/EditUser"; import AllUsers from "./views/AllUsers"; أعتقد أنّ أوّل تعليمتين واضحتين. بالنسبة للتعليمات الأربع الأخرى، فهي لاستيراد الواجهات التي سنستخدمها في التطبيق. هذه الواجهات هي بطبيعة الحال مكوّنات (لأنّها موجودة ضمن ملفات تحمل الامتداد vue). بعد ذلك نُخبر Vue.js أن يستخدم التوجيه من خلال التعليمة التالية: Vue.use(VueRouter); وبعد ذلك نصل إلى القسم الخاص بتعريف المسارات: const routes = [ { path: "/", name: "Home", component: Home }, { path: "/AddUser", name: "AddUser", component: AddUser }, { path: "/EditUser", name: "EditUser", component: EditUser }, { path: "/AllUsers", name: "AllUsers", component: AllUsers }, ]; لاحظ معي أننا نضع المسارات التي سنستخدمها في التطبيق ضمن مصفوفة اسمها routes. كل عنصر من هذه المصفوفة عبارة عن كائن له الحقول: path و name و component. انظر مثلًا إلى الكائن الأوّل من هذه المصفوفة: { path: "/", name: "Home", component: Home } يمثّل هذا الكائن المسار الخاص بالصفحة الرئيسية للتطبيق. فهو يُعرّف المسار الخاص بها ليكون / ويمثّل الصفحة الرئيسية في تطبيقات الويب عادة. أي يمكن الوصول إلى هذه الواجهة من خلال الرابط التالي: http://www.yourdomain.com/#/ أمّا اسم هذا المسار فهو Home (من الممكن أن نصل إلى أي مسار من خلال اسمه كما سنرى لاحقًا في هذا الدرس). بالنسبة للواجهة التي سترتبط بهذا المسار فهي Home (انظر إلى تعليمة الاستيراد الخاصة بالواجهة Home.vue). بالنسبة لباقي المسارات فتعمل بنفس الأسلوب تمامًا، فإذا أخذنا مثلًا المسار الثاني فنجد أنّ المسار الذي يُشير إليه هو /AddUser أي أنّنا نستطيع الوصول إليه عن طريق رابط مماثل لما يلي: http://www.yourdomain.com/#/AddUser نأتي إلى القسم التالي في الشيفرة البرمجية السابقة، حيث سنعرّف متغيّر جديد اسمه router ونسند إليه كائن جديد يمثّل الموجّه الذي سيدير عمليّات التوجيه ضمن التطبيق: const router = new VueRouter({ routes }); لاحظ كيف مرّرنا المصفوفة التي عرفنا ضمنها المسارات توًّا إلى VueRouter (وهو معرّف ضمن إحدى تعليمات الاستيراد). وأخيرًا نضع التعليمة المسؤولة عن تصدير الموجّه router خارج هذا الملف. لننتقل الآن إلى الملف main.js: import Vue from 'vue' import App from './App.vue' import router from './routes.js' import "bootstrap/dist/css/bootstrap.min.css"; import VueResource from 'vue-resource'; import VueSimpleAlert from "vue-simple-alert"; Vue.config.productionTip = false Vue.use(VueResource); Vue.use(VueSimpleAlert); new Vue({ router, render: h => h(App), }).$mount('#app') نستورد ضمن هذا الملف المكتبات التي سنحتاجها ضمن التطبيق. أعتقد أنّ أوّل تعليمتي استيراد واضحتين أيضًا. بالنسبة للتعليمة الثالثة، فهنا نستورد الموجّه router التي بنيناه مسبقًا ضمن الملف routes.js، أمّا بالنسبة للتعليمات الثلاث الباقية فهي على الترتيب: استيراد مكتبة Bootstrap للتنسيق. استيراد المكتبة vue-resource للاتصال بالخواديم البعيدة، والتي تعاملنا معها في الدرس السابق. استيراد مكتبة جديدة اسمها vue-simple-alert لم نتعامل معها مسبقًا. والهدف منها عرض رسائل ذات تنسيق جميل للمستخدم، سنتعلّم بعد قليل كيفية التعامل معها. ستحتاج بالتأكيد إلى تنصيب المكتبة vue-simple-alert بتنفيذ الأمر التالي ضمن موجّه الأوامر: npm install vue-simple-alert لاحظ الآن التعليمتين التاليتين: Vue.use(VueResource); Vue.use(VueSimpleAlert); هاتين التعليمتين لإخبار Vue.js أن يستخدم الكائنين: VueResource و VueSimpleAlert. نأتي الآن إلى آخر تعليمة ضمن هذا الملف: new Vue({ router, render: h => h(App), }).$mount('#app') التعليمة السابقة مألوفة، وقد استخدمنا مثلها مرارًا، ولكن لاحظ معي هنا كيف أضفت كائن الموجّه router إليها. هذه الإضافة ضرورية، ولن يعمل التوجيه في التطبيق ما لم نضف كائن الموجّه بهذه الصورة. سنستعرض فيما يلي الملف App.vue بالإضافة إلى ملفات واجهات التطبيق. الملف App.vue ستكون محتويات هذا الملف على الشكل التالي: <template> <div id="app"> <nav id="nav"> <p class="logo-place">Vue.js Remote Server (Enhanced)</p> <ul class="nav-links"> <li class="links"> <router-link to="/">Home</router-link> </li> <li class="links"> <router-link to="/AddUser">Add User</router-link> </li> <li class="links"> <router-link to="/AllUsers">All User</router-link> </li> </ul> </nav> <router-view></router-view> </div> </template> <script> export default { name: "App", components: {}, }; </script> <style> #nav { display: flex; margin-bottom: 24px; } #nav a { font-weight: bold; color: #2c3e50; text-decoration: none; margin-right:12px; } #nav a.router-link-active { color: #ab26ab; } .nav-links { padding-right: 20px; list-style: none; display: flex; margin:21px 0 0 0; } .links-hover { text-decoration: underline; } .logo-place { font-size: 20px; color: purple; font-weight: bold; margin:16px 0 0 16px; } </style> هناك أمران جديدان سنتحدث عنهما بالنسبة لهذا الملف. لاحظ أولًا وجود وسم جديد اسمه router-view. في الحقيقة أنّ هذا الوسم ما هو إلّا مكوّن موجود ضمن المكتبة vue-router، ووظيفته تحديد مكان إدراج واجهات التطبيق المختلفة ضمن الصفحة الرئيسية. لأّن تطبيقات SPA كما أشرنا من قبل، لا تتطلب إعادة تحميل الصفحة بالكامل عندما يريد المستخدم الانتقال إلى واجهة مختلفة. الأمر الآخر هو وجود وسم جديد آخر وهو router-link وهو مكوّن أيضًا ضمن نفس المكتبة. وظيفة هذا المكوّن توليد عنصر ارتباط تشعبي <a> بحيث ينتقل إلى إحدى الواجهات المعرّفة ضمنه. فمثلًا لاحظ معي المكوّن router-link التالي التي أخذته من الشيفرة السابقة: <router-link to="/AddUser">Add User</router-link> نجد من المقطع السابق وجود السمة to و هي مسؤولة عن تحديد المسار (Path) الذي سيُوجَّه إليه المستخدم عند نقر هذا الرابط. في المثال السابق سيُوجَّه المستخدم إلى المسار الكامل التالي: http://www.yourdomain.com/#/AddUser وذلك بسبب وجود /AddUser كقيمة للسمة to. الملف Home.vue وهو ملف الصفحة الرئيسية في التطبيق وستكون محتوياته على الشكل التالي: <template> <div class="container"> <div class="row"> <div class="col-6 offset-2"> <h1>Welcome in our website!</h1> </div> </div> <div class="row" style="margin-top:30px;"> <div class="card" style="width: 18rem;"> <div class="card-body"> <h5 class="card-title">ِAdd User</h5> <p class="card-text">Add a new user to the Firebase experimental website.</p> <router-link class="btn btn-primary" to="/AddUser">Add a user</router-link> </div> </div> <div class="card" style="width: 18rem; margin-left:18px;"> <div class="card-body"> <h5 class="card-title">All User</h5> <p class="card-text">Display all users info from the Firebase experimental website.</p> <router-link class="btn btn-primary" to="/AllUsers">Get all users</router-link> </div> </div> </div> </div> </template> <script> export default { name: "Home", }; </script> لا يوجد في الشيفرة السابقة أي جديد، سوى أنّك تمتلك الحق في إضافة أصناف تنسيقية إلى المكوّن router-link كما في: <router-link class="btn btn-primary" to="/AddUser">Add a user</router-link> ستبدو هذه الواجهة على النحو التالي: الملف AddUser.vue يحتوي هذا الملف على الواجهة المسؤولة عن إضافة مستخدم جديد. انظر معي إلى الشيفرة البرمجية الخاصّة به: <template> <div class="container"> <div class="row"> <div class="col-6 offset-2"> <h1>Add User</h1> </div> </div> <div class="row"> <div class="col-6 offset-2"> <div class="container"> <div class="row"> <div class="col-12"> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" v-model="user.username" /> </div> <div class="form-group"> <label>First name</label> <input type="text" class="form-control" v-model="user.firstname" /> </div> <div class="form-group"> <label>Last name</label> <input type="text" class="form-control" v-model="user.lastname" /> </div> </div> </div> </div> <button class="btn btn-primary float-left" style="margin-left:12px;" @click="postUser()">Add</button> </div> </div> </div> </template> <script> export default { name: "AddUser", methods: { postUser() { this.$http .post("https://vue-remote-servers.firebaseio.com/users.json", this.user) .then( () => { this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; this.$alert("A new user is added.", "Operation Succeeded", "success"); }, (error) => { console.log(error); } ); }, }, data() { return { user: { username: "", firstname: "", lastname: "", }, }; }, }; </script> <style scoped> .row { margin-top: 8px; } </style> الشيفرة البرمجية الموجودة ضمن هذا الملف تماثل تقريبًا تلك التي كانت مسؤولة عن إضافة مستخدم جديد في الدرس الماضي. مع ملاحظة أنّ الشيفرة أصبحت أكثر تنظيمًا، بسبب أنّنا نعزل هنا عملية إضافة مستخدم جديد عن غيرها من العمليات. على أية حال توجد ميزة جديدة استُخدِمت ضمن هذه الواجهة، وهي رسالة ستُعرَض للمستخدم في حال نجاح عملية الإضافة إلى قاعدة بيانات Firebase: this.$alert("A new user is added.", "Operation Succeeded", "success"); كما كان من الممكن إضافة رسالة شبيهة بالرسالة السابقة في حال فشلت عملية الإضافة للمستخدم (لم أُضف مثل هذه الرسالة إلى الشيفرة السابقة). التابع $alert موجود ضمن المكتبة vue-simple-alert وظيفته كما هو واضح، هو في عرض رسالة منسّقة للمستخدم. بالنسبة لمثالنا هذا، سنعرض رسالة تُخبر المستخدم بأنّ عملية إضافة بيانات المستخدم إلى قاعدة بيانات Firebase قد تمت بنجاح. يمثّل الوسيط الأوّل للتابع $alert الرسالة التي نريد عرضها، أما الوسيط الثاني فهو عنوان صندوق الرسالة، أمّا الوسيط الثالث فهو نوع الرسالة وهي في حالتنا هذه success. توجد أنواع أخرى للرسائل التي يمكن للتابع $alert عرضها مثل success و error. للاطلاع على المزيد من الوثائق المتعلّقة بهذه المكتبة يمكنك زيارة الصفحة الرسمية لها. ستبدو هذه الواجهة على النحو التالي: الملف AllUsers.vue يحتوي هذا الملف على الواجهة الخاصة بعرض بيانات المستخدمين الذين سبق إضافتهم إلى قاعدة بيانات Firebase. انظر إلى الشيفرة البرمجيّة الخاصة به: <template> <div class="container"> <div class="row"> <h2>All Users</h2> <button style="margin-left:16px;" class="btn btn-primary" @click="getData()">Retrieve</button> </div> <hr /> <div class="row" v-for="usr in users" v-bind:key="usr.username"> <div class="col-2">{{usr.username}}</div> <div class="col-4">{{usr.firstname}} {{usr.lastname}}</div> <div class="col-1"> <router-link :to="{name: 'EditUser', params: {user:usr}}">Edit</router-link> </div> </div> </div> </template> <script> export default { data() { return { user: { username: "", firstname: "", lastname: "", }, users: [], }; }, methods: { getData: function () { this.$http .get("https://vue-remote-servers.firebaseio.com/users.json") .then((response) => { return response.json(); }) .then((data) => { const tmpArray = []; for (let key in data) { let withId = data[key]; withId.id = key; tmpArray.push(data[key]); } this.users = tmpArray; }); }, }, }; </script> <style scoped> .row{ margin-top:8px; } </style> مرة أخرى، لا تختلف الشيفرة البرمجية الموجودة هنا عن تلك التي كانت موجودة في الدرس السابق. فعندما ينتقل المستخدم لهذه الواجهة وينقر على الزر Retrieve، ستُحدّث الواجهة بحيث تعرض جميع المستخدمين المخزنين حاليًا ضمن قاعدة بيانات Firebase. بعد عرض هؤلاء المستخدمين، ستلاحظ وجود رابط اسمه Edit يظهر بجوار كل مستخدم. عند نقر هذا الزر سينقلك التطبيق إلى واجهة تحرير بيانات المستخدم EditUser لإجراء التعديلات المطلوبة عليه. ولكنّ السؤال هنا، كيف ستتمكّن الواجهة EditUser من معرفة المستخدم المطلوب تحرير بياناته؟ الجواب هو في المكوّن router-link الموجود ضمن هذه الواجهة AllUsers. انظر معي إلى الاستخدام الجديد للمكوّن router-link: <router-link :to="{name: 'EditUser', params: {user:usr}}">Edit</router-link> استخدمنا في المقطع السابق السمة to بشكل مختلف عن استخدامنا لها في الواجهات السابقة. لاحظ أولًا كيف وضعت نقطتين رأسيتين قبل كلمة to مباشرةً (وهما ضروريتان في هذه الحالة)، ثم أسندت إلى السمة :to كائن وليس نص عادي كما فعلنا من قبل: {name: 'EditUser', params: {user:usr}} هذا الكائن لديه مفتاحين. الأول هو name ويمتلك القيمة 'EditUser'، ويمثّل اسم الواجهة التي نريد الانتقال إليها عندما ينقر المستخدم هذا الرابط. لاحظ أنّني قلت "اسم" وليس "مسار"، أي أنّ الاسم الذي نكتبه هنا يحب أن يكون موافقًا للقيمة name التي كانت موجودة ضمن ملف المسارات routes.js عندما عرفنا تلك المسارات إذا كنت تذكر. بالنسبة للمفتاح الثاني فهو params وهو مسؤول عن تمرير أية وسائط نرغبها إلى الواجهة التي نريد الانتقال إليها. في حالتنا هذه نرغب بتمرير بيانات المستخدم المراد تحريرها بشكل كامل. طريقة التمرير مفيدة للغاية، لاحظ كيف مرّرنا كائن جديد للمفتاح params يحتوي هذا الكائن في حالتنا هذه، على مفتاح وحيد اسمه user ونسند له القيمة usr التي تُعتبر كائنًا مستقلًا بحد ذاته يحتوي على بيانات المستخدم المراد تحرير بياناته. أي أنّ {user:usr} عبارة عن ثنائية تحوي اسم الوسيط user مع القيمة المرتبطة به usr. يمكن بالطبع تمرير أكثر من وسيط إلى الواجهة بالأسلوب السابق، وذلك بوضعها بجوار بعضها على شكل مفاتيح متعاقبة كما يلي: params: {key1:val1, key2:val2, ...} ستبدو الواجهة AllUsers على النحو التالي: الملف EditUser.vue في هذه الواجهة يمكننا تعديل بيانات المستخدم، بالإضافة إلى إمكانية حذف بيانات هذا المستخدم بشكل كلّي من قاعدة البيانات (هذه ميزة جديدة لم تكن مُضافة مسبقًا في الدرس السابق). إليك الشيفرة البرمجية الخاصة بهذا الملف: <template> <div class="container"> <div class="row"> <div class="col-6 offset-2"> <h1>Edit User</h1> </div> </div> <div class="row"> <div class="col-6 offset-2"> <div class="container"> <div class="row"> <div class="col-12"> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" v-model="user.username" /> </div> <div class="form-group"> <label>First name</label> <input type="text" class="form-control" v-model="user.firstname" /> </div> <div class="form-group"> <label>Last name</label> <input type="text" class="form-control" v-model="user.lastname" /> </div> </div> </div> </div> <button class="btn btn-primary float-left" style="margin-left:12px;" @click="putUser()">Save</button> <button class="btn btn-danger float-right" style="margin-right:12px;" @click="deleteUser()" >Delete</button> </div> </div> </div> </template> <script> export default { name: "EditUser", methods: { putUser() { this.$http .put( "https://vue-remote-servers.firebaseio.com/users/" + this.$route.params.user.id + ".json", this.user ) .then( () => { this.$alert( "The user is updated.", "Operation Succeeded", "success" ); }, (error) => { console.log(error); } ); }, deleteUser() { this.$confirm("Are you sure you want to delete the user?").then(() => { this.$http .delete( "https://vue-remote-servers.firebaseio.com/users/" + this.$route.params.user.id + ".json" ) .then( () => { this.$router.push('AllUsers'); }, (error) => { console.log(error); } ); }); }, }, computed: { user() { return this.$route.params.user; }, }, }; </script> <style scoped> .row { margin-top: 8px; } </style> الشيء الذي أود التركيز عليه من الشيفرة السابقة هو كيفية استلام الوسائط الممرّرة لهذه الواجهة من الواجهة الخاصة بعرض بيانات جميع المستخدمين AllUsers ومن ثمّ تعبئة هذه البيانات ضمن حقول البيانات المناسبة لها، ليتمكّن المستخدم من تعديلها إن أحب. أنشأت لهذا الغرض خاصية محسوبة اسمها user إليك الشيفرة البرمجية الخاصة بها: user() { return this.$route.params.user; } تحتوي هذه الخاصية المحسوبة على تعليمة برمجية واحدة ترُجع الوسيط الذي مرّرناه من الواجهة AllUsers قبل قليل: this.$route.params.user الوسيط هو user كما أسميناه ضمن الواجهة AllUsers، يحتوي هذا الوسيط على بيانات المستخدم username و firstname و lastname كما وردت من الواجهة AllUsers. هذا الوسيط موجود ضمن المفتاح params كما هو واضح، والموجود بدوره ضمن الكائن $route (الموجود ضمن المكتبة vue-router). وبما أنّ الخاصية user محسوبة فإنّ البيانات المستخلصة منها ستُعمّم مباشرة على الحقول الثلاثة الموجودة ضمن الواجهة EditUser كما يلي: <div class="col-12"> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" v-model="user.username" /> </div> <div class="form-group"> <label>First name</label> <input type="text" class="form-control" v-model="user.firstname" /> </div> <div class="form-group"> <label>Last name</label> <input type="text" class="form-control" v-model="user.lastname" /> </div> </div> أود أيضًا التحدث عن التابع $confirm الموجود ضمن المكتبة vue-simple-alert ووظيفته تخييرك بين أمرين. استخدمت التابع $confirm ضمن التابع deleteUser وذلك لكي نطلب من المستخدم تأكيد أنّه يريد حذف المستخدم الحالي من قاعدة البيانات. انظر الشيفرة البرمجية للتابع deleteUser: this.$confirm("Are you sure you want to delete the user?").then(() => { this.$http .delete( "https://vue-remote-servers.firebaseio.com/users/" + this.$route.params.user.id + ".json" ) .then( () => { this.$router.push('AllUsers'); }, (error) => { console.log(error); } ); }); في حال تمت الموافقة على حذف المستخدم الحالي، سيُنفَّذ تابع السهم المُمرّر لتابع then الأوّل. يحتوي تابع السهم هذا على الشيفرة البرمجية التي ستحذف بيانات المستخدم من قاعدة بيانات Firebase وهي مألوفة وتشبه أخواتها من المقاطع البرمجية الأخرى المسؤولة عن التعامل مع قاعدة بيانات Firebase. لاحظ أنّنا ننفّذ تابع then آخر يحتوي على تعليمة برمجيّة أخرى وظيفتها تحويل المستخدم إلى الواجهة AllUsers بعد الانتهاء من حذف المستخدم الحالي.انظر لهذه التعليمة: this.$router.push('AllUsers'); هذا هو الأسلوب المتبع للانتقال بين الواجهات بشكل برمجي، وذلك عن طريق استخدام التابع push من الكائن $router، حيث نمرّر "اسم" الواجهة التي نرغب بالانتقال إليها كوسيط للتابع push. ختامًا تناولنا في هذا الدرس مقدّمة إلى التوجيه في Vue.js. في الحقيقة يُعتبر هذا الموضوع كبيرًا بعض الشيء وله جوانب متعدّدة غطينا في هذا الدرس الأساسي منها. في حال احتجت إلى التوسّع في هذا الموضوع مستقبلًا، فأعتقد أنّ خير رفيق لك، سيكون الوثائق الرسمية للتوجيه على هذا الموقع. أقترح عليك بعد قراءة هذا الدرس أن تحاول تجريب بناء تطبيقات بسيطة تعتمد على التوجيه، لكي تعتاد على هذه التقنية. اقرأ أيضًا المقال التالي: نشر تطبيق Vue.js إلى الإنترنت المقال السابق: استخدام Vue.js للاتصال بالإنترنت النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: إنشاء قاعدة بيانات على Google Firebase تنصيب مكتبة الاتصال بالانترنت vue-resource بناء تطبيق باستخدام Vue.js للقراءة والإضافة من وإلى قاعدة البيانات إضافة ميزة تعديل البيانات للتطبيق السابق سنتعلم في هذا الدرس كيفية الاتصال بخواديم بعيدة باستخدام مكتبة مخصّصة لـ Vue.js لهذا الغرض. وبما أنّنا نهتم بتبسيط المعلومة من خلال التركيز على مفهوم جديد محدّد، فلن ندخل في مجال بناء تطبيق خلفية كامل (Backend Application) مخصّص لكي يتعامل معه تطبيق Vue.js، ولكن سنعتمد على إنشاء تطبيق بسيط مجاني على Google Firebase، وهو عبارة عن خدمة قاعدة بيانات سيتواصل معها تطبيقنا لتوضيح الفكرة المطلوبة. إنشاء قاعدة بيانات على Google Firebase لن نتوسّع في هذا الدرس في الشرح حول Google Firebase ومزاياها، إذ يمكنك الاطلاع حول المزيد عنها من خلال الوثائق الخاصة بها والتي سأزودك بالبعض منها بعد قليل. يكفيك الآن أن تعلم أنّ Google Firebase هي منصّة مملوكة من قبل Google وظيفتها مساعدة المطوّرين على بناء وتحسين وتنمية التطبيقات التي يعملون عليها، فهي عبارة عن خدمة خلفية (Backend Service) يمكنك من خلالها إنشاء قواعد بيانات وخدمات استيثاق (Authentication Services) وغيرها من المزايا المفيدة لتطبيقاتك المختلفة، سواء كانت تطبيقات لأجهزة الموبايل أو تطبيقات ويب أو غيرها. بالنسبة إلينا، سنستخدم Firebase في هذا الدرس لبناء قاعدة بيانات بسيطة كخدمة تمثّل دعمًا لتطبيق Vue.js بسيط. انتقل إلى الموقع الرسمي لـ Firebase، ثم من الزاوية اليمنى العليا اختر Sign in لتسجيل الدخول، ستحتاج إلى حساب Google للوصول إلى خدمات Firebase. بعد تسجيل الدخول بحساب Google ستظهر الصفحة الرئيسية التالية: انقر Go to console من الزاوية اليمنى العليا للانتقال إلى صفحة الخدمات، ثم انقر زر Add project (أو Create a project) لإضافة مشروع جديد، سيطلب منك بدايةً اسم المشروع بالإضافة إلى الموافقة على الاتفاقية كما في الشكل التالي: لقد اخترت الاسم vue-remote-servers ستضطر بالطبع إلى استخدام اسم آخر لمشروعك لأن الاسم الحالي أصبح محجوزًا. بعد ذلك انقر زر Continue للانتقال إلى المرحلة الثانية التي سيخيرك فيها بتفعيل Google Analytics من أجل هذا المشروع. انقر الزر Continue مجددا للانتقال إلى المرحلة الأخيرة قبل إنشاء المشروع. في حال كنت قد اخترت تفعيل Google Analytics في المرحلة الثانية، فسيطلب منك في المرحلة الأخيرة تحديد الحساب الذي ترغب باستخدامه مع Google Analytics (سيوفر لك حساب افتراضي اسمه Default Account For Firebase أو سيسمح لك بإنشاء حساب جديد إن أحببت)، بعد تحديد الحساب، سيظهر زر Create project في الاسفل، انقره لإنشاء المشروع. أمّا إذا لم تفعّل Google Analytics من المرحلة الثانية، فسيؤدي ذلك إلى إنشاء المشروع المطلوب فورًا. ملاحظة إذا كنت تزور Firebase للمرة الأولى، فسيطلب منك في المرحلة الأخيرة تحديد المنطقة (الدولة) الخاصة بـ Google Analytics بالإضافة إلى الموافقة على بنود الاستخدام. بعد إنشاء المشروع سينتقل المتصفح إلى الصفحة الخاصة به. من القائمة اليسرى انقر الزر Develop ثم انقر Database لأنّنا لن نهتم الآن سوى بقاعدة البيانات. بعد النقر على قاعدة البيانات ستحصل على واجهة تسمح لك ببناء قاعدة بيانات جديدة. استخدم شريط التمرير العمودي لكي تنتقل إلى أسفل الصفحة قليلًا حتى تصل إلى القسم الخاص بإنشاء قاعدة بيانات في الزمن الحقيقي (Realtime Database) كما في الشكل التالي: انقر زر Create database لإنشاء قاعدة البيانات المطلوبة. ستظهر نافذة صغيرة تعرض فيما إذا كنت تريد أن تجعل قاعدة البيانات مفتوحة للجميع بهدف اختبارها، أم مقفلة تحتاج إلى تسجيل دخول. سنختار أن تكون مفتوحة لجعل الأمر أسهل. اختر Start in test mode: لاحظ التحذير الذي سيظهر باللون الأحمر والذي يفيد بأنّ أي شخص سيصبح قادرًا على تعديل قاعدة البيانات أو القراءة منها. بعد النقر على زر Enable ستحصل على شكل شبيه بما يلي: أصبحت قاعدة البيانات جاهزة، لاحظ من جديد كيف يظهر تحذير باللون الأحمر يخبرك أنّ قاعدة البيانات مفتوحة ويمكن لأي شخص الوصول إليها. لاحظ أيضًا الرابط الذي يظهر مباشرة فوق التحذير: https://vue-remote-servers.firebaseio.com/ هذا الرابط هو عنوان نقطة الاتصال لخدمة قاعدة البيانات (Database Service Endpoint) التي أنشأناها توًّا. سنحتاج إلى هذا الرابط لاحقًا في هذا الدرس. قد تتساءل أيضًا أين الجداول ضمن قاعدة البيانات هذه؟ الحقيقة أنّ قواعد البيانات ضمن Firebase عبارة عن قواعد بيانات NoSQL لمعرفة المزيد حول قواعد البيانات هذه يمكنك زيارة هذه الصفحة. ملاحظة يمكنك الحصول على المزيد من المعلومات حول Firebase من خلال هذا الرابط الذي يحتوي على الوثائق الرسمية له. ملاحظة تحدث في بعض الأحيان تحديثات مستمرة على الواجهات الخاصة بـ Google Firebase، لذلك فقد يختلف الشرح الموجود هنا قليلًا عمّا تشاهده في الموقع. مع أنّني أرجو ألّا يحدث ذلك، منعًا لأي التباس قد يعتريك عزيزي القارئ. تنصيب مكتبة الاتصال بالانترنت vue-resource يمكن بالطبع استخدام أي طريقة للاتصال بالانترنت من خلال Vue.js، ولكنني سأختار في هذا الدرس أسلوب مخصص لـ Vue.js. سنستخدم مكتبة اسمها vue-resource يمكن من خلالها الاتصال بالخواديم بشكل سهل ومبسّط وبشكل منسجم تقريبًا مع المكتبات الشهيرة المستخدمة لهذا الغرض. يمكنك الوصول إلى المستودع الخاص بهذه المكتبة على Github من خلال هذا الرابط. في الحقيقة، أنصحك بزيارة مستودع هذه المكتبة على Github بعد أن تنتهي من هذا الدرس، لتطلع عليها بشكل جيّد وتتعرف على جميع الإمكانيات التي تقدّمها والتي تسهل عملك كمبرمج. في هذا الدرس، سنتحدّث عن جزء محدود من هذه المزايا. لتنصيب vue-resource افتح موجّه الأوامر ونفّذ الأمر التالي: npm install vue-resource بعد الانتهاء من التنصيب يمكن الآن إضافة هذه المكتبة إلى تطبيق Vue.js وذلك بإضافة السطر التالي إلى قسم المكتبات المستوردة ضمن الملف main.js: import VueResource from 'vue-resource'; واستخدام التعليمة التالية ضمن الملف main.js أيضًا: Vue.use(VueResource); أمّا كيفية الاستخدام الفعلي لهذه المكتبة فسنتعرف عليه في الفقرة التالية. بناء تطبيق باستخدام Vue.js للقراءة والإضافة من وإلى قاعدة البيانات كما جرت العادة، سنبني تطبيق عملي جديد باستخدام Vue CLI سنتعلّم من خلاله كيفية الاتصال بقاعدة البيانات التي أنشأناها في الفقرة الأولى من هذا الدرس على Google Firebase. أنشئ مشروع جديد اسمه vue-remote-servers عن طريق تنفيذ الأمر التالي ضمن موجّه الأوامر: vue create vue-remote-servers بعد إنشاء المشروع، افتح المجلّد الخاص به عن طريق Visual Studio Code. احذف الآن الملف HelloWorld.vue ثم احرص على أن تكون محتويات الملف main.js مماثلة لما يلي: import Vue from 'vue' import VueResource from 'vue-resource'; import App from './App.vue' import "bootstrap/dist/css/bootstrap.min.css"; Vue.config.productionTip = false Vue.use(VueResource); new Vue({ render: h => h(App), }).$mount('#app') واحرص أيضًا على أن تكون محتويات الملف App.vue مماثلة لما يلي: <template> <div class="container"> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <h2>Vue.js Remote Server</h2> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" v-model="user.username" /> </div> <div class="form-group"> <label>First name</label> <input type="text" class="form-control" v-model="user.firstname" /> </div> <div class="form-group"> <label>Last name</label> <input type="text" class="form-control" v-model="user.lastname" /> </div> <button class="btn btn-primary" @click="postData()">Add</button> <br /> <br /> <button class="btn btn-primary" @click="getData()">Retrieve</button> <br /> <br /> <ul class="list-group"> <li class="list-group-item" v-for="usr in users" v-bind:key="usr.username" >{{usr.username}} - {{usr.firstname}} {{usr.lastname}}</li> </ul> </div> </div> </div> </template> <script> export default { name: "App", data() { return { user: { username: "", firstname: "", lastname: "", }, users: [], }; }, methods: { postData: function () { this.$http .post("https://vue-remote-servers.firebaseio.com/users.json", this.user) .then( (response) => { console.log(response); this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; }, (error) => { console.log(error); } ); }, getData: function () { this.$http .get("https://vue-remote-servers.firebaseio.com/users.json") .then((response) => { return response.json(); }) .then((data) => { const tmpArray = []; for (let key in data) { tmpArray.push(data[key]); } this.users = tmpArray; }); }, }, }; </script> <style> </style> ستعطي الشيفرة السابقة الواجهة التالية: الفكرة من هذا التطبيق هي في إمكانية الإضافة المتعددة لمستخدمين افتراضيين. حيث تتكون بيانات كل مستخدم من: اسم المستخدم (Username) والاسم (First name) والكنية (Last name). سترسل بيانات كل مستخدم مفترض إلى قاعدة البيانات على Firebase. بعد ذلك يمكن استرداد بيانات المستخدمين المُضافة مسبقًا إلى قاعدة البيانات هذه، من خلال نقر الزر Retrieve. ملاحظة سنستخدم في هذا التطبيق تنسيقات Bootstrap على افتراض أنّك قد نصبته مسبقًا. وهذا ما يفسّر وجود السطر التالي ضمن ملف main.js: import "bootstrap/dist/css/bootstrap.min.css"; إذا أردت أن تتذكر كيفية استخدام تنسيقات Bootstrap ضمن Vue.js يمكنك مراجعة الفقرة: "استخدام إطار العمل Bootstrap" ضمن الدرس: "التعامل مع دخل المستخدم عن طريق نماذج الإدخال". بالنسبة لآلية عمل التطبيق، لاحظ معي بدايةً أن شيفرة HTML الموجودة ضمن القسم <template> هي بسيطة وواضحة. بالنسبة لحقول البيانات المستخدمة، فهي معرفة ضمن القسم <script> حيث عرّفت جميع الحقول التي تعود للمستخدم المفترض ضمن كائن اسمه user موجود ضمن القسم data ضمن كائن Vue.js. يحتوي هذا الكائن على الحقول التالية: username و firstname و lastname كما هو واضح. كما يحتوي القسم data أيضًا على مصفوفة اسمها users سنخزّن ضمنها بيانات المستخدمين التي سنحصل عليها من قاعدة بيانات Firebase عند نقر زر Retrieve كما سنوضّح ذلك بعد قليل. أمّا بالنسبة للقسم methods فيحتوي على تابعين فقط: postData و getData وهما معالجين لحدثي النقر على الزرين Add و Retrieve على الترتيب. بالنسبة للتابع postData تأمل معي الشيفرة البرمجيّة الموجودة ضمنه: this.$http .post("https://vue-remote-servers.firebaseio.com/users.json", this.user) .then( (response) => { console.log(response); this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; }, (error) => { console.log(error); } ); استخدمت الكائن المبيّت $http من الكائن this. في الحقيقة أنّ هذا الكائن مُتاح فقط من خلال المكتبة vue-resource التي أضفناها في هذا الدرس. يحتوي الكائن $http على عدة توابع مفيدة في استقبال وإرسال البيانات عبر الإنترنت من وإلى خواديم أو خدمات (Services) كما هي خدمة Firebase. بما أنّنا نريد إضافة بيانات جديدة، فسنستخدم التابع post كما هو واضح. يقبل هذا التابع وسيطين: الوسيط الأول هو عنوان نقطة الاتصال للخدمة المراد التواصل معها، وهي في حالتنا هذه عنوان خدمة قاعدة البيانات على Firebase التي أنشأناها في هذا الدرس: https://vue-remote-servers.firebaseio.com/users.json الرابط السابق حصلت عليه من الصفحة الخاصة بقاعدة البيانات vue-remote-servers إذا كنت تذكر من الفقرة الأولى من هذا الدرس، ولكن أضفت عليه اسم المصدر users.json. بما أنّني أريد إضافة مستخدمين افتراضيين إلى قاعدة البيانات فمن المنطقي إضافة بيانات هؤلاء المستخدمين إلى جدول (إن صح التعبير) اسمه users. في الحقيقة في قواعد البيانات من النمط NoSQL نسمي مثل هذه الجداول بالمجموعات (Collections). إذًا فالاسم users هو اسم المجموعة التي سنخزّن ضمنها بيانات المستخدمين الافتراضيين، أمّا الامتداد .json فهو إلزامي. أمّا الوسيط الثاني فهو عبارة عن البيانات المراد إرسالها. وقد أرسلت الكائن user جملةً واحدة، لأنّه يحتوي على البيانات الفرعية المطلوبة والتي هي مربوطة بدورها عن طريق v-model بعناصر HTML الموافقة. يُرجع التابع post وعد (Promise) لذلك فإنّنا نُتبع استدعاء التابع post بنقطة مباشرة وبعدها نكتب التابع then كما هو مألوف تمامًا عند العمل مع مكتبات JavaScript الشهيرة الأخرى. يقبل التابع then وسيطين أيضًا: الوسيط الأوّل هو تابع يحتوي على شيفرة برمجيّة تُعالج الرد الذي حصلت عليه مكتبة vue-resource بعد اتصالها مع الخادوم البعيد. انظر إلى الشيفرة التالية: (response) => { console.log(response); this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; } مرّرنا التابع السابق كتابع سهم (Arrow Function) يقبل وسيطًا اسمه response وهو يمثّل الرد الذي حصلت عليه المكتبة من الخادوم. ضمن هذا التابع وفي السطر الأوّل منه نلاحظ وجود التعليمة console.log(response) لعرض محتوى البيانات الواردة من الخادوم ضمن الطرفية (Console) الخاصة بأدوات المطوّر ضمن متصفّح الويب. يمكنك إزالة هذا السطر إن أحببت. أمّا التعليمات الثلاث التالية فهدفها تفريغ حقول بيانات المستخدم للإشارة إلى نجاح العملية. أمّا الوسيط الثاني للتابع then فهو أيضًا تابع آخر، ولكنّه يحتوي على الشيفرة البرمجية التي تعالج حالة وجود خطأ ما في عملية الإرسال. عند هذه النقطة بالتحديد، أفضّل تشغيل التطبيق لنرى ماذا سيحدث عند إضافة مستخدم جديد. شغل التطبيق عن طريق تنفيذ الأمر التالي ضمن موجّه الأوامر وضمن المجلّد vue-remote-servers الذي يحوي ملفات التطبيق: npm run serve زُر الآن الصفحة http://localhost:8080 لتحصل على واجهة التطبيق. أدخل قيم مناسبة لاسم المستخدم و الاسم والكنية، ثم انقر الزر Add. إذا اختفت القيم التي أدخلتها توًّا فهذا دليل على نجاح العملية! انتقل الآن إلى الصفحة الخاصة بقاعدة البيانات vue-remote-servers (عليك زيارة الموقع ثم نقر زر Go to console وبعد ذلك اختيار المشروع vue-remote-servers ثم نقر زر Database من القائمة اليسرى، وأخيرًا اختيار قاعدة البيانات الحقيقية (Realtime Database) التي تحمل نفس الاسم). بحسب البيانات التي أدخلتها أنا، حصلت على الشكل التالي: لاحظ معي اسم قاعدة البيانات vue-remote-servers يظهر في أعلى الشجرة، ثم يظهر اسم المجموعة users في العقدة التي تليها. يتفرّع عن المجموعة users التي هي بمثابة جدول كما اتفقنا، عقدة تالية تحمل رموزًا مبهمة، يتفرّع عنها من جديد البيانات التي أرسلتها إلى قاعدة البيانات وهي: Husam للاسم و Burhan للكنية و husam79 لاسم المستخدم. بالنسبة للرموز المبهمة التي تظهر فهي بمثابة مفتاح رئيسي (Primary Key) أو معرّف (ID) للبيانات التي تقع ضمنها. لأنّه من البديهي إرسال العديد من بيانات المستخدمين، فسيعمل Firebase على تخصيص شيفرة فريدة تحوي مثل هذه الرموز المبهمة لكل مستخدم مفترض تتم إضافته إلى قاعدة البيانات. يمكنك إن أحببت إضافة مستخدم آخر لترى كيف يحدث ذلك. كل الكلام السابق كان للتابع postData. أمّا بالنسبة للتابع getData فها هي الشيفرة البرمجيّة الموجودة ضمنه: this.$http .get("https://vue-remote-servers.firebaseio.com/users.json") .then((response) => { return response.json(); }) .then((data) => { const tmpArray = []; for (let key in data) { tmpArray.push(data[key]); } this.users = tmpArray; }); سنستخدم هذه المرة التابع get من الكائن $http وذلك للحصول على البيانات المخزّنة ضمن قاعدة البيانات. لا يحتاج هذا التابع كما هو واضح إلّا لوسيط واحد هو نفسه الوسيط الأوّل للتابع post الذي تحدثنا عنه قبل قليل. يُرجع التابع get أيضًا وعد (Promise)، لذلك وبنفس النقاش السابق فإنّنا نُتبع استدعاء التابع get بنقطة مباشرة وبعدها نكتب التابع then. اكتفيت هنا بكتابة وسيط واحد للتابع then -وهذا جائز تمامًا- وهو عبارة عن تابع سهم كما هو واضح يحتوي على تعليمة برمجية واحدة فقط: return response.json(); وظيفة هذه التعليمة هي إرجاع تمثيل JSON للرد الذي حصلت عليه المكتبة بعد استدعاء التابع get. وذلك من خلال استدعاء التابع json من الكائن response. وهنا ينبغي التنويه إلى أنّ التابع json يُرجع هو أيضًا وعد (Promise). لذلك فإنّ التعليمة السابقة تُرجع هذا الوعد مما يسمح لنا بوضع نقطة مباشرةً بعد استدعاء التابع then ثم استدعاء تابع then جديد يقبل وسيط وهو بالطبع عبارة عن تابع سهم جديد يقبل وسيطًا واحدًا أيضًا أسميته data يحتوي على البيانات الفعلية التي حصلنا عليها من قاعدة البيانات على الـ Firebase. ما تبقى ضمن تابع السهم الموجود ضمن تابع then الأخير عبارة عن شيفرة برمجيّة وظيفتها استخلاص البيانات الخام الواردة من المكتبة vue-resource التي تتواصل مع قاعدة البيانات، وتخزينها بشكل مناسب ضمن المصفوفة المؤقتة tmpArray. وبعد انتهاء عملية الاستخلاص، تُسنَد هذه المصفوفة إلى الحقل users وهو مصفوفة بالطبع، لتُعرض البيانات بالشكل المرغوب على المستخدم. انظر هذه الشيفرة: const tmpArray = []; for (let key in data) { tmpArray.push(data[key]); } this.users = tmpArray; إنّ سبب وجود حلقة for بالشكل السابق، يعود إلى شكل البيانات الواردة من المكتبة vue-resource والتي هي عبارة عن قاموس (Dictionary)، يكون المفتاح (Key) فيه عبارة عن المفتاح الرئيسي (تلك الرموز الغريبة) لأحد المستخدمين، أمّا القيمة (Value) لهذا المفتاح فهي عبارة عن كائن يتضمن بيانات المستخدم (في حالتنا هذه تكون هذه البيانات عبارة عن اسم المستخدم والاسم والكنية كما نعلم). انظر معي إلى البيانات الخام الواردة من المكتبة vue-resource، والتي حصلت عليها بإضافة تعليمة الكتابة إلى الطرفية (Console) عند ورود البيانات من المكتبة: في الحقيقة تعمدت إخفاء بعض التوابع الأخرى الموجودة ضمن الكائن للتركيز على ما يهمنا هنا. الشكل السابق هو نتيجة إضافة مستخدمين افتراضيين آخرين إلى قاعدة البيانات، وبعد نقر زر Retrieve بالطبع. انظر إلى الشكل التالي لترى كيف ستبدو نفس البيانات ولكن على صفحة الويب: إضافة ميزة تعديل البيانات للتطبيق السابق من الواضح أنّ تطبيقنا السابق يسمح بإضافة مستخدمين افتراضيين جدد، كما يسمح بقراءة بيانات هؤلاء المستخدمين وعرضهم على الصفحة. ولكن من المفيد أن يسمح التطبيق أيضًا بتعديل بيانات مستخدم افتراضي موجود مسبقًا. يمكن ذلك ببساطة من خلال استخدام التابع put من الكائن $http حيث يسمح هذا التابع بتعديل بيانات مستخدم موجود مسبقًا ضمن قاعدة البيانات بمجرّد أن نعرف المفتاح الرئيسي (تلك الرموز الغريبة) للمستخدم ضمن قاعدة البيانات. سأجري في الحقيقة تعديلًا كبيرًا من الناحية الشكلية على التطبيق السابق لكي يسمح بإنجاز هذه الميزة، ورغم التعديلات الكثيرة التي ستجري على الملف App.vue، إلّا أنّها تُعتبر في حقيقة الأمر بسيطة وواضحة خصوصًا بعد أن وصلنا إلى هذه المرحلة في العمل مع Vue.js. إليك الشيفرة البرمجية الجديدة للملف App.vue: <template> <div class="container"> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <h2>Vue.js Remote Server</h2> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" v-model="user.username" /> </div> <div class="form-group"> <label>First name</label> <input type="text" class="form-control" v-model="user.firstname" /> </div> <div class="form-group"> <label>Last name</label> <input type="text" class="form-control" v-model="user.lastname" /> </div> </div> </div> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <button class="btn btn-primary" @click="postOrPutData()">{{actionButtonTitle}}</button> <button class="btn btn-primary float-right" @click="reset()">Reset</button> </div> </div> <hr> <div class="row"> <div class="col-2"> <button class="btn btn-primary btn-dark" @click="getData()">Retrieve</button> </div> </div> <div class="row" v-for="usr in users" v-bind:key="usr.username"> <div class="col-2">{{usr.username}}</div> <div class="col-4">{{usr.firstname}} {{usr.lastname}}</div> <div class="col-1"> <button class="btn btn-link" @click="prepareToSave(usr.id)">Edit</button> </div> </div> </div> </template> <script> export default { name: "App", data() { return { user: { username: "", firstname: "", lastname: "", }, users: [], currentUserIdToSave: "", actionButtonTitle: "Add", }; }, methods: { postOrPutData: function () { if (this.actionButtonTitle === "Add") { this.$http .post( "https://vue-remote-servers.firebaseio.com/users.json", this.user ) .then( (response) => { console.log(response); this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; }, (error) => { console.log(error); } ); } else { this.$http .put( "https://vue-remote-servers.firebaseio.com/users/" + this.currentUserIdToSave + ".json", this.user ) .then( (response) => { console.log(response); }, (error) => { console.log(error); } ); } }, getData: function () { this.$http .get("https://vue-remote-servers.firebaseio.com/users.json") .then((response) => { return response.json(); }) .then((data) => { const tmpArray = []; for (let key in data) { let withId = data[key]; withId.id = key; tmpArray.push(data[key]); } this.users = tmpArray; }); }, prepareToSave: function (id) { for (var i = 0; i < this.users.length; i++) { if (this.users[i].id === id) { this.user.username = this.users[i].username; this.user.firstname = this.users[i].firstname; this.user.lastname = this.users[i].lastname; this.currentUserIdToSave = this.users[i].id; this.actionButtonTitle = "Save"; break; } } }, reset: function () { this.actionButtonTitle = "Add"; this.currentUserIdToSave = ""; this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; }, }, }; </script> <style> .row { margin-top: 8px; } </style> سأوضّح التعديلات الرئيسية التي طرأت على الشيفرة البرمجية. في البداية أجريت تعديلات تنسيقية على القسم <template> للسماح بإضافة زر جديد اسمه Reset، بالإضافة إلى أنّني غيرت العنصر المسؤول عن عرض بيانات المستخدمين من قاعدة البيانات (كان عبارة عن قائمة غير مرتبة ul)، وذلك لكي أستطيع إضافة زر تعديل Edit بجوار كل مستخدم للسماح بتعديل بياناته في حال الرغبة بذلك. أضفت أيضًا حقلين جديدين للقسم data، الأول هو currentUserIdToSave ووظيفته الاحتفاظ بالمفتاح الرئيسي للمستخدم الحالي الذي نرغب بتعديل بياناته، أمّا الحقل الثاني فهو actionButtonTitle والذي جعلت قيمته الحالية تظهر مباشرة على الزر Add الذي سيتغير النص الظاهر عليه بحسب السياق. أي عندما نرغب بإضافة مستخدم جديد سيظهر النص Add، أمّا عندما نرغب بتعديل بيانات مستخدم موجود مسبقًا سيظهر النص Save. بمعنى آخر، سيعمل التطبيق على تنفيذ مجموعة مختلفة من التعليمات البرمجية بحسب النص الذي يظهر حاليًا على هذا الزر. لذلك فمن المنطقي تغيير اسم التابع الذي سيُنفّذ عند النقر على هذا الزر، فقد غيرت اسم التابع من postData إلى postOrPutData للإشارة إلى وظيفته الجديدة المتمثلة في الإضافة الجديدة أو التعديل على بيانات موجودة مسبقًا. انظر إلى الشيفرة البرمجية الموجودة ضمن هذا التابع: if (this.actionButtonTitle === "Add") { this.$http .post( "https://vue-remote-servers.firebaseio.com/users.json", this.user ) .then( (response) => { console.log(response); this.user.username = ""; this.user.firstname = ""; this.user.lastname = ""; }, (error) => { console.log(error); } ); } else { this.$http .put( "https://vue-remote-servers.firebaseio.com/users/" + this.currentUserIdToSave + ".json", this.user ) .then( (response) => { console.log(response); }, (error) => { console.log(error); } ); } لاحظ عبارة if الموجودة في بداية التابع، وانتبه إلى الشرط الموجود ضمنها. يختبر هذا الشرط فيما إذا كانت القيمة الحالية للحقل actionButtonTitle تساوي القيمة Add. فإذا تحقق هذا الشرط، فهذا يعني أننا نريد إضافة مستخدم جديد، فتُنفّذ الكتلة البرمجية المتعلقة بتحقق ذلك الشرط، وهي نفس الكتلة البرمجية التي كانت موجودة ضمن التابع postData قبل التعديل. أمّا إذا لم يتحقق الشرط، فهذا يعني بالتأكيد أنّنا في السياق الذي يسمح بتعديل بيانات موجودة مسبقًا، لذلك تُنفّذ الكتلة البرمجية الموافقة التي تستخدم في هذه المرة التابع put كما هو واضح. يتم التحكّم في قيمة الحقل actionButtonTitle ضمن التابعين الجديدين prepareToSave و reset كما سنرى ذلك بعد قليل. لنركّز الآن قليلًا على التابع put: this.$http .put( "https://vue-remote-servers.firebaseio.com/users/" + this.currentUserIdToSave + ".json", this.user ) يقبل هذا التابع وسيطين. الوسيط الأوّل هو عنوان المصدر الذي سنعمل على تعديل بياناته. لاحظ معي كيف وضعت اسم الجدول (المجموعة) users يليه قيمة المفتاح الرئيسي الذي حصلت عليه من قيمة الحقل currentUserIdToSave ثم وضعت الإمتداد الإلزامي .json. أمّا الوسيط الثاني فهو بكل بساطة الكائن user الذي من المفترض الآن أن يحمل البيانات المعدّلة لهذا المستخدم. من الممكن أن نجري تعديل على جزء من البيانات أو على جميع البيانات أو أن لا نجري أية تعديلات. في كل حالة من الحالات السابقة سيتم استبدال بالقيم الحالية لبيانات الكائن user البيانات القديمة الموجودة ضمن قاعدة البيانات والتي لها نفس قيمة المفتاح الرئيسي. كما أوضحت قبل قليل، فقد أضفت تابعين جديدين هما: prepareToSave و reset، كما أجريت تعديلًا بسيطًا للتابع getData. سأبدأ من التعديل البسيط الذي أجريته على التابع getData. أضفت في الحقيقة حقلًا جديدًا إلى كل كائن يمثّل مستخدم مفترض تم جلبه من قاعدة بيانات. انظر معي إلى حلقة for التي حدث فيها التعديل: for (let key in data) { let withId = data[key]; withId.id = key; tmpArray.push(data[key]); } أنشأت متغير جديد اسمه withId تنحصر وظيفته في إضافة الحقل id إلى البيانات التي تأتي أصلًا من قاعدة البيانات وذلك بهدف الاحتفاظ بالمفتاح الرئيسي لكل مستخدم مع بياناته الأساسية. سنرى سبب هذا التعديل بعد لحظات. بالنسبة للشيفرة البرمجية لكل من التابعين prepareToSave و reset فهي بسيطة للغاية. بالنسبة للتابع prepareToSave فيُستدعى عند نقر الزر Edit بجوار مستخدم ما. يقبل هذا التابع وسيطًا واحدًا هو قيمة المفتاح الرئيسي لذلك المستخدم الذي نرغب بتعديل بياناته. بعد ذلك ندخل حلقة for، هدفها الوصول إلى الكائن الذي يمثّل المستخدم المراد تعديل بياناته. استطعت تحديد هذا الكائن، وذلك بمقارنة قيمة الوسيط id (الذي يحمل قيمة المفتاح الرئيسي للمستخدم المراد تعديله) مع قيمة الحقل id لكل مستخدم موجود ضمن المصفوفة users. لاحظ هنا أنّ الحقل id الموجود ضمن المصفوفة users قد أضفناه ضمن التابع getData. بعد الوصول إلى الكائن المطلوب ضمن المصفوفة، تعمل الشيفرة على تحديث بيانات الحقل user وبالتالي تتم تعبئة عناصر الإدخال على الصفحة ببيانات المستخدم المراد تعديله، ثم تُعدَّل قيمة الحقل currentUserIdToSave ليحمل قيمة المفتاح الرئيسي لهذا المستخدم المراد تعديله (لاحظ أنّنا استخدمنا قيمة هذا الحقل ضمن التابع postOrPutData بتمريره للتابع put)، وأيضًا تُعدَّل قيمة الحقل actionButtonTitle لتحمل القيمة Save، وهكذا نكون قد دخلنا في سياق حفظ البيانات، أي نكون جاهزين لتعديل بيانات المستخدم. وأخيرًا بالنسبة للتابع reset فهو يُعيد الأمور إلى أصلها الأول. أي يعمل على تفريغ حقول الإدخال، ويعمل على إعادة السياق إلى حالة إدخال مستخدم جديد بإسناد القيمة Add ضمن الحقل actionButtonTitle، كما يعمل على تفريغ الحقل currentUserIdToSave لأنّه لا يوجد حاليًا أي مستخدم نرغب بتعديل بياناته كما هو واضح. إذا شغلت التطبيق بعد هذه التعديلات يُفترض أن تحصل على شكل شبيه بما يلي (لاحظ أنّني قد نقرت أيضًا الزر Retrieve): جرب نقر زر Edit بجوار أحد المستخدمين، ثم عدّل بياناته، ثم انقر زر Save، وبعدها انقر زر Retrieve من جديد. يجب أن ترى الآن التعديلات التي أجريتها على ذلك المستخدم. انقر زر Reset للعودة إلى سياق إضافة مستخدم جديد. ملاحظة: لا يُعتبر الأسلوب الذي اتبعته في تعديل البيانات أسلوبًا عمليًا في التطبيقات الحقيقة. إذ كان من الواجب الانتقال إلى صفحة منفصلة عند النقر على زر Edit لتعديل بيانات المستخدم بشكل مستقل. لكنني آثرت اتباع هذا الأسلوب غير العملي الآن، لأنّ اتباع الأسلوب العملي يقتضي الدخول في بحث جديد تمامًا وهذا ما سأفعله لاحقًا في درس منفصل إن شاء الله. ملاحظة: من الممكن أيضًا إضافة ميزة حذف مستخدم موجود مسبقًا باستخدام التابع delete مع الكائن $http مع تمرير وسيط واحد فقط له. هو نفسه الوسيط الأوّل للتابع put. ختامًا لقد تعلّمنا الكثير في هذا الدرس! لقد تعلّمنا كيفية إنشاء قاعدة بيانات بسيطة على Google Firebase، كما وتعلّمنا كيفية استخدام المكتبة vue-resource لإكساب تطبيقات Vue.js القدرة على الاتصال بالانترنت، وبنينا أيضًا تطبيق عملي مبسّط يوضّح كيفية التواصل مع قاعدة البيانات على Google Firebase، وبالتالي كيفية إضافة وقراءة وتعديل البيانات الموجودة ضمنها. يُعتبر هذا الدرس مهمًّا بالفعل، فمن خلاله استطعت للمرة الأولى الخروج من "القمقم" والتواصل مع العالم الخارجي. ستحمل ما تبقى من دروس مفاهيم مهمّة أيضًا حول كيفية التعامل مع Vue.js تلك المكتبة القوية والمرنة. اقرأ أيضًا المقال التالي: بناء تطبيقات ذات صفحة واحدة باستخدام التوجيه Routing في Vue.js المقال السابق: المرشحات Filters والـمخاليط Mixins في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: المرشّح (Filter). المخلوط (Mixin). سنتابع عملنا في هذه السلسلة مع ميزتين مفيدتين في Vue.js وهما: المرشّحات (Filters) والمخاليط (Mixins). تُعتبر هاتين الميزتين على بساطتهما من المزايا المتقدّمة نسبيًا في Vue.js. سنوضّح المقصود بكل منهما في هذا الدرس، وذلك بكتابة مثالين تطبيقيين بسيطين، لتوضيح الغاية من استخدام هاتين التقنيتين بشكل جيّد. المرشّح (Filter) المرشّح (Filter) هو ميزة مفيدة في Vue.js يمكن من خلالها تطبيق إجراء معيّن على قيمة حقل ما، بحيث يطرأ تعديل على شكله النهائي عند عرضه للمستخدم. يمكن تعريف المرشّحات التي نرغب بكتابتها ضمن قسم خاص ضمن كائن Vue.js اسمه filters. فالمرشح في الحقيقة عبارة عن مجرّد تابع يقبل وسيطًا واحدًا، ويرجع قيمة بعد التعديل. كمثال على ذلك، يمكن استخدام مرشح يعمل على تحويل حالة الأحرف إلى كبيرة (بالنسبة للأحرف الانجليزية)، رغم أنّها قد تكون ضمن الحقل عبارة عن مزيج بين أحرف صغيرة وكبيرة. ربما تكتشف بعد قليل عددا كبيرًا من الأمثلة حول الإمكانات التي قد توفرها المرشّحات. ولكن تذكّر، أنّه من الأفضل دومًا أن تكون المعالجة البرمجية ضمن المرشحات بسيطة وسريعة. فلم تصمّم المرشّحات لإجراء عمليات معقدة على البيانات تكلّف الكثير من إمكانيات المعالجة. فإذا شعرت مثلًا أنّ الأمر يتجه إلى مزيد من التعقيد، فأنصحك بالتفكير باستخدام الخصائص المحسوبة بدلًا من المرشّحات. لنتناول الآن مثالًا برمجيًا يوضّح كيفية كتابة المرشحات. أنشئ مشروعًا جديدًا باستخدام الأمر vue من موجّه الأوامر وسمّه vue-filters كما يلي: vue create vue-filters ثم افتح المشروع باستخدام Visual Studio Code. احذف الملف HelloWorld.vue ثم استبدال بما يلي محتويات الملف App.vue: <template> <div id="app"> <h1>Filters</h1> {{text | toUpper}} </div> </template> <script> export default { data(){ return { text:"We will apply a filter on this field." } }, filters:{ toUpper(value){ return value.toUpperCase(); } } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> انظر إلى القسم الجديد filters كيف عرفت المرشّح toUpper ضمنه على شكل تابع يقبل قيمة وحيدة value. لاحظ كيف يُرجع هذا المرشّح حالة الأحرف الكبيرة لهذه القيمة عن طريق تابع جافاسكريبت وهو toUpperCase. لاحظ معي الآن طريقة تطبيق المرشّح كما هو ظاهر من الشيفرة السابقة. نضع المحرف | بعد اسم الحقل المراد تطبيق المرشّح عليه، ثم نكتب اسم المرشّح. أي كما في التالي: text | toUpper وهذا كل شيء. ينبغي الانتباه هنا إلى أنّ المرشّح لن يؤثّر على القيمة الأصلية للحقل text إنما سيجري التعديل على الخرج الذي سيظهر للمستخدم فحسب. جرب الآن معاينة التطبيق البسيط السابق لترى كيف أصبح النص يظهر بأحرف طباعية كبيرة. الأسلوب السابق في تعريف المرشّحات كقسم ضمن كائن Vue.js يُعتبر تعريفًا محليًا (Local). يمكن تعريف المرشّحات بشكل عام (Global) وذلك عن طريق استخدام التابع Vue.filter ضمن الملف main.js على الشكل التالي: import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false Vue.filter('toLower', function(value){ return value.toLowerCase(); }) new Vue({ render: h => h(App), }).$mount('#app') كما هو واضح، يٌمرّر وسيطان إلى التابع filter الأوّل هو اسم المرشّح العام، أمّا الثاني فهو التابع الذي يحتوي على الشيفرة البرمجيّة الخاصة بالمرشّح. المرشّح هنا هو toLower وسنجعله مسؤول عن تحويل الأحرف إلى الحالة الطباعية الصغيرة. سنعمل الآن على تطبيق المرشّح العام الجديد ضمن الملف App.vue وذلك على المرشّح المحلي القديم على النحو التالي: {{text | toUpper | toLower}} لاحظ كيف طبقنا المرشّح الجديد بشكل متسلسل على المرشّح القديم. الذي سيحدث الآن، أنّ محتويات الحقل text سيطبّق عليها المرشّح toUpper أولًا، مما سيعطينا حالة أحرف طباعية كبيرة، ثم سيتم تطبيق المرشّح toLower على النتيجة الأخيرة، مما سيؤدي إلى تحويل حالة الأحرف إلى الحالة الطباعية الصغيرة. كان يمكن بالطبع تطبيق المرشح toLower فقط دون المرشّح toUpper ولكنني فضلت استخدامها بالشكل المتسلسل السابق لتوضيح إمكانية تطبيق أكثر من مرشّح بنفس الوقت بشكل متسلسل كما فعلنا قبل قليل. المخلوط (Mixin) قد تُضطر في بعض الأحيان إلى كتابة مكوّنات تشترك بنفس الجزء من الشيفرة البرمجية تقريبًا. رغم أنّ ذلك لا يعد مشكلة برمجية أساسًا إلّا أنّه يشكل عادة برمجية غير جيدة. دائمًا ما نسعى في البرمجة إلى تجنّب تكرار الشيفرة البرمجية كما تعلم. جاء المخلوط (Mixin) ليوجد حلًا لهذه المسألة، حيث من الممكن جعل ذلك الجزء المشترك من المكوّنات ضمنه بحيث تتشارك تلك المكونات بالمخلوط مما يلغي الحاجة إلى تكرار الشيفرة البرمجية. سنحتاج بالتأكيد إلى مثال عملي يوضّح هذه الفكرة. أنشئ مشروعًا جديدًا وسمّه vue-mixins، ثم افتح المشروع باستخدام Visual Studio Code. احذف الملف HelloWorld.vue، ثم أضف ملفين ضمن المجلّد components، سمّهما على النحو التالي: BMI.vue و ImperialConverter.vue. وأضف أيضًا ملف اسمه ConverterMixin.js إلى المجلّد src. فكرة هذا التطبيق هي إنشاء مكوّنين الأول هو ImperialConverter يوفر إمكانية التحويل من الكيلوغرام والسنتيمتر إلى الباوند والبوصة على الترتيب. أمّا المكوّن الثاني فهو BMI ووظيفته إعطاء تقرير عن حالة مؤشّر كتلة الجسم (Body Mass Index). يشترك كل من المكونين السابقين بجزء من الشيفرة البرمجية موضوع ضمن المخلوط ConverterMixin. مع العلم أنّ كل من المكونين السابقين مستقلين تمامًا فيما يتعلّق بالبيانات. يحتوي المخلوط ConverterMixin على الشيفرة البرمجية المسؤولة عن التحويل من الكيلوغرام إلى الباوند، ومن السنتيمتر إلى البوصة. انظر محتويات الملف ConverterMixin.js: export const ConverterMixin = { data() { return { kg_value: 0, cm_value: 0 }; }, computed: { to_pounds: function () { return this.kg_value * 2.20462; }, to_inches: function () { return this.cm_value * 0.393701; } } } الشيفرة البرمجية الموجودة في المخلوط بسيطة جدًا، حيث تحتوي على حقلي بيانات kg_value و cm_value بالإضافة إلى خاصيتين محسوبتين: to_pounds و to_inches. بالنسبة للمكون ImperialConverter فهو بسيط أيضًا، انظر للشيفرة البرمجية الموجودة ضمنه: <template> <div> <h2>Kilograms to Pounds Converter</h2> <div> <input type="text" style="width:50px;text-align:center;" v-model="kg_value" /> <span style="margin-left:8px;">Kg = {{to_pounds}} pounds.</span> </div> <br /> <div> <input type="text" style="width:50px;text-align:center;" v-model="cm_value" /> <span style="margin-left:8px;">CM = {{to_inches}} Inches.</span> </div> </div> </template> <script> import { ConverterMixin } from "../ConverterMixin"; export default { name: "ImperialConverter", mixins: [ConverterMixin], }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style> يوفر هذا المكوّن عناصر HTML مناسبة لإدخال البيانات المراد تحويلها وذلك ضمن القسم <template>، أمّا بالنسبة للقسم <script> فنعمل على استيراد المخلوط باستخدام التعليمة import ثم نُخبر Vue.js أنّنا نريد استخدام هذا المخلوط عن طريق القسم mixins. لاحظ أنّه لا توجد شيفرة برمجية فعلية ضمن المكون لأنّه أغلب المنطق الحسابي يحدث ضمن المخلوط في مثالنا البسيط هذا. أمّا بالنسبة للمكوّن BMI فهو المكوّن المسؤول (كما أشرنا) على حساب مؤشّر كتلة الجسم بالإضافة إلى عرض تقرير بسيط للمستخدم حول القيمة المحسوبة. انظر إلى الشيفرة البرمجية لهذا المكون: <template> <div> <h2>Body Mass Index (BMI)</h2> <div> <div> <span>Weight (Kg):</span> <input type="text" style="width:50px;text-align:center;" v-model="kg_value" /> </div> <br /> <div> <span>Height (Cm):</span> <input type="text" style="width:50px;text-align:center;" v-model="cm_value" /> </div> <br /> <div> <span>BMI value: {{bmi_value}}</span> - <span>{{result}}</span> </div> </div> </div> </template> <script> import { ConverterMixin } from "../ConverterMixin"; export default { name: "BMI", mixins: [ConverterMixin], computed: { bmi_value: function () { return (703 * this.to_pounds) / (this.to_inches * this.to_inches); }, result: function () { if (this.bmi_value < 16) { return "Severe Thinness"; } else if (this.bmi_value < 17) { return "Moderate Thinness"; } else if (this.bmi_value < 18.5) { return "Mild Thinness"; } else if (this.bmi_value < 25) { return "Normal"; } else if (this.bmi_value < 30) { return "Overweight"; } else if (this.bmi_value < 35) { return "Obese Class I"; } else if (this.bmi_value < 40) { return "Obese Class II"; } else if (this.bmi_value >= 40) { return "Obese Class III"; } else { return "Not defined!"; } }, }, }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style> مرة أخرى، يوفّر القسم <template> واجهة المستخدم، أمّا القسم <script> فنستورد عن طريقه المخلوط ConverterMixin ونصرح عن استخدامه كما فعلنا تمامًا مع المكون ImperialConverter قبل قليل. بقي أخيرًا الملف App.vue الذي سنستخدم من خلاله المكونين السابقين. انظر إلى الشيفرة البرمجية لهذا الملف: <template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <imperial-converter/> <BMI/> </div> </template> <script> import ImperialConverter from './components/ImperialConverter.vue' import BMI from './components/BMI.vue' export default { name: 'App', components: { ImperialConverter, BMI } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> بعد تشغيل التطبيق ستحصل على واجهة شبيهة بما يلي: ختامًا تحدثنا في هذا الدرس عن ميزتين جديدتين من مزايا Vue.js، وهما: المرشّحات (Filters) والمخاليط (Mixins)، حيث قد وجدنا أنّ المرشحات هي عبارة عن شيفرات برمجية بسيطة تعمل على إجراء منطق برمجي بسيط وسريع دون الحاجة لاستخدام التوابع أو الخاصيات المحسوبة. أمّا المخاليط فوجدنا أنّها تقنية مفيدة للتخلص من تكرار الشيفرة البرمجية بين المكوّنات المختلفة، حيث يُوضع القسم البرمجي المشترك بين مكوّنين أو أكثر ضمن ملف منفصل دعوناه بالمخلوط، وتعمل المكوّنات المختلفة بعد ذلك على استخدام هذا المخلوط وتتجنّب تكرار الشيفرة البرمجية. اقرأ أيضًا المقال التالي: استخدام Vue.js للاتصال بالإنترنت المقال السابق: التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: بناء هيكل تطبيق بسيط لشرح أفكار الدرس استخدام إطار العمل Bootstrap استخدام بنى معطيات متقدمة مع عناصر الإدخال النصية المعدِّلات (Modifiers) في Vue.js التعامل مع مربعات الاختيار (Checkboxes) وأزرار الانتقاء (Radiobuttons) التعامل مع القائمة المنسدلة <select> إرسال البيانات لقد تعاملنا في بداية هذه السلسلة مع عناصر الإدخال العادية التي يستخدمها المستخدم لإدخال البيانات ضمن صفحات الويب. سنعمل في هذا الدرس على التوسّع في التعامل مع هذه العناصر بالإضافة إلى الاطلاع على طريقة التعامل مع عناصر إدخال HTML جديدة. كما سنتعلّم كيفية التعامل مع أُطر عمل CSS جاهزة، حيث لم يسبق لنا التعامل معها مسبقًا. بناء هيكل تطبيق بسيط لشرح أفكار الدرس كما جرت العادة، سنبني تطبيق بسيط من أجل هذا الدرس بهدف تطبيق الأفكار الواردة فيه. سيتكوّن تطبيقنا من صفحة واحدة فقط تحوي عدد من عناصر HTML التي ننوي التعامل معها. أنشئ مشروع جديد وسمّه input-forms-cli كما فعلنا مسبقًا في الدرس السابق (راجع الفقرة "بناء هيكل تطبيق بسيط لشرح أفكار الدرس"). بعد الانتهاء من عملية الإنشاء، استخدم Visual Studio Code في فتح المشروع (المجلّد) الذي أنشأته توًّا. لن نحتاج سوى ملف مكوّن واحد وهو App.vue لذلك احذف الملف HelloWorld.vue كما فعلنا في الدرس السابق. ثم افتح الملف App.vue واستبدل المحتوى الحالي بالشيفرة البرمجية التالية: <template> <div class="container"> <form> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <h1 class="text-right">الملف الشخصي للمستخدم</h1> <hr> <div class="form-group"> <label class="float-right" for="firstname">الاسم</label> <input type="text" id="firstname" class="form-control" v-model="userMainData.firstname"> </div> <div class="form-group"> <label class="float-right" for="lastname">الكنية</label> <input type="text" id="lastname" class="form-control" v-model="userMainData.lastname"> </div> <div class="form-group"> <label class="float-right" for="age">العمر</label> <input type="number" id="age" class="form-control" v-model="userMainData.age"> </div> <div class="form-group"> <label class="float-right" for="password">كلمة المرور</label> <input type="password" id="password" class="form-control" v-model="userMainData.password"> </div> </div> </div> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 form-group"> <label class="float-right" for="description">نبذة</label><br> <textarea id="description" rows="5" class="form-control" v-model="description"></textarea> </div> </div> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <div class="form-group"> <label class="float-right" for="graduate"> <input type="checkbox" id="graduate" value="متخرج" v-model="status"> متخرج </label> <label class="float-right" for="smoker"> <input type="checkbox" id="working" value="أعمل حاليًا" v-model="status"> أعمل حاليًا </label> </div> </div> </div> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 form-group"> <label class="float-right" for="male"> <input type="radio" id="male" value="ذكر" v-model="gender"> <span>ذكر</span> </label> <label class="float-right" for="female"> <input type="radio" id="female" value="أنثى" v-model="gender"> <span>أنثى</span> </label> </div> </div> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 from-group"> <label class="float-right" for="subscriptionKind">نوع الاشتراك</label> <select id="subscriptionKind" class="form-control" v-model="selectedSubscription"> <option v-for="kind in subscriptionKinds" v-bind:key="kind"> {{ kind }} </option> </select> </div> </div> <hr> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-1"> <button class="btn btn-primary">إرسال </button> </div> </div> </form> <hr> <div class="row"> <div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"> <div class="card card-info"> <div class="card-header text-right"> <h4>البيانات المُدخلة</h4> </div> <div class="card-body"> <div class="card-text text-right"> <p>الاسم:{{ userMainData.firstname }}</p> <p>الكنية:{{ userMainData.lastname }}</p> <p>العمر:{{ userMainData.age }}</p> <p>كلمة المرور:{{ userMainData.password }}</p> <p>نبذة: {{ description }}</p> <p><strong>الوضع الحالي</strong></p> <ul> <li v-for="item in status" v-bind:key="item">{{ item }}</li> </ul> <p>النوع:{{ gender }}</p> <p>نوع الاشتراك: {{ selectedSubscription }}</p> </div> </div> </div> </div> </div> </div> </template> <script> export default { data() { return { userMainData: { firstname: '', lastname: '', age: 0, password: '', }, description: 'اكتب نبذة قصيرة عنك!', status: [], gender: 'ذكر', selectedSubscription: 'فضي', subscriptionKinds: ['ذهبي', 'فضي', 'عادي'] } } } </script> <style> @import "./assets/styles/app.css"; </style> أضف مجلّدًا جديدًا ضمن المجلّد assets وسمّه styles ثم أضف ملفًا جديدًا ضمن المجلّد الأخير وسمّه app.css كما فعلنا في الدرس السابق. احرص أن تكون محتويات الملف app.css على الشكل التالي: @import url(//fonts.googleapis.com/earlyaccess/notonaskharabic.css); body{ font-family: 'Noto Naskh Arabic', serif; } احفظ جميع التعديلات، ثم افتح موجّه الأوامر في ويندوز، وبعدها انتقل إلى مجلد التطبيق input-forms-cli الذي أنشأته قبل قليل، ونفّذ الأمر: npm run serve يعمل هذا الأمر كما نعلم على تشغيل خادوم الويب الخاص بالتطوير، اذهب الآن إلى متصفح الويب لديك، وانتقل إلى العنوان http://localhost:8080/ ليظهر لك شكل شبيه بما يلي: استخدام إطار العمل Bootstrap ربما تكون قد استغربت قليلًا من عدم استخدامنا لأي مكتبة أو إطار عمل CSS في عمليات التنسيق التي كنا نجريها في تطبيقاتنا السابقة. بدلًا عن ذلك، كنا نستخدم شيفرة CSS عادية فحسب في تنسيق التطبيقات. يعود سبب ذلك إلى أنّه لا يُنصح أبدًا إضافة إطار عمل CSS بالطريقة التقليدية التي تتمثل في استخدام الوسم <link> كما يفعل أغلبنا عند تطوير تطبيقات أمامية (Frontend Applications). يعود السبب في ذلك لأنّ أغلب أُطر عمل CSS تحتوي على شيفرة JavaScript قد لا تكون متوافقة مع Vue.js. علينا إذًا استخدام إطار عمل CSS نضمن أن يكون متوافقًا مع Vue.js لكي لا تحصل مفاجآت غير مرغوبة! سأتحدث هنا عن استخدام إطار العمل الشهير Bootstrap لكي نُكسب تطبيقاتنا تنسيقات جميلة وقوية وبشكل متوافق مع Vue.js. افتح نافذة موجه الأوامر في ويندوز، ثم نفّذ الأمر التالي: npm install bootstrap@4.0.0 بعد الانتهاء وعودة موجه الأوامر إلى حالته الطبيعية، أضف السطر التالي إلى الملف main.js: import "bootstrap/dist/css/bootstrap.min.css"; ستصبح محتويات الملف main.js مشابهة لما يلي: import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false import "bootstrap/dist/css/bootstrap.min.css"; new Vue({ render: h => h(App), }).$mount('#app') نكون بهذا الشكل قد نصبنا إطار العمل Bootstrap ومن ثمّ أضفناه إلى التطبيق الخاص بنا ليصبح جاهزًا للاستخدام. وهكذا نكون قد استغنينا عن كتابة تنسيقات مخصّصة إلى حد كبير. استخدام بنى معطيات متقدمة مع عناصر الإدخال النصية في الحقيقة رغم أنّ عنوان هذه الفقرة مغرٍ بعض الشيء، إلّا أنّنا في الواقع سنستخدم كائن JavaScript عادي لتمثيل بيانات المستخدم الأساسية ضمن تطبيقنا الحالي. إذا راجعت شيفرة التطبيق الأساسية التي أوردتها في الفقرة الأولى، وتحديدًا ضمن القسم <script> ستجد البنية البسيطة التالية: userMainData:{ firstname:'', lastname:'', age:0, password:'', } كما أوضحت، سيحمل الحقل userMainData بيانات المستخدم الرئيسية التالية على الترتيب: الاسم، الكنية، العمر، كلمة المرور. السؤال هنا، كيف سنربط عناصر HTML الموافقة؟ الجواب بسيط، سنستخدم v-model للربط ثنائي الاتجاه. انظر إلى الشيفرة المقتطعة من الشيفرة الموجودة ضمن القسم <template>: <div class="form-group"> <label class="float-right" for="firstname">الاسم</label> <input type="text" id="firstname" class="form-control" v-model="userMainData.firstname"> </div> <div class="form-group"> <label class="float-right" for="lastname">الكنية</label> <input type="text" id="lastname" class="form-control" v-model="userMainData.lastname"> </div> <div class="form-group"> <label class="float-right" for="age">العمر</label> <input type="number" id="age" class="form-control" v-model="userMainData.age"> </div> <div class="form-group"> <label class="float-right" for="password">كلمة المرور</label> <input type="password" id="password" class="form-control" v-model="userMainData.password"> </div> لاحظ معي كيف استخدمت الحقل userMainData لربط كل خاصية من خصائصه بعنصر HTML الموافق. انظر مثلًا ماذا استخدمت لربط حقل الاسم firstname: userMainData.firstname لقد استخدمت النقطة للفصل بين userMainData وبين firstname وهذا جائز تمامًا! يمكنك مراجعة باقي عناصر HTML لترى كيف استخدمت نفس الترميز السابق. الفائدة في استخدام هذا الأسلوب هي في تنظيم وتمثيل البيانات بشكل منطقي في التطبيق. حاول كتابة أي شيء ضمن الحقول الأربعة السابقة، ستجد أنّ ذلك سينعكس مباشرة على القسم السفلي (البيانات المُدخلة) من الصفحة، والذي جعلته خصيصًا لمشاهدة النتائج التي سنجريها على البيانات في القسم العلوي. في الحقيقة لقد تعاملنا مسبقًا مع عناصر الإدخال العادية فيما سبق من دروس، وسنتعامل مع أنواع أخرى من عناصر الإدخال في هذا الدرس. أحد العناصر الجديدة التي سنتعامل معها هو العنصر <textarea> والذي نتعامل معه كما نتعامل مع عناصر الإدخال العادية تمامًا. إذ وضعت الموجّه v-model ضمن هذا العنصر ليرتبط مع الحقل description كما هو واضح من الشيفرة الواردة في الفقرة الأولى من هذا الدرس. المُعدِّلات (Modifiers) في Vue.js يحتاج المبرمج في بعض الأحيان إلى تعديل سلوك الاستجابة لـ Vue.js. فمثلًا إذا لاحظت في تطبيقنا هذا، أنّ أي شيء تكتبه ضمن أي عنصر من عناصر الإدخال النصية، سينعكس مباشرة ضمن القسم السفلي. قد يكون هذا السلوك غير مرغوب أحيانًا، فقد ترغب ربما بأن لا يستجيب Vue.js مباشرةً لما يكتبه المستخدم، بل بأن تؤجّل هذه الاستجابة حتى ينتهي المستخدم مما يكتبه وينتقل إلى حقل نصي آخر مثلًا. السيناريو السابق ممكن تمامًا باستخدام تقنية المعدِّلات (modifiers) حيث يمكن أن نستخدم المعدِّل lazy لكي نخبر Vue.js بأن يؤجّل الاستجابة لأي عنصر إدخال مرتبط معه ريثما يعمل المستخدم على مغادرة هذا العنصر (يُفقده التركيز Focus). انتقل إلى الشيفرة البرمجية الخاصة بعنصر الإدخال firstname ضمن <template> ثم أضف المعدّل lazy إلى الموجّه v-model على الشكل التالي: v-model.lazy="userMainData.firstname" احفظ التغييرات، ثم حاول كتابة شيء ما ضمن الحقل firstname ولا تحاول مغادرة الحقل، ستجد هذه المرة أنّ ذلك لن ينعكس مباشرة ضمن القسم السفلي. غادر الآن ذلك الحقل إلى حقل آخر، ستجد أنّ بيانات الاسم قد ظهرت دفعة واحدة في القسم السفلي. توجد معدّلات أخرى مثل number الذي يُستخدم لكي يُفسَّر دخل المستخدم على أنّه رقم بدلًا من النص. وهناك أيضًا المعدّل trim الذي يُستخدم للتخلص من المحارف الفارغة على يمين ويسار النص المُدخَل. يمكن استخدام أي معدّل كما استخدمنا المعدّل lazy قبل قليل، كما ويمكن استخدامها بشكل مركّب. انظر مثلًا كيف يمكن استخدام المعدلين lazy و trim بشكل مركّب: v-model.lazy.trim = "userMainData.firstname" التعامل مع مربعات الاختيار (Checkboxes) وأزرار الانتقاء (Radiobuttons) سنتعلّم في هذه الفقرة كيفية التعامل مع مربع الاختيار (Checkbox) وزر الانتقاء (Radiobutton). سنبدأ أولًا مع مربع الاختيار. في تطبيقنا هذا، سنستخدم مربّعي اختيار للتعبير عن كون المستخدم متخرّج أم غير متخرّج. بهدف التعامل مع هذين المربعين، عرّفت حقلًا جديدًا اسمه status على أنّه مصفوفة (انظر القسم <script>). ربطت هذه المصفوفة مع مربعي الاختيار بإسنادها إلى الموجّه v-model لكل من المربعين. انظر الشيفرة المقتطعة من القسم <template>: <label class="float-right" for="graduate"> <input type="checkbox" id="graduate" value="متخرج" v-model="status"> متخرج </label> <label class="float-right" for="smoker"> <input type="checkbox" id="working" value="أعمل حاليًا" v-model="status"> أعمل حاليًا </label> لاحظ كيف أنّنا ربطنا مربعي الاختيار بنفس الشكل. الذي سيحدث وراء الكواليس، هو أنّه عندما يختار المستخدم أحد المربّعين سيعمل Vue.js على إدراج قيمة الحقل الذي تمّ اختياره كعنصر في المصفوفة status، فإذا اختار المستخدم كلا المربعين، فسيدرج Vue.js عنصرين ضمن المصفوفة، يعبّر كل منهما عن قيمة مربّع الاختيار. أي أنّ عدد عناصر المصفوفة سيكون مساويًا لعدد المربعات التي اختارها المستخدم، طالما أنّ هذه المربعات قد تمّ ربطها بنفس الشكل. يمكن تطبيق نفس المبدأ تقريبًا على أزرار الانتقاء، في تطبيقنا هذا لدينا زري انتقاء يُعبّران عن نوع المستخدم (ذكر أم أنثى). عرّفت حقلًا جديدًا أسميته gender يحمل القيمة الافتراضية ذكر. انظر معي إلى الشيفرة المقتطعة من القسم <template> لزري الانتقاء: <label class="float-right" for="male"> <input type="radio" id="male" value="ذكر" v-model="gender"> <span>ذكر</span> </label> <label class="float-right" for="female"> <input type="radio" id="female" value="أنثى" v-model="gender"> <span>أنثى</span> </label> لاحظ معي بدايةً أنّني قد وضعت القيمة value لكل من الزرين السابقين لتكونا ذكر و أنثى على الترتيب. لاحظ أيضًا كيف استخدمت نفس أسلوب الربط باستخدام الموجّه v-model لكل من هذين الزرين. الذي سيحدث عند بدء تشغيل التطبيق أنّ الحقل gender سيحمل القيمة ذكر بشكل افتراضي كما ذكرت قبل قليل، وبالتالي سيختار Vue.js الزر المعبّر عن الـ "ذكر" لأنّ قيمته ستكون مماثلة لقيمة الحقل gender في هذه الحالة. وهذا ما يبرّر الاختيار الافتراضي لهذا الزر عند البدء بتشغيل التطبيق. جرب أن تجري الآن بعض التجارب على مربعي الاختيار وزري الانتقاء، ولاحظ النتائج التي ستحدث ضمن القسم السفلي المخصّص لعرض البيانات. التعامل مع القائمة المنسدلة <select> بقي لنا أن نتعلّم كيفية الربط مع القائمة المنسدلة <select>. سينقسم عملنا هنا إلى قسمين. الأوّل هو تعبئة هذه القائمة، والثاني هو الربط مع Vue.js باستخدام الموجّه v-model. بالنسبة لتعبئة هذه القائمة بالبيانات الأولية التي سيختار منها المستخدم، فقد عرّفت الحقل subscriptionKinds وهو على شكل مصفوفة تحوي العناصر التالية: subscriptionKinds: ['ذهبي', 'فضي', 'عادي'] تعبّر هذه المصفوفة عن نوع الاشتراك الذي يرغب المستخدم باعتماده. سيكون لدينا كما هو واضح ثلاثة اشتراكات. الشيفرة البرمجية المسؤولة عن تعبئة عنصر القائمة هي التالية: <option v-for="kind in subscriptionKinds" v-bind:key="kind"> {{ kind }} </option> هذه الشيفرة مأخوذة من القسم <template> بالطبع. وهي عبارة عن حلقة بسيطة تعمل على تعبئة القائمة من خلال توليد عناصر <option> العمود الفقري لعنصر القائمة <select>. بالنسبة لعملية الربط فقد عرّفت حقلًا آخرًا أسميته selectedSubscription وأسندت له القيمة الافتراضية فضي. لاحظ معي أنّ هذه القيمة مماثلة للعنصر الثاني من عناصر المصفوفة subscriptionKinds. بعد ذلك، ربطت عنصر القائمة باستخدام v-model على النحو التالي: v-model="selectedSubscription" وهكذا وعند تشغيل التطبيق للمرة الأولى، سيظهر العنصر الثاني فضي وقد اختير بشكل افتراضي. جرب الآن أن تغير خياراتك ضمن القائمة وانظر كيف سينعكس ذلك ضمن القسم السفلي المخصّص لعرض البيانات. إرسال البيانات كما هو معلوم، عند وضع عنصر الزر button ضمن عنصر النموذج form، فسيؤدي نقر هذا الزر إلى إرسال بيانات النموذج (form) إلى الخادوم. في تطبيقنا البسيط هذا، وضعنا زرًا لإرسال البيانات كما تعلم، ولكن بما أنّه ليس لدينا حاليًا أي تطبيق يعمل على الخادوم لمعالجة لبيانات المرسلة، لذلك سنعمل على معالجة البيانات محليًّا بشكل وهمي، لذلك سنغيّر من سلوك هذا الزر لكي نمنعه من التصرف بالشكل الافتراضي. سنستخدم لهذه الغاية المعدِّل prevent مع الموجّه v-on:click. اعمل على تعديل شيفرة HTML الخاصة بزر الإرسال لتصبح على النحو التالي: <button v-on:click.prevent="submitInfo" class="btn btn-primary">إرسال </button> سنضيف القسم method لكي نتمكّن من تعريف التابع submitInfo. سأضع هنا كامل قسم <script> بعد التعديل: methods:{ submitInfo(){ alert("تمت معالجة البيانات"); } } وضعت رسالة بسيطة تشير إلى أنّ البيانات قد تمت معالجتها بشكل افتراضي محليًّا. يمكن أن تضع بدلًا من هذه الرسالة أن شيفرة قد تجدها مناسبة للبيانات التي أدخلها المستخدم. الهدف هنا هو أن تفهم المبدأ بحيث يمكنك تكييفه فيما بعد بحسب احتياجاتك. ختامًا تعلّمنا في هذا الدرس كيفية استخدام أُطر عمل جاهزة لتنسيق المحتوى، حيث استخدمنا في هذا الدرس إطار العمل الشهير Bootstrap. كما تعلّمنا كيف نتعامل مع عدد من عناصر HTML المخصّصة لاستقبال الدخل من المستخدم، حيث تعاملنا مع عناصر الإدخال النصية البسيطة بالإضافة إلى عنصر الإدخال ذو الأسطر المتعدّدة <textarea> وأيضًا مربعات الاختيار وأزرار الانتقاء وعنصر القائمة المنسدلة. في الحقيقة، يمكن أن تبني عناصر إدخال مخصّصة بك وفق احتياجاتك الخاصة وذلك باستخدام المكوّنات كما مرّ معنا في دروس سابقة. أرجو لك الفائدة من هذا الدرس، أراك في الدروس القادمة إن شاء الله. اقرأ أيضًا المقال التالي: المرشحات Filters والـمخاليط Mixins في Vue.js المقال السابق: إنشاء مشاريع Vue.js باستخدام Vue CLI النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: لماذا Vue CLI؟ إعداد Vue CLI إنشاء مشروع جديد باستخدام Vue CLI نظرة عامة على هيكل المشروع تعديل تطبيق مشاريب حسوب حسب الأسلوب الجديد سنتعرّف في هذا الدرس على Vue CLI ولماذا نحتاجه. وسنتعرّف أيضًا على كيفية إعداد Vue CLI بالتفصيل، ثم سنعمل على موائمة تطبيق "مشاريب حسوب" الذي بنيناه في الدرس السابق لكي يتوافق مع Vue CLI. لماذا Vue CLI؟ بدأنا سلسلة Vue.js بكتابة التطبيقات التي نحتاج لها ضمن موقع JSFiddle، ثم طورنا عملنا بأن استخدمنا محرّر الشيفرة البرمجية Visual Studio Code بحيث أصبحنا نكتب تطبيقاتنا محليًّا باستخدمه، ثم نجرب هذه التطبيقات عن طريق الإضافة Live Server. السيناريو الأخير جيّد في الواقع ولكن قد يكون في بعض الأحيان غير كاف. وخاصةً إذا بدأت بكتابة تطبيقات أكبر وأكثر تعقيدًا باستخدام المكوّنات التي تكتبها أنت والتي تحتاجها في تطبيقاتك. المشكلة التي ستواجهها هي التداخل بين الشيفرة الخاصة بالمكوّنات مع الشيفرة الخاصة بالتطبيق، وهذا يحدث على مستوى شيفرة JavaScript و شيفرة HTML وحتى تنسيقات CSS. في حال التطبيقات الكبيرة سيكون هذا التداخل مزعج وأكثر عرضة للأخطاء. لذلك فمن الحكمة أحيانًا الانتقال إلى مستوى أعلى في بناء تطبيقات عملية وواقعية باستخدام أدوات تطوير فعًالة ومناسبة. بغية تحقيق هذا الهدف، سنستخدم في هذا الدرس Vue CLI وهي واجهة موجّه الأوامر الخاص بـ Vue.js. باختصار، هي عبارة عن حزمة برمجيّة تسمح لنا ببناء الهيكل الأساسي لتطبيق Vue.js تمهيدًا لكتابة الشيفرة، بالإضافة إلى تزويدنا بخادوم تطوير بسيط (مختلف عن الخادوم Live Server) يسمح لنا بمحاكاة الظروف الفعلية التي سيعمل ضمنها هذا التطبيق. توجد عدة قوالب مُتاحة يستطيع Vue.js توليدها لك، سنستخدم أبسطها في هذا الدرس. إعداد Vue CLI نستطيع تثبيت Vue CLI على مختلف أنواع أنظمة التشغيل الأساسية (Windows, Linux, Mac OS) بنفس الأسلوب تقريبًا. سأركّز هنا على نظام التشغيل Windows. تثبيت Node.js رغم أنّنا سنحتاج إلى Node.js إلّا أنّنا لن نكتب أي شيفرة باستخدامه حاليا، إنما سنستخدم مدير الحزم npm وهو ضروري لتثبيت Vue CLI، بالإضافة إلى احتواءه على خادوم التطوير الذي تحدثنا عنه قبل قليل، والذي سيستضيف تطبيقاتنا أثناء تطويرها. في البداية يتوجب علينا زيارة الموقع الخاص بـ Node.js وتنزيل الإصدار الأخير منه. انقر على الإصدار الحالي (الزر الأخضر الأيمن). بعد تنزيل برنامج التثبيت، شغّل هذا البرنامج (ستحتاج إلى صلاحيات مدير النظام)، واتبع خطوات التثبيت مع ترك الخيارات الافتراضية كما هي. ستأخذ عملية التثبيت زمنًا قصيرًا نسبيًا. بعد الانتهاء يمكنك الانتقال إلى الخطوة التالية. تثبيت Vue CLI شغّل موجّه الأوامر في Windows (يمكنك ذلك من خلال ضغط مفتاح الويندوز مع المفتاح R، ثم كتابة الأمر cmd مباشرةً، ونقر زر موافق OK). بعد ذلك اكتب الأمر التالي: npm install -g @vue/cli ستبدأ عندها عملية تثبيت Vue CLI والتي ستأخذ أيضًا وقتًا قصيرًا نسبيًا. بعد الانتهاء، سيعود موجّه الأوامر إلى حالته الطبيعية. عند هذه النقطة أصبح Vue CLI جاهزًا للاستخدام، حيث يمكنك البدء بتنفيذ الأمر vue مباشرةً ضمن موجّه الأوامر لأنه أصبح متاحًا بعد تثبيته باستخدام npm. ملاحظة قد تحتاج إلى إغلاق نافذة موجه الأوامر الحالية، وفتح نافذة موجّه أوامر جديدة حتى تستطيع استخدام الأمر vue. تثبيت Git سنحتاج أيضًا إلى تطبيق إدارة الإصدار Git. يمكنك تحميله من هذا الرابط مع اختيار نسخة الويندوز. بعد تنزيل الملف، نصّب التطبيق (ستحتاج إلى صلاحيات مدير النظام). اترك الخيارات الافتراضية كما هي باستثناء الشاشة التي تطلب منك فيها تحديد الطرفية الافتراضية للـ Git. اختر الخيار الثاني Use Windows' default console window. ثم تابع بنقر الزر Next حتى تصل للنهاية. بعد الانتهاء من تثبيت Git. افتح نافذة موجّه الأوامر (نافذة جديدة) واكتب الأمر التالي لكي نتأكّد من أنّ عملية التثبيت قد تمّت بشكل صحيح: git --version ستحصل على إصدار النسخة الحالي إذا جرت الأمور بشكل سليم. إنشاء مشروع جديد باستخدام Vue CLI الآن وبعد أن انتهينا من إعداد Vue CLI أصبح بإمكاننا الشروع باستخدامه. سنبدأ بإنشاء مشروع جديد. وبما أنّنا سنعمل على نقل تطبيق "مشاريب حسوب" إلى Vue CLI بعد قليل، لذلك فسنسمي المشروع الجديد الذي سنعمل على إنشائه الآن بالاسم hsoub-drinks-cli. لنبدأ الآن! اكتب الأمر التالي ضمن موجّه الأوامر: vue create hsoub-drinks-cli من السطر السابق: vue هو الأمر الخاص بتنفيذ Vue CLI كما أشرنا، الوسيط create يشير كما هو واضح إلى إنشاء مشروع جديد، أمّا hsoub-drinks-cli فهو اسم المشروع، عند تنفيذ الأمر السابق سيعمل Vue CLI على إنشاء المشروع ضمن مجلّد يحمل نفس اسم المشروع، وذلك في نفس الدليل الحالي الذي ننفّذ فيه الأمر السابق. اضغط المفتاح Enter لتنفيذ الأمر ، سيطلب منك Vue CLI المزيد من المعلومات قبل أن يُنشأ المشروع. من الشكل السابق، يسأل Vue CLI عن نوع القالب الذي نريد استخدمه، توجد العديد من القوالب، حيث يمكنك ضغط مفتاح السهم السفلي من لوحة المفاتيح لاختيار Manually select features والاطلاع على القوالب الجاهزة مسبقًا. لمشروعنا البسيط، سأختار القالب الافتراضي default (babel, eslint)، أي اضغط على المفتاح Enter فحسب. سيستغرق ذلك بعض الوقت لكي تُحمّل الملفات المطلوبة ويُحضّر المشروع الجديد للاستخدام. لا تقلق من عدد الملفات الكبير التي ستنزّل من الانترنت (رغم أنّ المشروع بسيط). ملاحظة في بعض الأحيان القليلة، يمكن أن يحدث جمود في عملية إنشاء المشروع لسبب أو لآخر. إذا شعرت أنّ هذه العملية قد استغرقت وقتًا طويلًا غير منطقي، أو أنّ عملية التحميل تقف عند حزمة محدّدة ولا تنتقل إلى حزمة تالية، عندها يمكنك ضغط Ctrl + C لإيقاف Vue CLI، ثم أغلق نافذة موجّه الأوامر، ثم احذف المجلّد hsoub-drinks-cli الذي أُنشأ توًّا، وأعد تشغيل الحاسوب.وكرّر العملية من جديد. بعد الانتهاء من إنشاء المشروع. ادخل إلى المجلّد hsoub-drinks-cli باستخدام الأمر التالي: cd hsoub-drinks-cli ثم نفّذ الأمر التالي لتشغيل خادوم التطوير البسيط الذي يأتي مع npm: npm run serve سيؤدي ذلك إلى تشغيل هذا الخادوم على المنفذ الافتراضي 8080 ليعمل على تخديم ملفات المشروع hsoub-drinks-cli الذي أنشأناه توًّا. من الممكن أن تُفتَح نافذة جديدة من المتصفح الافتراضي لديك بشكل تلقائي بحيث تعرض مباشرة الصفحة الرئيسية الافتراضية. أو يمكنك أن تفعل أنت ذلك بأن تفتح نافذة (أو لسان تبويب) جديدة من المتصفح لديك وتنتقل إلى العنوان التالي: http://localhost:8080. ستحصل على شكل شبيه بما يلي: ملاحظة من الممكن في بعض الأحيان أن يظهر لك تنبيه أمني من نظام التشغيل، يطلب منك السماح لخادوم التطوير بالعمل. كما في الشكل التالي: انقر الزر Allow access للسماح بذلك. نظرة عامة على هيكل المشروع انتقل الآن إلى Visual Studio Code ثم افتح مجلّد المشروع الذي أنشأناه توًّا. يمكنك ذلك باختيار الأمر File -> Open Folder ثم انتقل إلى المجلّد الذي أنشأت فيه المشروع. انقر نقرًا مزدوجًا على هذا المجلّد لكي تدخل ضمنه، ثم اختر Select Folder. سيظهر المشروع ضمن نافذة المستكشف في القسم الأيسر من الشاشة بشكل شبيه بما يلي: الملف أو المجلد الوصف public/index.html عبارة عن ملف HTML عادي يرسله الخادوم إلى متصفح الويب عند زيارة الموقع: http://localhost:8080/ من المتصفح. src يحتوي هذا المجلد على ملفات الشيفرة البرمجية، بالإضافة إلى أصول التطبيق من صور وتنسيقات وغيرها. src/main.js هذا ملف JavaScript وهو مسؤول عن إعدادات تطبيق Vue JS. كما يحتوي هذا الملف على أيّة حزم من طرف ثالث يمكن أن يستخدمها التطبيق src/App.vue الملفات ذات الامتداد vue عمومًا، هي الملفات التي تُوضع ضمنها المكوّنات (Components)، حيث سنطبّق مبدأ فصل المكوّنات بشكل كامل عن ملفات HTML العادية. src/assets يحتوي على أصول (assets) الموقع مثل الصور وشعار الموقع وملفات التنسيق وغيرها. components المجلّد الخاص بالمكوّنات التي سنكتبها للتطبيق. components/HelloWorld.vue ملف vue يحتوي على مكوّن تجريبي بسيط، لن نحتاج إلى هذا الملف بالطبع. حيث سنستبدل به المكوّنات الخاصة بتطبيق "مشاريب حسوب" table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } احذف الملف HelloWorld.vue باختياره من نافذة المستكشف ثم ضغط الزر Delete، ثم أنشئ ملفًا جديدًا ضمن المجلد Components بالنقر بزر الفأرة الأيمن ثم اختيار الأمر New File. سمّ الملف الجديد بالاسم drink.vue. سيحوي هذا الملف المكوّن الممثّل لمشروب محدّد (راجع الدرس السابق). انسخ الشيفرة البرمجية التالية إلى الملف drink.vue الجديد: <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> </template> <script> export default { name: "drink", props: { name: { type: String }, selectedDrink: { type: String } }, computed: { is_selected() { return this.selectedDrink === this.name; } }, methods: { select() { this.$emit("drink_selected_event", this.name); } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .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 } .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 } </style> لاحظ معي أنّ الملف السابق يتكوّن من الأقسام الثلاثة التالية: <template> ... </template> <script> ... </script> <style> ... </style> يمكن أن يحتوي أي ملف يحمل الامتداد vue (وبالتالي أي مكوّن) على ثلاثة أقسام نُعبّر عنها بالوسوم <template> و <script> و <style>. الوسم <template> إلزامي، والوسمين الآخرين اختيارين. قد تظن أنّ الشيفرة السابقة معقدة بعض الشيء إلّا أنّها بسيطة في الواقع. الشيفرة الموجودة هنا مماثلة لتلك الموجودة في الدرس السابق إلى حدّ كبير. حيث عملت على تجميع جميع الشيفرات البرمجية أو التنسيقات الخاصة بالمكوّن drink ضمن الملف drink.vue. أي أننا استطعنا عزل كل ما يتعلق بالمكوّن drink ضمن هذا الملف. وفي ذلك فائدة كبيرة تتمثّل في فصل وتنظيم الشيفرة في أجزاء منطقية يسهل التعامل معها. يحتوي القسم <template> على شيفرة HTML الخاصة بقالب المكوّن. أما القسم <script> فيحتوي على شيفرة Vue.js اللازمة للمكوّن. أما القسم <style> فكما هو واضح يحتوي على التنسيق اللازم للمكوّن. يمكن في بعض الأحيان (كما في حالتنا هنا) استخدام الكلمة scoped مع الوسم <style> بهدف أن نجعل التنسيق خاصًا بالمكوّن الحالي. أريد التركيز على الشيفرة الموجودة ضمن القسم <script>. لاحظ أنّني قد استخدمت التعليمة export default. هذه التعليمة هي تعليمة JavaScript ووظيفتها السماح بتصدير كائن Vue.js الذي يأتي بعدها إلى خارج الوحدة البرمجية (الملف) drink.vue وبالتالي يمكن استيراده فيما بعد باستخدام التعليمة import كما سنرى بعد قليل. أمّا كائن Vue.js نفسه فهو مماثل لذلك الموجود في الدرس السابق. جاء الآن دور المكوّن drinksselector والذي أجريت تعديلًا طفيفًا على اسمه الذي كان في الدرس السابق وذلك لجعل الأمر أكثر سهولة في التعامل معه. أنشئ ملفًا جديدًا ضمن المجلد Components بالنقر بزر الفأرة الأيمن ثم اختيار الأمر New File. سمّ الملف الجديد بالاسم drinksselector.vue. سيحوي هذا الملف المكوّن الأساسي الذي ينظّم عملية اختيار المشاريب (مكوّنات drink) ضمنه (راجع الدرس السابق). انسخ الشيفرة البرمجية التالية إلى الملف drinksselector.vue الجديد: <template> <div class="drinks"> <drink v-for="drink in drinks" v-bind:key="drink.name" v-bind:name="drink" v-on:drink_selected_event="drink_selected_handler" v-bind:selectedDrink="current_drink" ></drink> </div> </template> <script> import drink from "./drink.vue"; export default { name: "drinksselector", components: { drink }, data() { return { drinks: ["شاي", "قهوة", "شاي أخضر", "زهورات", "بابونج"], current_drink: null }; }, methods: { drink_selected_handler(drink_name) { this.current_drink = drink_name; } } }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style> لاحظ معي أنّ القسم <style> هنا فارغ. قد تتسائل كيف سنطبّق التنسيق الخاص بهذا المكوّن كما فعلنا في الدرس السابق. في الحقيقة سيطبّق التنسيق اللازم لهذا المكوّن من خلال المكوّن الابن drink. رغم أنّنا وضعنا الكلمة scoped ضمن القسم <style> من المكوّن drink كما مرّ معنا قبل قليل، إلّا أنّ التنسيقات الخاصة بالمكوّن drinksselector ستطبّق عليه. السبب في ذلك هو أنّ المكوّن drink هو مكوّن متداخل (مكوّن ابن) مع المكوّن drinksselector فتطبق التنسيقات على المكوّن الأعلى (الأب) في هذه الحالة بشكل تلقائي. لاحظ أيضًا السطر الذي يلي وسم الفتح <script>: import drink from "./drink.vue"; تعمل هذه التعليمة البرمجية على استيراد كائن الكائن drink من الملف drink.vue. استطعنا استخدام التعليمة import هنا بسبب وجود التعليمة export default ضمن الملف drink.vue كما مرّ معنا قبل لحظات. جاء الآن دور ملف التطبيق الرئيسي App.vue والذي يعد بحد ذاته مكوّنًا، ولكنّه المكوّن الرئيسي في تطبيق ما. افتح هذا الملف وانسخ إليه الشيفرة التالية: <template> <div> <div class="header"> <span id="logo">مشاريب حسوب</span> </div> <div id="app" class="container"> <div class="content"> <h1 class="title">المشروبات المتوفرة</h1> <drinksselector></drinksselector> </div> </div> </div> </template> <script> import drinksselector from "./components/drinksselector.vue"; export default { name: "App", components: { drinksselector } }; </script> <style> @import "./assets/styles/app.css"; </style> لاحظ كيف أدرجنا المكوّن drinksselector ضمن القسم components الموجود بدوره ضمن القسم <script>. لاحظ أيضًا كيف استخدمنا التنسيقات ضمن القسم <style>. لقد ربطنا ملف التنسيقات app.css الذي سننشئه بعد قليل بهذا المكوّن عن طريق التعليمة @import. أضف الآن مجلّدًا جديدًا ضمن المجلّد src وذلك بالنقر بزر الفأرة الأيمن على المجلد src ثم اختيار الأمر New Folder. سمّ المجلّد الجديد بالاسم assets. ثم أنشئ مجلّد آخر ضمن المجلّد assets بنفس الأسلوب السابق وسمّه styles. أنشئ الآن ضمن المجلّد styles ملفًا اسمه app.css وانسخ إليه التنسيقات التالية: @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 } انتقل الآن إلى الملف index.html واحرص على أن يكون محتواه مماثلًا للشيفرة التالية: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html> لقد انتهينا الآن من جميع الملفات! اذهب إلى المتصفّح ثم انتقل إلى العنوان http://localhost:8080 لتجد تطبيق "مشاريب حسوب" وقد عمل من جديد. ولكن هذه المرة باستخدام Vue CLI ملاحظة بعد الانتهاء من عملية التطوير بشكل كامل، يمكن استخدام الأمر التالي لبناء التطبيق وتجهيزه ضمن بيئة الإنتاج (Production Environment) للاستخدام النهائي باستخدام الأمر: npm run build يمكنك الاطلاع على المزيد حول هذا الموضوع من خلال من هذا الرابط. ختامًا تعلّمنا في هذا الدرس كيفية إنشاء تطبيق Vue.js متكامل باستخدام قالب جاهز منحنا إيّاه Vue CLI. تعلّمنا بدايةً ما هو مفهوم Vue CLI وكيفية إعداده، وكيف ننشئ مشروع جديد باستخدامه. كما عملنا على تحويل كامل تطبيق "مشاريب حسوب" من الأسلوب القديم والمفيد في حال كنّا نريد تجربة مزايا جديدة، إلى أسلوب Vue CLI بالكامل. بهذا الدرس نكون قد خطونا خطوةً مهمةً ومتقدمة في العمل مع Vue.js. اقرأ أيضًا المقال التالي: التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js المقال السابق: المزيد حول المكونات في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: بناء تطبيق نموذجي (مشاريب حسوب). إضافة وسائل تنقيح متطوّرة لتطبيقات Vue.js. المكوّنات المتداخلة. تسجيل المكوّنات محليًّا وتسجيلها على المستوى العام. تحديد المكوّن الذي اختاره المستخدم. التخاطب بين المكوّنات باستخدام أحداث مخصّصة. سنتعلّم في هذا الدرس المزيد عن المكوّنات، حيث سنتعرّف على المزيد من المزايا المتعلّقة بها، والتي ستساعدنا على بناء مكوّنات عملية ومفيدة. سنبدأ هذا الدرس ببناء تطبيق نموذجي سيكون الهيكل الأساسي الذي سنستخدمه لتعلّم المزايا الجديدة حول المكوّنات، ثم سنتعرّف على كيفية استخدام أدوات تنقيح متطوّرة مكتوبة خصيصًا لـ Vue.js، ثم نتعرّف على المكوّنات المتداخلة وكيفية استخدامها، ثم سنتعلّم كيفية تسجيل المكوّنات محليًا وعلى المستوى العام في التطبيق، ونختم بتعلّم كيفية التخاطب بين المكوّنات المتداخلة. بناء تطبيق نموذجي (مشاريب حسوب) سنبني من أجل هذا الدرس تطبيق نموذجي بسيط لكي نطبق عليه الأفكار التي سنتاولها هنا. سيكون هذا التطبيق عبارة عن واجهة بسيطة لمحل افتراضي بيع مشروبات متنوعة. سيعبّر المكوّن في هذه المرة عن مشروب معيّن من قائمة المشروبات المتاحة. انظر إلى الشكل النهائي المقترح: كما ترى فإنّني أستخدم اللغة العربية في التطبيق هذه المرّة! أنشئ مجلّدًا جديدًا سمّه 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 (قد تحتاج إلى نقر زر عرض المزيد في حال لم تجد لسان التبويب هذا). انظر الشكل التالي لترى كيف تبدو. المكونات المتداخلة نحتاج في بعض الأحيان إلى جعل المكونات متداخلة فيما بينها، كأن يكون هناك مكوّن أساسي (مكوّن أب) يستخدم مكوّنات أصغر (مكوّنات أبناء) ضمنه. سنوظف هذا المفهوم ضمن التطبيق النموذجي السابق. سأضيف مكوّنًا جديدًا ضمن الملف 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. انشر العقد في حال احتجت ذلك، ستحصل على شكل شبيه بما يلي بالنسبة للمشروب الأول "شاي": جرب أن تعاين حالة المشروب التالي "قهوة" ستجد أن قيمة الحقل 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 } عد إلى المتصفح واختر المشروب الأول والثالث مرة أخرى لتحصل على شكل شبيه بما يلي: لقد تمّت المهمة بنجاح! ولكن، ماذا لو أردنا اختيار مشروب واحد فقط من القائمة؟ سنحل هذه المشكلة في الفقرة التالية حيث سنتعلّم كيفية التخاطب بين المكوّنات المختلفة باستخدام أحداث مخصّصة. التخاطب بين المكونات باستخدام أحداث مخصصة برزت الحاجة في الفقرة السابقة إلى اختيار مشروب واحد فقط من قائمة المشروبات المتاحة. أي أنّنا بحاجة إلى آلية معيّنة تسمح باختيار مشروب واحد فقط، بحيث تعمل هذه الآلية على إلغاء أي اختيار سابق لمشروب ما (في حال وجوده) ضمن القائمة، والإبقاء على المشروب الذي اختاره المستخدم أخيرًا. يمكن تحقيق هذا الأمر بالتخاطب بين المكوّن الابن 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. حتى الدرس القادم أرجو أن تكونوا بأفضل حال! اقرأ أيضًا المقال التالي: إنشاء مشاريع Vue.js باستخدام Vue CLI المقال السابق: مدخل إلى التعامل مع المكونات في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: تجهيز هيكل التطبيق على حاسوب محلّي. بناء مكوّن جديد: مكوّن المهام. تحسين تجربة الاستخدام للمكوّن. تمرير وسائط إلى المكوّنات. إنشاء أكثر من نسخة من المكون ضمن نفس الصفحة. إضافة ميزة التصفية لمكوّن المهام. إضافة ميزة مهمة جديدة لمكوّن المهام. سنتابع في هذا الدرس التعامل مع المكوّنات، حيث سنتعلّم كيف نبني المكوّنات بصورة عمليّة. سنبني في هذا الدرس مكوّنًا بسيطًا لكنّه عملي، وهو مكوّن إدارة مهام مبسّط. الهدف من هذا التطبيق هو التعرّف على أسس بناء المكوّنات بشكل جيّد. سيعمل هذا التطبيق البسيط على السماح للمستخدم بعرض بعض المهام التي ينوي تنفيذها فيما بعد، مع إمكانية تحديد فيما إذا كان قد أنجز المهام أم لا باستخدام زر اختيار بسيط. بالإضافة إلى امتلاكه ميزة تصفية بسيطة لإخفاء أو إظهار المهام المنجزة، بالإضافة إلى إمكانية إضافة مهام جديدة. تجهيز هيكل التطبيق على حاسوب محلّي سنسلك هذه المرّة منحًى مغايرًا عمّا اعتدناه في الدروس السابقة. كان تركيزنا في الدروس السابقة منصبًّا على كتابة تطبيقات vue.js ضمن موقع JSFiddle، وهذا الأمر جيّد في الواقع عندما نريد التعلّم أو تجريب بعض المزايا الخفيفة. ولكن عند الانتقال إلى مستوى أعلى ببناء تطبيقات عملية وواقعية، ينبغي علينا بالتأكيد الانتقال إلى أدوات تطوير فعًالة ومناسبة. تشتمل هذه الأدوات بطبيعة الحال على خادوم نستخدمه أثناء بناء التطبيق، نحاكي من خلاله سلوك الخواديم الفعلية التي ستستضيف تطبيقنا النهائي الذي سيعمل عليه المستخدم في نهاية المطاف. في الحقيقة، أنصح دومًا بجعل ظروف تجريب التطبيق مماثلة قدر المستطاع لما سيكون عليه الوضع النهائي للتطبيق. سنستخدم في هذا الدرس وغيره من الدروس اللاحقة، محرّر الشيفرة البرمجيّة Visual Studio Code من Microsoft. يمكنك في الواقع اختيار المحرّر الذي ترغب به، أو حتى يمكنك استخدام بيئة تطوير متكاملة إن أحببت. يوجد العديد من محرّرات النصوص البرمجية الأخرى مثل Atom و Sublime Text و Brackets. لتنصيب Visual Studio Code يمكنك زيارة الصفحة التالية code.visualstudio.com/download. يمكنك اختيار نظام التشغيل المناسب لك من الأسفل. بالنسبة لنا سنختار النسخة الخاصة بويندوز. بعد التنزيل، نصّب التطبيق مع ترك الخيارات الافتراضية كما هي (قد تحتاج إلى صلاحيات مدير النظام). بعد تثبيت Visual Studio Code انتقل إلى الإضافات Extensions الخاصّة به، واعمل على تثبيت الإضافة Live Server التي سنستخدمها كخادوم بسيط. انظر الشكل التالي: يمكنك الوصول مباشرةً إلى مدير الإضافات من الناحية اليسرى من الشاشة، كما هو ظاهر من الشكل السابق. بعد ذلك أدخل اسم الإضافة: Live Server في خانة البحث. بعد أن يجده، اختره، لتظهر النافذة الخاصة به كما في الشكل السابق، ثم انقر الزر الأخضر Install لتثبيته. من المفيد أيضًا تثبيت الإضافتين التاليتين: الإضافة Vetur لتنسيق الشيفرة الخاصة بـ vue.js. الإضافة HTML5 Boilerplate لتنسيق شيفرة HTML. اكتب اسم كل من هاتين الإضافتين في خانة البحث، واعمل على تثبيتهما كما فعلنا قبل قليل مع الإضافة Live Server. ملاحظة: أنصح بإعادة تشغيل Visual Studio Code عند هذه المرحلة حتى ولو لم يطلب منك ذلك. لنبدأ ببناء مشروعنا! انتقل إلى المكان الذي ترغب فيه بإنشاء المشروع على القرص الصلب، وأنشئ مجلّدًا سمّه veujs-mytasks. انتقل مرّة أخرة إلى Visual Studio Code ثم اختر الأمر File -> Open Folder واختر المجلّد الذي أنشأته توًّا. من نافذة المستكشف Explorer الموجودة في الطرف الأيسر، لاحظ الأيقونتين الصغيرتين، ضع مؤشّر الفأرة للحظات فوق كل منهما لتكتشف وظيفة كل منهما. الأيقونة الأولى من اليسار وظيفتها إنشاء ملف جديد ضمن المجلّد الحالي، والأيقونة الأخرى وظيفتها إنشاء مجلّد جديد ضمن المجلّد الحالي. استخدم زر ملف جديد لتُنشئ ملفين، واختر الاسمين index.html و app.js لهما على الترتيب. ستحصل في النهاية على شكل شبيه بما يلي: اختر الملف index.html لكي تفتحه، ثم انسخ شيفرة HTML التالية إليه: <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <title>My Tasks</title> <meta name='viewport' content='width=device-width, initial-scale=1'> </head> <body> <h1>Welcome to MyTasks Application</h1> <p>This application is built to explain how to deal with components</p> <div id='app'> <tasks></tasks> </div> <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script> <script src="app.js"></script> </body> </html> شيفرة HTML السابقة عبارة عن شيفرة بسيطة، الأمر الجديد الوحيد فيها هو إضافة العنصر tasks وهو المكوّن الذي سنبنيه بعد قليل وسيمثّل قائمة المهام التي نرغب ببنائها. لاحظ أيضًا أنّنا وضعنا هذا العنصر الجديد ضمن عنصر div له الوسم id = 'app' وهو العنصر المستهدَف في كائن vue.js كما اعتدنا سابقًا. وأخيرًا، لاحظ كيف أضفت مرجعين لملف إطار العمل vue.js بالإضافة إلى مرجع للملف app.js الذي سيحتوي على الشيفرة البرمجية لكل من المكوّن والتطبيق: <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script> <script src="app.js"></script> لننتقل الآن إلى الملف app.js، انسخ الشيفرة البرمجية التالية إليه: Vue.component('tasks', { template: '<strong><p>{{name}} - Tasks</p></strong>', data() { return { name: 'Husam' } } }) new Vue({ el: '#app' }) الشيفرة البرمجيّة هنا مماثلة لتلك التي تعاملها معها في الدرس السابق، حيث نسجّل مكوّن جديد باسم tasks بحيث نُسند له قالب بسيط، يعرض اسم الشخص الذي سنُسند إليه هذه المهام عن طريق الخاصية {{name}} كما هو واضح. ثم ننشئ كائن vue.js بسيط ونعيّن العنصر المستهدف. انتقل الآن إلى نافذة المستكشف Explorer، وانقر بزر الفأرة الأيمن على الملف index.html ثم اختر الأمر Open with Live Server (تذكّر أننا ثبتنا الإضافة Live Server قبل قليل)، سيؤدي ذلك إلى فتح نافذة أو تبويب جديد ضمن متصفّح الانترنت الافتراضي لديك بحيث يتجه إلى العنوان http://127.0.0.1:5500/index.html وهو العنوان مع المنفذ الافتراضي الذي ينصت عنده الخادوم Live Server. ستحصل على شكل شبيه بما يلي: هذا دليل على أنّ الأمور تسير على ما يرام. وأنّ نجحنا ببناء الهيكل العام للتطبيق. لننتقل الآن للمرحلة التالية. بناء مكوّن جديد: مكوّن المهام لنبدأ الآن بالعمل الفعلي في بناء المكوّن الخاص بالمهام، والذي أسميناه tasks. سأنقل أولًا شيفرة HTML المسندة للحقل template ضمن المكوّن، وأضعها ضمن مكان منفصل لأنّها ستصبح بعد قليل كبيرة ومعقدة بعض الشيء لتُوضع في مكان كهذا. أجرِ التعديل التالي في الملف app.js ضمن الحقل template للمكوّن ليصبح على النحو التالي: template: '#tasks-template' لاحظ أنني قد عرضت مكان التعديل فقط طلبًا للاختصار. الجديد هنا أنّني وضعت معرّف القالب الجديد الذي سيحتوي على الشيفرة. انتقل الآن إلى الملف index.html وأضف الشيفرة التالية مباشرةً بعد عنصر div المُستهدَف الخاص بتطبيق vue.js: <script type='text/x-template' id='tasks-template'> <div> <h3>{{ name }} - Tasks</h3> </div> </script> كما ترى أجريت بعض التعديل على شيفرة HTML التي كانت موجودة سابقًا. انتقل الآن إلى الصفحة index.html في المتصّح ثم حدثها (إن لم تُحدّث بشكل تلقائي)، يجب أن تحصل على شكل قريب من الشكل الذي حصلنا عليه في الفقرة السابقة. الجديد هنا هو فصل القالب ووضعه ضمن مكان مخصّص له. في هذه الحالة سيكون ضمن العنصر script والذي تحمل السمة type له القيمة text/x-template كما هو واضح. بالإضافة لذلك لاحظ كيف جعلت قيمة المعرف id له نفس القيمة التي أسندتها للحقل template ضمن المكوّن قبل قليل. من المهم جدًّا تحقيق مبدأ الفصل في بناء المكوّنات. لأنّه كلما أصبح المكوّن أكثر تعقيدًا كما سترى بعد قليل، كلما برزت الحاجة إلى تحقيق مبدأ الفصل بشكل أفضل. لنُكسِب الآن مكوّننا الوليد بعض المزايا الإضافية لكي يصبح قادرًا على عرض بعض المهام للمستخدم. أجر التعديلات التالية ضمن الملف app.js ليصبح مشابهًا لما يلي: Vue.component('tasks', { template: '#tasks-template', data() { return { name: 'Husam', tasks_list: [ { title: "Write an introduction about vue.js components.", done: true }, { title: "Drink a cup of team.", done: false }, { title: "Call Jamil.", done: false }, { title: "Buy new book.", done: true } ] } } }) new Vue({ el: '#app' }) في الواقع، لقد أضفت حقلًا جديدًا أسميته tasks_list يحتوي على بيانات المهام التي أرغب بعرضها. لاحظ كيف أنّ كل مهمّة ضمن القائمة عبارة عن كائن بسيط، يحتوي على خاصيتين: تضم الأولى النص الخاص بالمهمة، أمّا الثانية فتضم قيمة منطقية تشير إلى أنّ المهمة قد نُفّذت true أم ليس بعد false. انتقل الآن إلى الملف index.html وأجر بعض التعديلات التي ستكون أكبر هذه المرة، ليصبح مماثلًا لما يلي: <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <title>My Tasks</title> <meta name='viewport' content='width=device-width, initial-scale=1'> <style> .tasks-container { border-width: 1px; border-style: solid; display: inline-block; margin-right: 20px; padding: 8px; } .w3-table-all { border-collapse: collapse; border-spacing: 0; width: 100%; display: table; border: 1px solid #ccc } .w3-table-all tr { border-bottom: 1px solid #ddd } .w3-table-all tr:nth-child(odd) { background-color: #fff } .w3-table-all tr:nth-child(even) { background-color: #f1f1f1 } .w3-table-all td, .w3-table-all th { padding: 8px 8px; display: table-cell; text-align: left; vertical-align: top } .w3-table-all th:first-child, .w3-table-all td:first-child { padding-left: 16px } .w3-table-all th{ background-color: #d0d0d0; } </style> </head> <body> <h1>Welcome to MyTasks Application</h1> <p>This application is built to explain how to deal with components</p> <div id='app'> <tasks></tasks> </div> <script type='text/x-template' id='tasks-template'> <div class='tasks-container'> <table class='w3-table-all'> <colgroup> <col style="width:15%"> <col style="width:85%"> </colgroup> <tbody> <tr> <th colspan="2"> <center>{{ name }} - Tasks</center> </th> </tr> <tr> <td> <strong>Done</strong> </td> <td> <strong>Title</strong> </td> </tr> <tr v-for="task in tasks_list" v-bind:key="task.title"> <td> {{ task.done }} </td> <td> {{ task.title }} </td> </tr> </tbody> </table> </div> </script> <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script> <script src="app.js"></script> </body> </html> حدّث الصفحة index.html ضمن متصفّح الانترنت لديك لتحصل على شكل شبيه بما يلي: ربما تشعر أنّ الشيفرة قد أصبحت كبيرة ومعقّدة نسبيًا، إلّا أنّ الأمر في الحقيقة ليس كذلك. حدثت نسبة كبيرة من التعديلات عندما أضفت عنصر التنسيق style مع تنسيقاته إلى الملف index.html. كان من الأفضل وضع تنسيقات CSS ضمن ملف مستقل، وهذا ما سأعمل عليه بعد قليل. التنسيقات المستخدمة هنا استعرتها من موقع W3Schools الشهير. بالإضافة إلى ذلك، لاحظ أنّني قد استخدمت عنصر الجدول table لعرض قائمة المهام. عدا عن ذلك، أعتقد أنّ معظم الشيفرة البرمجيّة واضحة، باستثناء الموجّه الجديد: v-bind:key: <li v-for="task in tasks_list" v-bind:key="task.title"> في الواقع لقد استخدمنا موجّه آخر مسبقًا، وهو v-bind:href وذلك في الدرس الثاني (استخدام vue.js للتعامل مع DOM). لكننا سنستخدم اليوم الكلمة key بدلًا من href. لاستخدام v-bind:key مزيّة مهمّة تتمثّل في الأداء (Performance)، وخصوصًا عندما يكون حجم البيانات كبيرًا. نستخدم هذا الموجّه لتعيين مفتاح ربط key للموجّه التكراري for. من الواضح أنّه في الوضع الحالي، من غير الممكن لنا أن نعدّل على أي مهمة بحيث تصبح منفّذة أو غير منفّذة. سنعمل في الفقرة التالية على تحسين تجربة الاستخدام، من خلال توفير إمكانية إجراء هذه التعديلات. تحسين تجربة الاستخدام للمكوّن سنعمل الآن على السماح للمستخدم بتعديل حالة المهمة وذلك بإضافة عنصر اختيار Checkbox. قبل ذلك دعنا ننقل تنسيقات CSS إلى ملف منفصل. أنشئ ملف جديد ضمن نافذة المستكشف في Visual Studio Code لهذا الغرض ولنسمه tasks.css. انقل محتويات العنصر style في الملف index.html إلى ملفنا الجديد، ثم احذف العنصر style. لاستخدام التنسيقات ضمن الملف الجديد، أضف مرجعًا إليه في الملف index.html ضمن القسم head على النحو التالي: <link rel="stylesheet" href="tasks.css"> سنجري الآن تغييرًا ضمن الشيفرة البرمجية للقالب فقط، وتحديدًا بالقسم الخاص بإنهاء أو عدم إنهاء المهمة أي في القسم Done فقط. استبدل بالشيفرة التالي: ... <td> {{ task.done }} </td> ... الشيفرة الجديدة التالية: ... <td> <input type="checkbox" v-model="task.done"/> </td> ... أعد تحديث الصفحة لتحصل على شكل شبيه بما يلي: لاحظ أنّني قد استخدمت الربط ثنائي الاتجاه v-model="task.done" للربط الثنائي للبيانات. ملاحظة: الاختصارات في vue.js يمكن دومًا استبدال الرمز @ بالموجّه v-on. أي أنّ v-on:click مثلًا سيصبح: @:click. وبنفس الأسلوب، يمكننا حذف الموجّه v-bind بالكامل، وسيفهم vue.js أنّ هذا الموجّه موجود. فمثلًا يمكن كتابة :href فقط بدلًا من v-bind:href، وكتابة :key بدلًا من v-bind:key. تمرير وسائط إلى المكوّنات لعلك قد لاحظت أنّنا استخدمنا بيانات ثابتة في التعامل مع مكوّن المهام. ولكن في الواقع العملي سنحتاج إلى أن تكون هذه البيانات قابلة للتغيير كما هو واضح, لهذا الهدف توفّر المكوّنات في vue.js ميزة الخصائص props، حيث يمكن وضع قسم جديد اسمه props ضمن الوسيط الممرّر إلى المكوّن عند إنشاءه لهذه الغاية. في الحقيقة للخصائص الموجودة ضمن القسم props فوائد مهمة عندما سنتحدث عن المكونات المتداخلة لاحقًا في الدرس التالي، حيث سنستخدمها لتمرير البيانات من المكون الأساسي إلى المكون الفرعي. لنبدأ بإجراء التعديلات اللازمة. أجر التعديلات التالية في الملف app.js ليصبح على الشكل التالي: Vue.component('tasks', { template: '#tasks-template', props: { name: String, tasks_list: Array } }) new Vue({ el: '#app' }) لقد أزلت قسم data وأضفت بدلًا منه القسم props (يمكن بالطبع أن يكونا معًا بنفس الوقت). عرفت الوسائط التي يمكن أن أمرّرها للمكوّن ضمن القسم props بالشكل التالي: props: { name: String, tasks_list: Array } الوسيط الأول هو name وهو من النوع String أي نص، والوسيط الثاني هو tasks_list وهو من نوع Array أي مصفوفة كما هو واضح. بالنسبة لطريقة الاستخدام فهي سهلة جدا. أجر التعديل التالي على الوسم tasks ضمن الملف index.html: <tasks name='Husam' v-bind:tasks_list='[{ title: "Write an introduction about vue.js components.", done: true }, { title: "Drink a cup of team.", done: false }, { title: "Call Jamil.", done: false }, { title: "Buy new book.", done: true }]'></tasks> مرّرت البيانات إلى المكوّن كما لو أنّها وسوم عادية. الشيء الوحيد الملفت للنظر هو أنّني قد استخدمت الموجه v-bind: عند تمرير المصفوفة tasks_list وهذا الأمر إلزامي عند تمرير وسائط ديناميكية إلى المكوّنات، في حين أنّه لا يجب استخدام هذا الموجه عند تمرير وسائط نصية ساكنة كما فعلنا عند تمرير الوسيط name. ملاحظة: يوجد العديد من أنواع الوسائط التي يمكن تمريرها إلى المكونات وهي: String, Number, Boolean, Array, Object, Function, Promise إنشاء أكثر من نسخة من المكون ضمن نفس الصفحة لنعمل الآن على استخدام أكثر من مكون واحد ضمن الصفحة لنكتشف قوة المكونات. انسخ شيفرة HTML الأخيرة والصقها مرة آخرى مع إجراء بعض التعديلات البسيطة عليها: <tasks name='My house' v-bind:tasks_list='[{ title: "Do a cleaning for windows.", done: false}, { title: "Bring some vegetables and fruits.", done: true}, { title: "Wash clothes", done: false}]'></tasks> حدث الصفحة index.html من جديد لتحصل على شكل شبيه بما يلي: يمكننا بسهولة أن نستخدم هذا المكون في المكان الذي نحتاج إليه في الصفحة، كما يمكن مشاركته مع الآخرين. مازال هناك الكثير للتحدث عنه حول المكوّنات، سنتناول في الدروس اللاحقة العديد من المزايا الخاصة بها، والطرق المثالية للتعامل معها. يمكنك الحصول على الشيفرات الكاملة بالملفات الثلاثة التي عملنا عليها في هذا الدرس في المقاطع الثلاثة التالية: الملف index.html: <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <title>My Tasks</title> <meta name='viewport' content='width=device-width, initial-scale=1'> <link rel="stylesheet" href="tasks.css"> </head> <body> <h1>Welcome to MyTasks Application</h1> <p>This application is built to explain how to deal with components</p> <div id='app'> <tasks name='Husam' v-bind:tasks_list='[{ title: "Write an introduction about vue.js components.", done: true }, { title: "Drink a cup of tea.", done: false }, { title: "Call Jamil.", done: false }, { title: "Buy a new book.", done: true }]'></tasks> <tasks name='My house' v-bind:tasks_list='[{ title: "Clean windows.", done: false}, { title: "Bring some vegetables and fruits.", done: true}, { title: "Wash clothes", done: false}]'></tasks> </div> <script type='text/x-template' id='tasks-template'> <div class='tasks-container'> <table class='w3-table-all'> <colgroup> <col style="width:15%"> <col style="width:85%"> </colgroup> <tbody> <tr> <th colspan="2"> <center>{{ name }} - Tasks</center> </th> </tr> <tr> <td> <strong>Done</strong> </td> <td> <strong>Title</strong> </td> </tr> <tr v-for="task in tasks_list" v-bind:key="task.title"> <td> <input type="checkbox" v-model="task.done"/> </td> <td> {{ task.title }} </td> </tr> </tbody> </table> </div> </script> <script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script> <script src="app.js"></script> </body> </html> الملف tasks.css: .tasks-container { border-width: 1px; border-style: solid; display: inline-block; margin-right: 20px; padding: 8px; } .w3-table-all { border-collapse: collapse; border-spacing: 0; width: 100%; display: table; border: 1px solid #ccc } .w3-table-all tr { border-bottom: 1px solid #ddd } .w3-table-all tr:nth-child(odd) { background-color: #fff } .w3-table-all tr:nth-child(even) { background-color: #f1f1f1 } .w3-table-all td, .w3-table-all th { padding: 8px 8px; display: table-cell; text-align: left; vertical-align: top } .w3-table-all th:first-child, .w3-table-all td:first-child { padding-left: 16px } .w3-table-all th{ background-color: #d0d0d0; } الملف app.js: Vue.component('tasks', { template: '#tasks-template', props: { name: String, tasks_list: Array } }) new Vue({ el: '#app' }) إضافة ميزة الترشيح لمكوّن المهام كثيرًا ما ستحتاج في التطبيقات التي ستكتبها إلى ميزة الترشيح بصرف النظر عن نوع التطبيق الذي تبنيه. سنتناول في هذه الفقرة كيفية إضافة هذه الميزة إلى مكوّن المهام. رغم أنّ هذه الميزة بسيطة، وتنحصر وظيفتها في إخفاء (أو إظهار) المهام المنجزة، إلّا أنّها ستعطيك فكرة جيدة عن كيفية عمل مثل هذه المزايا. سنكمل على الشيفرة الأخيرة لتطبيق المهام. ستكون ميزة التصفية على شكل صندوق اختيار Checkbox موجود أعلى قائمة المهام. عندما يختاره المستخدم فإنّ المهام المنجزة ستختفي، والعكس صحيح. سنبدأ بإضافة عنصر الاختيار هذا إلى الملف index.html ضمن القسم الخاص بقالب المكوّن. أضف الشيفرة التالية بعد الوسم <div class='tasks-container'> مباشرة: <input id="hide_cmp_tasks" type="checkbox" v-model="hide_completed_tasks"/> <label for="hide_cmp_tasks">Hide completed tasks</label> لاحظ معي أنّ هذا العنصر مرتبط بحقل اسمه hide_competed_tasks سنعرفه بعد قليل ضمن مكوّن المهام. افتح الآن الملف app.js وأضف القسمين data و computed للمكوّن tasks. سيحتوي قسم data على تعريف الحقل hide_competed_tasks المرتبط بعنصر صندوق الاختيار الذي أضفناه قبل قليل، وسيكون من النوع المنطقي Boolean، في حين أنّ القسم computed فسيحتوي على الخاصية المحسوبة filtered_tasks. سيصبح المكوّن tasks على الشكل التالي: Vue.component('tasks', { template: '#tasks-template', props: { name: String, tasks_list: Array, }, data() { return { hide_completed_tasks: false } }, computed: { filtered_tasks() { return this.hide_completed_tasks ? this.tasks_list.filter(t => !t.done) : this.tasks_list; } } }) إذا تأملت معي محتويات الخاصية المحسوبة filtered_tasks فستجدها عبارة عن شيفرة برمجية بسيطة لتصفية المهام المنجزة من خلال اختبار قيمة الخاصية done. أي أنّ عملية التصفية ستحدث في هذا المكان تحديدًا بناءً على كون الحقل hide_completed_tasks يحمل القيمة true أو false. هذا كلّ شيء! إذا أحببت الآن، انقر بزر الفأرة الأيمن على الملف index.html الموجود ضمن نافذة المستكشف Explorer، واختر الأمر Open with Live Server كما اعتدنا من قبل. ستحصل على شكل شبيه بما يلي: جرب الآن أن تنقر بشكل متكرّر على صندوق الاختيار Hide completed tasks لتجد كيف أنّ المهام المنجزة تختفي وتظهر وفقًا لذلك. إضافة ميزة مهمة جديدة لمكوّن المهام لنحسن مكوّن المهام بميزة إضافة مهمة جديدة للمهام الموجودة مسبقًا. سأضع أسفل قائمة المهام عنصر إدخال نصي مع زر للإضافة. و سأضيف أيضًا بعض اللمسات باستخدام CSS لكي أحصل على مظهر مقبول لهما. افتح الملف tasks.css وأضف أصناف CSS التالية له: .add-task-container { position: relative; margin-top: 10px; } .add-task-container div{ position: absolute; top: 0; right: 60px; left: 45px; } .add-task-container div input{ width: 100%; } .add-task-container input{ position: absolute; width:50px; top: 0; right: 0; } انتقل الآن إلى الملف index.html وأضف الشيفرة التالية بعد نهاية وسم الإغلاق للجدول مباشرةً (ضمن القالب الخاص بمكوّن المهام): <div class="add-task-container"> <span>Task: </span> <div> <input type="text" v-model="new_task_text"/> </div> <input type="button" value="Add" v-on:click="add_new_task"/> </div> حان الآن دور الشيفرة البرمجية ضمن الملف app.js. افتح هذا الملف، واحرص على أن تكون محتوياته مطابقة لما يلي: Vue.component('tasks', { template: '#tasks-template', props: { name: String, tasks_list: Array, }, data() { return { hide_completed_tasks: false, new_task_text : "" } }, computed: { filtered_tasks() { return this.hide_completed_tasks ? this.tasks_list.filter(t => !t.done) : this.tasks_list; } }, methods: { add_new_task(event) { this.tasks_list.push({ title: this.new_task_text, done: false }); this.new_task_text = ""; } } }) new Vue({ el: '#app' }) الجديد هنا هو أنّني أضفت حقلًا جديدًا إلى القسم data وأسميته new_task_text سيرتبط بمربع النص الذي سندخل من خلاله عنوان المهمة. كما قد أضفت القسم methods وعرفت ضمنه التابع الجديد add_new_task، وهذا التابع الذي سيُستدعى عند نقر زر إضافة المهمة. الشيفرة البرمجية الموجودة في هذا التابع بسيطة، فهي تعمل على إضافة المهمة الجديدة إلى مصفوفة المهام tasks_list ثم تفرّغ عنوان المهمة من جديد لاستقبال المهمة التالية. جرب كتابة المهمة التالية: My new task! ضمن مربع النص، ثم انقر الزر Add. ستحصل على شكل شبيه بما يلي: ختامًا بدأنا في هذا الدرس بالولوج في عالم المكوّنات. يُعتبر ما تناولناه في هذا الدرس مقدّمة بسيطة إلّا أنها مهمة في التعامل مع المكونات. حيث تعلمنا كيف نبني هيكلًا لتطبيق بسيط على حاسوبنا الشخصي، وكيفية إضافة خادوم مبسّط لمحاكاة عمل التطبيق بشكل فعلي، كما بنينا مكوّنًا مفيدًا، وتعلّمنا أساسيات فصل شيفرة vue.js عن باقي التطبيق. هنالك المزيد لتعلّمه حول المكوّنات وحول vue.js بصورة عامة في الدروس القادمة إن شاء الله. تمارين داعمة تمرين 1 أنشئ مكوّنًا سمّه vu-countdowntimer وظيفته العد التنازلي بمقدار ثانية واحدة كل مرّة، ابتداءً من قيمة محددة يمكن تمريرها للمكون، وحتى الصفر. اقرأ أيضًا المقال التالي: المزيد حول المكونات في Vue.js المقال السابق: التعرف بالتفصيل على كائن Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: إنشاء أكثر من كائن Vue.js الوصول إلى عناصر HTML مباشرةً باستخدام $refs الخصائص المحسوبة ضمن كائن Vue.js الخصائص المراقبة ضمن كائن Vue.js تثبيت قالب جديد باستخدام $mount() فصل القالب عن عنصر HTML المُستَهدَف ما هو المكوّن (Component)؟ سنتعلّم في هذا الدرس المزيد عن كائن Vue.js، حيث سنتعرّف على كيفية إنشاء أكثر من كائن Vue.js وكيف نصل إلى خصائص كل كائن من خلال شيفرة JavaScript عادية. وكما سنتعرّف على الخصائص المحسوبة والخصائص المراقبة، وسنعود أيضًا إلى القوالب، وكيف نثبّت قالب ما إلى عنصر HTML بشكل برمجي.كما سنتعرّف على المكوّنات والحاجة إليها، وسنبني تطبيق بسيط للغاية يعتمد على مكوّن يعمل على التحويل من واحدة الكيلو غرام إلى واحد الباوند. إنشاء أكثر من كائن Vue.js من الممكن إنشاء أكثر من كائن Vue.js ضمن نفس التطبيق. حيث يمكن ربط كل كائن بعنصر div كما كنّا نفعل مسبقًا. يمكن لكل كائن أن يعمل بصورة مستقلة عن الكائن الآخر، وفي ذلك فائدة كبيرة، إذ يمكننا تقسيم واجهة المستخدم الرئيسية إلى أجزاء متعدّدة، كل منها ينفّذ وظيفة محدّدة، بدون أن تتداخل مع بعضها. في الحقيقة تقودنا هذه الميزة المهمة إلى بناء المكوّنات (Components). يلعب المكوّن دورًا مهمًّا في تنظيم الشيفرة البرمجيّة وفي عملية إعادة الاستخدام للشيفرة من قِبَلك، أو من قِبَل أي شخص آخر. سنتحدّث عن المكونات في Vue.js بشكل مبدئي في هذا الدرس. لنأخذ الآن مثالًا بسيطًا يوضّح كيفية إنشاء أكثر من كائن واحد بنفس الوقت: <div id="app1"> {{title}} </div> <div id="app2"> {{title}} </div> var instance1 = new Vue({ el:'#app1', data: { title: 'From first instance' } }) var instance2 = new Vue({ el:'#app2', data: { title: 'From second instance' } }) الشيفرة السابقة سهلة ومباشرة. بدايةً عرّفت عنصري div أسندت للأوّل المعرّف app1 وللثاني المعرّف app2. كل من العنصرين السابقين لا يحتوي إلا على الاستبدال النصي {{title}}. بالنسبة لشيفرة JavaScript فالأمر بسيط أيضًا. فقد عرّفت كائني Vue.js وأسندت كل منهما إلى متغيرين منفصلين instance1 و instance2. الكائنين متشابهين من حيث الشكل العام ولكنهما يختلفان بالقيمة النصيّة للخاصيّة title لكل منهما كما هو واضح. عند تنفيذ التطبيق السابق ستحصل على شكل شبيه بما يلي: من الواضح ظهور نصين مختلفين من كائنين مختلفين، رغم أنّه لكل منهما نفس اسم الخاصية title. يمكن بالطبع وجود أكثر من كائنين، ويمكن بديهةً أن يكون لكل كائن عتاده الخاص من الخصائص والدوال وغيرها. توجد أيضًا ميزة مهمّة لكائنات Vue.js، وهي إمكانية الوصول لأي كائن من شيفرة JavaScript عادية. انظر إلى المثال المعدّل عن المثال السابق (التعديل سيكون على شيفرة JavaScript فقط): var instance1 = new Vue({ el:'#app1', data: { title: 'From first instance' } }) var instance2 = new Vue({ el:'#app2', data: { title: 'From second instance' } }) instance1.$data.title='This text from outside!'; التعديل الوحيد الذي حدث هو في السطر الأخير. انظر كيف كتبت شيفرة JavaScript عادية للوصول إلى الخاصية title للكائن instance1. بعد التنفيذ ستحصل على شكل شبيه بما يلي: لاحظ معي كيف أصبح السطر الأوّل. استطعت تعديل النص من خارج الكائن. وهناك أمر آخر، لعلّك قد انتبهت إلى الخاصيّة $data. في الحقيقة هو كائن يولّده Vue.js بشكل تلقائي لكي يسمح للمبرمجين بالوصول إلى الخصائص الداخلية للقسم data. هذا دليل واضح على أنّ Vue.js مندمجة بشكل ممتاز مع JavaScript وليست بديلًا عنها، إنّما مكمّلة لها. سنشاهد عددًا من هذه الكائنات والدوال المولّدة بهذه الطريقة. الوصول إلى عناصر HTML مباشرة باستخدام $refs توجد أكثر من طريقة للوصول إلى عناصر HTML ضمن DOM. تضيف Vue.js طريقة أخرى لها باستخدام المفتاح ref. يسمح هذا المفتاح للمبرمج بالوصول إلى أي عنصر HTML بسهولة كبيرة. يمكن استخدام هذا الأسلوب بالشكل التالي: <div id="app"> <button v-on:click='changeText' ref='testButton'> Old Text </button> </div> var app = new Vue({ el: '#app', methods: { changeText: function() { this.$refs.testButton.innerText = 'New Text!'; } } }) الجديد هنا هو وضع الكلمة ref كما لو أنّها سمة ضمن العنصر button وإسناد القيمة testButton لها. الكلمة ref ليست سمة قياسية في HTML بالتأكيد، إنما هي كلمة تابعة لـ Vue.js. انظر الآن إلى كود JavaScript وتحديدًا ضمن التابع changeText ستلاحظ السطر التالي: this.$refs.testButton.innerText = 'New Text!'; الخاصية الجديدة هنا هي $refs وهي عبارة عن كائن JavaScript مولّد تلقائيًّا. انتبه إلى وجود حرف s الخاص بالجمع آخر كلمة $refs، لأنّه من الممكن استخدام أكثر من كلمة ref مع عناصر HTML مختلفة. انظر أيضًا إلى الخاصيّة testButton وهي تحمل نفس الاسم الذي عيّنّاه ضمن الكلمة key في HTML. في الحقيقة إنّ testButton عبارة عن كائن JavaScript أيضًا يمثّل عنصر في HTML، ولذلك استطعنا استخدام الخاصية innerText منه. عند تنفيذ التطبيق السابق. ستحصل على زر وحيد يحمل النص Old Text. بعد نقر الزر، ستحصل على النص الجديد New Text! كما هو متوقع. في الواقع لا يُنصَح بتعديل خصائص عناصر HTML بهذا الأسلوب، أنصح باستخدام هذا الأسلوب فقط لقراءة خصائص عناصر HTML في حال الحاجة. الخصائص المحسوبة في Vue.js سنتحدّث في هذه الفقرة عن مفهوم الخاصيات المحسوبة (Computed Properties)، والحاجة إليها عند بناء تطبيقات باستخدام Vue.js. تشبه بنية الخصائص المحسوبة البنية الخاصة بالتوابع، في أنّهما عبارة عن دوال، مع فرق بسيط يتمثّل في أنّ الخاصيّة المحسوبة يجب أن تُرجع قيمة ما. تُعرّف الخصائص المحسوبة ضمن قسم جديد اسمه computed يُوضع على نفس مستوى القسمين data و methods أي على الشكل التالي: var app = new Vue({ el: "#app", data: { ... }, computed: { ... }, methods: { ... } }) تلعب الخصائص المحسوبة دورًا مهمّا عندما يكون التطبيق كبيرًا نسبيًا، حيث تسمح بتنفيذ الشيفرة البرمجية بشكل محسَّن (Optimized)، فهي تتجنّب المعالجة غير الضرورية لأجزاء من الشيفرة في حال لم يطرأ تغيير ما عليها. لفهم الخصائص المحسوبة بشكل أفضل، سنأخذ تطبيق يقيّم درجات الحرارة برسائل نصيّة بسيطة لنفهم الحاجة إلى الخصائص المحسوبة: <div id="app"> <span>Current temperature:</span><input type='text' v-model='temperature' /> <span> - Clouds:</span> <select v-model='clouds'> <option>Yes</option> <option>No</option> </select> <p> <b>Result:</b> (Computed) {{evaluation_computed}} | (Method) {{evaluation_method()}} </p> <p> {{clouds == 'Yes'? 'With some clouds.':'And the sky is clear.'}} </p> </div> var app = new Vue({ el: '#app', data: { temperature: 30, clouds: 'Yes' }, computed: { evaluation_computed: function() { console.log('Computed'); return this.temperature > 35 ? 'Hot' : this.temperature < 20 ? 'Cold' : 'Moderate'; } }, methods: { evaluation_method: function() { console.log('Method'); return this.temperature > 35 ? 'Hot' : this.temperature < 20 ? 'Cold' : 'Moderate'; } } }) بعد تنفيذ البرنامج السابق ستحصل على خرج شبيه بما يلي: بالنسبة لقسم HTML أعتقد أنّ الأمور واضحة، فقد عرّفت مربع نص، بالإضافة إلى عنصر قائمة منسدلة يحمل القيمتين Yes و No للإشارة إلى وجود غيوم في السماء أم لا. كل من العنصرين السابقين مربوطين ربطًا ثنائي الاتجاه باستخدام الموجّه v-model بالحقلين الموافقين من القسم data في كائن Vue.js. ثمّ عرّفت بعد ذلك عنصري p لعرض النتائج. عنصر p الأوّل مسؤول عن عرض تقييم الوضع الحالي للجو فيما إذا كان حارًا أم معتدلًا أم باردًا بحسب قيمة الحقل temperature، ولاحظ هنا أنّني أعرض التقييم من الخاصية المحسوبة evaluation_computed والتابع evaluation_method() على التوالي لغرض سأوضّحه لك بعد قليل. أمّا عنصر p الثاني فيُستخدم لإخبار المستخدم في حال وجود بعض الغيوم أم أنّ السماء صافية بحسب القيمة التي اختارها المستخدم من عنصر القائمة المنسدلة السابق. لا أدري إن كنت قد انتبهت إلى أنّني لا أضع قوسي الاستدعاء بعد اسم الخاصية المحسوبة على عكس التابع العادي المعرّف ضمن القسم methods. بالنسبة لشيفرة JavaScript، لاحظ بدايةً أنّ الشيفرة البرمجية الموجودة سواءً في الخاصية المحسوبة evaluation_computed أم في التابع evaluation_method() متطابقة تمامًا. ولاحظ أيضًا أنّني قد وضعت في كل منهما تعليمة الكتابة إلى الطرفية (console) الخاصة بأدوات المطوّر ضمن المتصفّح لنراقب كيف ستُنفَّذ كل منهما. اعرض أدوات المطوّر في المتصفّح الآن (اضغط المفتاح F12)، ثم غيّر درجة الحرارة في مربّع النص. ستلاحظ ظهور الكلمتين Computed و Method على التوالي، مما يشير إلى أنّ التنفيذ قد دخل إلى الخاصية المحسوبة evaluation_computed والتابع evaluation_method(). ستستمرّ الكلمتان السابقتان بالظهور على هذا النمط، كلّما أجريت أي تغيير في درجة الحرارة. الأمر المثير الآن، هو أنّه إذا غيّرت الاختيار الحالي ضمن عنصر القائمة المنسدلة، ستجد الكلمة Method قد ظهرت وحدها ضمن الطرفية، ولن تظهر الكلمة Computed مما يشير إلى أنّ الشيفرة البرمجية الموجودة ضمن الخاصية المحسوبة لم تُنفَّذ! إذًا، فالخاصية المحسوبة ذكية بما يكفي لكي تدرك أنّ درجة الحرارة لم تتغيّر فليس هناك حاجة لتنفيذ الشيفرة البرمجية الخاصّة بها كل مرّة. بعبارة أخرى، تدرك الخاصية المحسوبة أنّ الشيفرة البرمجية الموجودة ضمنها لا تحتوي على خصائص تمّ تغييرها بشكل أو بآخر، فتختصر عملية التنفيذ وتعرض التقييم السابق لدرجة الحرارة طالما أنّها لم تتغيّر! الشكل التالي نتج معي بعد تغيير حالة الغيوم عدّة مرات دون تغيير قيمة درجة الحرارة سوى المرّة الأولى فقط: الخصائص المراقبة ضمن كائن Vue.js رغم أنّ الخصائص المحسوبة كافية في معظم الأحيان. إلّا أنّه هناك بعض الحالات التي يكون استخدام الخصائص المراقبة أفضل. والخصائص المراقبة (Watched Properties) تشبه إلى حدّ كبير الخصائص المحسوبة، وتتميّز عنها بميزتين أساسيتين: يمكن تنفيذ شيفرة برمجية غير متزامنة ضمنها، بمعنى أنّه يمكن تنفيذ مهام بشكل متوازي مع التطبيق الأساسي كالاتصال بخادوم بعيد مثلًا. لا نحتاج إلى إرجاع قيمة من الخصائص المراقبة كما كنّا نفعل مع الخصائص المحسوبة. تُعرّف الخصائص المحسوبة ضمن قسم جديد اسمه watch يُوضع على نفس مستوى باقي الأقسام الأخرى مثل data و methods و computed أي على الشكل التالي: var app = new Vue({ el: "#app", data: { ... }, computed: { ... }, watch: { ... }, methods: { ... } }) لنأخذ مثالًا بسيطًا: <div id="app"> <input type="text" v-model='content' /> <p> The input has changed: {{counter}} times. </p> </div> var app = new Vue({ el: '#app', data: { content: '', counter: 0 }, watch: { content: function() { tmp = this; setTimeout(function() { tmp.counter++; }, 2000); } } }) استخدمت في المثال السابق مربع إدخال نصي يسمح للمستخدم بكتابة ما يرغب. عندما يكتب المستخدم أي شيء ضمن مربّع النص، سيعمل التطبيق على إحصاء عدد مرّات التعديل التي أجراها المستخدم، ويُظهر ذلك في الأسفل ضمن رسالة مناسبة. استخدمت الربط ثنائي الاتجاه باستخدام v-model للربط مع الحقل content (راجع الدرس الثاني). سنراقب الحقل content (المرتبط مع دخل المستخدم) في حال حدث أي تغيير على قيمته. وهكذا فقد وضعت نفس اسم الحقل content ضمن القسم watch كما هو واضح، وأسندت إليه تابع. هذه هي الصيغة المعتمدة لمراقبة أي حقل. الذي يحدث ضمن الخاصية المراقبة بسيط في هذا المثال. فقد استخدمت التابع المُضمّن setTimeout لزيادة قيمة الحقل counter بعد ثانيتين في حال حدث أي تغيير عليه. اعتمدت هذا الأسلوب، لكي ألفت انتباهك إلى أنّ عملية العد تحدث بشكل غير متزامن، وبشكل منفصل تمامًا عن التعديلات التي يُجريها المستخدم ضمن مربّع النص. نفّذ التطبيق السابق، وابدأ بالكتابة والتعديل ضمن مربّع النص. ستلاحظ أنّ قيمة العدّاد ستتزايد بينما تجري التعديلات ضمن مربّع النص، ولكن هذا التزايد لا يحدث فورًا، إنّما بتأخير زمني قدره ثانيتين عن التعديلات الفعلية. ملاحظة لاحظ أنّني قد استخدمت المتغيّر المؤقت tmp لتخزين المرجع this قبل استخدام التابع setTimeout ضمن الخاصية المراقبة. والسبب في ذلك أنّ الشيفرة البرمجيّة ستُنفّذ ضمن مغلّف (Closure) وبالتالي لن تُشير الكلمة this إذا استُخدِمت مباشرةً إلى كائن Vue.js كما اعتدنا سابقًا. تثبيت قالب جديد باستخدام $mount() استخدمنا في جميع الأمثلة التي تعاملنا معها حتى الآن القسم el من كائن Vue.js لتحديد العنصر المُستَهدف الذي سيمثّل القالب template الذي سيعمل التطبيق على تعديله والتعامل معه كما وسبق أن أوضحنا من قبل (راجع "فهم قوالب Vue.js" من الدرس الثاني). إلّا أنّه يمكن الاستغناء عن القسم el بشكل كامل، إذا لم نكن نعرف مسبقًا العنصر الذي سنستهدفه (وبالتالي لا نعرف القالب). في هذه الحالة يمكن استخدام التابع المولّد تلقائيًّا $mount() الذي ينوب عن القسم el في هذه الحالة، وذلك في اللحظة التي نريد فيها الربط مع عنصر محدّد. انظر إلى المثال البسيط التالي لتوضيح هذه الفكرة: <div id="app"> {{title}} </div> var app = new Vue({ data: { title:'Hello!' } }) لاحظ أنّني قد حذفت القسم el، وهكذا، وعند تنفيذ المثال السابق ستحصل على الخرج التالي: {{title}} أي أنّ Vue.js لم يعرف في هذه الحالة القالب الذي سيتعامل معه. لحل هذه المشكلة وإظهار الرسالة المناسبة، سنضيف سطرًا واحدًا فقط إلى شيفرة JavaScript السابقة، لتصبح الشيفرة على النحو التالي: var app = new Vue({ data: { title:'Hello!' } }); app.$mount('#app'); إذًا فقد استخدمنا التابع $mount() ومرّرنا له معرّف العنصر المُستهدَف (وهو #app في مثالنا) ليعمل عندها Vue.js على ربط (mount) القالب المراد التعامل معه، وبالتالي إظهار الرسالة المناسبة للمستخدم. تُعتبر هذه الميزة، من المزايا المهمّة جدًّا في Vue.js حيث أنّها تسمح ببناء المكوّنات التي سنتحدّث عنها بعد قليل. فصل القالب عن عنصر HTML المُستَهدَف توجد ميزة مهمّة أخرى سنحتاجها لاحقًا عند العمل الموسّع مع المكوّنات، وهي إمكانية عدم كتابة شيفرة HTML التي (تُعبّر عن القالب) ضمن العنصر المُستَهدَف. بمعنى آخر، يمكن فصل شيفرة HTML التي تُعبّر عن القالب عن العنصر المُستَهدَف، ووضعها ضمن قسم جديد ضمن كائن Vue.js. اسم هذا القسم هو template ويوضع على نفس المستوى مع بقيّة الأقسام الرئيسية. انظر إلى الشكل التالي: var app = new Vue({ el: "#app", template: "HTML CODE GOES HERE", data: { ... }, computed: { ... }, watch: { ... }, methods: { ... } }) لنأخذ مثالًا بسيطًا يوضّح هذه الفكرة: <div id="app"> </div> var app = new Vue({ el:'#app', template:'<h2>Hsoub Academy</h2>' }); بالنسبة لشيفرة HTML فهي لا تحتوي سوى العنصر المُستَهدف وهو فارغ بالطبع. أما بالنسبة لشيفرة JavaScript فهي تحتوي على كائن Vue.js بسيط، عرّفنا ضمنه العنصر المُستهدَف عن طريق el، وأيضًا وضعنا القالب الذي نريد التعامل معه ضمن القسم template. جعلت كود HTML في هذا القالب بسيطًا للغاية بهدف شرح الفكرة فقط. نفّذ المثال السابق لتحصل على الجملة: Hsoub Academy من الممكن أيضًا الاستغناء عن القسم el كليًّا واستبداله بالتابع $mount(). انظر شيفرة JavaScript الجديدة: var app = new Vue({ template:'<h2>Hsoub Academy</h2>' }); app.$mount('#app'); بعد التنفيذ، ستحصل على النتيجة السابقة. الآن لنجري تطويرًا بسيطًا على المثال الأخير، بحيث نعرض قيمة حقل اسمه title معرّف ضمن القسم data. سيكون التعديل ضمن شيفرة JavaScript فقط: var app = new Vue({ template:'<div><h2>Hsoub Academy</h2><p>{{title}}</p></div>', data:{ title:'Welcome dear user!' } }); app.$mount('#app'); الشيفرة السابقة سهلة ومباشرة. بعد التنفيذ ستحصل على شكل شبيه بما يلي: لاحظ كيف حدث الاستبدال النصّي ضمن شيفرة HTML الموجودة ضمن القالب، وذلك بسبب وجود {{title}}. هناك ملاحظة بسيطة أخرى حول شيفرة HTML المكتوبة ضمن القالب. يجب أن يحتوي القالب المُسنَد إلى القسم template على عنصر جذر واحد، في مثالنا السابق أنشأت عنصر div لهذا الغرض، ووضعت فيه العنصرين h2 و p كما هو واضح. مرّة أخرى يُعد هذا الأسلوب أساسيًّا في بناء المكوّنات واستخدامها في Vue.js. في الحقيقة لا نستخدم هذا الأسلوب كما هو في التطبيقات العملية عادةً. الذي يهمّنا هنا هو المفهوم فقط. ما هو المكون (Component)؟ المكوّن بصورة عامّة عبارة عن وحدة برمجية مستقلة بذاتها، تُنجز عملًا واحدًا على الغالب. نصادف المكوّنات كثيرًا في عالم البرمجيات. وإذا أردت أمثلة عنها في عالم الويب، فجداول البيانات التي تعرض المعلومات المختلفة للمستخدم مع مزايا الترشيح والترتيب، وأيضًا المساحات الصغيرة الموجودة على جانب الصفحة التي تعرض درجة الحرارة الحالية أو أسعار الصرف للعملات، كلها عبارة عن مكوّنات. وبصورة عامة أي ناحية وظيفية يمكن أن تُستخدَم بشكل متكرر في نفس المشروع البرمجي أو في مشاريع برمجيّة مختلفة يمكن أن تُرشّح لتصبح مكونًا. سنلامس المكوّنات في هذا الدرس، ولن ندخل في تفاصيلها، حيث سنؤجّل ذلك إلى دروس لاحقًا. سأبني في هذا الدرس مكوّن بسيط للغاية، وظيفته التحويل من واحدة الكيلوغرام إلى واحدة الباوند. انظر إلى التطبيق التالي: <div id="app"> <weightconverter></weightconverter> <weightconverter></weightconverter> <weightconverter></weightconverter> </div> var wcComponent = Vue.component('weightconverter', { template: `<div style='margin-bottom:10px;'> <input type='text' v-on:input='inputChanged' /> <span>Kg. is equivalent to: <b>{{pounds}}</b> pounds.</span> </div>`, data: function() { return { pounds: 0 } }, methods: { inputChanged: function(event) { this.pounds = Number(event.target.value) * 2.20462; } } }); var app = new Vue({ el: '#app', components: { 'weightconverter': wcComponent } }); نفّذ التطبيق السابق، ستحصل على شكل شبيه بما يلي: سأتحدث عن شيفرة HTML بعد قليل. لننظر الآن إلى شيفرة JavaScript. لنبدأ بالقسم الأوّل من هذه الشيفرة حيث سجّلنا مكوّن جديد باستخدام التابع Vue.component: var wcComponent = Vue.component('weightconverter', { template: `<div style='margin-bottom:10px;'> <input type='text' v-on:input='inputChanged' /> <span>Kg. is equivalent to: <b>{{pounds}}</b> pounds.</span> </div>`, data: function() { return { pounds: 0 } }, methods: { inputChanged: function(event) { this.pounds = Number(event.target.value) * 2.20462; } } }); كما هو واضح فإنّنا سنسند المكوّن الذي يُرجعه التابع component() إلى المتغيّر wcComponent. يقبل التابع component() وسيطين. الوسيط الأوّل هو اسم المكوّن المراد إنشاءه، وهو عبارة عن قيمة نصيّة، أمّا الوسيط الثاني فهو كائن آخر يحوي إعدادات المكوّن. لو تمعنت النظر بهذا الكائن، فستجد أنّه يطابق كائن Vue.js قياسي مع اختلاف واحد بسيط. يكمن الاختلاف في القسم data. في كائنات Vue.js التي أنشأناها حتى الآن كنّا نُسند للقسم data كائن عادي يحتوي على الحقول المراد التعامل معها ضمن التطبيق. أمّا في الشيفرة السابقة فيجب تعريف القسم data على أنّه تابع يُرجع الكائن الذي يحتوي الحقول المراد التعامل معها: data: function() { return { pounds: 0 } } ما عدا ذلك تبقى الأمور كما هي! أمّا القسم الثاني من شيفرة JavaScript فتحتوي على تعريف كائن Vue.js عادي، ولكن مع وجود قسم جديد وهو القسم components. هذا القسم يحتوي على أيّة مكونات سيستخدمها التطبيق، وهي في حالتنا هذه المكوّن weightconverter الذي عرّفناه قبل قليل: var app = new Vue({ el: '#app', components: { 'weightconverter': wcComponent } }); لاحظ كيف نسجّل المكونات التي نريد استخدامها: اسم المكوّن يليه المرجع له (موجود ضمن المتغيّر wcComponent بالطبع). بالنسبة إلى شيفرة HTML فهي بسيطة، حيث نستخدم العنصر الجديد weightconverter ضمن شيفرة HTML كما لو أصبح عنصر HTML نظامي. استخدمت هذا العنصر ثلاث مرّات من باب توضيح أنّه يمكنك استخدامه في أكثر من مكان ضمن الصفحة. ختامًا لقد كان هذا الدرس غنيًّا بالمعلومات المتنوّعة والمهمّة خصوصًا لما يليه من دروس. سأحيلك بشكل متكرّر إلى هذا الدرس في المستقبل، كلّما اقتضت الضرورة إلى الرجوع إلى مفهوم ذُكر هنا. سننتقل في الدرس التالي إلى مستوى جديد، حيث سنتعلّم كيفية بناء تطبيقات عمليّة حقيقية، وسنتوسّع بمفهوم المكوّنات. تمارين داعمة تمرين 1 أجر تعديلًا على تطبيق تحويل الواحدات الذي بنيناه قبل قليل، بحيث يسمح بالتحويل بالاتجاهين: من كيلوغرام إلى باوند، ومن باوند الى كيلو غرام بشكل مباشر. تمرين 2 أنشئ مكوّن جديد، عبارة عن مؤقّت زمني تنازلي (Countdown Timer). قيمته الابتدائية دقيقة واحدة، ثم يتناقص حتى يصل للصفر. ثم استخدام هذا المكوّن ضمن تطبيق بسيط لتجربته فقط. اقرأ أيضًا المقال التالي: مدخل إلى التعامل مع المكونات في Vue.js المقال السابق: الموجهات الشرطية والتكرارية في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: كتابة شفرات JavaScript مباشرةً ضمن القوالب التصيير الشرطي باستخدام v-if و v-else و v-else-if الفرق بين v-if و v-show تصيير القوائم باستخدام v-for المرور على خاصيات كائن نتابع عملنا في هذا الدرس وهو الدرس الثالث من سلسلة دروس تعلّم Vue.js. سنتعلّم في هذا الدرس كيفية كتابة شفرات JavaScript مباشرةً ضمن القوالب دون الحاجة إلى الاستعانة بالتوابع كما كنَّا نفعل من قبل، بالإضافة إلى استخدام التصيير (rendering) الشرطي باستخدام 'v-if' وأخواته 'v-else' و 'v-else-if'. كما سنتحدّث عن الفرق بين 'v-if' و 'v-show' اللذان لهما نفس التأثير من الناحية الشكليّة، ونختم الدرس بالحديث عن التكرار باستخدام 'v-for'. هيَّا بنا لنسبر أغوار Vue.js! كتابة شفرات JavaScript مباشرة ضمن القوالب يمكننا في الكثير من الأحيان أن نكتب شيفرة JavaScript مباشرةً ضمن القوالب، دون الحاجة إلى إنشاء تابع خاص ووضعه في قسم methods. نلجأ إلى هذا الأسلوب، في حال كانت الشيفرة قصيرة ولا تحتوي على الكثير من التعقيد، بالإضافة إلى ضرورة أن يكون الناتج النهائي للشيفرة عبارة عن تعبير (expression). انظر معي إلى المثال التالي: <div id="app"> <span>Current temperature:</span><input type='text' v-model='temperature' /> <p> {{temperature > 35 ? 'Hot': temperature < 20 ? 'Cold' : 'Moderate' }} </p> </div> var app = new Vue({ el: '#app', data: { temperature: 30 } }) يعمل التطبيق السابق على عرض مربّع إدخال للمستخدم، حيث يعرض عليه إدخال درجة الحرارة الحالية، ثم يُقيّم دخل المستخدم آنيًّا أثناء الكتابة على النحو التالي: إذا كانت درجة الحرارة أكبر من 35، سيعرض التطبيق الرسالة Hot إذا كانت درجة الحرارة أصغر من 20، سيعرض التطبيق الرسالة Cold إذا لم يتحقّق الشرطان السابقان سيعرض التطبيق الرسالة Moderate أي معتدل. إذا نظرت إلى قسم HTML فستجد الشيفرة التالية ضمن الاستبدال النصّي: temperature > 35 ? 'Hot': temperature < 20 ? 'Cold' : 'Moderate' شيفرة JavaScript هذه عبارة عن تعبير بسيط يستخدم العامل (operator) الثلاثي :? بشكل متداخل لاختبار الحالات الثلاث السابقة. لاحظ معي أنّنا قد استخدمنا هنا الربط ثنائي الاتجاه 'v-model' لربط قيمة الحقل temperature مباشرة بدخل المستخدم (يمكنك العودة للدرس السابق لمراجعة هذا الموضوع). لاحظ أيضًا أنّ كائن Vue.js في قسم JavaScript بسيط للغاية ولا يحتوي على أية توابع. التصيير الشرطي باستخدام v-if و v-else و v-else-if نحتاج في الكثير من الأحيان إلى إخفاء جزء من الصفحة في حال تحقّق (أو عدم تحقّق) شرط ما. يمكن تحقيق هذا الأمر بسهولة في Vue.js من خلال استخدام الموجّه v-if وأخواته. لنستخدم أولًا الموجّه v-if لوحده: <div id="app"> <button v-on:click="show = !show"> Click this! </button> <p v-if="show"> Welcome to Hsoub Academy! </p> </div> var app = new Vue({ el: '#app', data: { show: true } }) التطبيق السابق بسيط، تتكوّن الواجهة من زر عادي ورسالة. عند نقر هذا الزر بشكل متكرّر، تظهر الرسالة في الأسفل أو تختفي وفقًا لذلك. نلاحظ أولًا الحقل show المعرّف ضمن قسم data، والذي أسندت إليه القيمة 'true' افتراضيًا. بالنسبة لشيفرة HTML لاحظ الموجّه v-on:click وكيف أسندت إليه شيفرة JavaScript مباشرةً: v-on:click = “show = !show” اعتدنا مسبقًا على إدراج تابع معالج للحدث، ولكن من الممكن كتابة شيفرة JavaScript أيضًا. ما تفعله هذه الشيفرة بسيط جدًّا، فهي تعمل على تغيير قيمة show بين 'true' و 'false' بشكل متناوب كلّما نقر المستخدم على الزر. وضعت الموجّه v-if ضمن عنصر p الذي سيحتوي على النص Welcome to Hsoub Academy. أسندت لهذا الموجّه قيمة الحقل show. فعندما تكون قيمة show تساوي true سيظهر عنصر p مع الرسالة المطلوبة للمستخدم، وإلّا سيختفي عنصر p بشكل كامل، ليس من أمام المستخدم فحسب، وإنّما من كامل بنية DOM، وهذا النقطة من الضروري الانتباه إليها. من الممكن أيضًا استخدام الموجه v-else بعد الموجّه v-if. يلعب الموجّه v-else نفس الدور الذي تلعبه عبارة else في JavaScript، انظر إلى المثال البسيط التالي وهو تعديل بسيط عن المثال السابق، سيكون التعديل على شيفرة HTML فقط: <div id="app"> <button v-on:click="show = !show"> Click this! </button> <p v-if="show"> Welcome in Hsoub Academy! </p> <p v-else> Welcome in Vue.js! </p> </div> الإضافة الوحيدة هي: <p v-else> Welcome in Vue.js! </p> استخدمت الموجّه v-else. وهكذا فعندما ينقر المستخدم الزر بشكل متكرر، ستتناوب قيمة show تبعًا لذلك بين 'true' و 'false'، عندما تكون قيمة show تساوي ture يظهر النص Welcome to Hsoub Academy! أمّا عندما تكون false يظهر النص Welcome in Vue.js!. ويمكن أيضًا اعتبارًا من الإصدار 2.1 للمكتبة Vue.js استخدام الموجّه v-else-if الذي يقابل else if في لغة JavaScript . يمكن استخدام كل من الموجّهات الشرطية السابقة مع عناصر HTML أخرى مثل div و template. الميزة في استخدام العنصر template هو أنّه لا يظهر في DOM عند عرض الصفحة. فهو يلعب دور حاوية لا تُصيَّر عند عرض الصفحة. سأعدّل المثال السابق ضمن قسم HTML فقط: <div id="app"> <button v-on:click="show = !show"> Click this! </button> <template v-if="show"> <h2> Welcome to Hsoub Academy! </h2> <p> Here you can learn Vue.js. </p> </template> </div> وضعت العنصرين h1 و p ضمن العنصر template. هذه المرة، وضعت الموجّه v-if ضمن العنصر template وهكذا سنتمكّن من إخفاء كامل العنصر template مع العناصر الموجودة ضمنه، أو إظهاره بعناصره، وذلك بحسب قيمة الحقل show كما مرّ معنا. يمكن استخدام العنصر div مكان العنصر template بنفس الأسلوب تمامًأ، ولكن في هذه الحالة يظهر العنصر div ضمن DOM في حال كان شرط الإظهار محقّقًا (قيمة الحقل show تساوي true) في حين لا يظهر العنصر template في DOM حتى ولو كان شرط الإظهار محقّقًا، فالأمر يعود إليك في تحديد العنصر المناسب لاحتياجاتك. الفرق بين v-if و v-show من الممكن استخدام الموجّه v-show عوضًا عن الموجّه v-if، في إخفاء أو إظهار عنصر وفقًا لشرط معيّن كما وسبق أن رأينا قبل قليل. يكمن الفرق الأساسي بين الموجّهين السابقين، في أنّ الموجّه v-if يؤدي إلى إزالة العنصر بشكل كامل من DOM كما لو أنّه غير موجود بالأصل. أمّا الموجّه v-show فيعمل على إخفاء العنصر فقط، دون إزالته بالكامل من DOM. يمكن أن تلحظ الفرق بينهما بسهولة من خلال المثال البسيط التالي: <div id="app"> <button v-on:click="show = !show"> Click this! </button> <p v-if="show"> Welcome to Hsoub Academy! </p> </div> لاحظ أنّني استخدمت الآن الموجّه v-if. بالنسبة لقسم JavaScript فهو نفسه كما في الأمثلة السابقة. عندما يكون النص Welcome to Hsoub Academy!1 ظاهرًا. انقر عليه بزر الفأرة الأيمن واختر Inspect من القائمة المنبثقة إذا كنت تستخدم Google Chrome أو اختر Inspect Element إذا كنت تستخدم FireFox. سترى شيئًا شبيهًا بما يلي: انقر الآن على الزر ، سيختفي النص بالطبع. انظر إلى نافذة العناصر مرّة أخرى، لتجد أنّ عنصر p الذي يحتوي النص السابق قد اختفى تمامًا من المستند، وحلّ مكانه رمز تعليق (comment) كما في الشكل التالي (لاحظ المستطيل الأحمر): كرّر الآن نفس التجربة السابقة، ولكن بعد أن تستبدل بالموجّه v-if الموجّه v-show. هذه المرّة لن يختفي العنصر p كليًّا من DOM، إنّما ستستخدم Vue.js تنسيق CSS واسمه display لإخفائه من أمام المستخدم دون إزالته تمامًا من DOM. انظر الشكل التالي (لاحظ المستطيل الأحمر مرّة أخرى): تصيير القوائم باستخدام v-for من الممكن توليد قائمة من العناصر اعتبارًا من بيانات موجودة على شكل مصفوفة وذلك بشكل تلقائي من خلال استخدام الموجّه v-for، وهو يشبه إلى حدّ كبير حلقة for التكراريّة في لغات البرمجة عمومًا. ليكن لدينا مصفوفة اسمها fruits تحتوي على العناصر التالية: Apple و Banana و Orange و Kiwi. نريد إظهار هذه البيانات ضمن قائمة غير مرتّبة. يمكن استخدام البرنامج التالي: <div id="app"> <ul> <li v-for='fruit in fruits'>{{fruit}}</li> </ul> </div> var app = new Vue({ el: "#app", data: { fruits:['Apple', 'Banana', 'Orange', 'Kiwi'] } }) وضعنا الموجّه v-for ضمن العنصر li على النحو التالي: v-for='fruit in fruits' اسم المتغيّر fruit هنا كيفي، يمكنك استخدام أي اسم آخر. أمّا fruits فهو نفس الحقل المعرّف ضمن القسم data في كائن Vue.js. النتيجة ستكون على النحو التالي: من الممكن بالطبع أن نحصل على البيانات الموجودة ضمن الحقل fruits من خدمة ويب (Web Service) مثلًا. إذا أردت أن تحصل على دليل (index) العنصر أيضًا، يمكن ذلك بسهولة بإجراء التعديل التالي على الموجّه v-for: <li v-for='(fruit, i) in fruits'>{{fruit}} - ({{i}})</li> الإضافتين الجديدتين: (fruit, i) و ({{i}}). لاحظ كيف وضعنا المتغير fruit أولًا ثم المتغير الذي سيعبّر عن الدليل بعده، والاثنان ضمن قوسين عاديين (fruit, i) والترتيب بهذه الصورة مهم بالطبع. عند التنفيذ سيظهر دليل كل عنصر بجواره بين قوسين عاديين، مع الملاحظة أنّ الترقيم سيبدأ من الصفر، أي أنّ دليل العنصر الأوّل سيكون صفرًا، وهذا بديهي بالطبع. في الواقع يذكرني هذا الأسلوب المتمثّل في استخلاص الدليل، بأسلوب لغة البرمجة بايثون في ذلك. المرور على خصائص كائن يمكن استخدام الموجّه v-for أيضًا في المرور على خصائص كائن ما. سيكون هذا الكائن عبارة عن كائن JavaScript عادي سنعمل باستخدام هذا الموجه على استخلاص قيمة كل خاصيّة مع اسمها. ليكن لدينا الكائن التالي: customer: {name: 'Ahmad', age:30, items: 3} يتألف هذا الكائن من ثلاث خصائص: name و age و items. سنمر الآن على قيم هذه الخصائص على النحو التالي: <div v-for='value in customer'> <p> {{value}} </p> </div> var app = new Vue({ el: "#app", data: { customer: {name: 'Ahmad', age:30, items: 3} } }) يمكنك بالطبع استخدام أي عنصر من HTML لعرض هذه القيم. ستظهر كل قيمة ضمن عنصر p خاص بها. إذا أردنا تطوير المثال البسيط السابق، بحيث يصبح من الممكن أن يكون لدينا مجموعة من الزبائن (customers) ونريد أن نمرّ على هذه المجموعة، مع عرض خصائص كل كائن (زبون) منها، فإنّنا سنستخدم موجّهي v-for متداخلين على النحو التالي: <div id="app"> <div v-for='customer in customers'> <div v-for='value in customer'> <p> {{value}} </p> </div> <hr> </div> </div> var app = new Vue({ el: "#app", data: { customers: [{ name: 'Ahmad', age: 30, items: 3 }, { name: 'Saeed', age: 28, items: 13 }, { name: 'Majd', age: 21, items: 12 } ] } }) لاحظ كيف أجريت تعديلًا على اسم الحقل customer ليصبح customers ضمن القسم data من كائن Vue.js. لاحظ أيضًا موجّهي v-for المتداخلين، يمرّ الخارجي على كل عنصر من عناصر المصفوفة customers، في حين يمرّ الداخلي على جميع قيم الخصائص الموجودة ضمن كائن customer محدّد. ستحصل عند التنفيذ على خرج شبيه بما يلي: في المثالين الأخيرين حصلنا على قيمة الخاصيّة دون اسمها. يمكن بأسلوب مشابه لعملية الحصول على دليل العنصر ضمن المصفوفة، الحصول على اسم الخاصيّة بالإضافة إلى قيمتها. أجرِ التعديل البسيط التالي على موجّه v-for الداخلي الذي يمرّ على الخصائص ليصبح على النحو التالي: <div v-for='(value, key) in customer'> والترتيب هنا مهم أيضًا. ملاحظة تراقب Vue.js بعض توابع المصفوفة customers التي تعاملنا معها في المثال السابق باستخدام الموجّه v-for. هذه التوابع التي سأسردها الآن، تُغيّر في الحالة الداخلية للمصفوفة، وبالتالي تعمل Vue.js إلى إحداث تغيير فوري على الخرج يتوافق مع التغيير الذي حدث. هذه التوابع هي: push() pop() shift() unshift() splice() sort() reverse() جرّب أن تضيف إلى المثال الأخير زر، وأسند إليه الموجّه v-on:click على النحو التالي: <button v-on:click='customers.push({name: "Hasan", age:24, items:15})'> Add new customer </button> بعد تنفيذ البرنامج ونقر هذا الزر، ستلاحظ أنّ الزبون الذي اسمه Hasan قد أُضيف إلى الخرج تلقائيًّا رغم أنّنا أضفناه إلى المصفوفة customers فقط. ختامًا تعلّمنا في هذا الدرس كيفيّة التعامل مع الموجّهات الشرطية مثل v-if و v-else و v-else-if و v-show وكيفيّة استخدامها في تطبيقات Vue.js. كما ميّزنا بين الموجهين v-if و v-show وتعلّمنا متى نستخدم كل منهما. كما تعرّفنا أيضًا في هذا الدرس على الموجّه v-for الذي يلعب نفس الدور الذي تلعبه الحلقات التكرارية في لغات البرمجة بشكل عام، واستخدمناه للمرور على عناصر المصفوفات، بالإضافة إلى المرور على خصائص كائن محدّد. تمارين داعمة تمرين 1 أجر تعديلًا على التطبيق الزبائن customers السابق. في هذه المرة يجب أن تظهر أسماء الزبائن فقط، ضمن عنصر القائمة المنسدلة select وذلك باستخدام v-for أيضًا. تمرين 2 يُطلب في هذا التمرين، تطوير التطبيق الموجود في التمرين 2 من الدرس السابق. هذه المرة ستجعل الشيفرة أسهل بكثير بعد استخدام الموجّه v-for بدلًا من الأسلوب الذي استخدمته هناك. اقرأ أيضًا المقال التالي: التعرف بالتفصيل على كائن Vue.js المقال السابق: استخدام Vue.js للتعامل مع DOM النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعلّم في هذا الدرس: فهم قوالب Vue.js الوصول إلى البيانات والتوابع من كائنات Vue.js الربط مع السمات Attributes كتابة شيفرة HTML خام التعامل مع الأحداث Events استخدام الربط ثنائي الاتجاه نتابع عملنا في هذا الدرس وهو الدرس الثاني من سلسلة دروس تعلّم Vue.js. سنتعلم هذه المرّة كيفية الوصول والتعامل مع DOM، حيث سنتعلّم كيف نستخدم موجّهات Vue.js مختلفة للوصول إلى بيانات كائن Vue.js والتفاعل معها، وسنتوسّع في التعامل مع الأحداث events بالإضافة إلى كيفية استخدام الربط ثنائي الاتجاه مع العناصر. فهم قوالب Vue.js تعاملنا في الدرس السابق مع تطبيقات استخدمت مزايا بسيطة من Vue.js، وإذا كنت تذكر أنّنا قد كتبنا شيفرة HTML بسيطة ومن ثمّ استخدمنا الاستبدال النصي '{{ message }}' ، واستخدمنا أيضًا موجّه 'v-on' للاستجابة إلى دخل المستخدم. ما يقوم به Vue.js من وراء الكواليس، هو أخذ نسخة عن شيفرة HTML وحفظها داخليًّا على شكل قالب template، بعد ذلك يتم إجراء عملية تصيير (rendering) على نسخة من القالب السابق، باستخدام الموجّهات وتعابير الاستبدال النصي (في حال وجودها ضمن القالب)، ثمّ بعد الانتهاء من التصيير يتم عرض الخرج النهائي على المستخدم. الآن، وعندما يُحدَث أي تغيير جديد في قيمة أي حقل مرتبط من كائن Vue.js، ستُجرى عملية تصيير جديدة على نسخة جديدة من القالب السابق المخزّن داخليًّا، ثم يُعرض الخرج النهائي من جديد على المستخدم. أي كما لو أنّنا أنشأنا ارتباطًا دائمًا بين كائن Vue.js وبين شيفرة HTML. وهذا ما رأيناه فعليًا في التطبيقات البسيطة التي تناولناها في الدرس السابق. ملاحظة من باب التذكير، نستخدم في هذه السلسلة الموقع jsfiddle.net بشكل افتراضي لتشغيل جميع التطبيقات التي نكتبها. ونحتاج بالطبع إلى إدراج ملف إطار العمل Vue.js لكي نستطيع تنفيذ هذه التطبيقات. إذا أردت أن تعرف كيف ذلك، يمكنك العودة إلى الدرس السابق. الوصول إلى البيانات والتوابع من كائنات Vue.js انظر إلى المثال التالي (مثل العادة، أول مقطع يمثّل شيفرة HTML وثاني مقطع يمثّل شيفرة JavaScript): <div id="app"> {{ title }} </div> var app = new Vue({ el: '#app', data: { title: 'Hello Vue!' } }) عندما نستخدم الاستبدال النصي '{{ title }}' كما وسبق أن فعلنا مسبقًا، لا نستخدم الكلمة 'this' قبل 'title' كما هو واضح. في الحقيقة أنّ أي كلمة تُشير إلى حقل (مثل 'title') موجودة ضمن حاضنة مزدوجة، سيتم اعتبارها على أنّها حقل ضمن القسم data في كائن Vue.js الموافق. الآن عند تنفيذ التطبيق السابق سيؤدي إلى ظهور الجملة Hello Vue! كما هو متوقّع. وبالمثل أيضًا، يمكننا في الواقع استخدام تابع مثل displayMessage() ليحقّق نفس الخرج السابق تمامًا، وبنفس الأسلوب تقريبًا. استخدم المثال التالي: <div id="app"> {{ displayMessage() }} </div> var app = new Vue({ el: '#app', data: { title: 'Hello Vue!' }, methods:{ displayMessage: function(){ return this.title; } } }) لاحظ أنّنا استخدمنا هذه المرة التابع displayMessage() ضمن الحاضنة المزدوجة {{ displayMessage() }} ومرّة أخرى لم نستخدم الكلمة this قبل اسم التابع. أي تابع يُكتب بهذه الطريقة سيُعتَبر افتراضيًّا على أنّه تابع موجود ضمن القسم methods من كائن Vue.js. ولكن هذا السلوك الافتراضي لا يسري على شيفرة JavaScript الموجودة ضمن كائن Vue.js حيث يجب استخدام الكلمة this في كلّ مرّة أردنا فيها الوصول إلى أحد أعضاء كائن Vue.js. الربط مع السمات Attributes لا يمكن استخدام تقنية الاستبدال النصي (باستخدام الحاضنة المزدوجة) لإدراج قيم ضمن سمات العناصر. لفهم هذا الموضوع بشكل جيّد، انظر معي إلى المثال التالي: <div id="app"> <p> {{ message }} - <a href='{{link}}'>Hsoub Academy</a> </p> </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!', link: 'https://academy.hsoub.com/' } }) عند تنفيذ التطبيق السابق في jsfiddle.net ستحصل على الخرج التالي: لاحظ أنّه خرج منسّق كما هو متوقّع، ولكن إذا جربت النقر على الرابط لن ينقلك إلى موقع أكاديمية حسوب كما هو متوقّع، في الحقيقة سينقلك هذا الرابط إلى صفحة ضمن نفس موقع jsfiddle.net وهذه الصفحة بالطبع ستكون غير موجودة. السبب في ذلك أنّ تقنية الاستبدال النصّي تُعامِل محتويات الحقل link كنص مجرّد، يمكنك ملاحظة الرابط الناتج بعد النقر: https://fiddle.jshell.net/_display/{{title}} الحل لهذه المشكلة بسيط، ويتمثّل في تجنّب استخدام السمة href بهذا الشكل، إنّما ينبغي استخدام الموجّه v-bind مع الوسيط href على النحو التالي: v-bind:href = 'link' حيث link هو نفسه الحقل الموجود ضمن كائن Vue.js. استبدل بالتعبير السابق السمة href القديمة الموجودة ضمن شيفرة HTML الموجود في المثال السابق، بعد الاستبدال سيصبح شكل شيفرة HTML على النحو التالي: <div id="app"> <p> {{ message }} - <a v-bind:href = 'link'>Hsoub Academy</a> </p> </div> أعد تشغيل التطبيق مرّة أخرى، ستحصل على نفس الخرج، ولكن هذه المرّة إذا نقرت على الرابط ستنتقل إلى موقع أكاديمية حسوب. إذًا كخلاصة على ما سبق، إذا أردت أن تربط مع السمات فعليك استخدام موجّه مع الوسيط المناسب بدلًا من استخدام تقنية الاستبدال النصّي. سنتناول عددًا من هذه الموجّهات خلال هذه السلسلة، ويمكنك دومًا زيارة الصفحة الرسمية لإطار العمل Vue.js على الرابط رابط للاطلاع على جميع الموجّهات المتوفّرة. كتابة شيفرة HTML خام نحتاج في بعض الأحيان إلى كتابة شيفرة HTML خام مباشرةً في الصفحة. قد يتبادر إلى ذهنك أن تستخدم الاستبدال النصي مع الحاضنة المزدوجة، ولكن لن تنفع هذه التقنية في هذه الحالة. تأمّل معي المثال التالي لفهم أفضل حول هذه المشكلة: <div id="app"> {{ raw }} </div> var app = new Vue({ el: '#app', data: { raw: '<ul><li>First Item</li><li>Second Item</li><li>Third Item</li></ul>' } }) الهدف من التطبيق السابق هو عرض قائمة غير مرتّبة عن طريق العنصر 'ul' تُظهر ثلاثة عناصر فقط: First Item و Second Item و Third Item. ولكن عند التنفيذ لن تحصل على ما هو متوقع، ستحصل على الخرج التالي: First Item Second Item Third Item أي أنّك ستحصل على نص عادي دون أن يتعرّف عليه المتصفّح على أنّه شيفرة HTML. يمكن هذه المشكلة بسهولة بإجراء تعديل على شيفرة HTML فقط على النحو التالي: <div id="app"> <p v-html='raw'> </p> </div> التعديل الذي أجريته هو استخدام الموجّه v-html الذي يسمح في حالتنا هذه باستخدام محتويات الحقل raw كشيفرة HTML نظامية وليس مجرّد نص عادي (لاحظ أنّني قد تخلصت من الاستبدال النصّي {{ raw }}. أعد تنفيذ التطبيق، لتحصل على قائمة مُنسّقة بشكل صحيح. التعامل مع الأحداث (Events) للأحدث كما نعلم أهميّة عظيمة في تطوير التطبيقات التي تتفاعل مع المستخدم. تدعم Vue.js الأحداث بشكل جيّد، ولقد تعاملنا في الدرس السابق مع نوعين من الأحداث: حدث الإدخال ضمن مربّع النص، وحدث النقر بزر الفأرة على عنصر HTML. سنتناول في هذا الدرس الأحداث بشيء من التفصيل، حيث سنتعلّم كيف ننصت إلى أحداث الفأرة، بالإضافة إلى الإنصات إلى أحداث لوحة المفاتيح. الإنصات إلى أحداث الفأرة نبدأ بالتعامل مع أحداث الفأرة، لتُنعش ذاكرتك، انظر معي إلى التطبيق البسيط الموجود في الدرس السابق: <div id="app"> <input type='text' v-on:input="updateInfo"/> {{ message }} </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }, methods:{ updateInfo:function(event){ this.message = event.target.value; } } }) سبق وأن ذكرنا في الدرس السابق أنّه يتم الإنصات إلى أي حدث ضمن عنصر ما باستخدام الموجّهv-on، حيث يتم تغيير الوسيط المُمرّر لهذا الموجّه بتغيّر نوع الحدث المراد الإنصات له. بعد تحديد نوع الوسيط المراد تمريره للموجّه v-on يتم تحديد التابع الذي سيستجيب (سيعالج) هذا الحدث، وهو عبارة عن تابع ضمن القسم methods ضمن كائن Vue.js يتم استدعاؤه عند وقوع الحدث. بالنسبة للتابع المعالج للحدث (التابع updateInfo في المثال السابق) سيتم توليد كائن يحتوي على معلومات مهمّة حول الحدث الذي وقع، ويتم تمرير هذا الكائن بشكل تلقائي إلى التابع المعالج للحدث. على العموم، يمكن الاستغناء عن هذا السلوك التلقائي، وتمرير قيمة كيفيّة للتابع المعالج للحدث. انظر معي إلى المثال التالي: <div id='app'> <button v-on:click="increase(2)">Increase!</button> <p>{{counter}}</p> </div> var app = new Vue({ el: '#app', data: { counter: 0 }, methods:{ increase: function(value){ this.counter += value; } } }) الشيفرة السابقة مألوفة، مع ملاحظتين جديدتين. الأولى أنّنا لم نكتفي بكتابة اسم التابع المعالج للحدث ضمن الموجّه v-on:click فحسب، إنّما قد مرّرنا القيمة 2 كوسيط لهذا التابع على الشكل: increase(2) (لاحظ شيفرة HTML). بالمقابل، إذا تأملت شيفرة JavaScript ضمن تعريف التابع increase في القسم methods، فستلاحظ أنّنا نعامل الوسيط value كمتغيّر يحمل قيمة عددية وليس ككائن يحمل معلومات حول الحدث الذي وقع. أي أنّنا قد استطعنا تغيير السلوك الإفتراضي لعمليّة استدعاء التابع المعالج. بالنسبة للمثال السابق، فكما هو واضح، يعمل التطبيق على زيادة قيمة المتغيّر counter بمقدار القيمة value (في مثالنا السابق ستكون تساوي 2) في كل مرّة يتم فيها نقر الزر. في بعض الحالات قد نحتاج إلى تمرير كائن الحدث بالإضافة إلى تمرير قيمة كيفيّة بنفس الوقت. تدعم Vue.js هذا الأمر ببساطة من خلال تمرير الكلمة المحجوزة $event إلى التابع المعالج بالإضافة إلى القيمة الكيفية المراد تمريرها. إذا أردنا تطبيق ذلك على المثال الأخير فسيصبح تعريف الزر button على النحو التالي: <button v-on:click="increase(2, $event)">Increase!</button> وبالنسبة لتعريف التابع المعالج ضمن القسم methods فسيصبح على النحو التالي: increase: function(value, event){ this.counter += value; } أي مجرّد إضافة وسيط آخر. التعديل على كيفية الاستجابة للأحداث نحتاج في بعض الأحيان أن نُعدّل على كيفيّة الاستجابة للأحداث، فربما نحتاج في وقت ما إلى إيقاف الاستجابة لحدث ما لأحد العناصر دونًا عن العناصر الأخرى في HTML. لكي أضرب لك مثالًا جميلًا حول هذا الأمر، اسمح لي أولًا أن أقدّم لك حدث حركة الفأرة mousemove. يُولّد هذا الحدث عند مرور مؤشّر الفأرة فوق عنصر ما، ويتم استخدامه كما هو متوقّع مع الموجّه v-on. انظر إلى المثال البسيط التالي: <div id="app"> <p v-on:mousemove='updateCoordinates'> Mouse cursor at: ({{x}}, {{y}}) </p> </div> var app = new Vue({ el: '#app', data: { x:0, y:0 }, methods:{ updateCoordinates:function(event){ this.x = event.clientX; this.y = event.clientY; } } }) جرّب تنفيذ التطبيق البسيط السابق، ثمّ حرّك الفأرة فوق العنصر الوحيد الظاهر أمامك. ستحصل على خرج شبيه بما يلي: الجديد هنا هو استخدام الموجّه v-on:mousemove حيث أسندنا إليه التابع المعالج updateCoordinates المعرَّف بطبيعة الحال ضمن القسم methods في كائن Vue.js. لاحظ معي أيضًا كيف نحصل على الإحداثيات الحالية لمؤشّر الفأرة (الفاصلة x والتراتيب y) ضمن التابع updateCoordinates: this.x = event.clientX; this.y = event.clientY; الآن إذا أردنا أن ننشئ منطقة "ميتة" (ضمن عنصر span مثلًا) ضمن عنصر p الذي يعرض الإحداثيات، بحيث لايؤدّي مرور مؤشّر الفأرة فوق هذه المنطقة إلى توليد الحدث mousemove، فينبغي علينا عندها التعديل على الحدث mousemove كما يلي (سألوّن التعديلات الإضافية بالأخضر): <div id="app"> <p v-on:mousemove='updateCoordinates'> Mouse cursor at: ({{x}}, {{y}}) - <span v-on:mousemove='uncoveredArea'>Uncovered Area</span> </p> </div> var app = new Vue({ el: '#app', data: { x:0, y:0 }, methods:{ updateCoordinates:function(event){ this.x = event.clientX; this.y = event.clientY; }, uncoveredArea: function(event){ event.stopPropagation(); } } }) أضفت الموجّه v-on:mousemove إلى العنصر span وأسندت المعالج uncoveredArea له. بالنسبة للتابع uncoveredArea فقد أجريت تعديل على الحدث من خلال استدعاء التابع stopPropagation() من الكائن event. المعنى الحرفي لهذا التابع هو "إيقاف الانتشار" أي أنّنا سنمنع الإستجابة لهذا الحدث عندما يمر مؤشّر الفأرة فوق العنصر span. جرّب تنفيذ التطبيق السابق، ولاحظ التغيير الذي سيحدث عندما يمر مؤشّر الفأرة فوق عنصر span. إذا أردت الإحساس بالفرق، يمكنك أن تحذف التعليمة event.stopPropagation() ثم أعد تنفيذ التطبيق مرّة أخرى، لترى كيف أنّ الإحداثيات ستتغيّر عندما يمر مؤشّر الفأرة فوق عنصر span هذه المرة. يمكن استخدام صيغة أبسط للتعديل على الأحداث، فمن الممكن حذف التابع uncoveredArea بالكامل من قسم methods، والاكتفاء بالقسم الخاص بالموجّه على النحو التالي: v-on:mousemove.stop='' أي أنّنا قد استغنينا عن الشيفرة اللازمة لإيقاف انتشار الحدث mousemove. نسمي .stop هنا بمعدِّل الحدث (event modifiers). هناك عدّة معدّلات أحداث مفيدة سنستعرض بعضها منها خلال مسيرتنا في هذه السلسلة. الإنصات إلى أحداث لوحة المفاتيح يمكننا أحيانًا أن نحتاج إلى الإنصات أيضًا إلى الأحداث الناشئة من لوحة المفاتيح. والأسلوب المتبع هنا، يشبه إلى حدّ كبير ما كنّا نفعله مع أحداث الفأرة. إذا أردنا مثلًا الإنصات إلى حدث تحرير مفتاح من لوحة المفاتيح يمكن أن نستخدم الوسيط 'keyup' للموجّه v-on على النحو التالي: v-on:keyup='methodName' حيث 'methodName' هو اسم التابع المعالج للحدث 'keyup' والذي يجب أن يُوضَع ضمن القسم methods. دعنا الآن نوظّف ذلك في مثال بسيط: <div id="app"> <input type='text' v-on:keyup='keyIsUp' /> <p> {{message}} </p> </div> var app = new Vue({ el: '#app', data: { message: '' }, methods: { keyIsUp: function(event) { this.message = event.target.value; } } }) يعمل هذا التطبيق البسيط على تحديث الحقل 'message' كلّما تمّ تحرير مفتاح من لوحة المفاتيح، وبالتالي سيؤدّي ذلك إلى تحديث محتويات عنصر 'p' ضمن الواجهة. ولكن دعنا نتساءل، ماذا لو أردنا أن يستجيب المعالج 'keyIsUp' كلّما حُرِّر مفتاح المسافة (space) فقط، وليس عند أيّ مفتاح يُحرِّره المستخدم. الجواب ببساطة، هو في استخدام معدّل الحدث '.space' بعد 'keyup'. أضف فقط الكلمة '.space' إلى 'keyup' إلى المثال السابق. أي على النحو التالي: v-on:keyup.space = 'keyIsUp' أعد تنفيذ التطبيق لترى أنّ محتويات عنصر 'p' أصبحت لا تُحدَّث إلّا بعد تحرير المفتاح space. يوجد بالطبع العديد من المعدّلات التي تمثّل جميع المفاتيح على لوحة المفاتيح، فهناك مثلًا 'enter' و 'tab' و 'up' لمفتاح السهم العلوي، و 'down' لمفتاح السهم السفلي وهكذا. لمعدّلات أحداث لوحة المفاتيح الكثير من الفوائد، يتمثّل أبسطها في إرسال المحتوى الذي أدخله المستخدم بمجرد ضغطه للمفتاح Enter، أو إرسال البيانات مباشرةً بينما يكتبها المستخدم للحصول على مقترحات أثناء عملية الكتابة (كما يفعل محرّك البحث غوغل أثناء كتابة المستخدم للمفردات المراد البحث عنها). وغيرها الكثير من الاستخدامات. استخدام الربط ثنائي الاتجاه في معظم الأمثلة السابقة عمدنا إلى استخدام ربط باتجاه واحد، من الشيفرة إلى عنصر HTML. وفي بعض الحالات استطعنا أن نعكس هذا الأمر. أي استطعنا تعديل قيمة الحقل عن طريق الانصات إلى حدث الإدخال v-on:input وبالتالي معالج حدث مخصّص لهذه الغاية. ولكن توجد طريقة مباشرة وسهلة لإيجاد ربط ثنائي الاتجاه فعلي في Vue.js وذلك باستخدام الموجّه v-model وبدون الحاجة إلى معالج حدث، كمال في المثال التالي: <div id="app"> <input type='text' v-model='name'/> {{ name }} </div> var app = new Vue({ el: '#app', data: { name: 'Hello Vue!' } }) التطبيق السابق بسيط، وهو يعمل على إجراء ربط ثنائي الاتجاه بين الحقل name وبين عنصر مربّع النص، أي سيكون هناك ارتباط آني بين الحقل name وبين عنصر مربّع النص، فإذا حدث تغيّر لأحدهما سينعكس مباشرةً على الآخر. نفّذ التطبيق السابق وسترى مباشرةً الخرج التالي: لاحظ كيف أنّ محتوى مربّع النص قد تمّت تعبئته تلقائيًا بقيمة الحقل name، وبالمثل إذا حاولت الآن كتابة أي شيء ضمن مربّع النص سيتم تعديل قيمة الحقل name فورًا وفقًا له، وبالتالي سيُعدّل محتوى النص الموجود في الطرف الأيمن بسبب وجود الاستبدال النصي {{name}}. ختامًا تعلّمنا في هذا الدرس كيفيّة التعامل مع DOM، حيث تحدثنا عن القوالب، وكيف نصل إلى البيانات والتوابع بشيء من التفصيل، كما تعلّمنا كيفيّة الربط مع السمات، والتعامل مع الأحداث، وتعلّمنا كيفية الربط ثنائي الاتجاه الذي يسمح لنا بمزامنة البيانات بالاتجاهين. تمارين داعمة تمرين 1 يُطلب في هذا التمرين تطوير تطبيق الآلة الحاسبة البسيط الذي بنيناه في الدرس السابق. بحيث يسمح التطبيق الجديد بإجراء العمليات الحسابية الأربع بدلًا من عملية الجمع الوحيدة التي كان يدعمها التطبيق السابق. أقترح الواجهة التالية للتطبيق: لاحظ أنّني قد استخدمت عنصر 'select'، يمكنك استخدام أي طريقة أخرى لاختيار العمليات الحسابية الأربع. من الضروري أن يُوجِد التطبيق الناتج النهائي إذا حدث أحد الأمرين التاليين: تغيير قيمة أحد المعاملين على طرفي العملية الحسابية. تغيير العملية الحسابية عن طريق القائمة المنسدلة. أرجو أن يتمكن التطبيق من تمييز حالة القسمة على صفر، وإظهار رسالة مناسبة للمستخدم. تمرين 2 يُطلب في هذا التمرين إنشاء تطبيق يقدّم للمستخدم مقترحات نصيّة بينما يكتب المستخدم ضمن مربّع نص. سنحاكي عملية الاتصال مع خادوم بعيد عن طريق استخدام مصفوفة نصيّة ضمن الشيفرة. وسأعتبر أنّ المستخدم يحاول أن يُدخل اسم دولة عربية، فتظهر قائمة المقترحات بالأسفل بينما تتم عملية الإدخال. كما في الشكل التالي: ستحتاج بالطبع إلى التعامل مع أحداث لوحة المفاتيح. ولكي يكون الأمر أكثر سهولة بالنسبة إليك. يمكنك استخدام المصفوفة الجاهزة التالية كمصدر للبيانات التي يُفترَض أن تكون قادمة من الخادوم: [ 'السعودية', 'البحرين', 'مصر', 'السودان', 'ليبيا', 'الجزائر', 'المغرب', 'تونس', 'موريتانيا', 'العراق', 'سوريا', 'لبنان', 'قطر', 'الإمارات', 'الصومال', 'جزر القمر', 'الكويت', 'سلطنة عُمان', 'الأردن', 'اليمن', 'فلسطين' ] اقرأ أيضًا المقال التالي: الموجهات الشرطية والتكرارية في Vue.js المقال السابق: مقدمة إلى Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js