Перейти к содержимому
Поиск
Разработка

Alpine.js: интерактивные интерфейсы на HTML — мастерская и 6 примеров

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

Александр Некрасов
Alpine.js — интерфейсы на HTML: поля формы, переключатели и состояние приложения

Пользователь меняет количество — сумма пересчитывается. Добавляет строку заявки — появляется ещё одна позиция. Открывает редактирование, передумывает и нажимает «Отменить» — карточка остаётся прежней. Именно такие небольшие, но законченные взаимодействия удобно писать на Alpine.js.

В этой статье можно пройти весь путь: собрать реактивную карточку в мини-мастерской, проверить шесть интерфейсов, раскрыть данные и код, скачать HTML или архив исходников. Примеры используют настоящий Alpine.js 3.17.5, локальные стили Bootstrap 5.3.8 и официальный плагин Persist. Библиотеки размещены вместе с мастерской на сайте.

Что попробовать за пять минут

  1. В мини-мастерской измените название и количество: сравните карточку, JSON и разметку.
  2. В строках заявки удалите одну позицию и убедитесь, что значения соседних строк сохранились.
  3. В редакторе измените название, нажмите «Отменить», затем повторите с сохранением.
  4. В настройках выберите другой вид карточек, обновите страницу и проверьте сохранение.
  5. Скачайте HTML понравившегося примера и откройте его отдельно.

Цены, карточки и заявки здесь демонстрационные. Формы показывают локальный результат в браузере. Настройки отображения могут сохраняться на этом устройстве; реальные заявки серверу мастерская не отправляет.

1. Как Alpine связывает данные и интерфейс

У компонента есть состояние: название, количество, список строк, номер шага или признак открытой панели. HTML описывает, как это состояние показать. Событие пользователя меняет данные, а Alpine обновляет связанные элементы. Вместо отдельного поиска каждого поля и ручного обновления каждой подписи вы задаёте связи в разметке.

Посмотрим на маленькую задачу: поле количества и подпись рядом. На обычном JavaScript достаточно обработчика. Это хороший выбор, если на странице всего одна такая связь:

<label for="plain-count">Количество</label>
<input id="plain-count" type="number" min="1" max="20" value="2">
<p>Выбрано: <span id="plain-result">2</span></p>
<script>
const input = document.getElementById('plain-count');
const result = document.getElementById('plain-result');
input.addEventListener('input', () => {
  const count = Number(input.value);
  result.textContent = input.value !== '' && Number.isInteger(count)
    && count >= 1 && count <= 20 ? count : 'проверьте количество';
});
</script>

Тот же пример с уже подключённым Alpine:

<div x-data="{ count: 2 }">
  <label for="alpine-count">Количество</label>
  <input id="alpine-count" type="number" min="1" max="20"
         x-model.number="count">
  <p>Выбрано:
    <span x-text="Number.isInteger(count) && count >= 1 && count <= 20
      ? count : 'проверьте количество'"></span>
  </p>
</div>

x-data создаёт область данных, x-model.number связывает поле с числом, x-text выводит результат. Выгода становится заметнее, когда одно значение участвует сразу в нескольких местах: сумма, доступность кнопки, сообщение и список. При этом правила проверки и сама формула остаются кодом приложения.

Задача Обычный JavaScript Alpine.js
Одно простое действие Небольшого обработчика часто достаточно Добавляется библиотека ради небольшой связи
Несколько связанных полей Нужно согласовывать обновление элементов Связи задаются через состояние и директивы
Добавляемые строки Создание, обновление и удаление DOM вручную Массив и template с x-for
HTML с сервера PHP Можно добавить обработчики после загрузки Можно оживить отдельные области готовой страницы
Проверка прав и сохранение Требуется серверная логика Также требуется серверная логика

Документация: первые компоненты Alpine.

2. Мини-мастерская: соберите реактивную карточку

Начните с одной карточки услуги. Меняйте её текст, цену и количество, включайте подробности, выбирайте оформление и редактируйте список работ. Рядом виден результат, а ниже — состояние и код. Так проще понять, какая часть HTML отвечает за конкретное поведение.

Попробуйте: измените название, затем количество. Название должно обновиться в карточке, а сумма — пересчитаться. Спрячьте подробности и снова покажите их: введённые данные сохранятся. Добавьте пункт списка и проверьте, как он появился в повторяемой разметке.

Смотрите на три представления одной задачи: поля, живой результат и данные. Ошибка в числовом поле не должна превращаться в красивую, но неверную сумму. Сначала исправьте ввод, затем сохраняйте пример.

Короткая разметка помогает изучать директивы. Полный HTML содержит необходимый код и библиотеку; скачанный файл можно открыть отдельно без подключения к CDN. Экспорт сохраняет состояние примера на момент скачивания. Сброс мастерской возвращает её исходные данные, а сброс скачанного HTML — именно вариант, сохранённый при экспорте.

3. Шесть полноценных интерфейсов

Здесь Alpine управляет целым сценарием: от ввода до результата. Переключайте вкладки, проверяйте обычный и ошибочный ввод, раскрывайте JSON и готовый HTML. В каждом примере есть отдельный сброс. Назначение элементов, выбранные параметры и результат подписаны прямо в интерфейсе.

Скачать все исходники ZIP. В архиве находятся обе мастерские, собственные стили и JavaScript, локальные библиотеки и лицензии. Кнопка скачивания внутри интерфейса сохраняет отдельный HTML выбранного сценария.

Ниже разобраны ключевые приёмы. Короткие фрагменты иллюстрируют конкретную идею; полный запускаемый код каждого интерфейса находится в мастерской и исходниках.

4. Калькулятор: ввод, формула и итог

Сценарий подходит для выбора тарифа, комплектации услуги или предварительного расчёта. Пользователь меняет количество, период и дополнительные опции, а карточка результата показывает состав расчёта. Важно выводить не только крупную сумму: человек должен понимать, какие параметры на неё повлияли.

Попробуйте: выберите 10 пользователей и 12 месяцев без поддержки. До скидки получится 58 800 ₽, после скидки 15% — 49 980 ₽. Затем включите поддержку и сравните строки расчёта. После этого очистите количество или введите отрицательное число: интерфейс должен объяснить ошибку, а не считать пустую строку нулевым заказом.

document.addEventListener('alpine:init', () => {
  Alpine.data('smallEstimate', () => ({
    quantity: 2,
    unitPrice: 1500,
    get valid() {
      return Number.isInteger(this.quantity)
        && this.quantity >= 1 && this.quantity <= 100;
    },
    get total() {
      return this.valid ? this.quantity * this.unitPrice : null;
    }
  }));
});

Вычисляемый getter получает результат из текущих данных. Хранить ещё одну независимо изменяемую переменную total здесь не нужно: иначе количество и сумма могут разойтись. Если цена приходит с сервера, в браузере она служит для предварительного показа; окончательную стоимость сервер пересчитывает сам.

Модификатор .number помогает преобразовать ввод, но не заменяет проверку диапазона, целого значения и пустого поля. Ограничения min и max в HTML тоже не следует считать полноценной бизнес-проверкой.

Попробовать калькулятор ↑

Документация: x-model и модификаторы ввода.

5. Строки заявки: массив, ключи и суммы

Здесь пользователь собирает список работ: добавляет позиции, меняет названия, количество и цену, удаляет лишнее. Для каждой строки показан подытог, для списка — общая сумма. Такой приём применим к заявке на материалы, составу заказа и небольшому редактору сметы.

Попробуйте: в исходном списке измените количество первой позиции на 3 — общий итог станет 8 500 ₽. Удалите эту позицию — останется 4 000 ₽. Добавьте ещё несколько строк с разными названиями и удалите среднюю: оставшиеся значения должны принадлежать прежним позициям. Удаление всех строк оставляет объяснение и возможность добавить первую позицию.

<div x-data="{ nextId: 3, rows: [
  { id: 1, title: 'Подготовка', qty: 1 },
  { id: 2, title: 'Проверка', qty: 2 }
] }">
  <template x-for="row in rows" :key="row.id">
    <div>
      <label :for="'row-title-' + row.id">Работа</label>
      <input :id="'row-title-' + row.id" x-model="row.title">
      <button type="button"
              @click="rows = rows.filter(item => item.id !== row.id)">
        Удалить строку
      </button>
    </div>
  </template>
  <button type="button"
          @click="rows.push({ id: nextId++, title: '', qty: 1 })">
    Добавить строку
  </button>
</div>

У template один корневой элемент, а у каждой позиции устойчивый идентификатор. Индекс массива не подходит в качестве личности строки, если позиции удаляются или переставляются. Ключ row.id помогает Alpine сопоставлять данные с нужным DOM-элементом.

В прикладном интерфейсе проверка выполняется по каждой строке. Пустое название и неверное количество объясняются рядом с соответствующей позицией. Для денежных расчётов в реальном сервисе заранее выберите правила округления; часто удобнее считать в целых копейках. Сервер должен повторно проверить состав и разрешённые цены.

Собрать список позиций ↑

Документация: x-for и устойчивые ключи.

6. Пошаговая форма: проверять по ходу работы

Длинную заявку удобнее разделить на три понятные части: задача, параметры, проверка результата. Но сами номера шагов ещё не делают форму удобной. Нужны сохранение введённого при возврате, объяснение ошибки возле поля и итог, который человек может проверить перед завершением.

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

// Фрагмент метода компонента: проверка зависит от текущего шага.
next() {
  this.errors = this.validateStep(this.step);
  const firstField = Object.keys(this.errors)[0];
  if (firstField) {
    this.$nextTick(() => this.$refs[firstField]?.focus());
    return;
  }
  this.step += 1;
  this.$nextTick(() => this.$refs.stepTitle.focus());
}

$nextTick нужен, когда действие зависит от уже обновлённого DOM: например, заголовок нового шага появился после изменения step. Чтобы заголовок принимал программный фокус, ему можно задать tabindex="-1". Ошибки связываются с полями через aria-describedby, а неправильное поле получает aria-invalid.

При возврате назад сохраняйте данные, но не обходите окончательную проверку. Пользователь мог изменить ранее заполненный шаг. В настоящем сервисе успешный ответ сервера, ожидание и ошибка отправки должны быть отдельными состояниями; нажатие кнопки само по себе ещё не означает, что заявка сохранена.

Пройти пошаговую заявку ↑

Документация: $nextTick после обновления интерфейса.

7. Редактирование карточки: сохранить или отменить

Карточка проекта показывает подтверждённые данные. Кнопка редактирования открывает поля с рабочей копией. «Сохранить» проверяет её и обновляет карточку, «Отменить» закрывает редактор, сохраняя прежнее содержание. Этот сценарий полезен для названий проектов, реквизитов и параметров личного кабинета.

Попробуйте: изменить название и отменить. Затем повторить изменение и сохранить. Проверьте, что карточка меняется только во втором случае. Пустое обязательное название должно давать понятную ошибку.

// Методы внутри Alpine.data(). В примере объект плоский.
beginEdit() {
  this.draft = { ...this.saved };
  this.editing = true;
},
cancelEdit() {
  this.draft = { ...this.saved };
  this.editing = false;
},
saveEdit() {
  const title = this.draft.title.trim();
  if (!title) {
    this.error = 'Введите название проекта.';
    return;
  }
  this.saved = { ...this.draft, title };
  this.error = '';
  this.editing = false;
}

Если связать поля прямо с saved, то «Отменить» не сможет вернуть старое содержимое без дополнительной копии. Разделение saved и draft отражает пользовательское ожидание: пока сохранение не подтверждено, рабочие правки можно отбросить.

Оператор spread копирует только верхний уровень объекта. Для вложенных настроек выбирайте явное копирование нужных полей или подходящий способ глубокого копирования. Не переносите всю внутреннюю реактивную структуру в сетевой запрос: сформируйте объект разрешённых полей.

Проверить сохранение и отмену ↑

8. Фильтры: панель, применение и выбранные условия

Панель позволяет сначала выбрать условия, затем применить их к карточкам. Пока пользователь редактирует набор, текущая выдача остаётся связанной с применёнными фильтрами. После подтверждения видны число результатов и отдельные метки условий; каждое условие можно снять.

Попробуйте: открыть панель, изменить условия и закрыть без применения. Затем повторить с применением, снять одно условие через метку и выполнить общий сброс. Подберите сочетание без результатов: интерфейс должен предложить понятный выход.

// Два отдельных набора условий. Все поля этого объекта простые.
openFilters() {
  this.filterDraft = { ...this.filterApplied };
  this.$refs.filterDialog.showModal();
},
applyFilters() {
  // В полном примере перед этим проверяется максимальная цена.
  this.filterApplied = { ...this.filterDraft };
  this.$refs.filterDialog.close();
}

Здесь важна работа не только с массивом карточек, но и с фокусом. У панели должно быть имя, заметная кнопка закрытия, управление с клавиатуры и возврат к кнопке открытия. В мастерской используется нативный dialog. Простое появление блока по x-show само по себе не превращает его в доступное модальное окно.

Выбранные условия полезно показывать вне панели: человек понимает, почему видит именно эти результаты. Закрытие панели и сброс фильтра — разные действия. В реальном каталоге данные могут приходить с сервера; тогда дополнительно нужны ожидание, обработка ошибки и защита от устаревшего ответа.

Открыть фильтры карточек ↑

9. Настройки отображения и Persist

Последний пример меняет оформление только демонстрационной области: плотность карточек, порядок и цветовой режим. Это безопасный способ показать локальное сохранение предпочтений. Выбор должен пережить обновление страницы, а отдельный сброс — вернуть исходный вид.

Попробуйте: изменить несколько настроек и обновить страницу. Вернитесь на вкладку настроек и сравните результат. Затем выполните сброс. Если браузер запрещает хранилище, интерфейс продолжит работать в текущем сеансе и сообщит об ограничении.

<!-- При подключении через script плагин идёт перед ядром. -->
<script defer src="vendor/persist.min.js"></script>
<script defer src="vendor/alpine.min.js"></script>

<div x-data="{
  compact: $persist(false).as('example-card-compact-v1')
}">
  <label>
    <input type="checkbox" x-model="compact">
    Компактные карточки
  </label>
  <div :class="{ 'is-compact': compact }">Карточки</div>
</div>

Это короткий пример механизма. Полная мастерская добавляет обработку недоступного хранилища и проверку сохранённых настроек. Собственный ключ через .as() отделяет предпочтения компонента от других данных сайта. Сохраняйте только нужные значения, а не весь объект приложения.

Persist сохраняет состояние в браузере. Это не профиль пользователя на сервере и не способ хранить секреты. Разные браузеры и устройства имеют отдельное хранилище. При изменении структуры настроек учитывайте старый формат; для сброса затрагивайте только ключи своего компонента.

Настроить вид карточек ↑

Документация: официальный плагин Persist.

10. Подключение: готовый HTML, локальные файлы и npm

Для первого проекта не требуется сборка. Создайте HTML, подключите Alpine с defer и задайте компоненту x-data. Если подключаете CDN, закрепляйте конкретную версию. В наших мастерских используются локальные файлы, поэтому выполнение не зависит от доступности CDN.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Первая карточка Alpine</title>
  <style>[x-cloak] { display: none !important; }</style>
  <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.17.5/dist/cdn.min.js"></script>
</head>
<body>
  <section x-data="{ open: false }">
    <h1>Описание услуги</h1>
    <button type="button" @click="open = !open"
            :aria-expanded="open" aria-controls="service-details">
      Подробности
    </button>
    <p id="service-details" x-show="open" x-cloak>
      Здесь условия и состав услуги.
    </p>
  </section>
</body>
</html>

В проекте со сборщиком установите пакеты и зарегистрируйте плагин до старта:

npm install alpinejs@3.17.5 @alpinejs/persist@3.17.5
import Alpine from 'alpinejs';
import persist from '@alpinejs/persist';

Alpine.plugin(persist);
// Здесь регистрируются Alpine.data(...) и Alpine.store(...).
Alpine.start();

В модульном варианте вызовите Alpine.start() один раз. CDN-сборка запускается сама; не добавляйте ей второй старт. Собственный файл, который подписывается на alpine:init, должен успеть загрузиться до инициализации ядра.

Для архива исходников распакуйте все файлы в отдельную папку и запустите в ней один из локальных серверов:

# Если установлен PHP:
php -S 127.0.0.1:8000

# Либо, если установлен Python:
python -m http.server 8000 --bind 127.0.0.1

Откройте http://127.0.0.1:8000/index.html для шести интерфейсов или /lab.html для конструктора. HTML, скачанный кнопкой внутри примера, является отдельным автономным файлом и открывается без такого сервера.

Документация: способы установки и порядок запуска.

11. Директивы, которые используются в мастерской

Не нужно запоминать весь API сразу. Сначала найдите данные, затем поле, событие и место вывода. После этого добавьте условия и повторяемые элементы. В таблице — роль каждого основного инструмента:

Директива Задача Пример применения
x-data Область состояния компонента Значения формы и методы
x-model Связь поля с данными Название, checkbox, select
x-text Вывод текста Сумма и название карточки
@click, @submit.prevent Обработка события Добавить позицию, обработать форму без обычной отправки
:disabled, :class, :aria-expanded Значение HTML-атрибута из состояния Доступность кнопки, оформление, состояние панели
x-show Показ и скрытие существующего элемента Подробности карточки
x-if Создание и удаление ветки template Условный блок с отдельным жизненным циклом
x-for и :key Повторение по коллекции Строки заявки и карточки результатов
x-cloak Скрыть неинициализированную разметку Предотвратить вспышку скрытых панелей

@click — краткая запись x-on:click, а :class — x-bind:class. Такие сокращения не означают другой механизм. Для обычных пользовательских строк выбирайте текстовый вывод; вставка HTML требует отдельной модели доверия к содержимому.

x-show сохраняет элемент в DOM и меняет его видимость. x-if управляет существованием ветки через template. Поэтому удаляемый виджет может потребовать освобождения ресурсов и повторной настройки. Для x-cloak добавьте правило [x-cloak] { display: none !important; }.

Документация: x-show.

Документация: x-if.

Документация: x-cloak.

12. Когда вынести логику в отдельный компонент

Небольшой объект удобно записать в x-data. Но форму с проверками, несколькими действиями и вычисляемыми значениями легче читать в JavaScript-файле. Разметка тогда описывает связи, а именованный компонент — поведение.

document.addEventListener('alpine:init', () => {
  Alpine.data('projectCard', () => ({
    title: 'Новый проект',
    editing: false,
    init() {
      // Начальная настройка конкретного экземпляра.
    },
    begin() {
      this.editing = true;
      this.$nextTick(() => this.$refs.titleInput.focus());
    },
    destroy() {
      // Здесь освобождаются созданные вами таймеры и подписки.
    }
  }));
});

В HTML компонент подключается как x-data="projectCard". Метод init() Alpine вызывает при инициализации, destroy() — при удалении соответствующего компонента. Не запускайте одну и ту же настройку одновременно из init() и дублирующего x-init.

Вычисляемые значения подходят для суммы или отфильтрованного списка. Наблюдение $watch полезно для побочного действия, но не стоит превращать его в цепочку взаимных присваиваний. Если наблюдатель меняет отслеживаемое значение без условия выхода, можно получить повторные вызовы.

Для нескольких независимых карточек создавайте отдельные экземпляры компонента. Общее состояние имеет смысл выносить в Alpine.store, когда оно действительно должно быть общим: например, количество выбранных позиций в разных областях страницы.

Документация: Alpine.data и жизненный цикл.

Документация: $watch.

13. Как совместить Alpine с PHP

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

Начальные данные передавайте как JSON с корректным экранированием для контекста HTML. Например, сервер формирует безопасный атрибут x-data:

<?php
$initial = ['title' => 'Проект «Север»', 'quantity' => 2];
$json = json_encode(
    $initial,
    JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP
    | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR
);
?>
<section x-data="<?= htmlspecialchars($json, ENT_QUOTES, 'UTF-8') ?>">
  <h2 x-text="title"></h2>
  <label for="project-quantity">Количество</label>
  <input id="project-quantity" name="quantity" type="number"
         min="1" max="100" x-model.number="quantity">
</section>

Это способ передачи начального состояния, а не сохранения. Обычная HTML-форма по-прежнему может отправляться PHP-обработчику. Для отправки через fetch сформируйте явный payload из нужных полей, обработайте HTTP-ошибку и покажите результат только после ответа сервера.

Ни скрытая кнопка, ни клиентская проверка не обеспечивают права доступа. Сервер проверяет пользователя, разрешённые действия, значения и актуальные цены. Для действий, изменяющих данные, сохраняются принятые в проекте механизмы защиты запросов. Секреты и служебные токены не должны попадать в x-data, HTML или локальное хранилище.

Если в проекте действует строгая Content Security Policy, проверьте совместимость заранее: у Alpine есть отдельная CSP-сборка с отличиями в поддерживаемых выражениях. Не меняйте защитную политику всего сайта только ради подключения примера. Документация CSP, json_encode и htmlspecialchars.

14. Частые ошибки и проверка удобства

Директивы отображаются, но ничего не происходит

Проверьте загрузку файла Alpine, наличие родительского x-data, порядок скриптов и консоль. При подключении CDN-сборки нужен defer. Если компонент зарегистрирован позже старта, его имя ещё не будет известно при разборе страницы.

Сумма стала строкой или NaN

HTML-поле может дать строку или пустое значение. Проверьте преобразование и границы до вычисления. Не выводите последний удачный итог так, будто он соответствует уже неверному вводу.

После удаления строки значения оказались у соседа

Проверьте устойчивые уникальные ключи x-for. Ключ должен принадлежать сущности, а не текущему месту в массиве.

Отмена редактирования не возвращает данные

Поля, вероятно, связаны с подтверждённым объектом. Создавайте отдельную рабочую копию и учитывайте вложенные массивы и объекты.

Скрытая панель мелькает при загрузке

Добавьте x-cloak и соответствующий CSS. Проверьте также, что при ошибке загрузки библиотеки человек увидит понятное сообщение, а не бесконечно пустую область.

Настройки не сохраняются после обновления

Проверьте порядок Persist перед Alpine, ключи хранилища и доступность storage. Приватный режим, ограничения браузера и очистка данных могут изменить поведение. Интерфейс должен оставаться рабочим и без постоянного хранения.

Модальная панель открылась, но клавиатура осталась на фоне

Видимость блока не управляет всем поведением диалога. Используйте native dialog или продуманное управление фокусом, закрытие по Escape, доступное имя и возврат к кнопке открытия.

Перед внедрением пройдите сценарий на узком экране: длинные подписи, пустое поле, ошибочное значение, возврат, отмена и сброс. Проверьте Tab и Enter. После добавления строки фокус должен помогать продолжить ввод, а после ошибки — находить проблемное поле. Цветовое изменение дополняйте текстом.

15. Где такой подход особенно полезен

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

Оценивайте сложность всего приложения. Если интерфейс требует большой системы компонентов, сложной навигации и разделяемого состояния на многих экранах, архитектуру стоит выбирать шире одной библиотеки. Если задача решается коротким обработчиком, обычный JavaScript тоже остаётся хорошим инструментом.

Сначала определите результат действия: что увидит человек после изменения поля, что произойдёт при ошибке и как вернуться к исходному состоянию. Затем выберите директивы. В этих мастерских именно сценарий задаёт структуру кода.

16. Источники и связанные материалы

Для отдельных элементов интерфейса пригодятся Tom Select, IMask и noUiSlider. Alpine помогает связать подобные элементы с состоянием страницы; при совместном использовании синхронизацию значений и жизненный цикл стороннего виджета нужно продумать отдельно.