سنتعلم في هذه السلسلة من المقالات كيفية التعامل مع إطار العمل 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.