Админка
пожалуйста подождите

Alpine.js добавляет реактивность в HTML без сложности полноценных фреймворков. При размере всего 15 KB он идеально подходит для добавления интерактивности на страницы, отрендеренные на сервере, многостраничные приложения или в любые случаи, когда нужен «ровно достаточный» JavaScript. В этом руководстве рассматривается создание интерактивных UI с Alpine.js с точки зрения senior-разработчика.

Почему Alpine.js

Alpine предлагает убедительные преимущества:

  1. Минимальный размер: ~15 KB в gzipped, загружается мгновенно
  2. Без шага сборки: Работает напрямую в HTML
  3. Декларативность: Поведение задаётся в HTML-атрибутах
  4. Синтаксис в стиле Vue: Знакомые паттерны, ниже сложность
  5. Идеален для улучшения: Добавляйте к существующим страницам, отрендеренным на сервере

Начало работы

Установка

CDN (самый простой вариант):

<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

NPM:

npm install alpinejs
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Основные директивы

x-data: состояние компонента

<!--Определить компонент с состоянием-->
<div x-data="{ open: false, count: 0, name: '' }">
<p>Count: <span x-text="count"></span></p>
<button @click="count++">Increment</button>
</div>
<!--Вложенные компоненты имеют изолированную область видимости-->
<div x-data="{ outer: 'parent' }">
<div x-data="{ inner: 'child' }">
<span x-text="outer"></span> <!--Работает-->
<span x-text="inner"></span> <!--Работает-->
</div>
</div>

x-on: обработка событий

<!--События клика-->
<button x-on:click="open = !open">Toggle</button>
<button @click="open = !open">Toggle (shorthand)</button>
<!--События клавиатуры-->
<input @keydown.enter="submitForm()" />
<input @keydown.escape="cancel()" />
<!--Модификаторы-->
<button @click.prevent="handleClick()">Prevent Default</button>
<button @click.stop="handleClick()">Stop Propagation</button>
<button @click.once="runOnce()">Run Once</button>
<button @click.debounce.500ms="search()">Debounced</button>
<!--Несколько событий-->
<input @focus="focused = true" @blur="focused = false" />

x-bind: динамические атрибуты

<!--Привязка атрибутов-->
<button x-bind:disabled="isLoading">Submit</button>
<button :disabled="isLoading">Submit (shorthand)</button>
<!--Динамические классы-->
<div :class="{ 'active': isActive, 'hidden': !isVisible }"></div>
<div :class="isActive ? 'bg-blue-500' : 'bg-gray-500'"></div>
<!--Динамические стили-->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>

x-model: двустороннее связывание

<div x-data="{ message: '', selected: 'a', checked: false, items: [] }">
<!--Текстовый ввод-->
<input type="text" x-model="message" />
<p x-text="message"></p>
<!--Модификаторы-->
<input x-model.lazy="message" /> <!--Синхронизация при потере фокуса-->
<input x-model.number="count" /> <!--Приведение к числу-->
<input x-model.debounce.500ms="search" /> <!--Debounce-->
<!--Select-->
<select x-model="selected">
<option value="a">Option A</option>
<option value="b">Option B</option>
</select>
<!--Checkbox-->
<input type="checkbox" x-model="checked" />
<!--Несколько checkbox-->
<input type="checkbox" value="item1" x-model="items" />
<input type="checkbox" value="item2" x-model="items" />
<!--Radio-->
<input type="radio" value="a" x-model="selected" />
<input type="radio" value="b" x-model="selected" />
</div>

x-show и x-if: условный рендеринг

<!--x-show: переключает CSS display (остаётся в DOM)-->
<div x-show="isVisible">I'm visible</div>
<!--С переходами-->
<div x-show="open" x-transition>Fade in/out</div>
<div x-show="open"
x-transition.duration.500ms
x-transition:enter.opacity
x-transition:leave.opacity>
Custom transition
</div>
<!--x-if: фактически удаляет из DOM (нужно использовать template)-->
<template x-if="showForm">
<form>...</form>
</template>

x-for: циклы

<div x-data="{ items: ['Apple', 'Banana', 'Orange'] }">
<template x-for="(item, index) in items" :key="index">
<div>
<span x-text="index + 1"></span>.
<span x-text="item"></span>
</div>
</template>
</div>
<!--С объектами-->
<template x-for="user in users" :key="user.id">
<div>
<span x-text="user.name"></span>
<span x-text="user.email"></span>
</div>
</template>

Magic Properties

$el: текущий элемент

<button @click="$el.innerText = 'Clicked!'">Click me</button>

$refs: ссылки на элементы

<div x-data="{ }">
<input type="text" x-ref="input" />
<button @click="$refs.input.focus()">Focus Input</button>
<button @click="$refs.input.value = ''">Clear</button>
</div>

$watch: реактивный наблюдатель

<div x-data="{ count: 0 }" x-init="$watch('count', value => console.log('Count is:', value))">
<button @click="count++">Increment</button>
</div>

$dispatch: пользовательские события

<!--Дочерний компонент отправляет событие-->
<div x-data @notify="alert($event.detail.message)">
<button @click="$dispatch('notify', { message: 'Hello!' })">
Notify Parent
</button>
</div>
<!--Взаимодействие между компонентами-->
<div x-data @item-selected.window="selectedItem = $event.detail">
Selected: <span x-text="selectedItem"></span>
</div>
<div x-data>
<button @click="$dispatch('item-selected', 'Item 1')">Select Item 1</button>
</div>

$nextTick: после обновления DOM

<div x-data="{ items: [] }">
<button @click="
items.push('New Item');
$nextTick(() => {
$refs.list.scrollTop = $refs.list.scrollHeight;
});
">Add Item</button>
<ul x-ref="list" style="max-height: 100px; overflow-y: auto;">
<template x-for="item in items">
<li x-text="item"></li>
</template>
</ul>
</div>

Глобальное состояние со Stores

<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
add(item) {
this.items.push(item);
},
remove(index) {
this.items.splice(index, 1);
},
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
});
Alpine.store('user', {
name: '',
isLoggedIn: false,
login(name) {
this.name = name;
this.isLoggedIn = true;
},
logout() {
this.name = '';
this.isLoggedIn = false;
}
});
});
</script>
<!--Используйте store где угодно-->
<div x-data>
<template x-if="$store.user.isLoggedIn">
<p>Welcome, <span x-text="$store.user.name"></span>!</p>
</template>
<p>Cart items: <span x-text="$store.cart.items.length"></span></p>
<p>Total: $<span x-text="$store.cart.total"></span></p>
<button @click="$store.cart.add({ name: 'Product', price: 9.99 })">
Add to Cart
</button>
</div>

Переиспользуемые компоненты

Вынос в функции

<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = !this.open;
},
close() {
this.open = false;
}
}));
Alpine.data('counter', (initialCount = 0) => ({
count: initialCount,
increment() {
this.count++;
},
decrement() {
if (this.count > 0) this.count--;
}
}));
});
</script>
<!--Используйте переиспользуемые компоненты-->
<div x-data="dropdown">
<button @click="toggle()">Menu</button>
<ul x-show="open" @click.outside="close()">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
<div x-data="counter(10)">
<button @click="decrement()">-</button>
<span x-text="count"></span>
<button @click="increment()">+</button>
</div>

Практические примеры

Модальное окно

<div x-data="{ showModal: false }">
<button @click="showModal = true">Open Modal</button>
<!--Подложка модального окна-->
<div x-show="showModal"
x-transition.opacity
@click="showModal = false"
class="fixed inset-0 bg-black bg-opacity-50 z-40">
</div>
<!--Содержимое модального окна-->
<div x-show="showModal"
x-transition
@click.stop
@keydown.escape.window="showModal = false"
class="fixed inset-0 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg max-w-md w-full mx-4">
<h2 class="text-xl font-bold mb-4">Modal Title</h2>
<p>Modal content goes here.</p>
<button @click="showModal = false" class="mt-4 btn">Close</button>
</div>
</div>
</div>

Компонент вкладок

<div x-data="{ activeTab: 'tab1' }">
<div class="tabs">
<button @click="activeTab = 'tab1'"
:class="{ 'active': activeTab === 'tab1' }">
Tab 1
</button>
<button @click="activeTab = 'tab2'"
:class="{ 'active': activeTab === 'tab2' }">
Tab 2
</button>
<button @click="activeTab = 'tab3'"
:class="{ 'active': activeTab === 'tab3' }">
Tab 3
</button>
</div>
<div class="tab-content">
<div x-show="activeTab === 'tab1'">Content for Tab 1</div>
<div x-show="activeTab === 'tab2'">Content for Tab 2</div>
<div x-show="activeTab === 'tab3'">Content for Tab 3</div>
</div>
</div>

Получение данных

<div x-data="{
users: [],
loading: false,
error: null,
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (e) {
this.error = 'Failed to load users';
} finally {
this.loading = false;
}
}
}" x-init="fetchUsers()">
<div x-show="loading">Loading...</div>
<div x-show="error" x-text="error" class="text-red-500"></div>
<ul x-show="!loading && !error">
<template x-for="user in users" :key="user.id">
<li x-text="user.name"></li>
</template>
</ul>
<button @click="fetchUsers()" :disabled="loading">Refresh</button>
</div>

Валидация формы

<form x-data="{
form: { email: '', password: '' },
errors: {},
submitted: false,
validate() {
this.errors = {};
if (!this.form.email) {
this.errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(this.form.email)) {
this.errors.email = 'Invalid email format';
}
if (!this.form.password) {
this.errors.password = 'Password is required';
} else if (this.form.password.length < 8) {
this.errors.password = 'Password must be at least 8 characters';
}
return Object.keys(this.errors).length === 0;
},
submit() {
if (this.validate()) {
this.submitted = true;
// Submit form...
}
}
}" @submit.prevent="submit()">
<div>
<input type="email" x-model="form.email" placeholder="Email"
:class="{ 'border-red-500': errors.email }">
<p x-show="errors.email" x-text="errors.email" class="text-red-500 text-sm"></p>
</div>
<div>
<input type="password" x-model="form.password" placeholder="Password"
:class="{ 'border-red-500': errors.password }">
<p x-show="errors.password" x-text="errors.password" class="text-red-500 text-sm"></p>
</div>
<button type="submit">Sign In</button>
<p x-show="submitted" class="text-green-500">Form submitted!</p>
</form>

Интеграция с backend

Laravel Blade

<div x-data="{
items: @js($items),
deleteItem(id) {
if (confirm('Delete this item?')) {
fetch(`/items/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).then(() => {
this.items = this.items.filter(i => i.id !== id);
});
}
}
}">
<template x-for="item in items" :key="item.id">
<div>
<span x-text="item.name"></span>
<button @click="deleteItem(item.id)">Delete</button>
</div>
</template>
</div>

Ключевые выводы

  1. Начинайте с простого: x-data, x-on, x-show покрывают большинство случаев
  2. Используйте stores для глобального состояния: Это позволяет избежать prop drilling
  3. Выносите переиспользуемые компоненты: Alpine.data() для паттернов
  4. Используйте modifiers: .prevent, .stop, .debounce сокращают код
  5. Комбинируйте с server rendering: Alpine дополняет, а не заменяет
  6. Держите логику в HTML: В этом сила Alpine

Alpine.js идеально подходит, когда нужна интерактивность без накладных расходов полноценного SPA-фреймворка — особенно для прогрессивного улучшения приложений, отрендеренных на сервере.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив