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

برمجة تطبيق ملاحظات باستخدام إطار العمل 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>

بعد فتح الملف على المتصفح ستحصل على الشكل التالي:

design

يتكون القالب من قسمين، قائمة جانبية تحتوي على الملاحظات الموجودة والتي أنشأها المستخدم، زر إنشاء ملاحظة جديدة، عُنصر الملاحظة يتكون من عنوان ومحتوى وتاريخ آخر تعديل، القسم الثاني عبارة عن عرض للملاحظة الفعالة يتكون من حقل إدخال للعُنوان وحقل للمحتوى وفي الأسفل زر حذف الملاحظة.

شرح بناء التطبيق

سنعتمد في بناء التطبيق على الملاحظة الفعالة بحيث عند فتح الصفحة لأول مرة سيتم مباشرة إنشاء ملاحظة بقيم فارغة وعرضها في قسم العرض ليبدأ المستخدم في كتابة البيانات مباشرة وسيتم حفظ الملاحظة تلقائيًا مع الكتابة، أما إن كان للمستخدم ملاحظات سابقة سنجعل آخر ملاحظة قام المستخدم بتعديلها هي الملاحظة الفعالة وسنقوم بفتحها في قسم العرض لتعديلها، في قسم عرض الملاحظات سنعتمد على حقل آخر تعديل حتى نقوم بترتيب الملاحظات وبعد كل تعديل على الملاحظة الفعالة نُعدل على حقل آخر تعديل الخاص بتلك الملاحظة.

تضمين 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 محرف.

ستكون النتيجة:

listing notes

وبطبيعة الحال عند التعديل في قسم العرض ستتحدث بيانات الملاحظة الفعالة وهي الملاحظة الأولى.

إضافة تاريخ آخر تعديل وتغييره بعد كل تعديل على الملاحظة الفعالة

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

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...