البحث في الموقع
المحتوى عن 'alpine.js'.
-
بعد أن تعرفنا على إطار العمل Alpine.js واستخدمناه لإنشاء بعض الأمثلة البسيطة حيث تعرفنا على معظم الخصائص التي يتيحها الإطار من موجهات وتوابع وما إلى ذلك، سنقوم في هذا المقال بإنشاء تطبيق شامل عبارة عن تطبيق ملاحظات يتيح إنشاء ملاحظات، تعديلها وحذفها، وسنستخدم في هذا التطبيق التابع Alpine.store وهو عبارة عن ميزة في Alpine لإدارة الحالة العامة، يُمكنك التفكير في الأمر على أنه عبارة عن مخزن مشترك بين المكونات بحيث يمكن لأي مكون في الصفحة الوصول لهذا المخزن واستعماله بسهولة عبر الخاصية store$. سنستعمل أيضاً الموجه x-for وهو عبارة عن موجه للعبور على عناصر مصفوفة أو قائمة (سنستخدمه لعرض قائمة الملاحظات)، سنبدأ في البداية باستخدام مصفوفة لتخزين الملاحظات ثم في الأخير سنستخدم الذاكرة المحلية الخاصة بالمتصفح لتخزين البيانات ومزامنتها حتى لا نفقدها بعد كل تحديث للصفحة. تصميم التطبيق سنستخدم في هذا التطبيق قالب بسيط جاهز مبني على مكتبة TailwindCSS، لن تكون بحاجة إلى معرفة مسبقة بالمكتبة TailwindCSS لبناء التطبيق كما يُمكنك الاعتماد على قالب خاص بك فالشيفرات التي سنكتبها لن تتغير، يُمكنك تحميل ملف القالب design.html من رابط المشروع المرفق في نهاية المقال، أو بإمكانك إنشاء ملف HTML فارغ ثم نسخ الشيفرة التالية إليه: <!DOCTYPE html> <html lang="ar" dir="rtl"> <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="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic:wght@500;700&display=swap" rel="stylesheet"> <title>تطبيق ملاحظات</title> <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet"> <style> body { font-family: 'Noto Naskh Arabic', serif; } </style> </head> <body> <div class="h-screen overflow-hidden bg-gray-100 flex flex-col"> <main class="min-w-0 flex-1 border-t border-gray-200 flex min-h-0 overflow-hidden"> <div class="min-h-0 flex-1 overflow-y-scroll bg-white bg-white h-full w-full flex"> <div class="p-6 w-full flex flex-col"> <input type="text" class="text-lg font-medium text-gray-900 w-full mb-6" placeholder="ملاحظة بدون عنوان"> <textarea class="w-full mb-6 flex-1 outline-none" placeholder="إبدأ الكتابة ..." autofocus></textarea> <div> <button class="text-sm text-gray-900">حذف الملاحظة</button> </div> </div> </div> <aside class="block flex-shrink-0 order-first h-full relative flex flex-col w-96 border-r border-gray-200 bg-gray-100"> <div class="flex-shrink-0 h-16 bg-white px-6 flex flex-col justify-center"> <div class="flex justify-between space-x-3"> <div class="flex items-baseline"> <h2 class="text-lg font-bold text-gray-900 mr-3"> ملاحظاتي </h2> <p class="text-sm font-medium text-gray-500"></p> </div> <button class="text-sm">ملاحظة جديدة</button> </div> </div> <nav class="min-h-0 flex-1 overflow-y-auto"> <ul class="border-b border-gray-200 divide-y divide-gray-200"> <li class="relative bg-white py-5 px-6 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"> <div class="flex justify-between space-x-3"> <a href="#" class="block focus:outline-none"> <span class="absolute inset-0"></span> <p class="text-sm text-gray-500 truncate">عُنوان الملاحظة</p> </a> <time class="flex-shrink-0 whitespace-nowrap text-sm text-gray-500">00:00</time> </div> <div class="mt-1"> <p class="text-sm text-gray-600">محتوى الملاحظة</p> </div> </li> </ul> </nav> </aside> </main> </div> </body> </html> بعد فتح الملف على المتصفح ستحصل على الشكل التالي: يتكون القالب من قسمين، قائمة جانبية تحتوي على الملاحظات الموجودة والتي أنشأها المستخدم، زر إنشاء ملاحظة جديدة، عُنصر الملاحظة يتكون من عنوان ومحتوى وتاريخ آخر تعديل، القسم الثاني عبارة عن عرض للملاحظة الفعالة يتكون من حقل إدخال للعُنوان وحقل للمحتوى وفي الأسفل زر حذف الملاحظة. شرح بناء التطبيق سنعتمد في بناء التطبيق على الملاحظة الفعالة بحيث عند فتح الصفحة لأول مرة سيتم مباشرة إنشاء ملاحظة بقيم فارغة وعرضها في قسم العرض ليبدأ المستخدم في كتابة البيانات مباشرة وسيتم حفظ الملاحظة تلقائيًا مع الكتابة، أما إن كان للمستخدم ملاحظات سابقة سنجعل آخر ملاحظة قام المستخدم بتعديلها هي الملاحظة الفعالة وسنقوم بفتحها في قسم العرض لتعديلها، في قسم عرض الملاحظات سنعتمد على حقل آخر تعديل حتى نقوم بترتيب الملاحظات وبعد كل تعديل على الملاحظة الفعالة نُعدل على حقل آخر تعديل الخاص بتلك الملاحظة. تضمين Alpine.js: إنشاء store وإنشاء ملاحظة بدون عنوان نبدأ بتضمين Alpine في قسم head عبر شبكة CDN بالشكل التالي: <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> ثم نقوم بإنشاء مخزن store نُسميه notes دلالة على الملاحظات، لكن قبل إنشائه ننتظر تحميل Alpine ولتحقيق ذلك نستمع إلى حدث alpine:init كما هو موضح في التوثيق: <script> document.addEventListener('alpine:init', () => { // سنقوم هنا بكتابة معظم ميزات التطبيق }) </script> لإنشاء مخزن store نقوم باستدعاء التابع store بالشكل التالي: document.addEventListener('alpine:init', () => { Alpine.store('notes', { data: [], }) }) بهذا الشكل أنشأنا مخزنًا بالاسم notes وبداخله أنشأنا مصفوفة data سنحفظ بداخلها الملاحظات. الوسيط الثاني للتابع store عبارة عن كائن JavaScript، كما ذكرنا سابقًا أننا سنقوم بإنشاء ملاحظة ونقوم بتفعيلها في حالة لم يكن للمستخدم ملاحظات سابقة، وسنعتمد على التابع init لأن Alpine ستقوم بتنفيذه تلقائيًا، يُمكنك البدء بالتالي: <script> document.addEventListener('alpine:init', () => { Alpine.store('notes', { data: [], init() { console.log('Started!') }, }) }) </script> ستجد عبارة Started في طرفية المتصفح console بعد فتح الصفحة على المتصفح، الآن سنُعدل التابع init بالشكل التالي: init() { // في حالة كان للمستخدم ملاحظات لن نقوم بإنشاء ملاحظة if (this.data.length) { // سنقوم بجعل أول ملاحظة في المصفوفة هي الملاحظة الفعالة } else { this.createNote() // إستدعينا تابع لإنشاء ملاحظة } }, ثم نُعرف التابع createNote بالشكل التالي: createNote() { let id = Date.now() this.data = [{id, title: '', body: ''}, ...this.data] }, جعلنا للملاحظة مُعرف فريد عبارة فقط عن الوقت الحالي أي وقت إنشاء الملاحظة لجعل الأمر بسيط، واستخدمنا ما يُسمى بالإسناد عبر التفكيك destructuring assignment. الآن عند فتح الصفحة سيتم إنشاء ملاحظة بقيم فارغة ومُعرف فريد خاص بها، يُمكنك التأكد من ذلك عبر إنشاء عُنصر span بالشكل التالي في صفحة html: <span x-data="{ init() { console.log(this.$store.notes.data) } }"></span> وستجد في الطرفية console أنه تم طباعة كائن data وستجد الملاحظة هناك. تعريف الملاحظة الفعالة والوصول لها في قسم العرض سنقوم في كائن store الخاص بنا بإنشاء خاصية بالاسم currentNoteId دلالة على مُعرف الملاحظة الفعالة: currentNoteId: null, ثم في التابع createNote نُسند لها قيمة id الملاحظة التي أنشأناها: this.currentNoteId = id نقوم بإنشاء جالب getter لجلب كائن الملاحظة الفعالة حتى نستطيع استخدامه في المكونات الخاصة بنا بالأعلى: get current() { return this.data.find(n => n.id === this.currentNoteId) }, وهو عبارة عن جالب getter يبحث في المصفوفة data عن الملاحظة عبر المُعرف الخاص بها ويقوم بإرجاع الملاحظة التي يطابق مُعرفها قيمة currentNoteId. ستُصبح شيفرات JavaScript بالشكل التالي: document.addEventListener('alpine:init', () => { Alpine.store('notes', { data: [], currentNoteId: null, get current() { return this.data.find(n => n.id === this.currentNoteId) }, init() { // في حالة كان للمستخدم ملاحظات لن نقوم بإنشاء ملاحظة if (this.data.length) { // سنقوم بجعل أول ملاحظة في المصفوفة هي الملاحظة الفعالة } else { this.createNote() } }, createNote() { let id = Date.now() this.data = [{id, title: '', body: ''}, ...this.data] this.currentNoteId = id } }) }) سنقوم الآن بعرض الملاحظة الفعالة في قسم العرض في الصفحة، في العُنصر التالي بالضبط: <div class="p-6 w-full flex flex-col" x-data> <span x-text="$store.notes.current.title"></span> <span x-text="$store.notes.current.body"></span> <input type="text" class="text-lg font-medium text-gray-900 w-full mb-6" x-model="$store.notes.current.title" placeholder="ملاحظة بدون عنوان"> <textarea class="w-full mb-6 flex-1 outline-none" x-model="$store.notes.current.body" placeholder="إبدأ الكتابة ..." autofocus></textarea> <div> <button class="text-sm text-gray-900">حذف الملاحظة</button> </div> </div> لاحظ يجب تحديد الموجه x-data للعُنصر لجعله مكون Alpine وحتى نستطيع استخدام خصائص Alpine بداخله، في كلا حقلي الإدخال استخدمنا الموجه x-model لربط قيمة حقل الإدخال بالخاصية الموافقة في الملاحظة الفعالة واستخدمنا الخاصية store$ في Alpine للوصول إلى بيانات الملاحظة الفعالة عبر استدعاء الجالب getter الذي أنشأناه. عناصر span التي قمنا بإضافتها فقط للتجربة ومعاينة أن القيمة تتغير بالكتابة. جرب الكتابة في الحقول للتأكد من ذلك. عرض قائمة الملاحظات باستخدام x-for لعرض الملاحظات في القائمة الجانبية سنقوم بوضع بعض البيانات التجريبية في المصفوفة data بالشكل التالي: data: [ {id: 1, title: 'ملاحظتي الأولى', body: 'محتوى الملاحظة الأولى'}, {id: 2, title: 'ملاحظتي الثانية', body: 'محتوى الملاحظة الثانية'}, ], لكن قبل أن نقوم بعرض الملاحظات سنُحدد مُعرف الملاحظة الفعالة، حيث أننا ذكرنا سابقًا في حالة كان للمستخدم ملاحظات سابقة سنجعل الملاحظة الفعالة هي أول ملاحظة في المصفوفة (حاليًا فقط، سنقوم لاحقًا بترتيب الملاحظات حسب تاريخ آخر تعديل حتى تظهر الملاحظات مرتبة وتكون الملاحظة الفعالة هي آخر ملاحظة قام المستخدم بتعديلها). سنكتب في التابع init وفي الجزء المخصص لذلك ما يلي: if (this.data.length) { this.setCurrentNoteByIndex(0) } ونقوم بتعريف التابع setCurrentNoteByIndex: setCurrentNoteByIndex(index) { this.currentNoteId = this.data[index].id } وكما تلاحظ أن ما قمنا به هو بالضبط ما شرحته في الأعلى، مررنا الفهرس 0 لتحديد أول ملاحظة في المصفوفة ثم أسندنا مُعرفها للخاصية currentNoteId. نأتي الآن لعرض الملاحظات في القائمة الجانبية، وبالضبط عُنصر ul في الصفحة، سنستخدم الموجه x-data لجعله مكون Alpine ثم نستخدم x-for للمرور على عناصر المصفوفة data: <ul class="border-b border-gray-200 divide-y divide-gray-200" x-data> <template x-for="note in $store.notes.data" :key="note.id"> <li class="relative bg-white py-5 px-6 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"> <div class="flex justify-between space-x-3"> <a href="#" class="block focus:outline-none"> <span class="absolute inset-0"></span> <p class="text-sm text-gray-500 truncate" x-text="note.title || 'ملاحظة بدون عنوان'"></p> </a> <time class="flex-shrink-0 whitespace-nowrap text-sm text-gray-500">00:00</time> </div> <div class="mt-1"> <p class="text-sm text-gray-600" x-text="note.body.substring(0, Math.min(100, note.body.length))"></p> </div> </li> </template> </ul> استخدمنا الموجه x-for للمرور على عناصر المصفوفة data، كما هو موضح في التوثيق حتى نستطيع استخدام x-for يجب أن نُعرفها بداخل عُنصر template وهذا العُنصر يجب أن يكون بداخله عُنصر جذر واحد. أيضًا من المهم جدًا تحديد مفاتيح فريدة لكل تكرار عبر الخاصية key وذلك لأن القائمة ستتحدث فقد يتغير ترتيب الملاحظات أثناء التصفح إذا تغير تاريخ التعديل، وبدون المفاتيح الفريدة ستجد Alpine صعوبة في تحديث شجرة DOM وقد لا تحصل على نفس النتيجة التي ترغب فيها. بداخل العناصر استخدمنا الموجه x-text لعرض بيانات الملاحظة، في المُحتوى استخدمنا التابع substring حتى لا يتجاوز عدد المحارف 100 محرف. ستكون النتيجة: وبطبيعة الحال عند التعديل في قسم العرض ستتحدث بيانات الملاحظة الفعالة وهي الملاحظة الأولى. إضافة تاريخ آخر تعديل وتغييره بعد كل تعديل على الملاحظة الفعالة نقوم بإنشاء تابع جديد بالاسم touchCurrentNote: touchCurrentNote() { this.current.lastEdited = Date.now() }, ثم نستدعي التابع عند إنشاء ملاحظة جديدة حيث سيتم تهيئة حقل lastEdited بالتاريخ الحالي: createNote() { let id = Date.now() this.data = [{id, title: '', body: '', lastEdited: 0}, ...this.data] this.currentNoteId = id this.touchCurrentNote() }, ثم نقوم أيضًا باستدعائه بعد كل تعديل على الحقول في قسم العرض بالاستماع إلى حدث keyup في حقول الإدخال: <input type="text" class="text-lg font-medium text-gray-900 w-full mb-6" x-model="$store.notes.current.title" placeholder="ملاحظة بدون عنوان" @keyup="$store.notes.touchCurrentNote()"> <textarea class="w-full mb-6 flex-1 outline-none" x-model="$store.notes.current.body" @keyup="$store.notes.touchCurrentNote()" placeholder="إبدأ الكتابة ..." autofocus></textarea> قد تكون الحالة مناسبة لاستعمال المُحدد debounce لفرض بعض التأخير في تنفيذ المعالجة بعد إطلاق الحدث، حتى لا يتم تنفيذ التابع touchCurrentNote بعد كل ضغطة زر لحظيًا: <input type="text" class="text-lg font-medium text-gray-900 w-full mb-6" x-model="$store.notes.current.title" placeholder="ملاحظة بدون عنوان" @keyup.debounce.200ms="$store.notes.touchCurrentNote()"> <textarea class="w-full mb-6 flex-1 outline-none" x-model="$store.notes.current.body" @keyup.debounce.200ms="$store.notes.touchCurrentNote()" placeholder="إبدأ الكتابة ..." autofocus></textarea> سنقوم بإنشاء تابع يقوم بتنسيق تاريخ التعديل بالشكل 18:25 أي الساعة والدقيقة الموافقة: getLastEditedFormatted(note) { return `${new Date(note.lastEdited).getHours().toString().padStart(2, '0')}:${new Date(note.lastEdited).getMinutes().toString().padStart(2, '0')}` }, يستقبل هذا التابع معامل note لأننا سنستخدمه بداخل الحلقة لعرض التاريخ ونُمرر له الملاحظة التي يتم المرور عليها: <time class="flex-shrink-0 whitespace-nowrap text-sm text-gray-500" x-text="$store.notes.getLastEditedFormatted(note)"></time> يُمكنك تنسيق التاريخ بالشكل الذي تريد، جرب التعديل على الملاحظة الفعالة وسيستجيب عُنصر الوقت للتعديل. ترتيب الملاحظات حسب تاريخ آخر تعديل نقوم بإنشاء جالب getter لجلب قائمة الملاحظات مرتبة بالشكل التالي: get orderedByLastEdited() { return this.data.sort((a, b) => b.lastEdited - a.lastEdited) }, وهي طريقة في JavaScript لترتيب مصفوفة. ثم نستخدم الجالب getter الذي أنشأناه orderedByLastEdited في التابع setCurrentNoteByIndex لتحديد مُعرف الملاحظة الفعالة من المصفوفة المرتبة بدل المصفوفة الأصلية بالشكل التالي: setCurrentNoteByIndex(index) { this.currentNoteId = this.orderedByLastEdited[index].id }, ثم للتجربة نضع البيانات الاختبارية بالشكل التالي: data: [ {id: 1, title: 'ملاحظتي الأولى', body: 'محتوى الملاحظة الأولى', lastEdited: 1}, {id: 2, title: 'ملاحظتي الثانية', body: 'محتوى الملاحظة الثانية', lastEdited: 2}, ], سنقوم أيضًا باستخدام المصفوفة المرتبة في الحلقة بدل المصفوفة الأصلية باستدعاء الجالب getter الذي أنشأناه orderedByLastEdited: <template x-for="note in $store.notes.orderedByLastEdited" :key="note.id"> بهذا الشكل من المفترض أن الملاحظة الثانية ستظهر أولاً في القائمة. تبديل الملاحظة الفعالة من القائمة وتفعيل زر إنشاء ملاحظة جديدة لتبديل الملاحظة الفعالة نستمع إلى حدث النقر على عُنصر الملاحظة ونُغير currentNoteId إلى مُعرف الملاحظة التي تم الضغط عليها بالحلقة for: <a href="#" class="block focus:outline-none" @click.prevent="$store.notes.currentNoteId = note.id"> <span class="absolute inset-0"></span> <p class="text-sm text-gray-500 truncate" x-text="note.title || 'ملاحظة بدون عنوان'"></p> </a> الآن إذا جربت، بعد الضغط على أي ملاحظة سيتم تحديدها وعرضها في قسم العرض للتعديل عليها، بمجرد تعديل العنوان أو المحتوى سيتم دفعها لتُصبح في أعلى القائمة وهذا لأن تاريخ التعديل تغير وبالتالي الترتيب سيتغير ويتم تحديث شجرة DOM تلقائيًا. يُمكنك الآن إفراغ مصفوفة الملاحظات من البيانات الاختبارية: data: [], ثم نقوم بتفعيل زر إضافة ملاحظة جديدة، لقد قمنا بالفعل بكتابة منطق إضافة ملاحظة سنستمع إلى حدث النقر على الزر ونستدعي التابع createNote: نقوم أولا بإضافة الموجه x-data لتحديد العُنصر كمُكون Alpine ثم نستخدم click@ في الزر: <div class="flex-shrink-0 h-16 bg-white px-6 flex flex-col justify-center" x-data> <div class="flex justify-between space-x-3"> <div class="flex items-baseline"> <h2 class="text-lg font-bold text-gray-900 mr-3"> ملاحظاتي </h2> <p class="text-sm font-medium text-gray-500"></p> </div> <button class="text-sm" @click="$store.notes.createNote()">ملاحظة جديدة</button> </div> </div> سنقوم بإضافة عداد بجانب الكلمة ملاحظاتي لعرض عدد الملاحظات: <h2 class="text-lg font-bold text-gray-900 mr-3"> ملاحظاتي (<span x-text="$store.notes.data.length"></span>) </h2> مزامنة الملاحظات مع الذاكرة المحلية بالمتصفح سنقوم بإنشاء تابع بالاسم persistNotes يقوم بحفظ الملاحظات في الذاكرة المحلية بالشكل التالي: persistNotes() { localStorage.setItem('notes', JSON.stringify(this.data)) }, سنستدعي هذا التابع عند التعديل على الملاحظة الفعالة بجانب touchCurrentNote، يمكنك استدعاء التابعين مباشرةً في العنصر لكننا سنُنشئ تابع بالاسم currentNoteUpdated: currentNoteUpdated() { this.touchCurrentNote() this.persistNotes() }, لاحظ أننا استدعينا كلا التابعين الآن عند التعديل في الحقول وعند الاستماع إلى حدث keyup@ سنستدعي هذا التابع currentNoteUpdated: <input type="text" class="text-lg font-medium text-gray-900 w-full mb-6" x-model="$store.notes.current.title" placeholder="ملاحظة بدون عنوان" @keyup.debounce.200ms="$store.notes.currentNoteUpdated()"> <textarea class="w-full mb-6 flex-1 outline-none" x-model="$store.notes.current.body" @keyup.debounce.200ms="$store.notes.currentNoteUpdated()" placeholder="إبدأ الكتابة ..." autofocus></textarea> نحتاج أيضاً إلى استدعاء التابع persistNotes بداخل التابع createNote حتى يتم حفظ الحالة بعد إنشاء ملاحظة جديدة: createNote() { let id = Date.now() this.data = [{id, title: '', body: '', lastEdited: 0}, ...this.data] this.currentNoteId = id this.touchCurrentNote() this.persistNotes() }, الآن سيتم حفظ الملاحظات لكن نحتاج إلى تهيئة المصفوفة data فإن كانت هناك بيانات في الذاكرة المحلية قمنا بجلبها وإلا نهيئ data بمصفوفة فارغة: data: JSON.parse(localStorage.getItem('notes')) || [], يُمكنك الآن إنشاء ملاحظاتك والتعديل عليها وعند تحديث الصفحة ستبقى موجودة. تفعيل زر الحذف آخر شيء سنقوم به هو تفعيل زر حذف ملاحظة، لتحقيق ذلك سنقوم بإنشاء تابع بالاسم deleteNote يستقبل مُعرف الملاحظة التي نريد حذفها، بداخلها نتحقق: إذا كانت الملاحظة التي يريد المستخدم حذفها هي آخر ملاحظة في القائمة فنقوم في هذه الحالة بإنشاء ملاحظة جديدة قبل حذف الملاحظة الأخيرة حتى تبقى دائماً ملاحظة فعالة في التطبيق، نحذف الملاحظة من المصفوفة data، ثم نستدعي persistNotes لحفظ الحالة وأخيراً نستدعي setCurrentNoteByIndex لإعادة تحديد الملاحظة الفعالة: deleteNote(id) { if (this.data.length === 1) { this.createNote() } this.data = this.data.filter(n => n.id !== id) this.persistNotes() this.setCurrentNoteByIndex(0) } نستدعي التابع الذي أنشأناه في زر الحذف عبر الاستماع إلى حدث الضغط على الزر: <button class="text-sm text-gray-900" @click="if (window.confirm('تأكيد الحذف؟')) {$store.notes.deleteNote($store.notes.currentNoteId)}" >حذف الملاحظة</button> قمنا بعرض رسالة تأكيد للمستخدم حتى يؤكد الحذف، واستدعينا التابع deleteNote. إليك الملف النهائي الخاص بالمشروع project-20240112T115021Z-001.zip خاتمة في نهاية رحلتنا في برمجة تطبيق ملاحظات بسيط باستخدام إطار العمل Alpine.js، نجد أن هذا الإطار يعد أداة قوية وسهلة الاستخدام لتحقيق تجارب مستخدم ديناميكية على الويب. مكنتنا ميزات Alpine.js من بناء تطبيق ملاحظات بسيط بكود نظيف وقليل التعقيد، مع القدرة على التفاعل المباشر مع العناصر والبيانات. تمثل قدرة Alpine.js على إدارة الحالة وتفعيل السمات بشكل مبسّط جزءًا أساسيًا من تجربة التطوير. يظهر الكود القصير والسلس كيف يمكن للمطورين بسهولة إضافة وظائف جديدة وتحسين التطبيق دون الحاجة إلى التورط في تفاصيل معقدة. يعتبر تصميم تطبيق الملاحظات باستخدام Alpine.js فعّالًا لفهم كيف يمكن دمج هذا الإطار في مشاريع الويب الحقيقية. إن تنظيم الكود وتسهيل عمليات الربط بين الواجهة الأمامية والواجهة الخلفية يعزز الإنتاجية ويوفر تجربة تطوير فعّالة. ختامًا، يظهر أن Alpine.js يعد خيارًا ممتازًا للمطورين الذين يسعون إلى تطوير تطبيقات ويب صغيرة إلى متوسطة الحجم بطريقة سهلة وفعّالة. يمكن لهذا الإطار أن يكون أحد الأدوات المفضلة لتحقيق تفاعل المستخدم بشكل سلس وسهل التكامل. اقرأ أيضًا تعلم لغة جافاسكربت الدليل السريع إلى لغة البرمجة جافاسكريبت JavaScript أطر عمل الويب من طرف الخادم
-
بعد أن تعرفنا في المقال السابق على إطار العمل Alpine.js وأنشأنا نموذج مُصغر للإطار من الصفر بهدف التعلم وفهم آلية عمل هذا الإطار، سنقوم في هذا المقال بإنشاء بعض الأمثلة البسيطة باستخدام Alpine.js وسنُغطي مجموعة من المواضيع المهمة في الإطار كإنشاء المكونات، التعامل مع الأحداث، عرض البيانات وإنشاء رسومات متحركة. إنشاء نافذة منبثقة Modal هذا المثال لإنشاء نافذة منبثقة عبر Alpine.js، نقوم أولًا بإنشاء هيكلية HTML: <!doctype html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>مثال لنافذة منبثقة</title> </head> <body> <div class="modal-wrapper"> <div class="modal"> محتوى النافذة المنبثقة </div> </div> </body> </html> سنضيف بعض تنسيقات CSS لتظهر النافذة بشكل جيد، في عنصر head نضيف وسم style ونضع به التنسيقات التالية: <style> body { margin: 0; } .modal-wrapper { position: absolute; width: 100%; height: 100%; background-color: rgba(0, 0, 0, .5); display: flex; align-items: center; justify-content: center; } .modal { background-color: #fff; width: 30%; padding: 20px; box-shadow: 5px 5px 5px #333; border-radius: 5px; } </style> وسيكون مظهر النافذة بعد فتح صفحة HTML بالشكل التالي: نريد التحكم الآن في إخفاء هذه النافذة، بحيث عند الضغط بزر الفأرة خارج حاوية النافذة البيضاء أو عند الضغط على زر الهروب escape تختفي النافذة، نقوم أولاً بتضمين إطار العمل Alpine.js عبر شبكة CDN بالشكل التالي: <script src="https://unpkg.com/alpinejs" defer></script> وكما تعلمنا سابقاً نستعمل أولاً الموجه x-data لنستطيع استعمال كافة ما تتيحه Alpine داخل المكون، بما أننا نريد التحكم في إظهار أو إخفاء النافذة، سنعرف خاصية بالاسم open، بحيث إذا كانت true فذلك يعني أن النافذة مفتوحة وإذا كانت false فإن النافذة مُغلقة، ونتحكم في ذلك عبر المُوجه x-show ونمرر له اسم الخاصية open: <div class="modal-wrapper" x-data="{ open: true }" x-show="open" > <div class="modal"> محتوى النافذة المنبثقة </div> </div> سترى أن النافذة مفتوحة افتراضيًا عند فتح الصفحة، يُمكنك تجربة تغيير القيمة الابتدائية من true إلى false وسترى أنها تستجيب لذلك. بما أننا نرغب عند النقر خارج الحاوية البيضاء بإخفاء النافذة فإننا سنستخدم المُوجه x-on للاستماع الى حدث النقر، لكني افضل استخدام الطريقة المختصرة @ ثم اسم الحدث أي click@ يُمكنك استخدام أي طريقة تريد سواء المختصرة click@ أو x-on:click ونحدد المُعدل outside للدلالة على أن الإستماع سيكون عند النقر خارج العُنصر المحدد: <div class="modal-wrapper" x-data="{ open: true }" x-show="open" > <div class="modal" @click.outside="open = false"> محتوى النافذة المنبثقة </div> </div> الآن عند فتح الصفحة ستظهر النافذة وعند النقر خارج الحاوية البيضاء ستختفي النافذة المنبثقة، وهذا لأننا حددنا أنه عندما يتم النقر قم بتغيير قيمة open إلى false وبالتالي يتم تحديث شجرة DOM وتختفي النافذة المنبثقة. بخصوص الضغط على زر الهروب escape لإخفاء النافذة المنبثقة سنستخدم حدث keyup للإستماع إلى حدث الضغط على الزر المطلوب ثم نمرر المعدل window لتحديد أن الإستماع سيحدث على مستوى الصفحة بشكل كامل: <div class="modal-wrapper" x-data="{ open: true }" x-show="open" @keyup.escape.window="open = false" > <div class="modal" @click.outside="open = false"> محتوى النافذة المنبثقة </div> </div> لحد الآن لا توجد طريقة لإظهار النافذة المنبثقة سوى بتحديث الصفحة، سنتيح ذلك الآن، نقوم بتغيير قيمة open الافتراضية إلى false: x-data="{ open: false }" ثم نضيف خارج المكون زر لفتح النافذة المنبثقة: <button>فتح النافذة المنبثقة</button> بما أن الزر موجود خارج المكون فهو لا يعلم أي شيء عن بيانات ذلك المكون ولا يستطيع الوصول لها وبالتالي لا يستطيع التحكم فيها، سنُعرف بداخله الخاصية x-data حتى نتمكن من استعمال Alpine بداخله، ثم نستخدم الخاصية dispatch$ التي تُتيحها Alpine لإطلاق حدث خاص: <button x-data @click="$dispatch('open-modal')">فتح النافذة المنبثقة</button> يمكن تسمية الحدث بأي اسم لقد أسميته open-modal، ثم في المكون الأساسي يمكن الاستماع إلى الحدث الخاص الذي تم إطلاقه بنفس الطريقة التي نستخدمها في الإستماع إلى الأحداث العادية: @open-modal.window="open = true" بحيث تُصبح هيكلية HTML بالشكل التالي: <div class="modal-wrapper" x-data="{ open: false }" x-show="open" @keyup.esc.window="open = false" @open-modal.window="open = true" > <div class="modal" @click.outside="open = false"> محتوى النافذة المنبثقة </div> </div> <button x-data @click="$dispatch('open-modal')">فتح النافذة المنبثقة</button> بهذا الشكل نكون قد أنشأنا مثال بسيط عن مكون نافذة منبثقة، وتحكمنا في إظهاره وإخفائه. إنشاء مفسر لصيغة ماركداون Markdown Parser إن لم تكن لديك معرفة مسبقة بصيغة ماركداون فأدعوك للإطلاع على المقالتين: ماركداون للمبرمجين و كيف تكتب بصيغة ماركداون ببساطة، لن نقوم بإنشاء مفسر من الصفر وإنما سنستخدم مكتبة marked وسيكون مثالنا في إنشاء حقل إدخال نكتب من خلاله بصيغة ماركداون ثم نضغط على زر ليقوم بتفسير تلك الصيغة ويعرض لنا ما يوافقها: نبدأ بإنشاء هيكلية HTML: <!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>مفسر صيغة ماركداون</title> <script src="https://unpkg.com/alpinejs" defer></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> </head> <body> <div x-data="{ body: '', markdown: '' }"> <div> <div> <textarea cols="50" rows="10"></textarea> </div> <button>عرض</button> </div> <div> المحتوى </div> </div> </body> </html> المحتوى بسيط فقط قمنا بتضمين إطار Alpine.js ثم حزمة marked التي ستساعدنا في عملية التفسير، عرفنا مكون يحتوي على خاصيتين body هو المحتوى الذي سيتم كتابته عبر حقل الإدخال، و markdown هو نتيجة التفسير والذي سيكون عبارة عن html. نقوم بربط الخاصية body مع حقل الإدخال باستخدام المُوجه x-model بحيث عند الكتابة في حقل الإدخال تتغير القيمة: <textarea cols="50" rows="10" x-model="body"></textarea> نستمع الى حدث النقر ونقوم بتفسير القيمة المدخلة في حقل الإدخال ونسندها إلى الخاصية markdown: <button @click="markdown = marked.parse(body)">عرض</button> نقوم بعرض المحتوى باستخدام المُوجه x-html: <div x-html="markdown"></div> الآن إذا أدخلت محتوى بصيغة ماركداون في حقل الإدخال وضغطت على عرض سيتم عرضه بشكل مُفسر جرب إدخال التالي في حقل الإدخال: # عنوان أول ## عنوان ثاني ### عنوان ثالث #### عنوان رابع **نص ثخين** *نص مائل* ~~نص مشطوب~~ > هذا مثال على اقتباس مثال عن رابط: [أكاديمية حسوب](https://academy.hsoub.com/) قائمة عناصر: * عنصر * عنصر آخر * عنصر آخر قائمة مرتبة: 1. عنصر أول 2. عنصر ثانٍ 3. عنصر ثالث. بعد الضغط على زر عرض ستحصل على النتيجة التالية: يُمكن في الكائن x-data إنشاء توابع وغير ذلك مما يمكن استعماله في كائنات جافاسكربت، سنقوم بإنشاء تابع يقوم بعملية التفسير ونستدعيه عند الاستماع إلى حدث الضغط حيث يصبح المكون بالشكل التالي: <div x-data="{ body: '', markdown: '', parseMarkdown() { this.markdown = marked.parse(this.body) } }"> <div> <div> <textarea cols="50" rows="10" x-model="body"></textarea> </div> <button @click="parseMarkdown">عرض</button> </div> <div x-html="markdown"></div> </div> إنشاء شريط تقدم دائري متحرك circular progress bar سنقوم أولا بإنشاء هيكلية عناصر HTML واستخدام CSS لتنسيق شريط التقدم: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Animated circular progress bar</title> <script src="https://unpkg.com/alpinejs" defer></script> <style> body { margin: 0; } .box { position: absolute; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; width: 100%; } .progress { position: relative; height: 250px; width: 250px; border-radius: 50%; background: conic-gradient(slategray 306deg, #ddd 0deg); display: flex; align-items: center; justify-content: center; } .progress::before{ content: ""; position: absolute; height: 210px; width: 210px; border-radius: 50%; background-color: #fff; } .progress-inner{ position: relative; font-size: 40px; font-weight: 600; color: slategray; } .text{ font-size: 30px; font-weight: 500; color: #606060; margin-top: 10px; } </style> </head> <body> <div class="box"> <div class="progress"> <div class="progress-inner">85%</div> </div> <span class="text">Alpine Js</span> </div> </body> </html> سيكون المظهر بالشكل التالي: يُمكنك استخدام أي تنسيقات ترغب بها، ما يهم أننا سنستهدف الخاصية: background: conic-gradient(slategray 306deg, #ddd 0deg); القيمة 306deg بالضبط، حيث أننا سنحصل عليها من ضرب نسبة التقدم في 3.6، أي لما تكون النسبة 100% فإن القيمة تكون 360 درجة فهي عبارة عن قيس زاوية. وعندما تكون نسبة التقدم 50% تكون قيمة الزاوية 180 درجة وهي حاصل ضرب 50 في 3.6 لأننا سنبدأ التحريك من 0 إلى غاية القيمة النهائية. لذلك سنجعل القيمة الابتدائية 0 بالشكل التالي: background: conic-gradient(slategray 0deg, #ddd 0deg); ثم نأتي لنبني المكون عبر Alpine.js ونبدأ بتعريف x-data في العُنصر progress، بحيث نحدد الخصائص التالية: currentValue وسنعتمد عليها في زيادة قيمة التقدم وتكون مُهيأة بالقيمة 0، endValue وهي القيمة الأعظمية للتقدم، speed خاصية زمنية سنعتمد عليها في تسريع التحريك. <div class="box"> <div class="progress" x-data="{ currentValue: 0, endValue: 85, speed: 20, // ms }" > <div class="progress-inner" x-text="currentValue + '%'"></div> </div> <span class="text">Alpine Js</span> </div> ستلاحظ أننا حددنا x-text للعنصر progress-inner حتى تكون القيمة ديناميكية وتتحدث كلما تغيرت قيمة currentValue. الآن سنقوم بتعريف تابع بالاسم init داخل x-data هذا التابع سيتم تنفيذه مباشرةً بعد تهيئة المكون يمكنك تجربة التالي: <div class="progress" x-data="{ currentValue: 0, endValue: 85, speed: 20, // ms init() { console.log('Initialized') } }" > ستجد أنه يتم طباعة العبارة Initialized في الطرفية console عند تحديث الصفحة، نحن لم نستدعي التابع، هذا لأن Alpine.js تقوم بذلك تلقائيًا، يُمكن أيضًا استخدام الموجه x-init لنفس الغرض. سنقوم بداخل التابع init استخدام setInterval لتنفيذ دالة بعد كل زمن معين (هنا سنستخدم speed حيث أن الدالة سيتم تنفيذها بعد كل 20ms) بداخل الدالة سنقوم بزيادة قيمة currentValue بـ 1 ونتحقق إذا وصلنا للقيمة الأعظمية للتقدم حيث نقوم بإيقاف تنفيذ الدالة: <div class="progress" x-data="{ currentValue: 0, endValue: 85, speed: 20, // ms init() { let progress = setInterval(() => { this.currentValue++ if (this.currentValue === this.endValue) { clearInterval(progress) } }, this.speed) } }" > الآن إذا تصفحت ستجد أن القيمة تتغير من 0 إلى 85 كما هو مطلوب وعندما نصل إلى القيمة النهائية يتوقف التنفيذ. متبقي الآن ربط التنسيق الذي ذكرته في الأعلى بالقيمة الحالية، ولعمل هذا الشيء نحتاج إلى استخدام الموجه x-bind فهو يتيح ربط تعبير برمجي بخاصية من الخصائص في html، والخاصية التي سنتعامل معها هي style، هناك كتابة مختصرة لـ x-bind وهي رمز النقطتين : مثلما هو الحال مع x-on في الأحداث لذلك فبدل كتابة: x-bind:style="" يمكن كتابة: :style="" وبالتالي، لربط قيمة currentValue بخاصية style سنكتب: :style="`background: conic-gradient(slategray ${currentValue * 3.6}deg, #ddd 0deg)`" ليُصبح المكون بالشكل التالي: <div class="box"> <div class="progress" x-data="{ currentValue: 0, endValue: 85, speed: 20, // ms init() { let progress = setInterval(() => { this.currentValue++ if (this.currentValue === this.endValue) { clearInterval(progress) } }, this.speed) } }" :style="`background: conic-gradient(slategray ${currentValue * 3.6}deg, #ddd 0deg)`" > <div class="progress-inner" x-text="currentValue + '%'"></div> </div> <span class="text">Alpine Js</span> </div> بهذا الشكل سيُصبح كل من الشريط والنص المكتوب في الوسط يتحرك من القيمة 0 إلى القيمة النهائية 85، يمكنك تعديل القيمة النهائية 85 وقيمة السرعة speed لمعاينة النتائج: خاتمة في ختام هذا المقال، نستنتج أن إطار العمل Alpine.js يعد أداة قوية وبسيطة في تطوير تجارب المستخدم الديناميكية على الويب. قدمت الأمثلة التي استعرضناها في هذا السياق نظرة عامة على قوة وسهولة استخدام Alpine.js في تحقيق العديد من الوظائف الديناميكية دون الحاجة إلى اللجوء إلى أطر عمل JavaScript الكبيرة والمعقدة. تعتبر قدرة Alpine.js على توفير عمليات DOM الديناميكية والربط الفعّال بين العناصر والبيانات من خلال السماح بالتفاعل المباشر والتحكم السهل، مميزة بشكل خاص. التعابير البرمجية القصيرة والبنية البسيطة تجعل من السهل إدماجه في مشاريع الويب المختلفة دون تعقيدات زائدة. من خلال الأمثلة البسيطة التي قمنا بإنشائها، يمكن للمطورين الآن البدء بفهم كيفية استخدام Alpine.js في مشاريعهم الشخصية أو العملية. عليهم استكشاف الميزات الإضافية واستخدام الإمكانيات الكاملة لهذا الإطار لتحسين تفاعل المستخدم وتبسيط التجارب على الويب. اقرأ أيضًا دليل تعلم لغة جافاسكربت متى نستعمل إطار عمل للتطوير باستخدام JavaScript تعلم لغة جافا سكريبت من الصفر حتى الاحتراف
-
سنتعلم في هذه السلسلة من المقالات كيفية التعامل مع إطار العمل Alpine.js وسنبني نموذج MVP مصغر يحاكي بعض ما تفعله Alpine.js من الصفر بهدف تعلم جافا سكريبت JavaScript وفهم آلية عمل هذا الإطار وتبسيط المفاهيم المرتبطة به. ما هو Alpine.js؟ إن كنت لم تسمع عن هذا الإطار Alpine.js من قبل فهو إطار عمل للغة جافا سكريبت JavaScript صغير الحجم يساعد مطوري الويب كثيرًا ويمكّنهم من إضافة بعض التأثيرات التفاعلية على عناصر HTML وعلى صفحات الويب بشيفرات بسيطة من خلال العناصر نفسها، وكما هو موضح في صفحة التوثيق الخاصة بالإطار فإن Alpine.js يعد أداة قوية وبسيطة لتكوين السلوك مباشرة في شيفرة HTML التي تكتبها، يمكنك التفكير فيه على أنه مشابه لمكتبة jQuery لكن للويب الحديث كما يوفر Alpine بُنية تفاعلية مثل أطر العمل والمكتبات الشهيرة مثل Vue و React لكن بكلفة وجهد أقل بكثير، وإن كنت قد استخدمت Tailwind للغة CSS فهدف إطار العمل Alpine.js مشابه لها لكنه للغة JavaScript. يتميز إطار العمل Alpine.js كذلك بسهولة الاستخدام فهو يمكن المستخدم من تحقيق التفاعل بشكل فعّال دون الحاجة إلى الاعتماد على مكتبات JavaScript كبيرة أو معقدة، ودون الحاجة إلى إعادة تحميل الصفحة. كما يتميز Alpine.js بالتكامل السهل مع صفحات HTML المبنية بالفعل، مما يجعله خيارًا جيدًا للمشاريع الصغيرة إلى المتوسطة. مبدأ عمل Alpine.js يعتمد Alpine.js على فكرة الديناميكية الخفيفة، حيث يُضاف السلوك التفاعلي مباشرة إلى عناصر HTML، ويمكّنك من تعريف المتغيرات والدوال المرتبطة بالواجهة مباشرة في سمات HTML مما يقلل من الحاجة إلى كتابة الكثير من الأكواد البرمجية. ميزات Alpine.js سهولة التكامل: إذ يُدمج Alpine.js بسهولة مع صفحات HTML المبنية مسبقًا، ويتيح للمطورين إضافة تفاعل المستخدم بسرعة دون الحاجة إلى تغيير الهيكل الأساسي للصفحة. بنية بسيطة: يستخدم الإطار بنية بسيطة تتيح للمطورين فهمها بسرعة والبدء في استخدامها دون الحاجة إلى تعلم مفاهيم معقدة. تحكم ديناميكي: يسمح Alpine.js بربط العناصر بشكل ديناميكي بينما يُحدّث التطبيق، ما يمكن من بناء تطبيقات الصفحة الواحدة الصفحة SPA وإضافة تفاعل مستمر دون إعادة تحميل الصفحة. الفرق بين Alpine.js وأطر العمل Vue و React و Angular سأذكر مقارنة سريعة بين Alpine وأشهر أطر عمل ومكتبات لغة جافاسكريبت: Vue.js و React: Alpine.js أصغر حجمًا ويمكن تضمينه بسهولة في صفحات HTML بينما Vue.js و React يتطلبان هياكل مشروع معقدة أكثر. Alpine.js يستند إلى تقنية JavaScript الأساسية المدمجة في المتصفح دون الحاجة إلى ترجمة أو بناء مسبق، بينما Vue.js و React يتطلبان تحويل الشيفرة إلى شيفرة JavaScript قابلة للتنفيذ. Angular: Angular يقدم إمكانيات كبيرة لتطوير تطبيقات الويب الكبيرة والمعقدة، بينما يناسب Alpine.js المشاريع الصغيرة والمتوسطة. Angular يتطلب هياكل مشروع محددة ويتميز بتفصيل وتعقيد الإعدادات، بينما يعتمد Alpine.js على البساطة وسهولة الاستخدام. كيفية استخدام Alpine.js سنبدأ التعرف على إطار عمل Alpine.js من خلال مثال بسيط وهو عبارة عن عداد يتكون من زرين أحدهما يقوم بزيادة قيمة العداد والثاني يقوم بإنقاصها، سنقوم بإنشاء ملف HTML بالاسم index.html ونضع بداخله بنية العداد كما يلي: <!-- index.html --> <html> <div> <button>+</button> <span></span> <button>-</button> </div> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> </html> كما تلاحظ فبنية الملف بسيطة، فهو يحتوي فقط على وسم html بداخله قسم لبنية العداد، ووسم script لتضمين ملف Alpine.js، والعداد نفسه عبارة عن حاوية بداخلها زرين وعنصر span، الزر الأول لزيادة قيمة العداد والزر الثاني لإنقاص قيمة العداد، بالإضافة لعنصر span لعرض قيمة العداد. ولاستخدام Alpine.js نبدأ أولاً بالمُوجّه x-data وتكون قيمته عبارة عن كائن، نضع بداخله البيانات الخاصة بالمكون والتي نريد التعامل معها، يمكن أيضاً كتابة بعض التوابع بداخله، وبهذا الشكل ستتعرف Alpine على المكون ويمكن كتابة أي شيء توفره Alpine بداخل الحاوية، كما يمكن الوصول لقيم بيانات الكائن data والتعديل عليها بشكل ديناميكي دون تحديث الصفحة، تتحدث حالة المكون تلقائيًا مع كل تغيير، ولعرض قيمة الخاصية count التي تعبر عن قيمة العداد نستخدم المُوجّه x-text، وفي الزرين نستخدم click@ للاستماع إلى حدث النقر على الأزرار ثم نقوم بالمعالجة على حسب الزر (في زر الزيادة نقوم بزيادة قيمة count بقيمة واحد، وفي زر الإنقاص نقوم بإنقاص قيمة count بقيمة واحد) بحيث تُصبح هيكلية شيفرة HTML بهذا الشكل: <!-- index.html --> <html> <div x-data="{ count: 0 }"> <button @click="count++">+</button> <span x-text="count"></span> <button @click="count--">-</button> </div> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> </html> يخبر الموجه x-data إطار Alpine بتهيئة المكوّن الجديد بكائن البيانات المعرّف والمحدّد مسبقًا، أما الموجه click@ فما هو إلا اختصار إلى x-on:click وهذا الموجه يستخدم للاستماع إلى حدث معين على عنصر ما، ويمكن استخدام أي حدث على حسب الحالة مثل submit@ أو mouseenter@ وغير ذلك. وأخيرًا، يضبط الموجه x-text محتوى نص العنصر على نتيجة تعبير برمجي معين. بعد فتح الصفحة على المتصفح وتجربة الكود سنحصل على النتيجة التالية: تطبيق عملي باستخدام Alpine.js بعد أن أخذنا نظرة مبدئية على Alpine وكتبنا أول مثال بسيط بها، سنأتي لإنشاء نموذج MVP مُصغر من الصفر بهدف التعلم وفهم الطريقة أو الآلية التي يعمل بها Alpine، سنبدأ خطوة بخطوة حتى نصل للمنتج النهائي، سنرجع للمثال السابق، ونزيل سطر تضمين المكتبة، ونقوم بكتابة شيفرات JavaScript خاصة بنا كما يلي: <!-- index.html --> <html> <div x-data="{ count: 0 }"> <button @click="count++">+</button> <span x-text="count"></span> <button @click="count--">-</button> </div> <script> // سنكتب الشيفرات الخاصة بنا هنا </script> </html> بطبيعة الحال الآن إذا قمت بتجربة الضغط على الأزرار فلن تعمل معك خاصية العداد، لأن Alpine.js لم يعد لها وجود في الملف. يقوم إطار العمل Alpine.js أو أطر العمل الحديثة مثل Vue وغيرها على مبدأين أساسيين وهما: مراقبة أو تتبع بعض البيانات. تحديث شجرة DOM عندما تتغير تلك البيانات. بغض النظر عن المكتبة أو إطار العمل أو كيفية تحقيق ذلك، إلا أن هذين المبدأين هما الأساس في العمل. سنحاول لذا سنحاول في إنشائنا لهذا النموذج المصغر الاعتماد عليهما، قد يُخيل لك أن الأمر بسيط جداً لكن هناك أشياء كثيرة تحدث في الخلفية لتحقيق تلك البساطة في الأخير. فهم كيفية عمل التفاعلية في إطار العمل Alpine.js نعود لملف HTML الخاص بنا وفي الجزء الذي حددنا أننا سنكتب فيه شيفرة JavaScript سنحاول تحقيق أول مبدأ وأول خُطوة هي تكوين كائن البيانات انطلاقا من العنصر: let root = document.querySelector('[x-data]') let dataString = root.getAttribute('x-data') let data = eval(`(${dataString})`) console.log(data); في السطر الأول حددنا عُنصر أو حاوية المكون الأساسية عبر استخدام التابع querySelector وعبر الخاصية x-data ثم قمنا بتخزينه في المتغير root بعد أن أصبح العُنصر مخزن لدينا ككائن. استخدمنا التابع getAttribute لجلب قيمة الخاصية x-data وقمنا بتخزينها في متغير بالاسم dataString، لكن قيمته عبارة عن سلسلة نصية إذا طبعت القيمة من خلال الطرفية console ستحصل على: '{ count: 0 }' لذلك استخدمنا بعدها الدالة eval لتحويل السلسلة النصية إلى كائن JavaScript، وإذا قمت بتصفح ملف index.html بعد إضافة الشيفرات السابقة ستحصل في الطرفية console على الناتج: {count: 0} بهذا الشكل حققنا أول خطوة وهي أن البيانات أصبحت متاحة لدينا في JavaScript، والآن يمكن التعديل عليها ومراقبتها. سنقوم في الخطوة القادمة بتحسين كتابة الكود السابق، وذلك عبر استخراج دالة سميناها getInitialData ليُصبح الكود بالشكل التالي: let root = document.querySelector('[x-data]') let data = getInitialData() console.log(data) function getInitialData() { let dataString = root.getAttribute('x-data') return eval(`(${dataString})`) } سنقوم في الخطوة القادمة بإنشاء دالة تقوم بتحديث لشجرة DOM، وستجد أننا نستخدم الطريقة التالية في كتابة الشيفرة والتوسع: استدعاء الواجهة البرمجية (في حالتنا ستكون دالة) إنشاء الواجهة البرمجية تحسين الشيفرة أو إعادة بنائها بشكل أفضل نعود للشيفرة السابقة ونزيل سطر طباعة البيانات، ونضيف الدالة التي نريد إنشاءها وهي refreshDOM(): let root = document.querySelector('[x-data]') let data = getInitialData() refreshDOM() function getInitialData() { let dataString = root.getAttribute('x-data') return eval(`(${dataString})`) } الآن نُنشئ الدالة refreshDOM وسيكون الهدف منها تحديث شجرة DOM: function refreshDOM() { walkDOM(root, el => { console.log(el.outerHTML) }) } كما تلاحظ فإن الدالة refreshDOM تقوم فقط باستدعاء دالة أخرى تُسمى walkDOM سنقوم بإنشائها بعد حين، هذه الدالة تقوم بالمرور على عناصر مكون التعداد انطلاقا من العنصر الممرر في المعامل الأول ونحن حددنا root وهو العنصر الخاص بمكون العداد، وفي المعامل الثاني تقبل دالة رد نداء callback function يتم تنفيذها من داخل الدالة walkDOM، في حالتنا رد النداء callback عبارة عن دالة سهمية تستقبل معامل el وتقوم بطباعة outerHTML الخاص به (بمعنى العُنصر بشكل كامل وسم البداية والنهاية بالإضافة إلى المحتوى الداخلي له)، وهنا استخدمنا تعليمة الطباعة بغرض الشرح وتبسيط الفهم لكننا سنقوم بتغيير محتوى الدالة السهمية لاحقًا. كما أسلفت بالذكر فإن الدالة walkDOM ستعبر على عناصر المكون فقط انطلاقا من عُنصر المكون نفسه، ثم الابن الأول للعنصر وإن كان للابن أبناء تمر عبرهم أيضًا، وهكذا كل ما تجد أن للعنصر الحالي أبناء تعبر عليهم وفي كل مرة تعود تعبر على عناصر الابن الموالي للأب حتى لا تجد عناصر تعبر عليهم. سنستخدم في بنائنا للدالة walkDOM مفهوم يُسمى التعاودية وهو مفهوم برمجي متبع في معظم لغات البرمجة، يسمح باستدعاء الدالة لنفسها بمعنى آخر تنفيذ الدالة كجزء من الدالة نفسها، وسيساعدنا في مسألة العبور على العناصر المتداخلة في المكون. الآن بعد أن وضحنا ما الذي سنقوم به، نكتب الدالة walkDOM: function walkDOM(el, callback) { callback(el) el = el.firstElementChild while(el) { walkDOM(el, callback) el = el.nextElementSibling } } كما تلاحظ فالدالة walkDOM لا تحتوي على أسطر كثيرة، فهي تقوم أولًا بتنفيذ دالة callback الممررة كمعامل ثاني على المعامل الأول ودالة callback ستقوم بطباعة العنصر فقط، ثم في السطر الثاني نُغير العنصر لأول ابن للعُنصر الأساسي باستخدام الخاصية firstElementChild، ثم نقوم بتنفيذ حلقة while للمرور على كافة الأبناء ونستخدم الخاصية nextElementSibling التي تجلب العُنصر الموالي (الأخ) للعنصر الحالي. ونستدعي الدالة walkDOM من داخل الحلقة حتى نضمن العبور على أبناء العنصر الحالي في الحلقة وبهذا الشكل نكون قد استخدمنا التعاودية للعبور على كافة العناصر المتداخلة. الآن إذا قمت بتصفح الملف index.html من خلال المتصفح وفتحت نافذة الطرفية console ستجد أنه كافة عناصر المكون ستطبع، وإن قمت بالتعديل على هيكلية عناصر المكون بإضافة عناصر HTML متداخلة ستطبع كما هو متوقع وهذا هو الناتج: بما أننا استطعنا المرور على جميع عناصر المكون جاء الدور الآن على تغيير بُنية دالة callback التي نقوم بتنفيذها أثناء العبور فنحن لا نريد الطباعة، وإنما نريد تغيير نص العناصر التي بها الخاصية x-text بالبيانات الموافقة لها: walkDOM(root, el => { if(el.hasAttribute('x-text')) { el.innerText = "foo" } }) استخدمنا التابع hasAttribute للتحقق أن العنصر يملك خاصية بالاسم x-text ثم باستخدام خاصية innerText غيرنا النص الموافق له، ستجد أن النص الموافق للعنصر span والذي به الخاصية x-text تغير إلى foo لكننا نريد وضع القيمة الخاصة بما تحمله x-text، في حالتنا ستكون قيمة العداد count: let expression = el.getAttribute('x-text') استخدمنا التابع getAttribute لجلب القيمة ستكون عبارة عن سلسلة نصية لاسم البيانات (في حالتنا هي count في مثال آخر ستكون ما يتم كتابته بداخل الخاصية x-text قد تكون تعبير برمجي). الآن سنجلب القيمة الموافقة للتعبير expression ونضعها كنص للعُنصر، سنستخدم الدالة eval كما فعلنا سابقًا لكن لا يمكن كتابة التالي: el.innerText = eval(`(${expression})`) لأن القيمة ليست متاحة كمتغير وإنما موجودة ضمن الكائن data للوصول إليها نستخدم التعليمة with بالشكل التالي: el.innerText = eval(`with (data) { (${expression}) }`) ملاحظة: إن التعليمة with حسب التوثيق مهملة ولا يُنصح باستخدامها وقد يتم إزالتها بسبب مشاكل دعم المتصفحات والتوافقية، وهناك طرق أخرى بطبيعة الحال لكن للسهولة ولأغراض تعليمية سنستمر على هذه الطريقة فهي لازالت تعمل. الآن أصبح بالإمكان تحديث شجرة DOM عبر الدالة التي بنيناها والتي أصبح شكلها كما يلي: function refreshDOM() { walkDOM(root, el => { if(el.hasAttribute('x-text')) { let expression = el.getAttribute('x-text') el.innerText = eval(`with (data) { (${expression}) }`) } } } وأصبحت شيفرة جافاسكربت بالكامل كما يلي: let root = document.querySelector('[x-data]') let data = getInitialData() refreshDOM() function getInitialData() { let dataString = root.getAttribute('x-data') return eval(`(${dataString})`) } function refreshDOM() { walkDOM(root, el => { if(el.hasAttribute('x-text')) { let expression = el.getAttribute('x-text') el.innerText = eval(`with (data) { (${expression}) }`) } } } function walkDOM(el, callback) { callback(el) el = el.firstElementChild while(el) { walkDOM(el, callback) el = el.nextElementSibling } } الآن إذا فتحت الصفحة على المتصفح ستلاحظ أن العداد يأخذ القيمة الابتدائية وهي 0 وهذا لأننا نقوم بتنفيذ refreshDOM بعد جلب البيانات: يمكنك فتح نافذة الطرفية ومحاولة تغيير قيمة count إلى قيمة أخرى مثلًا: data.count = 5 بهذا الشكل ستغيير القيمة الفعلية للعداد، لكن لن تتحدث القيمة في العنصر الموافق، ونحتاج إلى استدعاء refreshDOM بعد تعديل القيمة حتى تتحدث في الصفحة: data.count = 5 refreshDOM() وستلاحظ أن القيمة تتغير بمجرد استدعاء الدالة، وكلما غيرت قيمة data.count إلى قيمة أخرى واستدعيت الدالة refreshDOM ستستجيب العناصر، وهذا ما ذكرته في البداية عن المبدأين (مراقبة البيانات وتحديث شجرة DOM عندما تتغير البيانات) كما هو موضح أدناه: لكن كما تلاحظ فإن عملية تحديث شجرة DOM لا تعمل بشكل تلقائي أو ديناميكي، والخطوة التالية هي تحقيق ذلك، بحيث عند تعديل البيانات يتم مباشرة تحديث شجرة DOM دون أن نفعل ذلك بشكل صريح. سيساعدنا الكائن Proxy في هذه المهمة. قبل أن نستعمله في الشيفرة الخاصة بنا سنفتح المتصفح ثم نافذة الطرفية ونعرف كائن بالشكل التالي: let myData = {x: 2} ثم سنقوم بإنشاء كائن Proxy ونوكل له مهمة التفاعل مع الكائن myData، يتيح لنا كائن الوكيل Proxy إعادة تعريف التوابع المتاحة للكائنات، فمثلاً يمكن إعادة تعريف التابع get وset بالشكل التالي: let proxy = new Proxy(myData, { get(target, key) { console.log('getting ...'); return target[key]; }, set(target, key, value) { console.log('setting ...'); target[key] = value;} }) وعند تعريف الوكيل تُصبح خصائص الكائن الأساسي متاحة لكائن الوكيل بالشكل التالي عند جلب قيمة الخاصية: proxy.x سنفذ ما حددناه داخل get وترى الناتج التالي: getting ... 2 ونفس الأمر عند التعديل: proxy.x = 10 setting ... 10 وكما تلاحظ أصبح كائن الوكيل يُغلف الكائن الأساسي الخاص بنا، بطريقته الخاصة، كان هذا شرح بسيط لكائن الوكيل، قد تقول فيما ستفيدني هذه الميزة! في الواقع تساعدك هذه الميزة في تغليف كائن البيانات الخاص بك بكائن وكيل بحيث تعيد تعريف التابع set وتستدعي بداخله الدالة refreshDOM بحيث عندما تتغير قيمة البيانات يتم تلقائيًا استدعاء الدالة ويتم تحديث شجرة DOM. سنقوم أولا بإعادة تعريف المتغير data إلى rawData بالشكل التالي: let rawData = getInitialData() ثم نُعرف متغير بالاسم data بحيث نُسند له كائن الوكيل لكن سنقوم بإنشاء دالة تقوم بذلك نُسميها observe: let data = observe(rawData) function observe(data) { return new Proxy(data, { set(target, key, value) { target[key] = value refreshDOM() } }) } بحيث تُصبح شيفرة JavaScript بالشكل التالي: let root = document.querySelector('[x-data]') let rawData = getInitialData() let data = observe(rawData) refreshDOM() function observe(data) { return new Proxy(data, { set(target, key, value) { target[key] = value refreshDOM() } }) } function getInitialData() { let dataString = root.getAttribute('x-data') return eval(`(${dataString})`) } function refreshDOM() { walkDOM(root, el => { if(el.hasAttribute('x-text')) { let expression = el.getAttribute('x-text') el.innerText = eval(`with (data) { (${expression}) }`) } } } function walkDOM(el, callback) { callback(el) el = el.firstElementChild while(el) { walkDOM(el, callback) el = el.nextElementSibling } } الآن إذا فتحنا الصفحة الخاصة بنا على المتصفح، ثم غيرنا البيانات من الطرفية كما فعلنا سابقًا: data.count = 5 ستتغير القيمة الفعلية وسيستجيب المكون للتغيير تلقائيًا. وهذا ما يسمى بالتفاعلية reactivity. فهم كيفية معالجة الأحداث في Alpine.js الخطوة القادمة هي معالجة الأحداث، فحتى اللحظة الحالية لا تفعل الأزرار أي شيء ولا تستجيب عند الضغط عليها، لتحقيق ذلك نحتاج إلى إنشاء دالة تقوم بتسجيل المستمعات، سنُسمي الدالة registerListeners ومن داخلها سنمر على عناصر شجرة DOM باستعمال الدالة walkDOM كما فعلنا سابقًا وفي دالة callback إن كان العُنصر يملك خاصية بالاسم click@ نُسجل مستمع على حدث النقر بالعُنصر والمعالج يكون حسب قيمة الخاصية click@ المحددة في HTML: registerListeners() function registerListeners() { walkDOM(root, el => { if(el.hasAttribute('@click')) { let expression = el.getAttribute('@click') el.addEventListener('click', () => { eval(`with (data) { (${expression}) }`) }) } }) } الآن إذا فتحت الصفحة وجربت الضغط على الأزرار ستستجيب كما هو متوقع، وبهذا الشكل نكون قد وصلنا لنفس ما تقوم به Alpine ونفس المثال الذي بدأنا به. لكن إلى الآن النموذج الذي أنشأناه يستمع ويعالج فقط حدث النقر فماذا عن الأحداث الأخرى وماذا عن موجهات أخرى، إذ أضفنا الموجه x-text فقط وسنحاول في الخطوة القادمة تحقيق ذلك. نبدأ أولاً بالأحداث ولنُغير أولاً السطر: <button @click="count++">+</button> نقوم بربط زيادة العداد بالحدث mouseenter أي حدث تمرير مؤشر الفأرة على الزر: <button @mouseenter="count++">+</button> ثم سنعدل الشيفرة لتشمل ذلك، وبالتحديد سنعدل الدالة registerListeners في دالة callback الممررة للدالة walkDOM فبدل أن نبحث بشكل صريح عن الخاصية click@ نمر على كافة الخصائص المتاحة في العُنصر فإن لم تكن الخاصية تبدأ بالرمز @ نتجاهلها وإلا فالخاصية عبارة عن ربط حدث بالعنصر، نجلب الخاصية والتي ستكون عبارة عن اسم الحدث مسبوق برمز @ فقط نزيل رمز @ ليُصبح لدينا اسم الحدث: function registerListeners() { walkDOM(root, el => { Array.from(el.attributes).forEach(attribute => { if (!attribute.name.startsWith('@')) return; let event = attribute.name.replace('@', ''); el.addEventListener(event, () => { eval(`with (data) { (${attribute.value}) }`) }) }) }) } نفس التوجه، فقط وسعنا المنطق ليشمل أحداث متعددة وليس فقط حدث النقر، الآن إذا فتحت الصفحة ومررت مؤشر الفأرة فوق زر الزيادة سيستجيب المكون للحدث وتزداد قيمة العداد. وبهذا الشكل جعلنا النموذج يقبل أحداث متنوعة بإمكانك تجربتها وستجد أن المكون يستجيب لها. نأتي الآن إلى مسألة زيادة الموجهات، سنضيف على سبيل المثال الموجه x-show الذي يعرض عُنصر حسب شرط معين نحدده له، أولاً نضيف عُنصر span جديد للمكون يعرض رسالة في حالة تجاوزت قيمة العداد مثلاً القيمة 5 وإذا كانت أقل لا يعرضها: <span x-show="count > 5">Greater than 5</span> ونقوم بتوسيع المنطق ليشمل هذا التعديل، وسنقوم بذلك على مستوى الدالة refreshDOM، فهي تنظر إلى الآن للخاصية x-text فقط. في البداية لنوسع الموجه الحالي ثم نضيف الموجه الجديد، لذا سنعرف كائن يحمل الموجهات المتاحة، بحيث تكون المفتاح فيه عبارة عن اسم الموجه وتكون القيمة دالة سهمية موافقة له لتنفيذها: let directives = { 'x-text': (el, value) => { el.innerText = value } } ثم سنستخدم في الدالة refreshDOM بدالة callback نفس الطريقة التي استخدمناها مع الأحداث ونمر على كل خاصيات العنصر، ونتجاهل كل خاصية إذا لم تكن ضمن المفاتيح المعرفة في الكائن directives، أما إذا كانت موجودة فننفذ الدالة السهمية الموافقة لها ونمرر العُنصر والقيمة الموافقة: function refreshDOM() { walkDOM(root, el => { Array.from(el.attributes).forEach(attribute => { if (!Object.keys(directives).includes(attribute.name)) return; directives[attribute.name](el, eval(`with (data) { (${attribute.value}) }`)) }) }) } الآن بعدما وسعنا المنطق ليشمل موجهات أخرى نقوم بإضافة موجه x-show للكائن directives: let directives = { 'x-text': (el, value) => { el.innerText = value }, 'x-show': (el, value) => { el.style.display = value ? 'block' : 'none' } } استخدمنا في الدالة السهمية الموافقة لـ x-show خاصية display من style التي تقوم بإخفاء العنصر إذا كانت القيمة none وإن كانت block يظهر العنصر، الآن إذا فتحت الصفحة ستجد أن الأحداث تستجيب والعداد تتغير قيمته، وإذا تجاوزت القيمة العدد 5 ظهرت الرسالة وإذا أنقصنا القيمة لتصبح أقل أو تساوي 5 اختفت الرسالة كما هو متوقع تمامًا: الخطوة الأخيرة من هذا المقال ستكون بتحسين الكود، وذلك عبر إنشاء كائن بالاسم Alpine وإسناده إلى المتغير العام window، بحيث تصبح المتغيرات التي أنشأناها خصائص لهذا الكائن والدوال توابع له، وبداخله ننُشئ تابع بالاسم start يضم الأسطر الأولى لتشغيل النموذج. ثم نقوم باستخراج النموذج إلى ملف منفصل بالاسم alpine.js ثم نقوم باستدعائه: <html> <div x-data="{ count: 0 }"> <button @click="count++">+</button> <span x-text="count"></span> <button @click="count--">-</button> <span x-show="count > 5">Greater than 5</span> </div> <script src="./alpine.js"></script> </html> ويكون محتوى ملف alpine.js كالتالي: window.Alpine = { directives: { 'x-text': (el, value) => { el.innerText = value }, 'x-show': (el, value) => { el.style.display = value ? 'block' : 'none' } }, start() { this.root = document.querySelector('[x-data]') this.rawData = this.getInitialData() this.data = this.observe(this.rawData) this.registerListeners() this.refreshDOM() }, getInitialData() { let dataString = this.root.getAttribute('x-data') return eval(`(${dataString})`) }, registerListeners() { this.walkDOM(this.root, el => { Array.from(el.attributes).forEach(attribute => { if (!attribute.name.startsWith('@')) return; let event = attribute.name.replace('@', ''); el.addEventListener(event, () => { eval(`with (this.data) { (${attribute.value}) }`) }) }) }) }, observe(data) { var self = this return new Proxy(data, { set(target, key, value) { target[key] = value self.refreshDOM() } }) }, refreshDOM() { this.walkDOM(this.root, el => { Array.from(el.attributes).forEach(attribute => { if (!Object.keys(this.directives).includes(attribute.name)) return; this.directives[attribute.name](el, eval(`with (this.data) { (${attribute.value}) }`)) }) }) }, walkDOM(el, callback) { callback(el) el = el.firstElementChild while (el) { this.walkDOM(el, callback) el = el.nextElementSibling } } } window.Alpine.start(); ستجد الملفات الخاصة بهذا المقال في المستودع التالي على موقع Github: إنشاء نموذج MVP مصغر لمكتبة Alpine خاتمة في الختام يمكن القول أن إطار العمل Alpine.js هو إطار عمل صغير وبسيط سهل التعلم والاستخدام يحتوي على مجموعة من الأدوات تسمح بإضافة تفاعلية على صفحات التطبيق بشكل سلس دون حاجة إلى تعقيدات كبيرة إضافة إلى أنه لا يؤثر على أداء الموقع أو صفحات التطبيق الخاص بك، باختصار إذا كنت تبحث عن إطار عمل سهل الاستخدام وفعّال في نفس الوقت، فإن Alpine.js قد يكون الخيار المثالي الذي يجمع بين البساطة والقوة. يشكل هذا الإطار نقلة نوعية في تحسين تجربة المستخدم وتسريع تطوير تطبيقات الويب. تعرفنا في هذا المقال على بعض مميزات هذا الإطار واستطعنا من خلال بنائنا للنموذج فهم آلية عمل هذا الإطار وكيفية تحقيقه للتفاعلية والبساطة، وسنتعرف في المقالات القادمة على مميزات أخرى لهذا الإطار من خلال إنشاء بعض الأمثلة البسيطة وإنشاء تطبيق كامل عبر Alpine.js. اقرأ أيضًا تعلم لغة جافا سكريبت من الصفر حتى الاحتراف تعرف على مفهوم إطار العمل Framework وأهميته في البرمجة متى نستعمل إطار عمل للتطوير باستخدام JavaScript مقدمة إلى أطر عمل تطوير الويب من طرف العميل