Большие фронтенд-фреймворки часто требуют сложной настройки: сборщики, роутинг, состояния, хуки, конфигурации. Но для небольших задач вроде выпадающих меню, модалок или табов этого слишком много — иногда хочется просто взять HTML и быстро добавить в него немного интерактивности.

Здесь отлично подходит Alpine.js — лёгкий (менее 10 KB), быстрый и удобный фреймворк, который добавляет реактивность прямо в HTML. Он похож на Vue.js в миниатюре: декларативные шаблоны и реактивные данные, но без этапа сборки — достаточно подключить <script> и можно сразу использовать.

Если ты знаешь HTML, можешь сразу начинать. В этой статье мы разберём Alpine.js шаг за шагом — от простых примеров до более продвинутых приёмов и полезных плагинов.

Что такое Alpine.js — в двух словах

Alpine позволяет писать поведение прямо в HTML через «директивы» (x-...) и «магические свойства» ($...). Он маленький, понятный и идеально подходит для интерактивных элементов: меню, модалки, таблицы, фильтры и т. п. Официальное «Start here» показывает дух фреймворка: «вставь <script> — и поехали».

Реактивность — это когда фреймворк «следит» за данными и сам обновляет интерфейс, когда эти данные меняются. Тебе не нужно вручную искать элементы в DOM и менять их содержимое — Alpine делает это сам.

Небольшое примечание про стили

Во всех примерах в этой статье используются классы вроде px-3, py-1, bg-pink-200, rounded и другие.
Это классы из фреймворка Tailwind CSS — он не обязателен, но помогает быстро добавлять оформление, чтобы примеры выглядели аккуратно.

Чтобы эти классы работали, нужно подключить Tailwind через Play CDN — самый быстрый способ без настройки сборки:

<script src="https://cdn.tailwindcss.com"></script>

Добавьте эту строчку в <head> вашей страницы перед подключением Alpine.js, и все примеры из статьи будут отображаться с готовым оформлением.

Если не хочется подключать Tailwind, можно просто удалить эти классы — функциональность Alpine от этого не изменится, будет только без стилей.

Установка

Через CDN (самый быстрый старт)

<!doctype html>
<html lang="ru">
  <head>
    <!-- Подключаем актуальный Alpine 3.15.0 -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js"></script>
    <style>
      /* x-cloak прячет элементы до инициализации Alpine (без «мигания») */
      [x-cloak] { display: none !important; }
    </style>
  </head>
  <body>
    <h1 x-data="{ msg: 'Привет, Alpine!' }" x-text="msg" x-cloak></h1>
  </body>
</html>

Совет: подключайте скрипт с defer, а x-cloak используйте для скрытия содержимого до инициализации. Документация рекомендует именно такой подход.

Через npm

Самый простой компонент: x-data и x-text

Чтобы сделать страницу «живой», нужно уметь хранить данные и показывать их на странице. Для этого в Alpine есть x-data (задаёт данные) и x-text (выводит их в HTML). Это основа любого компонента — без них остальное не заработает.

<div x-data="{ likes: 0 }">
  <button 
    @click="likes++"
    class="px-3 py-1 bg-pink-200 rounded">
    ❤️ Лайк
  </button>

  <p class="mt-2">
    Лайков: <span x-text="likes"></span>
  </p>
</div>
  • Сначала likes равен 0.
  • Каждый раз, когда нажимаешь кнопку, @click увеличивает likes на 1.
  • x-text сразу показывает новое количество лайков — потому что Alpine сам отслеживает likes и обновляет текст при каждом изменении.

@click — это короткая запись x-on:click, так в Alpine.js удобнее писать обработчики событий. Рассмотрим x-on позже.

Привязка атрибутов и классов: x-bind

x-bind нужен, чтобы менять внешний вид или свойства элементов, когда меняются данные. Например, пусть у нас есть кнопка, которая включает и выключает «свет». Его часто записывают коротко — например, :class вместо x-bind:class.

Пример: изменение цвета кнопки

<div x-data="{ light: false }">
  <button
    @click="light = !light"
    :class="light ? 'bg-yellow-300' : 'bg-gray-300'"
    x-text="light ? 'Выключить свет' : 'Включить свет'"
  ></button>
</div>
  • Сначала light равен false, и кнопка серая — как будто свет выключен.
  • Когда мы нажимаем кнопку, @click меняет light на true, и :class делает кнопку жёлтой — свет включён.

Пример: блокировка кнопки при загрузке

<div x-data="{ loading: false }">
  <button 
    @click="loading = true"
    :disabled="loading"
    class="px-3 py-1 bg-blue-200 rounded">
    Отправить
  </button>

  <p class="mt-2" x-text="loading ? 'Загрузка...' : ''"></p>
</div>
  • Сначала loading равен false, и кнопка активна.
  • Когда нажимаем её, @click меняет loading на true.
  • Атрибут :disabled="loading" делает кнопку неактивной, как только loading становится true.

Показ и скрытие элементов: x-show

x-show нужен, чтобы показывать или скрывать элементы в зависимости от данных. Элемент не удаляется, просто меняется его display, и он исчезает с экрана.

<div x-data="{ visible: false }">
  <button @click="visible = !visible">
    Показать / Скрыть
  </button>

  <p x-show="visible" class="mt-2">
    Привет! Я появляюсь и исчезаю.
  </p>
</div>
  • Сначала visible равен false, и абзац скрыт.
  • Когда нажимаем кнопку, @click меняет visible на true, и x-show делает элемент видимым.

Удаление и создание элементов: x-if

x-if нужен, чтобы добавлять элемент в HTML только когда он нужен, а не просто прятать его. Когда условие ложное, элемент полностью удаляется из DOM, а не просто скрывается.

<div x-data="{ open: false }">
  <button @click="open = !open">
    Показать / Скрыть
  </button>

  <template x-if="open">
    <p class="mt-2">
      Я создаюсь заново каждый раз, когда появляюсь.
    </p>
  </template>
</div>
  • Сначала open равен false, и элемента вообще нет в HTML.
  • Когда нажимаем кнопку, @click меняет open на true, и x-if создаёт абзац.
  • Когда снова нажимаем, open становится false, и Alpine удаляет этот абзац из DOM.

В Alpine.js <template> — это невидимый контейнер. С x-if он нужен, чтобы создавать элемент, когда условие true, и удалять его, когда false. В отличие от x-show, который просто прячет элемент стилями, x-if действительно добавляет и убирает его из HTML.

Это отличается от x-show: x-show только прячет элемент (display: none), а x-if реально его удаляет и создаёт заново.

Вывод списков: x-for

x-for нужен, чтобы повторять один и тот же кусок HTML для каждого элемента из массива данных. Так можно легко вывести список товаров, сообщений, ссылок — чего угодно.

<div x-data="{ items: ['Яблоко', 'Банан', 'Апельсин'] }">
  <ul>
    <template x-for="item in items" :key="item">
      <li x-text="item"></li>
    </template>
  </ul>
</div>

Сначала в x-data мы задали массив items.

  • x-for="item in items" берёт каждый элемент массива и создаёт для него отдельный <li>.
  • :key="item" помогает Alpine отслеживать, какой элемент какой, если список будет меняться.
  • x-text="item" выводит название фрукта внутрь <li>.

Если ты изменишь массив items (например, добавишь или удалишь элемент), Alpine автоматически обновит список — это снова работает благодаря реактивности.

Связь с полями ввода: x-model

x-model нужен, чтобы связывать значение поля ввода с переменной. Когда пользователь что-то вводит, данные автоматически обновляются, и наоборот — если данные изменятся, обновится и поле.

<div x-data="{ name: '' }">
  <input 
    type="text" 
    x-model="name" 
    placeholder="Введите имя">

  <p class="mt-2">
    Привет, <span x-text="name || 'гость'"></span>!
  </p>
</div>
  • Сначала name пустой, и на странице выводится «гость».
  • Когда мы начинаем печатать в поле, x-model сразу меняет значение name.
  • Alpine следит за этим и автоматически обновляет текст — приветствие меняется прямо во время ввода.

Это и есть двусторонняя связь данных: поле обновляет переменную, а переменная — поле.

Автоматические действия при изменении данных: x-effect

x-effect позволяет реагировать на изменение данных без кликов и событий. Он «следит» за выражением внутри и каждый раз, когда его значение меняется, запускает код. Это похоже на «наблюдателя», который автоматически выполняет нужные действия.

Пример: логирование при изменении счётчика

<div x-data="{ count: 0 }">
  <button @click="count++">Увеличить</button>

  <p class="mt-2">Счётчик: <span x-text="count"></span></p>

  <div x-effect="console.log('Новое значение:', count)"></div>
</div>
  • Сначала count равен 0.
  • Каждый раз, когда ты нажимаешь кнопку и count увеличивается, x-effect срабатывает и выводит новое значение в консоль.

Внутри x-effect пишется обычное JavaScript-выражение — почти так же, как в @click или x-init. Когда count меняется, x-effect перезапускается и выводит новое значение.

Где это полезно

  • Автоматически сохранять данные в localStorage при изменении
  • Отправлять запросы при изменении фильтра или поиска
  • Переключать классы или анимации без кликов
  • Обновлять внешние виджеты, если изменилась переменная

Обращение к элементам: x-ref и $refs

x-ref нужен, чтобы пометить элемент и потом легко получить к нему доступ из кода. А $refs — это объект, в котором хранятся все такие элементы по имени.

<div x-data>
  <input x-ref="name" type="text" placeholder="Введите имя">
  <button @click="$refs.name.focus()">
    Поставить фокус
  </button>
</div>
  • Здесь мы даём полю ввода ссылку x-ref="name".
  • Когда нажимаем кнопку, @click вызывает $refs.name.focus(), и Alpine ставит курсор в это поле.
  • Не нужно искать элемент через document.querySelector — Alpine делает всё проще и безопаснее.

$refs можно использовать для любых элементов: чтобы прокручивать, менять scrollTop, вызывать .play() у видео и т.д. Главное — сначала дать элементу x-ref="имя", а потом обращаться к нему как $refs.имя.

Код при загрузке компонента: x-init и init()

Иногда нужно что-то сделать сразу, как только компонент появился на странице — например, подгрузить данные, запустить таймер или показать приветствие. Для этого есть два способа: x-init и init().

Пример с x-init

<div x-data="{ message: '' }" x-init="message = 'Готово!'">
  <p x-text="message"></p>
</div>
  • Когда компонент загружается, x-init запускает код и записывает 'Готово!' в message.
  • Alpine сразу обновляет текст — и на странице появляется сообщение.

Пример с init() внутри x-data

<div x-data="{
  message: '',
  init() {
    this.message = 'Компонент инициализирован'
  }
}">
  <p x-text="message"></p>
</div>

Метод init() вызывается автоматически при инициализации компонента.

Такой вариант удобен, если нужно выполнить несколько строк кода или что-то посложнее (например, сделать fetch).

📢 Подписывайтесь на наш Telegram-канал.

Там вы найдете анонсы обучающих статей и видео, готовый код для ваших проектов и увлекательные курсы. Ничего лишнего — только практика, вдохновение и развитие.

👉 https://t.me/codelab_channel

Разница простая:

  • x-init — быстрый способ выполнить одну строку при запуске.
  • init() — если нужен настоящий мини-скрипт прямо внутри компонента.

Скрытие элементов до загрузки: x-cloak

Когда страница только загружается, Alpine ещё не успел инициализировать компоненты.
Если в них есть условное отображение (x-show, x-if и т.п.), может случиться неприятное «мигание» — элементы появятся на долю секунды, а потом пропадут.

Чтобы этого не было, используют x-cloak.

Как использовать

<style>
  [x-cloak] { display: none !important; }
</style>

<div x-data="{ open: false }">
  <button @click="open = !open">Показать</button>

  <p x-show="open" x-cloak>
    Этот текст не «мигнёт» при загрузке
  </p>
</div>
  • x-cloak добавляется на элемент, который не должен появляться до инициализации Alpine
  • CSS-правило [x-cloak] { display: none !important; } скрывает все такие элементы
  • Когда Alpine запускается, он удаляет атрибут x-cloak, и элемент снова становится видимым (если условие x-show выполнено)

Важно: x-cloak не управляет показом сам — он просто прячет элемент до загрузки Alpine, чтобы не было «мигания».

Вывод HTML-кода из данных: x-html

x-html нужен, чтобы выводить HTML-код прямо из данных. Он работает как x-text, но не экранирует HTML, а вставляет его «как есть».

Пример

<div x-data="{ content: '<b>Жирный текст</b>' }">
  <p x-html="content"></p>
</div>

Здесь x-html вставит содержимое content как настоящий HTML — на странице появится жирный текст, а не буквы <b>.

Если бы мы использовали x-text, получилось бы вот так:

<p x-text="content"></p>
<!-- Вывод: <b>Жирный текст</b> (просто текст, не жирный) -->

Важно: x-html нужно использовать только для доверенного содержимого, которое ты сам создаёшь. Если туда попадут данные от пользователя, в них может быть вредоносный код — он выполнится в браузере. Если данные от пользователя — используй x-text.

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

x-on позволяет выполнять код при событиях — например, при клике или вводе текста. Почти всегда используют короткую запись @ вместо x-on: — так короче и удобнее.

Пример: клик по кнопке

<div x-data="{ count: 0 }">
  <button 
    @click="count++"
    class="px-3 py-1 bg-blue-200 rounded">
    Нажми
  </button>

  <p class="mt-2">
    Нажатий: <span x-text="count"></span>
  </p>
</div>

Здесь @click срабатывает при нажатии на кнопку и увеличивает count. Alpine сам обновляет интерфейс, потому что он следит за count — это реактивность.

Полезные модификаторы событий

Модификаторы — это «дополнения» к событиям, которые меняют их поведение. Они пишутся через точку после события.

<!-- сработает только один раз -->
<button @click.once="alert('Привет!')">Нажми один раз</button>

<!-- остановит всплытие события -->
<button @click.stop="alert('Только эта кнопка')">Стоп</button>

<!-- отменит стандартное поведение -->
<a href="#" @click.prevent="alert('Переход отменён')">Ссылка</a>

<!-- клик вне элемента -->
<div x-data="{ open: true }" @click.outside="open = false">
  <p x-show="open">Я закроюсь при клике снаружи</p>
</div>

<!-- нажатие клавиши -->
<input @keyup.enter="alert('Нажата Enter')">

Часто используемые модификаторы x-on / @

📍 Управление выполнением

  • .once — срабатывает только один раз
  • .prevent — отменяет стандартное поведение браузера (например, переход по ссылке)
  • .stop — останавливает всплытие события
  • .self — срабатывает только если клик именно по самому элементу (не по вложенным)

📍 Работа с областью события

  • .outside — срабатывает, если клик был вне элемента
  • .window — слушает событие на window
  • .document — слушает событие на document

📍 Клавиши (для @keydown / @keyup)

  • .enter — клавиша Enter
  • .escape — Esc
  • .space — Пробел
  • .tab — Tab
  • .backspace — Backspace
  • .delete — Delete
  • .arrow-up, .arrow-down, .arrow-left, .arrow-right — стрелки
  • .shift, .ctrl, .alt, .meta — модификаторы клавиш

📌 Эти модификаторы можно комбинировать, например:

<input @keyup.enter.prevent="submitForm()">
<div @click.outside.window="open = false"></div>

Плавные анимации появления: x-transition

x-transition нужен, чтобы добавить плавную анимацию при показе и скрытии элемента. Он работает только вместе с x-show, потому что x-show не удаляет элемент, а просто меняет его display.

Пример

<div x-data="{ open: false }">
  <button 
    @click="open = !open"
    class="px-3 py-1 bg-blue-200 rounded">
    Показать / Скрыть
  </button>

  <p 
    x-show="open"
    x-transition
    class="mt-2 p-2 bg-green-100 rounded">
    Я появляюсь и исчезаю плавно
  </p>
</div>
  • Когда open становится true, Alpine показывает элемент и добавляет плавное появление.
  • Когда false — плавно скрывает его.
  • Анимация происходит автоматически, без дополнительного кода или CSS.

📌 Можно тонко настраивать анимацию, добавляя модификаторы:

  • x-transition:enter — классы при появлении
  • x-transition:leave — классы при скрытии
  • x-transition.scale, .opacity, .duration.500ms и т.д.

Пример

<p 
  x-show="open"
  x-transition:enter.scale.80
  x-transition:leave.opacity.duration.500ms>
  Плавная анимация
</p>

Перенос элементов: x-teleport

x-teleport нужен, чтобы вставить элемент в другое место на странице, не двигая его в коде. Это удобно для модалок, всплывающих меню и других элементов, которые логически находятся внутри компонента, но по HTML-структуре должны быть выше (например, в конце body).

Пример: модальное окно

<div x-data="{ open: false }">
  <button 
    @click="open = true"
    class="px-3 py-1 bg-blue-200 rounded">
    Открыть модалку
  </button>

  <template x-teleport="body">
    <div 
      x-show="open"
      class="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div class="bg-white p-4 rounded shadow">
        <p>Привет, я модалка!</p>
        <button @click="open = false" class="mt-2 px-3 py-1 bg-gray-200 rounded">
          Закрыть
        </button>
      </div>
    </div>
  </template>
</div>

Хотя код модалки написан внутри компонента, x-teleport="body" переносит её прямо в конец <body>. Это нужно, чтобы она не ломала верстку и всегда отображалась поверх всего.

x-teleport не копирует, а именно перемещает элемент в другое место. Все привязанные данные (open и др.) при этом продолжают работать, как будто элемент остался внутри компонента.

Отслеживание изменений: $watch

$watch позволяет следить за изменением конкретной переменной и выполнять код, когда она меняется. Его обычно вызывают внутри x-init или init().

Пример: отслеживаем count

<div x-data="{ count: 0 }" x-init="
  $watch('count', value => {
    console.log('Новое значение count:', value)
  })
">
  <button @click="count++" class="px-3 py-1 bg-blue-200 rounded">
    Увеличить
  </button>
  <p>Счётчик: <span x-text="count"></span></p>
</div>

Каждый раз, когда count меняется, $watch вызывает функцию и передаёт новое значение. Это удобно, если нужно запускать код при изменении данных (сохранение, валидация и т.д.).

Отправка событий: $dispatch

$dispatch нужен, чтобы отправлять собственные события, которые могут ловить родительские компоненты.

Пример: отправляем событие liked

<div x-data @liked.window="alert('Кто-то нажал лайк!')">
  <button 
    x-data 
    @click="$dispatch('liked')"
    class="px-3 py-1 bg-pink-200 rounded">
    ❤️ Лайк
  </button>
</div>
  • Когда нажимаешь на кнопку, она отправляет событие liked.
  • Родитель ловит его с помощью @liked.window.
  • Так компоненты могут «общаться» друг с другом, даже если они не связаны напрямую.

Выполнить код после обновления DOM: $nextTick

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

Если в этот же момент попробовать обратиться к элементу (например, поставить фокус, измерить высоту и т.д.), можно получить старое состояние. Вот тут и нужен $nextTick: он откладывает выполнение кода до тех пор, пока Alpine не обновит DOM.

Пример: поставить фокус после появления элемента

<div x-data="{ open: false }">
  <button 
    @click="
      open = true; 
      $nextTick(() => $refs.input.focus())
    "
    class="px-3 py-1 bg-blue-200 rounded">
    Открыть
  </button>

  <div x-show="open" class="mt-2">
    <input x-ref="input" type="text" placeholder="Введите имя"
           class="border px-2 py-1 rounded">
  </div>
</div>

Здесь при клике мы:

  • Меняем open = true, чтобы показать блок с полем.
  • Вызываем $nextTick, чтобы дождаться, когда Alpine отрендерит <input>.
  • Уже после обновления DOM вызываем $refs.input.focus().

Если вызвать focus() без $nextTick, Alpine ещё не вставит <input> в DOM, и будет ошибка.

Где ещё применяют $nextTick

  • дождаться, когда x-show или x-if создадут элемент
  • измерить размеры элемента (offsetHeight, scrollWidth)
  • прокрутить скролл (scrollIntoView()) после добавления новых элементов
  • запускать сторонние скрипты, которые должны примениться к уже обновлённому HTML

$nextTick — это не про задержку времени, а про задержку до завершения обновления DOM.Он гарантирует, что Alpine уже применил все изменения данных к разметке.

Глобальные сторы: Alpine.store()

Иногда одно и то же состояние нужно в разных местах страницы: тема (светлая/тёмная), корзина, счётчик уведомлений, авторизация и т. п. Держать это в одном x-data неудобно: компоненты получаются связаны. Alpine.store(name, value) создаёт глобальный реактивный стор (хранилище), доступный в любом компоненте через $store.name.

Когда и где объявлять

Инициализируйте стор до старта Alpine, в обработчике alpine:init (CDN-вариант) или сразу перед Alpine.start() (ESM-вариант). Так вы гарантируете, что все компоненты увидят стор.

Вариант через CDN

<!-- Alpine -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<script>
  document.addEventListener('alpine:init', () => {
    Alpine.store('theme', {
      dark: false,
      toggle() { this.dark = !this.dark }
    })

    Alpine.store('cart', {
      items: [],
      add(product) { this.items.push(product) },
      remove(index) { this.items.splice(index, 1) },
      clear() { this.items = [] },
      get count() { return this.items.length },       // вычисляемое свойство
      get total() { return this.items.reduce((s,p) => s + p.price, 0) }
    })
  })
</script>

Теперь в любой части страницы можно обратиться к theme и cart через $store.theme и $store.cart.

Вариант через ESM (бандлер)

// main.js
import Alpine from 'alpinejs'

Alpine.store('theme', {
  dark: false,
  toggle() { this.dark = !this.dark }
})

Alpine.store('cart', {
  items: [],
  add(product) { this.items.push(product) },
  remove(index) { this.items.splice(index, 1) },
  clear() { this.items = [] },
  get count() { return this.items.length },
  get total() { return this.items.reduce((s,p) => s + p.price, 0) }
})

window.Alpine = Alpine
Alpine.start()

Как использовать сторы в разметке

Чтение и изменение из любого компонента

<!-- Шапка сайта: иконка корзины -->
<div x-data class="flex items-center gap-2">
  <span>🛒</span>
  <span x-text="$store.cart.count"></span>
  <span>тов.</span>
</div>

<!-- Карточка товара: добавляет в корзину -->
<div x-data="{ product: { id: 1, title: 'Кружка', price: 350 } }" class="mt-4">
  <p class="font-medium" x-text="product.title"></p>
  <p class="text-sm text-gray-600" x-text="product.price + ' ₽'"></p>
  <button class="mt-2 px-3 py-1 bg-blue-200 rounded"
          @click="$store.cart.add(product)">
    В корзину
  </button>
</div>

<!-- Подвал/сайдбар: итог -->
<div x-data class="mt-6">
  <p>Позиций: <b x-text="$store.cart.count"></b></p>
  <p>Итого: <b x-text="$store.cart.total + ' ₽'"></b></p>
  <button class="mt-2 px-3 py-1 bg-gray-200 rounded"
          :disabled="$store.cart.count === 0"
          @click="$store.cart.clear()">
    Очистить корзину
  </button>
</div>
  • Доступ к значениям и методам: $store.cart.*
  • Вычисляемые свойства (get total() { ... }) ведут себя как обычные данные — реактивно пересчитываются.

Применение глобального состояния к оформлению

<!-- Переключатель темы -->
<div x-data class="mt-6">
  <button class="px-3 py-1 bg-gray-200 rounded"
          @click="$store.theme.toggle()">
    Переключить тему
  </button>
</div>

<!-- Применяем класс 'dark' на <html> или <body> -->
<html :class="$store.theme.dark && 'dark'">
  <!-- ... -->
</html>

При изменении $store.theme.dark класс переключится везде, где вы на него ссылаетесь.

Пример «целиком»: список товаров + корзина

<div x-data="{ products: [
  { id: 1, title: 'Кружка', price: 350 },
  { id: 2, title: 'Футболка', price: 1200 },
  { id: 3, title: 'Наклейки', price: 90 }
]}">

  <!-- Витрина -->
  <div class="grid gap-3 md:grid-cols-3">
    <template x-for="p in products" :key="p.id">
      <div class="p-3 border rounded">
        <div class="font-medium" x-text="p.title"></div>
        <div class="text-sm text-gray-600" x-text="p.price + ' ₽'"></div>
        <button class="mt-2 px-3 py-1 bg-blue-200 rounded"
                @click="$store.cart.add(p)">
          В корзину
        </button>
      </div>
    </template>
  </div>

  <!-- Корзина -->
  <div class="mt-6 p-3 border rounded">
    <div class="flex items-center gap-2">
      <span>🛒</span>
      <b x-text="$store.cart.count"></b>
      <span>тов.</span>
    </div>

    <ul class="mt-2 list-disc list-inside">
      <template x-for="(item, i) in $store.cart.items" :key="item.id + ':' + i">
        <li class="flex items-center justify-between">
          <span>
            <span x-text="item.title"></span>
            — <span x-text="item.price + ' ₽'"></span>
          </span>
          <button class="px-2 py-0.5 bg-gray-200 rounded"
                  @click="$store.cart.remove(i)">
            Удалить
          </button>
        </li>
      </template>
    </ul>

    <div class="mt-3">
      <b>Итого:</b> <span x-text="$store.cart.total + ' ₽'"></span>
    </div>

    <button class="mt-3 px-3 py-1 bg-gray-200 rounded"
            :disabled="$store.cart.count === 0"
            @click="$store.cart.clear()">
      Очистить корзину
    </button>
  </div>
</div>

Про сохранение между перезагрузками

  • Если хотите, чтобы значения стора переживали перезагрузку страницы (например, тема или корзина), два практичных пути:
  • Простой ручной способ: читать/писать localStorage внутри методов стора (init-подобная инициализация и сохранения при изменениях).
  • Плагин Persist: он упрощает хранение значений в localStorage. Чаще его используют в x-data, а для стора удобно вызывать сохранение из компонентов или методов стора. (Если понадобится — напишу готовый шаблон под вашу задачу.)

Комментарии

0

Без регистрации и смс