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

مدخل إلى إطار العمل Alpine.js


سمير عبود

سنتعلم في هذه السلسلة من المقالات كيفية التعامل مع إطار العمل 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

  1. سهولة التكامل: إذ يُدمج Alpine.js بسهولة مع صفحات HTML المبنية مسبقًا، ويتيح للمطورين إضافة تفاعل المستخدم بسرعة دون الحاجة إلى تغيير الهيكل الأساسي للصفحة.

  2. بنية بسيطة: يستخدم الإطار بنية بسيطة تتيح للمطورين فهمها بسرعة والبدء في استخدامها دون الحاجة إلى تعلم مفاهيم معقدة.

  3. تحكم ديناميكي: يسمح Alpine.js بربط العناصر بشكل ديناميكي بينما يُحدّث التطبيق، ما يمكن من بناء تطبيقات الصفحة الواحدة الصفحة SPA وإضافة تفاعل مستمر دون إعادة تحميل الصفحة.

الفرق بين Alpine.js وأطر العمل Vue و React و Angular

سأذكر مقارنة سريعة بين Alpine وأشهر أطر عمل ومكتبات لغة جافاسكريبت:

  1. Vue.js و React:

    • Alpine.js أصغر حجمًا ويمكن تضمينه بسهولة في صفحات HTML بينما Vue.js و React يتطلبان هياكل مشروع معقدة أكثر.
    • Alpine.js يستند إلى تقنية JavaScript الأساسية المدمجة في المتصفح دون الحاجة إلى ترجمة أو بناء مسبق، بينما Vue.js و React يتطلبان تحويل الشيفرة إلى شيفرة JavaScript قابلة للتنفيذ.
  2. 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 محتوى نص العنصر على نتيجة تعبير برمجي معين.

بعد فتح الصفحة على المتصفح وتجربة الكود سنحصل على النتيجة التالية:

counter with alpine js

تطبيق عملي باستخدام 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 وغيرها على مبدأين أساسيين وهما:

  1. مراقبة أو تتبع بعض البيانات.
  2. تحديث شجرة 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، وستجد أننا نستخدم الطريقة التالية في كتابة الشيفرة والتوسع:

  1. استدعاء الواجهة البرمجية (في حالتنا ستكون دالة)
  2. إنشاء الواجهة البرمجية
  3. تحسين الشيفرة أو إعادة بنائها بشكل أفضل

نعود للشيفرة السابقة ونزيل سطر طباعة البيانات، ونضيف الدالة التي نريد إنشاءها وهي 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 متداخلة ستطبع كما هو متوقع وهذا هو الناتج:

walkdom to print component elements

بما أننا استطعنا المرور على جميع عناصر المكون جاء الدور الآن على تغيير بُنية دالة 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 بعد جلب البيانات:

refreshdom and update elements with x text

يمكنك فتح نافذة الطرفية ومحاولة تغيير قيمة count إلى قيمة أخرى مثلًا:

data.count = 5

بهذا الشكل ستغيير القيمة الفعلية للعداد، لكن لن تتحدث القيمة في العنصر الموافق، ونحتاج إلى استدعاء refreshDOM بعد تعديل القيمة حتى تتحدث في الصفحة:

data.count = 5
refreshDOM()

وستلاحظ أن القيمة تتغير بمجرد استدعاء الدالة، وكلما غيرت قيمة data.count إلى قيمة أخرى واستدعيت الدالة refreshDOM ستستجيب العناصر، وهذا ما ذكرته في البداية عن المبدأين (مراقبة البيانات وتحديث شجرة DOM عندما تتغير البيانات) كما هو موضح أدناه:

update data and call refreshdom

لكن كما تلاحظ فإن عملية تحديث شجرة 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}) }`)
            })
        })
    })
}

نفس التوجه، فقط وسعنا المنطق ليشمل أحداث متعددة وليس فقط حدث النقر، الآن إذا فتحت الصفحة ومررت مؤشر الفأرة فوق زر الزيادة سيستجيب المكون للحدث وتزداد قيمة العداد. وبهذا الشكل جعلنا النموذج يقبل أحداث متنوعة بإمكانك تجربتها وستجد أن المكون يستجيب لها.

listen to others events

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

add new directive like x show

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

اقرأ أيضًا


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...