🏭 معرفی Alpine.js

Alpine.js یک فریم‌ورک جاوااسکریپت بسیار سبک و قدرتمند است که توسط Caleb Porzio ساخته شده است. این فریم‌ورک به شما امکان می‌دهد رفتارهای ریکتیو (واکنش‌پذیر) را مستقیماً در HTML خود بنویسید — دقیقاً مثل Vue یا React، اما با حجم بسیار کمتر (فقط حدود ۱۵ کیلوبایت gzipped).

ⓘ چرا Alpine.js؟
Alpine.js ایده‌آل است برای پروژه‌هایی که به تعاملات کوچک نیاز دارند — مثل منوهای کشویی، مودال‌ها، تب‌ها، فرم‌های اعتبارسنجی — بدون اینکه به یک SPA کامل نیاز باشد.

مقایسه با Vue و React

ویژگی Alpine.js Vue.js React
حجم (gzipped) ~۱۵KB ~۳۳KB ~۴۵KB+
نیاز به بیلد ندارد اختیاری دارد
منحنی یادگیری خیلی کم متوسط زیاد
مناسب برای SPA خیر بله بله
نصب با CDN بله (یک اسکریپت) بله محدود
مناسب برای صفحات چندتایی عالی متوسط ضعیف

چه زمانی از Alpine.js استفاده کنیم؟

نصب Alpine.js

سه روش برای نصب Alpine.js وجود دارد:

<!-- روش ۱: CDN (پیشنهادی برای شروع) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<!-- روش ۲: فایل محلی (همین صفحه) -->
<script defer src="alpine.min.js"></script>

<!-- روش ۳: npm -->
// npm install alpinejs
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()
⚠ نکته مهم: حتماً از defer استفاده کنید تا Alpine بعد از لود DOM اجرا شود. بدون defer، Alpine ممکن است عناصر HTML را نبیند.

دموی اولیه — شمارنده ساده

▶ دمو: شمارنده با Alpine.js
0
<!-- تنها چند خط HTML کافی است! -->
<div x-data="{ count: 0 }">
  <p x-text="count"></p>
  <button @click="count--">کاهش</button>
  <button @click="count++">افزایش</button>
</div>

📃 x-data و x-text

x-data قلب Alpine.js است. این دایرکتیو یک «حوزه داده» (scope) ریکتیو تعریف می‌کند. هر عنصر HTML که x-data داشته باشد، به یک کامپوننت Alpine تبدیل می‌شود و تمام فرزندانش به داده‌های آن دسترسی دارند.

ساختار x-data

<!-- داده ساده -->
<div x-data="{ name: 'علی', age: 25 }">
  ...
</div>

<!-- با متدها -->
<div x-data="{
  count: 0,
  increment() { this.count++ },
  decrement() { this.count-- },
  reset() { this.count = 0 }
}">
  ...
</div>

<!-- بدون داده (فقط رفتار) -->
<div x-data>
  ...
</div>

x-text — نمایش داده

x-text محتوای متنی یک عنصر را به یک مقدار ریکتیو متصل می‌کند. هر بار که داده تغییر کند، متن خودکار به‌روز می‌شود.

<p x-text="'سلام ' + name"></p>
<span x-text="price * quantity + ' تومان'"></span>
<div x-text="items.length + ' آیتم'"></div>

x-html — محتوای HTML

<div x-html="htmlContent"></div>
<!-- خروجی HTML رندر می‌شود، نه متن خام -->
⚠ هشدار: از x-html فقط با داده‌های مطمئن استفاده کنید. ورودی کاربر را هرگز بدون sanitize در x-html نگذارید (XSS).

دموی شمارنده پیشرفته با متدها

▶ دمو: شمارنده با متدها و وضعیت پویا

وضعیت:

عبارت محاسبه‌شده:
<div x-data="{
  count: 0,
  step: 1,
  increment() { this.count += this.step },
  decrement() { this.count -= this.step },
  reset() { this.count = 0 }
}">
  <p x-text="count"></p>
  <select x-model="step">
    <option value="1">۱</option>
    <option value="5">۵</option>
  </select>
  <button @click="decrement()">کاهش</button>
  <button @click="increment()">افزایش</button>
</div>
💡 نکته: در Alpine.js 3، می‌توانید کامپوننت‌های پیچیده را با Alpine.data() تعریف کنید تا کد HTML تمیزتر بماند و بتوان از آن‌ها بارها استفاده کرد.

x-init — اجرا هنگام راه‌اندازی

x-init یک expression است که دقیقاً یک‌بار، وقتی کامپوننت Alpine مقداردهی اولیه می‌شود اجرا می‌گردد. برای fetch داده، تنظیم state اولیه، یا راه‌اندازی third-party کتابخانه‌ها ایده‌آل است.

<!-- روش ۱: inline در x-data -->
<div x-data="{
  users: [],
  init() {
    fetch('/api/users')
      .then(r => r.json())
      .then(data => this.users = data);
  }
}">...</div>

<!-- روش ۲: x-init جداگانه -->
<div x-data="{ count: 0 }"
     x-init="count = parseInt(localStorage.getItem('count') || 0)">
  <span x-text="count"></span>
</div>

<!-- روش ۳: async init -->
<div x-data="{
  post: null,
  loading: true,
  async init() {
    this.post = await fetch('/api/post/1').then(r => r.json());
    this.loading = false;
  }
}">
  <div x-show="loading">در حال بارگذاری...</div>
  <div x-show="!loading" x-text="post?.title"></div>
</div>

Alpine.data() — تعریف کامپوننت قابل استفاده مجدد

با Alpine.data() می‌توانید کامپوننت‌های Alpine را بیرون از HTML تعریف کنید. این روش کد را تمیزتر نگه می‌دارد و امکان استفاده مجدد در چند جا را فراهم می‌کند.

<!-- تعریف کامپوننت در script -->
<script>
document.addEventListener('alpine:init', () => {
  Alpine.data('searchBox', () => ({
    query: '',
    results: [],
    loading: false,

    async search() {
      if (!this.query) { this.results = []; return; }
      this.loading = true;
      // شبیه‌سازی API call
      await new Promise(r => setTimeout(r, 300));
      this.results = ['نتیجه ۱', 'نتیجه ۲'];
      this.loading = false;
    }
  }));
});
</script>

<!-- استفاده ساده در HTML -->
<div x-data="searchBox">
  <input x-model="query" @input.debounce.300ms="search()">
  <div x-show="loading">در حال جستجو...</div>
  <template x-for="r in results">
    <div x-text="r"></div>
  </template>
</div>
ⓘ تفاوت init() با x-init:
init() به عنوان یک متد داخل x-data تعریف می‌شود و Alpine آن را خودکار فراخوانی می‌کند. x-init یک attribute جداگانه است و می‌توان هر دو را با هم استفاده کرد — ابتدا init() سپس x-init اجرا می‌شود.

🔗 x-bind و x-model

x-bind به شما امکان می‌دهد مقادیر attribute های HTML را به داده‌های Alpine متصل کنید. می‌توان از shorthand : هم استفاده کرد. x-model یک binding دوطرفه ایجاد می‌کند — تغییر داده، input را آپدیت می‌کند و بالعکس.

x-bind — اتصال یک‌طرفه

<!-- شکل کامل -->
<img x-bind:src="imageUrl" x-bind:alt="imageAlt">

<!-- shorthand با : -->
<img :src="imageUrl" :alt="imageAlt">

<!-- اتصال class پویا -->
<div :class="{ 'active': isActive, 'error': hasError }"></div>

<!-- اتصال style -->
<div :style="{ backgroundColor: color, fontSize: size + 'px' }"></div>

<!-- disabled پویا -->
<button :disabled="!isValid">ارسال</button>

x-model — اتصال دوطرفه

<input x-model="name">         <!-- text input -->
<textarea x-model="message"></textarea>
<input type="checkbox" x-model="agreed">
<select x-model="selected">...</select>

<!-- modifier: به‌روز رسانی با تاخیر (پس از blur) -->
<input x-model.lazy="name">

<!-- modifier: تبدیل به number -->
<input type="number" x-model.number="age">

<!-- modifier: حذف فاصله‌های اضافه -->
<input x-model.trim="username">

دمو: فرم نمایه‌ساز زنده

▶ دمو: پیش‌نمایش زنده با x-model و x-bind
💡 نکته: :style می‌پذیرد یک object با camelCase propertyها. مثلاً :style="{ backgroundColor: color }" معادل style="background-color: ..." است.

👁 x-show و x-if

دو دایرکتیو برای نمایش شرطی عناصر وجود دارد: x-show و x-if. هر کدام رویکرد متفاوتی دارند.

تفاوت x-show و x-if

ویژگی x-show x-if
روش کار display:none می‌گذارد از DOM حذف/اضافه می‌کند
عنصر در DOM همیشه هست فقط وقتی true است
پشتیبانی از transition بله (x-transition) بله (template)
سرعت toggle سریع‌تر کندتر (re-render)
مناسب برای toggle مکرر نمایش یکبار یا هرگز
<!-- x-show -->
<div x-show="isVisible">
  این عنصر پنهان/نمایان می‌شود
</div>

<!-- x-if (باید داخل template باشد) -->
<template x-if="isLoggedIn">
  <div>خوش آمدید!</div>
</template>

<!-- x-show با transition -->
<div x-show="open" x-transition>
  محتوا با انیمیشن ظاهر می‌شود
</div>

دمو: کنترل نمایش با x-show و x-if

▶ دمو: toggle ها با x-show و x-if
۱. نمایش/پنهان با x-show:
✅ این عنصر نمایان است!
۲. ورود/خروج با x-if:
۳. سیستم alert پویا:

📅 x-for — رندر لیست

x-for برای رندر پویای لیست‌ها به کار می‌رود. این دایرکتیو باید روی تگ <template> استفاده شود.

<!-- لیست ساده -->
<template x-for="item in items" :key="item.id">
  <li x-text="item.name"></li>
</template>

<!-- با index -->
<template x-for="(item, index) in items" :key="index">
  <li x-text="(index+1) + '. ' + item"></li>
</template>

<!-- حلقه عددی -->
<template x-for="i in 5">
  <span x-text="i"></span>
</template>
ⓘ :key چیست؟
Alpine از :key برای تشخیص هویت هر آیتم استفاده می‌کند تا DOM را بهینه به‌روز کند. همیشه از یک مقدار یکتا (مثل id) به عنوان key استفاده کنید.

دمو: لیست وظایف (Todo List)

▶ دمو: Todo List کامل با x-for
هیچ وظیفه‌ای یافت نشد
تعداد کل: انجام شده: باقیمانده:

حلقه تودرتو (Nested x-for)

<!-- x-for تودرتو برای داده‌های سلسله‌مراتبی -->
<div x-data="{
  categories: [
    { name: 'فرانت‌اند', items: ['HTML', 'CSS', 'JavaScript', 'Alpine.js'] },
    { name: 'بک‌اند',    items: ['PHP', 'Python', 'Node.js'] },
    { name: 'دیتابیس',  items: ['MySQL', 'MongoDB', 'Redis'] }
  ]
}">
  <template x-for="cat in categories" :key="cat.name">
    <div>
      <h4 x-text="cat.name"></h4>
      <ul>
        <template x-for="item in cat.items" :key="item">
          <li x-text="item"></li>
        </template>
      </ul>
    </div>
  </template>
</div>

فیلتر و مرتب‌سازی لیست (Computed)

با استفاده از get (computed property) می‌توانید لیست فیلترشده یا مرتب‌شده را مستقیماً در template استفاده کنید:

<div x-data="{
  search: '',
  sort: 'name',
  products: [
    { id: 1, name: 'لپتاپ',   price: 25000000, stock: 3 },
    { id: 2, name: 'موبایل',  price: 12000000, stock: 0 },
    { id: 3, name: 'تبلت',    price: 8500000,  stock: 5 }
  ],
  get filtered() {
    return this.products
      .filter(p => p.name.includes(this.search))
      .sort((a, b) => this.sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
  }
}">
  <input x-model="search" placeholder="جستجو...">
  <select x-model="sort">
    <option value="name">نام</option>
    <option value="price">قیمت</option>
  </select>
  <template x-for="p in filtered" :key="p.id">
    <div>
      <span x-text="p.name"></span>
      <span x-text="p.price.toLocaleString() + ' تومان'"></span>
      <span x-text="p.stock === 0 ? 'ناموجود' : 'موجود'"></span>
    </div>
  </template>
</div>

دمو: فروشگاه با جستجو و مرتب‌سازی

▶ دمو: لیست محصولات با فیلتر و مرتب‌سازی زنده
محصول یافت شد برای «»
🔍 محصولی با این نام یافت نشد
⚠ نکته مهم درباره :key: همیشه از یک مقدار یکتا و ثابت (مثل id) برای :key استفاده کنید. از index به عنوان key زمانی استفاده کنید که ترتیب لیست تغییر نمی‌کند، چون Alpine از key برای بهینه‌سازی DOM diffing استفاده می‌کند.

x-on — رویدادها

x-on (یا shorthand @) برای گوش دادن به رویدادهای DOM استفاده می‌شود. Alpine از تمام رویدادهای استاندارد HTML پشتیبانی می‌کند.

رویدادهای رایج

<!-- کلیک -->
<button @click="doSomething()"></button>
<button x-on:click="doSomething()"></button>  <!-- معادل -->

<!-- کیبورد -->
<input @keyup.enter="submit()">       <!-- Enter key -->
<input @keyup.escape="close()">      <!-- Escape key -->
<input @keydown.ctrl.s="save()">    <!-- Ctrl+S -->

<!-- فرم -->
<form @submit.prevent="handleSubmit()"></form>

<!-- موس -->
<div @mouseover="hovered = true" @mouseleave="hovered = false"></div>

<!-- Window events -->
<div @keydown.window="handleKey($event)"></div>

<!-- فقط یکبار اجرا شود -->
<button @click.once="doOnce()"></button>

<!-- اجرای debounced -->
<input @input.debounce.300ms="search()">

دمو: انواع رویدادها

▶ دمو: رویدادهای مختلف Alpine.js
رویداد کلیک:
رویداد کیبورد (@keyup):
رویداد موس:
رویداد @submit.prevent:
✅ فرم بدون reload ارسال شد! نام:
لاگ رویدادها:

modifier های رویداد

Modifierتوضیحمثال
.preventجلوگیری از رفتار پیش‌فرض@submit.prevent
.stopتوقف event bubbling@click.stop
.onceفقط یکبار اجرا@click.once
.windowروی window گوش دهد@keydown.window
.documentروی document گوش دهد@click.document
.debounceبا تاخیر اجرا@input.debounce.500ms
.throttleمحدودیت تعداد اجرا@scroll.throttle.100ms
.selfفقط اگر روی خود element بود@click.self
.outsideکلیک خارج از element@click.outside

🎪 x-transition — ترنزیشن‌ها

Alpine.js یک سیستم ترنزیشن داخلی دارد که با x-transition فعال می‌شود. می‌توانید انیمیشن‌های نمایش و پنهان‌سازی عناصر را به راحتی تعریف کنید.

روش‌های مختلف x-transition

<!-- ساده‌ترین حالت: transition پیش‌فرض -->
<div x-show="open" x-transition>محتوا</div>

<!-- با duration -->
<div x-show="open" x-transition.duration.500ms>محتوا</div>

<!-- با origin برای scale -->
<div x-show="open" x-transition.origin.top.right>منو</div>

<!-- کنترل کامل با modifier ها -->
<div x-show="open"
     x-transition:enter="transition ease-out duration-300"
     x-transition:enter-start="opacity-0 transform scale-90"
     x-transition:enter-end="opacity-100 transform scale-100"
     x-transition:leave="transition ease-in duration-300"
     x-transition:leave-start="opacity-100 transform scale-100"
     x-transition:leave-end="opacity-0 transform scale-90">
  محتوا
</div>

دموی ترنزیشن‌ها

▶ دمو: انواع ترنزیشن با Alpine.js
● Fade (پیش‌فرض)
Fade Transition
● طولانی (700ms)
Duration 700ms
● Origin از بالا
Origin: top left
● Slide Down (CSS)
Custom CSS
💡 نکته: Alpine به‌طور خودکار opacity و scale را انیمیت می‌کند. برای سفارشی‌سازی می‌توانید از Tailwind CSS یا CSS transitions استفاده کنید.

🌟 $store و ویژگی‌های پیشرفته

Alpine.js ابزارهای قدرتمندی برای موارد پیشرفته‌تر فراهم می‌کند: Alpine.store() برای state مشترک، $refs برای دسترسی مستقیم به DOM، $nextTick برای اجرای کد بعد از آپدیت DOM، و چندین خاصیت جادویی (magic properties).

Alpine.store() — حالت سراسری

Alpine.store() یک store (فروشگاه داده) سراسری ایجاد می‌کند که تمام کامپوننت‌ها می‌توانند به آن دسترسی داشته باشند — مثل Vuex ساده‌شده.

<!-- تعریف store (در script tag) -->
<script>
document.addEventListener('alpine:init', () => {
  Alpine.store('counter', {
    count: 0,
    increment() { this.count++ },
    decrement() { this.count-- }
  });

  Alpine.store('theme', {
    dark: false,
    toggle() { this.dark = !this.dark }
  });
});
</script>

<!-- استفاده در هر کامپوننتی -->
<div x-data>
  <span x-text="$store.counter.count"></span>
  <button @click="$store.counter.increment()">+</button>
</div>

$refs — دسترسی به DOM

<div x-data="{ focus() { this.$refs.myInput.focus() } }">
  <input x-ref="myInput" placeholder="...">
  <button @click="focus()">فوکوس</button>
</div>

خاصیت‌های جادویی (Magic Properties)

خاصیتتوضیح
$elعنصر DOM ریشه کامپوننت
$refsدسترسی به عناصر با x-ref
$storeدسترسی به Alpine.store
$dataشیء داده‌های کامپوننت
$watchمشاهده تغییرات یک property
$nextTickاجرا بعد از آپدیت DOM
$dispatchانتشار custom event
$eventآبجکت event در handler
$rootعنصر ریشه نزدیک‌ترین x-data
$idID یکتا برای accessibility

$watch — مشاهده تغییرات

<div x-data="{
  name: '',
  init() {
    this.$watch('name', value => {
      console.log('نام تغییر کرد:', value);
    })
  }
}">
  <input x-model="name">
</div>

$nextTick — بعد از رندر

<div x-data="{
  items: [],
  async addItem() {
    this.items.push('آیتم جدید');
    await this.$nextTick();
    // حالا DOM آپدیت شده
    this.$refs.list.scrollTop = this.$refs.list.scrollHeight;
  }
}">...</div>

$dispatch — انتشار رویداد

<!-- فرزند: ارسال رویداد -->
<button @click="$dispatch('notify', { message: 'عملیات موفق' })">
  ارسال اعلان
</button>

<!-- والد: دریافت رویداد -->
<div @notify.window="alert($event.detail.message)">
  ...
</div>

Alpine.data() — کامپوننت‌های قابل استفاده مجدد

<script>
document.addEventListener('alpine:init', () => {
  Alpine.data('dropdown', () => ({
    open: false,
    toggle() { this.open = !this.open },
    close() { this.open = false }
  }));
});
</script>

<!-- استفاده بارها بدون تکرار کد -->
<div x-data="dropdown">
  <button @click="toggle()">منو</button>
  <div x-show="open" @click.outside="close()">
    ...
  </div>
</div>

دمو: ترکیب ویژگی‌های پیشرفته

▶ دمو: $refs، $watch، $dispatch و store یکپارچه
۱. Alpine.store (حالت سراسری):

دو کامپوننت مستقل که یک store را به اشتراک می‌گذارند:

کامپوننت A
کامپوننت B

هر دو مقدار یکی هستند — state مشترک!

۲. $refs — دسترسی مستقیم به DOM:
۳. $watch — زیرنظر گرفتن تغییرات:
💡 خلاصه بهترین روش‌ها:
  • از x-data برای state محلی کامپوننت استفاده کنید
  • از Alpine.store() برای state مشترک بین کامپوننت‌ها استفاده کنید
  • از Alpine.data() برای کامپوننت‌های قابل استفاده مجدد استفاده کنید
  • همیشه defer را روی تگ script فراموش نکنید
  • از :key در x-for استفاده کنید

منابع بیشتر

🔗 alpinejs.dev 📚 مستندات رسمی 🆕 GitHub: alpinejs/alpine 💬 Discord Community 🎬 Alpine Toolbox ★ awesome-alpine

x-init، x-cloak و x-effect

این سه دایرکتیو چرخه حیات و رفتار ریکتیو کامپوننت را کنترل می‌کنند و برای ساخت تجربه‌های روان و بدون پرش (FOUC) ضروری هستند.

x-cloak — جلوگیری از Flash of Uninitialized Content

وقتی صفحه بار می‌شود، قبل از اینکه Alpine اجرا شود، ممکن است کاربر برای لحظه‌ای {{ message }} یا عناصر پنهان‌نشده را ببیند. x-cloak این مشکل را حل می‌کند.

<!-- ۱: یک بار این style را در <head> بگذارید -->
<style>
  [x-cloak] { display: none !important; }
</style>

<!-- ۲: روی کامپوننت اضافه کنید -->
<div x-data="{ name: 'علی' }" x-cloak>
  <!-- تا Alpine آماده نشود، این div پنهان است -->
  <p x-text="'سلام ' + name"></p>
</div>

<!-- بدون x-cloak، کاربر لحظه‌ای می‌بیند: x-text="'سلام ' + name" -->
<!-- با x-cloak، مستقیم نتیجه را می‌بیند: سلام علی             -->
ⓘ چه موقع از x-cloak استفاده کنیم؟
وقتی Alpine را از CDN لود می‌کنید (نه inline)، همیشه x-cloak روی کامپوننت‌های مهم صفحه قرار دهید تا تجربه بهتری داشته باشید.

x-effect — اثر ریکتیو

x-effect یک expression است که هر بار یکی از وابستگی‌هایش تغییر کند، دوباره اجرا می‌شود — بدون نیاز به صریحاً تعریف کردن $watch.

<!-- هر بار که count تغییر کند، title مرورگر آپدیت می‌شود -->
<div x-data="{ count: 0 }"
     x-effect="document.title = 'تعداد: ' + count">
  <button @click="count++">افزایش (<span x-text="count"></span>)</button>
</div>

<!-- مثال: ذخیره در localStorage هر بار که داده تغییر کند -->
<div x-data="{
  notes: JSON.parse(localStorage.getItem('notes') || '[]'),
  newNote: ''
}"
     x-effect="localStorage.setItem('notes', JSON.stringify(notes))">
  <input x-model="newNote" @keyup.enter="notes.push(newNote); newNote=''">
  <!-- هر بار notes تغییر کند، localStorage آپدیت می‌شود -->
</div>

مقایسه x-effect با $watch

ویژگیx-effect$watch
محل تعریفattribute روی elementداخل init() یا x-data
تشخیص وابستگیخودکاردستی (نام property)
دسترسی به مقدار قبلیندارددارد (oldValue)
چند propertyهر تعداد (خودکار)باید جداگانه $watch بزنید
اجرای اولیهبله (بلافاصله)خیر (فقط بعد از تغییر)

دمو: x-cloak و x-effect در عمل

▶ دمو: x-effect برای ذخیره‌سازی خودکار
کاراکتر ✓ ذخیره شد
💡 چه اتفاقی می‌افتد؟ x-effect هر بار که text تغییر می‌کند، متن را در localStorage ذخیره می‌کند. صفحه را Refresh کنید — متن باقی می‌ماند!

🌟 الگوهای واقعی — Modal، Tabs، Accordion، Dropdown

Alpine.js در ساخت کامپوننت‌های UI رایج بسیار می‌درخشد. در این بخش چهار الگوی پرکاربرد را با کد کامل می‌بینید که در پروژه‌های واقعی استفاده می‌شوند.

۱. Dropdown با @click.outside

یکی از رایج‌ترین موارد: منوی کشویی که با کلیک خارج از خودش بسته می‌شود.

<div x-data="{ open: false }" style="position:relative">
  <button @click="open = !open">
    منو <span x-text="open ? '▲' : '▼'"></span>
  </button>

  <div x-show="open"
       x-transition.origin.top.right
       @click.outside="open = false"
       style="position:absolute; top:100%; right:0; background:#fff; border:1px solid #eee;
              border-radius:8px; box-shadow:0 8px 25px rgba(0,0,0,0.12); min-width:200px; z-index:50">
    <a href="#" @click="open=false">👤 پروفایل</a>
    <a href="#" @click="open=false">⚙ تنظیمات</a>
    <a href="#" @click="open=false">🚪 خروج</a>
  </div>
</div>
▶ دمو: Dropdown با کلیک خارج

روی خارج از dropdown کلیک کنید تا بسته شود.

۲. Modal / Dialog

<div x-data="{ showModal: false }">
  <button @click="showModal = true">باز کردن مودال</button>

  <!-- Backdrop -->
  <div x-show="showModal"
       x-transition:enter="ease-out duration-300"
       x-transition:enter-start="opacity-0"
       x-transition:enter-end="opacity-100"
       @click.self="showModal = false"
       @keydown.escape.window="showModal = false"
       style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;
              align-items:center;justify-content:center;z-index:999">

    <!-- Modal box (کلیک روی این باعث بستن نمی‌شود: @click.self بالا) -->
    <div style="background:#fff;border-radius:12px;padding:32px;max-width:480px;width:90%">
      <h3>عنوان مودال</h3>
      <p>محتوای مودال اینجا قرار می‌گیرد.</p>
      <button @click="showModal = false">بستن</button>
    </div>
  </div>
</div>
▶ دمو: Modal با Escape و کلیک خارج

📚 راهنمای Alpine.js

Alpine.js یک فریم‌ورک سبک برای ساخت رابط‌های کاربری ریکتیو است. این مودال با کلیک روی خارج یا زدن Escape بسته می‌شود.

💡 نکته: @click.stop روی box باعث می‌شود کلیک داخل مودال آن را نبندد.

۳. Tabs

<div x-data="{ activeTab: 'intro' }">
  <!-- Tab buttons -->
  <div role="tablist">
    <button @click="activeTab = 'intro'"
             :class="{ 'active': activeTab === 'intro' }">معرفی</button>
    <button @click="activeTab = 'install'"
             :class="{ 'active': activeTab === 'install' }">نصب</button>
    <button @click="activeTab = 'usage'"
             :class="{ 'active': activeTab === 'usage' }">استفاده</button>
  </div>

  <!-- Tab panels -->
  <div x-show="activeTab === 'intro'">محتوای معرفی</div>
  <div x-show="activeTab === 'install'">محتوای نصب</div>
  <div x-show="activeTab === 'usage'">محتوای استفاده</div>
</div>
▶ دمو: Tabs کامل

Alpine.js یک فریم‌ورک جاوااسکریپت سبک (۱۵KB gzip) است که امکان افزودن رفتار ریکتیو به HTML را بدون build step می‌دهد. ساخته Caleb Porzio است.

  • نیاز به هیچ build pipeline ندارد
  • کاملاً درون HTML نوشته می‌شود
  • منحنی یادگیری بسیار کم دارد
  • با Laravel، Django، Rails کاملاً سازگار است
  • جایگزین عالی jQuery برای تعاملات ساده
✅ Alpine مناسب است برای: Dropdown، Modal، Tabs، Accordion، Form Validation، Toggle
❌ Alpine مناسب نیست برای: SPA کامل، Real-time apps، State management پیچیده
<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>
    سلام Alpine! 👋
  </div>
</div>

۴. Accordion

<div x-data="{
  active: null,
  items: [
    { id: 1, q: 'Alpine.js چیست؟', a: 'یک فریم‌ورک سبک ریکتیو...' },
    { id: 2, q: 'آیا نیاز به npm دارد؟', a: 'خیر، می‌توانید از CDN استفاده کنید...' }
  ]
}">
  <template x-for="item in items" :key="item.id">
    <div>
      <button @click="active = active === item.id ? null : item.id">
        <span x-text="item.q"></span>
        <span x-text="active === item.id ? '−' : '+'"></span>
      </button>
      <div x-show="active === item.id" x-transition>
        <p x-text="item.a"></p>
      </div>
    </div>
  </template>
</div>
▶ دمو: Accordion (سوالات متداول)
💡 الگوی Single-open Accordion: با نگه‌داشتن active به عنوان ID آیتم باز، همیشه فقط یک آیتم باز است. اگر بخواهید چند آیتم همزمان باز باشند، active را به یک Set تبدیل کنید و با active.has(id) چک کنید.

🔌 پلاگین‌های رسمی Alpine.js

Alpine.js دارای یک اکوسیستم پلاگین رسمی است که قابلیت‌های اضافی را بدون افزایش حجم اصلی فراهم می‌کند. هر پلاگین را جداگانه install می‌کنید و فقط چیزی که نیاز دارید را اضافه می‌کنید.

جدول پلاگین‌های رسمی

پلاگین CDN کاربرد Directive / Magic
Intersect @alpinejs/intersect تشخیص ورود/خروج element به viewport x-intersect
Persist @alpinejs/persist ذخیره state در localStorage خودکار $persist
Focus @alpinejs/focus مدیریت Focus Trap (برای Modal/Drawer) x-trap
Collapse @alpinejs/collapse انیمیشن بستن/باز شدن ارتفاع (Accordion) x-collapse
Mask @alpinejs/mask Input masking (تلفن، کارت بانکی، تاریخ) x-mask
Morph @alpinejs/morph انتقال روان DOM بین دو حالت Alpine.morph()
Sort @alpinejs/sort Drag and Drop مرتب‌سازی با Alpine x-sort

پلاگین Persist — ذخیره خودکار state

با $persist، مقدار یک property به‌صورت خودکار در localStorage ذخیره و بازیابی می‌شود:

<!-- نصب پلاگین -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<!-- استفاده: فقط $persist() اضافه کنید -->
<div x-data="{ count: $persist(0), theme: $persist('light') }">
  <button @click="count++">
    کلیک‌ها: <span x-text="count"></span>
  </button>
  <!-- بعد از refresh هم مقدار count حفظ می‌شود! -->
</div>

پلاگین Intersect — لود lazy

<!-- وقتی element به viewport می‌رسد، تصویر لود می‌شود -->
<div x-data="{ loaded: false }"
     x-intersect="loaded = true">
  <template x-if="!loaded">
    <div style="background:#eee;height:200px;border-radius:8px">در حال بارگذاری...</div>
  </template>
  <img x-show="loaded" src="image.jpg" alt="...">
</div>

<!-- اجرا فقط یکبار (enter) -->
<div x-intersect:enter.once="animate()">...</div>

پلاگین Mask — Input Formatting

<!-- شماره تلفن -->
<input x-mask="0999-999-9999" placeholder="09xx-xxx-xxxx">

<!-- شماره کارت بانکی -->
<input x-mask="9999-9999-9999-9999" placeholder="xxxx-xxxx-xxxx-xxxx">

<!-- تاریخ -->
<input x-mask="9999/99/99" placeholder="YYYY/MM/DD">

<!-- Mask داینامیک -->
<input x-mask:dynamic="$input.startsWith('0') ? '09999999999' : '9999-999-9999'">

پلاگین Collapse — Accordion انیمیشن ارتفاع

<!-- x-collapse انیمیشن تغییر ارتفاع را به شکل طبیعی انجام می‌دهد -->
<div x-data="{ open: false }">
  <button @click="open = !open">سوال ▼</button>
  <div x-show="open" x-collapse>
    <p>پاسخ با انیمیشن slide-down نمایش داده می‌شود</p>
  </div>
</div>
ⓘ ترتیب لود پلاگین‌ها:
پلاگین‌ها باید قبل از فایل اصلی Alpine لود شوند. Alpine در هنگام start کردن پلاگین‌ها را register می‌کند، پس اگر بعد از Alpine لود شوند کار نمی‌کنند.
💡 خلاصه ویژگی‌های کامل Alpine.js 3:
Directives:
x-data x-bind / : x-on / @ x-text x-html x-model x-show x-if x-for x-transition x-effect x-ref x-cloak x-init x-id x-ignore
Magic Properties:
$el $refs $store $watch $dispatch $nextTick $root $data $id $event

🚀 زمین بازی کد (Playground)

directiveهای Alpine را همین‌جا ویرایش کن و نتیجه را زنده ببین. یک مثال را از منوی بالا انتخاب کن، x-data، x-model، x-for و… را تغییر بده و دکمهٔ ▶ اجرا را بزن (یا فقط تایپ کن تا خودکار اجرا شود). نسخهٔ Alpine v3.13.5 همین صفحه به‌صورت آفلاین داخل تمرین بارگذاری می‌شود.

💡 این محیط کاملاً جداست (iframe ایزوله)؛ هر چه می‌خواهی امتحان کن — صفحهٔ آموزش خراب نمی‌شود. برای دیدن خروجی از console.log() استفاده کن.