Astro กับ Alpine.js: Lightweight Interactivity
#astro13 เม.ย. 2569
Astro กับ Alpine.js: Lightweight Interactivity
Alpine.js เป็น minimal JavaScript framework ที่เพิ่ม interactivity ให้กับ HTML โดยตรง ด้วยขนาดเพียง ~15KB และ syntax ที่คล้าย Vue.js ทำให้ Alpine.js เป็นตัวเลือกที่ยอดเยี่ยมสำหรับ Astro เมื่อต้องการ interactivity เล็กน้อยโดยไม่ต้องการ full framework
ทำไมต้องใช้ Alpine.js กับ Astro?
- Tiny Size: ~15KB minified+gzipped
- No Build Step: ใช้งานได้โดยตรงใน HTML
- Familiar Syntax: คล้าย Vue.js
- Progressive Enhancement: เพิ่ม interactivity ทีละน้อย
- No Virtual DOM: อัปเดต DOM โดยตรง
การติดตั้ง
วิธีที่ 1: ผ่าน CDN (ง่ายที่สุด)
---
// src/layouts/BaseLayout.astro
---
<html>
<head>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" is:inline></script>
</head>
<body>
<slot />
</body>
</html>
วิธีที่ 2: ผ่าน npm
npm install alpinejs
<script>
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
</script>
Alpine.js Directives พื้นฐาน
---
// src/pages/demo.astro
---
<html lang="th">
<body>
<!-- x-data: กำหนด component state -->
<div x-data="{ count: 0, name: 'นักพัฒนา' }">
<p>สวัสดี, <span x-text="name"></span>!</p>
<p>Count: <span x-text="count"></span></p>
<!-- x-on: event handlers -->
<button x-on:click="count++">เพิ่ม</button>
<button @click="count--">ลด</button>
<button @click="count = 0">Reset</button>
<!-- x-show: แสดง/ซ่อน element -->
<p x-show="count > 5" x-transition>Count มากกว่า 5!</p>
<!-- x-if: render/remove element -->
<template x-if="count === 0">
<p>Count เป็น 0</p>
</template>
<!-- x-bind: bind attributes -->
<button
:disabled="count >= 10"
:class="{ 'opacity-50': count >= 10 }"
@click="count++"
>
เพิ่ม (max 10)
</button>
</div>
</body>
</html>
Dropdown Menu
<div x-data="{ open: false }" class="relative">
<button
@click="open = !open"
@keydown.escape="open = false"
:aria-expanded="open"
class="btn"
>
เมนู
<svg
:class="{ 'rotate-180': open }"
class="w-4 h-4 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div
x-show="open"
@click.outside="open = false"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg"
>
<a href="/profile" class="block px-4 py-2 hover:bg-gray-100">โปรไฟล์</a>
<a href="/settings" class="block px-4 py-2 hover:bg-gray-100">ตั้งค่า</a>
<button class="block w-full text-left px-4 py-2 hover:bg-gray-100 text-red-600">
ออกจากระบบ
</button>
</div>
</div>
Modal Dialog
<div x-data="{ showModal: false }">
<button @click="showModal = true" class="btn-primary">
เปิด Modal
</button>
<!-- Modal Backdrop -->
<div
x-show="showModal"
x-transition.opacity
class="fixed inset-0 bg-black bg-opacity-50 z-40"
@click="showModal = false"
></div>
<!-- Modal Content -->
<div
x-show="showModal"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
@keydown.escape.window="showModal = false"
>
<div class="bg-white rounded-xl shadow-xl max-w-md w-full p-6" @click.stop>
<h2 class="text-xl font-bold mb-4">หัวข้อ Modal</h2>
<p class="text-gray-600 mb-6">เนื้อหาของ modal</p>
<div class="flex justify-end gap-3">
<button @click="showModal = false" class="btn-secondary">ยกเลิก</button>
<button @click="showModal = false" class="btn-primary">ยืนยัน</button>
</div>
</div>
</div>
</div>
Alpine.js Components (x-data functions)
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('searchBox', () => ({
query: '',
results: [],
loading: false,
open: false,
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
this.loading = true;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(this.query)}`);
const data = await res.json();
this.results = data.hits;
this.open = true;
} finally {
this.loading = false;
}
},
clear() {
this.query = '';
this.results = [];
this.open = false;
},
}));
Alpine.data('toast', () => ({
messages: [],
add(message, type = 'info', duration = 3000) {
const id = Date.now();
this.messages.push({ id, message, type });
setTimeout(() => this.remove(id), duration);
},
remove(id) {
this.messages = this.messages.filter((m) => m.id !== id);
},
}));
});
</script>
<!-- ใช้งาน searchBox component -->
<div
x-data="searchBox"
class="relative"
>
<input
type="search"
x-model="query"
@input.debounce.300ms="search"
@keydown.escape="clear"
placeholder="ค้นหา..."
/>
<div x-show="loading">กำลังค้นหา...</div>
<div x-show="open && results.length > 0" @click.outside="open = false">
<template x-for="result in results" :key="result.objectID">
<a :href="`/blog/${result.slug}`" x-text="result.title"></a>
</template>
</div>
</div>
Alpine.js Stores
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
get count() {
return this.items.reduce((sum, item) => sum + item.qty, 0);
},
add(product) {
const existing = this.items.find((i) => i.id === product.id);
if (existing) {
existing.qty++;
} else {
this.items.push({ ...product, qty: 1 });
}
},
remove(id) {
this.items = this.items.filter((i) => i.id !== id);
},
clear() {
this.items = [];
},
});
});
</script>
<!-- ใช้งาน store -->
<div x-data>
<span x-text="$store.cart.count">0</span> รายการ
<span x-text="$store.cart.total">0</span> บาท
</div>
สรุป
Alpine.js กับ Astro เป็นคู่ที่ลงตัวสำหรับ websites ที่ต้องการ interactivity เล็กน้อยโดยไม่ต้องการ full JavaScript framework ด้วยขนาดที่เล็ก, syntax ที่เรียบง่าย, และการทำงานโดยตรงใน HTML ทำให้ developer สามารถเพิ่ม interactivity ได้อย่างรวดเร็วและมีประสิทธิภาพ