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

SweetAlert2: диалоги для веб-сервисов — мастерская и 6 примеров

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

Александр Некрасов
SweetAlert2 — диалоги для сервисов: окно подтверждения, уведомление и кнопки действий

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

SweetAlert2 помогает строить такие окна на обычном JavaScript: с понятными кнопками, полями, состоянием ожидания и результатом, который удобно обработать через await. Здесь доступны мини-мастерская и шесть живых интерфейсов, исходники и самостоятельный HTML. Демонстрации используют SweetAlert2 11.26.25.

Начните с результата: отмените действие в мастерской и посмотрите объект ответа → переименуйте проект → вызовите ошибку и повторите попытку → верните изменение через «Отменить». Важно увидеть не только окно, но и то, как после него меняется интерфейс.

Чем SweetAlert2 отличается от alert, confirm и prompt

Нативные диалоги подходят для короткой проверки в консоли или простого служебного действия. Они встроены в браузер, но почти не дают управлять видом, подписями и содержимым. У confirm() результат — логическое значение, у prompt() — строка либо null при отмене. Эти вызовы синхронные: следующая строка кода ждёт завершения диалога.

const accepted = window.confirm('Удалить выбранные записи?');
if (accepted) {
  console.log('Пользователь подтвердил действие');
}
const name = window.prompt('Название проекта', 'Личный кабинет');
if (name !== null) console.log(name);

Swal.fire() возвращает Promise. Окно открывается, а приложение продолжает работать. await приостанавливает конкретную асинхронную функцию, затем отдаёт ей объект результата. Поэтому заменять confirm() механическим поиском по проекту нельзя: нужно перестроить место, где принимается решение.

async function askBeforeDelete() {
  const result = await Swal.fire({
    titleText: 'Удалить выбранные записи?',
    text: 'Проверьте список перед подтверждением.',
    icon: 'warning',
    showCancelButton: true,
    confirmButtonText: 'Удалить',
    cancelButtonText: 'Отмена',
    focusCancel: true
  });
  return result.isConfirmed;
}
Задача Нативный диалог SweetAlert2
Подтверждение confirm возвращает true/false Отдельные результаты Confirm, Deny и закрытие
Ввод Одна строка prompt Поле с подписью, проверкой и сообщением об ошибке
Ожидание запроса Отдельный интерфейс приходится писать рядом preConfirm, loader и контролируемое закрытие
Оформление и мобильный экран Управляются браузером Стили, размеры и поведение контролирует приложение

SweetAlert и SweetAlert2 — разные пакеты. Для этой статьи нужен sweetalert2 и вызов Swal.fire. Не смешивайте примеры с swal(...) из другой библиотеки. При этом не каждое действие нуждается в модальном окне: постоянную настройку обычно удобнее менять прямо на странице, а короткий результат можно показать уведомлением.

Источник: поведение нативного confirm.

Источник: тип Promise и объект результата SweetAlert2.

Мини-мастерская: соберите собственный диалог

В мастерской четыре исходных варианта: подтверждение, ввод, toast и сообщение об ошибке. Меняйте заголовок, описание, подписи кнопок и доступные параметры, затем открывайте настоящий диалог. Рядом появляется его результат: подтверждение, отмена, причина закрытия и введённое значение. Так проще понять связь между настройкой и поведением.

Первое упражнение: откройте «Сохранить настройки?» и нажмите «Отмена». В журнале будут isConfirmed: false, isDismissed: true и dismiss: "cancel". При повторном запуске нажмите «Сохранить»: подтверждение станет истинным. Настройки на сервер при этом не отправляются.

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

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

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

Шесть интерфейсов: попробуйте полный путь пользователя

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

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

Скачать исходники мастерской и шести примеров. У каждого сценария есть самостоятельный HTML для экспериментов. Меняйте данные, проверяйте результат, затем используйте подход в своём интерфейсе.

JSON из интерфейса сохраняет данные сценария, а HTML — ещё и возможность взаимодействовать с ними. Проверьте обе стороны переноса: в файле должны остаться введённые значения, а сброс должен вернуть именно экспортированный вариант. Журнал демонстрации не является историей серверных операций и не подтверждает, что данные были записаны в базу.

1. Подтверждение удаления: контекст важнее слова «Да»

В списке заявок человек должен понимать, что именно исчезнет. Покажите количество выбранных записей, назовите действие глаголом и оставьте безопасный путь назад. Кнопки «Удалить» и «Отмена» читаются быстрее, чем одинаково общие «Да» и «Нет».

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

async function confirmRemoval(ids) {
  if (ids.length === 0) return false;
  const result = await Swal.fire({
    titleText: `Удалить записи: ${ids.length}?`,
    text: 'Будут затронуты только выбранные строки.',
    icon: 'warning',
    showCancelButton: true,
    confirmButtonText: 'Удалить',
    cancelButtonText: 'Отмена',
    focusCancel: true,
    allowOutsideClick: false
  });
  return result.isConfirmed;
}

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

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

Открыть подтверждение удаления ↑

2. Переименование: ввод, проверка и понятная ошибка

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

Попробуйте: проект изначально называется «Личный кабинет». Отправьте пустую строку или только пробелы: окно останется открытым с пояснением. Затем задайте «Кабинет партнёра» и сравните заголовок карточки с результатом. В этом интерфейсе допустимы от 3 до 60 символов после удаления пробелов по краям.

async function askProjectName(currentName) {
  return Swal.fire({
    titleText: 'Переименовать проект',
    input: 'text',
    inputLabel: 'Название проекта',
    inputValue: currentName,
    inputAttributes: { maxlength: '60', autocomplete: 'off' },
    showCancelButton: true,
    confirmButtonText: 'Сохранить',
    cancelButtonText: 'Отмена',
    inputValidator: value => {
      const name = value.trim();
      if (name.length < 3) return 'Введите название от 3 символов';
      if (name.length > 60) return 'Максимум 60 символов';
    }
  });
}
// После await: менять модель только если result.isConfirmed.

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

Открыть переименование проекта ↑

3. Асинхронное действие: подтверждение, ожидание и результат

Между нажатием и ответом сервиса проходит время. Если закрыть окно сразу и показать успех заранее, человек получит ложное подтверждение. Лучше оставить контекст, показать выполнение и завершить сценарий после результата операции.

Попробуйте: отправьте заполненную форму с темой, приоритетом и описанием. После подтверждения появится ожидание на 650 мс, затем квитанция DEMO-201. Измените тему и отправьте снова — получится DEMO-202. Ни одного сетевого запроса здесь нет; квитанция относится только к локальному примеру.

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

async function demonstrateCheck() {
  return Swal.fire({
    titleText: 'Проверить данные?',
    text: 'Локальная демонстрация ожидания, без отправки.',
    showCancelButton: true,
    confirmButtonText: 'Проверить',
    cancelButtonText: 'Отмена',
    showLoaderOnConfirm: true,
    allowOutsideClick: () => !Swal.isLoading(),
    allowEscapeKey: () => !Swal.isLoading(),
    preConfirm: async () => {
      await wait(800);
      return { checked: true, mode: 'local-demo' };
    }
  });
}

preConfirm связывает подтверждение с асинхронной функцией. Её результат станет result.value. Библиотека управляет ожиданием внутри окна, а приложение решает, когда операция считается законченной и какой статус показать на странице. Для длительных задач удобнее отдельная страница прогресса с возможностью вернуться позже.

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

Открыть асинхронную форму ↑

4. Ошибка и повтор: не теряйте введённые данные

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

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

async function demonstrateRetry() {
  let attempt = 0;
  return Swal.fire({
    titleText: 'Проверка соединения',
    text: 'Первая попытка в этой демонстрации завершится ошибкой.',
    confirmButtonText: 'Проверить',
    showCancelButton: true,
    cancelButtonText: 'Отмена',
    showLoaderOnConfirm: true,
    allowOutsideClick: () => !Swal.isLoading(),
    allowEscapeKey: () => !Swal.isLoading(),
    preConfirm: async () => {
      await new Promise(resolve => setTimeout(resolve, 600));
      attempt += 1;
      if (attempt === 1) {
        Swal.showValidationMessage('Не получилось. Попробуйте ещё раз.');
        return false;
      }
      return { attempt, checked: true };
    }
  });
}

return false оставляет диалог открытым. Это отличается от возврата объекта с полем ok: false: такой объект сам по себе не запрещает закрытие. В реальном приложении возможность повтора зависит от операции. Проверить статус обычно можно повторно; создать платёж или отправить письмо следует с серверной защитой от дублирования.

Открыть ошибку и повтор ↑

5. Пошаговый мастер: собираем решение до сохранения

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

Попробуйте: мастер содержит три шага: тема, приоритет и описание. На втором выберите «Высокий», вернитесь назад и измените тему. Закройте мастер, откройте снова и завершите его — обращение W-401 сохранит выбранные значения. До завершения сохраняются черновик и шаг, но готовое обращение ещё не создаётся.

async function twoStepRequest() {
  const nameStep = await Swal.fire({
    titleText: 'Название заявки',
    input: 'text', inputLabel: 'Короткое название',
    showCancelButton: true,
    confirmButtonText: 'Далее', cancelButtonText: 'Отмена',
    progressSteps: ['1', '2'], currentProgressStep: 0,
    inputValidator: value => !value.trim() && 'Введите название'
  });
  if (!nameStep.isConfirmed) return null;

  const review = await Swal.fire({
    titleText: 'Проверьте заявку',
    text: nameStep.value,
    showCancelButton: true,
    confirmButtonText: 'Создать', cancelButtonText: 'Отмена',
    progressSteps: ['1', '2'], currentProgressStep: 1
  });
  return review.isConfirmed ? { name: nameStep.value } : null;
}

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

Открыть пошаговое создание ↑

6. Toast и отмена: сообщаем результат без остановки работы

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

Попробуйте: переключите вид «Список» на «Карточки» и сохраните. Уведомление показывается 4,5 секунды; «Отменить» вернёт «Список». После исчезновения toast остаётся постоянная кнопка восстановления. Для следующего сохранения нужен новый снимок, иначе отмена вернёт слишком старые данные.

const Toast = Swal.mixin({
  toast: true,
  position: 'top-end',
  showConfirmButton: false,
  showCloseButton: true,
  closeButtonAriaLabel: 'Закрыть уведомление',
  timer: 5000,
  timerProgressBar: true
});

function announceSaved() {
  return Toast.fire({ icon: 'success', titleText: 'Изменения сохранены' });
}

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

function pauseWhileReading(popup) {
  let hovered = false;
  const syncTimer = () => {
    const focused = popup.contains(document.activeElement);
    if (hovered || focused) Swal.stopTimer();
    else Swal.resumeTimer();
  };
  popup.addEventListener('mouseenter', () => { hovered = true; syncTimer(); });
  popup.addEventListener('mouseleave', () => { hovered = false; syncTimer(); });
  popup.addEventListener('focusin', syncTimer);
  popup.addEventListener('focusout', () => queueMicrotask(syncTimer));
}
// В опциях toast: didOpen: pauseWhileReading
// Swal.getTimerLeft() вернёт оставшиеся миллисекунды или undefined.

Библиотека показывает уведомление и отсчитывает время. Восстановление данных, история изменений и срок доступности отмены — отдельные решения приложения. Плашка «Сохранено» должна появляться после действительного успеха, а не просто после нажатия кнопки.

Открыть уведомление с отменой ↑

Подключение: готовая HTML-страница, npm и локальные файлы

Для обычной страницы достаточно CSS и JavaScript. Пример ниже можно сохранить как index.html и открыть в браузере при доступе к CDN. Кнопка меняет текст результата только после решения пользователя. Это полный минимальный документ; остальные фрагменты статьи показывают отдельные приёмы.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Первый диалог SweetAlert2</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.26.25/dist/sweetalert2.min.css">
</head>
<body class="p-4">
  <main class="container">
    <h1 class="h3">Настройки проекта</h1>
    <button id="open-dialog" type="button" class="btn btn-primary">Сохранить</button>
    <p id="result" class="mt-3" role="status">Изменения ещё не подтверждены.</p>
  </main>
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.26.25/dist/sweetalert2.min.js"></script>
  <script>
    const button = document.getElementById('open-dialog');
    const output = document.getElementById('result');
    button.addEventListener('click', async () => {
      button.disabled = true;
      try {
        const result = await Swal.fire({
          titleText: 'Применить настройки?',
          text: 'Это локальный пример без отправки на сервер.',
          icon: 'question',
          showCancelButton: true,
          confirmButtonText: 'Применить',
          cancelButtonText: 'Отмена',
          returnFocus: false
        });
        output.textContent = result.isConfirmed
          ? 'Настройки подтверждены в этом примере.'
          : 'Изменения не применены.';
      } finally {
        button.disabled = false;
        button.focus();
      }
    });
  </script>
</body>
</html>

В проекте со сборщиком установите пакет и используйте импорт. Здесь указан точный номер для воспроизводимости; обновление зависимости делайте осознанно вместе с проверкой сценариев.

npm install sweetalert2@11.26.25
import Swal from 'sweetalert2';

document.querySelector('#help').addEventListener('click', () => {
  Swal.fire({ titleText: 'Подсказка', text: 'Выберите нужную строку.', icon: 'info' });
});

CDN необязателен: положите файлы библиотеки в свой проект и замените адреса. Вариант sweetalert2.min.js используется с отдельным CSS; сборка sweetalert2.all.min.js включает стили. Не подключайте оба варианта одновременно. Bare import из npm требует сборщика или корректной карты импортов и не заработает от простой вставки в обычный <script>.

<link rel="stylesheet" href="./vendor/sweetalert2.min.css">
<script src="./vendor/sweetalert2.min.js"></script>
<script src="./app.js"></script>

Источник: релиз и файлы SweetAlert2 11.26.25.

Результат, три действия и повторное использование настроек

Подтверждение, альтернативное действие и закрытие имеют разные смыслы. Например, «Сохранить», «Не сохранять» и «Вернуться к редактированию» нельзя свести к одному логическому значению. isDenied означает нажатие отдельной кнопки Deny; «Отмена», Escape и крестик относятся к закрытию.

async function askAboutChanges() {
  const result = await Swal.fire({
    titleText: 'Сохранить изменения перед выходом?',
    showDenyButton: true, showCancelButton: true,
    confirmButtonText: 'Сохранить',
    denyButtonText: 'Не сохранять',
    cancelButtonText: 'Продолжить редактирование'
  });
  if (result.isConfirmed) return 'save';
  if (result.isDenied) return 'discard';
  return 'stay';
}

Не проверяйте только if (result.value): ноль, пустая строка и false могут быть допустимыми значениями вашей задачи. Сначала выясните, подтвердил ли человек действие, затем проверяйте содержимое. Для диагностики можно различать конкретную причину закрытия:

function describeDismissal(result) {
  if (!result.isDismissed) return '';
  if (result.dismiss === Swal.DismissReason.cancel) return 'Нажата отмена';
  if (result.dismiss === Swal.DismissReason.esc) return 'Нажат Escape';
  if (result.dismiss === Swal.DismissReason.timer) return 'Истёк таймер';
  return 'Окно закрыто';
}

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

const Dialog = Swal.mixin({
  confirmButtonText: 'Продолжить',
  cancelButtonText: 'Отмена',
  buttonsStyling: false,
  customClass: {
    confirmButton: 'btn btn-primary m-1',
    cancelButton: 'btn btn-outline-secondary m-1',
    denyButton: 'btn btn-outline-danger m-1'
  }
});
// Dialog.fire({ titleText: 'Сохранить?', showCancelButton: true });

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

let dialogBusy = false;
async function openOnce(options) {
  if (dialogBusy) return null;
  dialogBusy = true;
  try {
    return await Swal.fire(options);
  } finally {
    dialogBusy = false;
  }
}

Настоящий fetch и PHP: где заканчивается диалог

В рабочем сервисе preConfirm может отправлять запрос. Проверяйте HTTP-статус, формат ответа и смысл результата: fetch не отклоняет Promise только из-за ответа 404 или 500. Сообщение пользователю выбирайте сами, не вставляйте произвольный HTML из серверной ошибки.

Ниже — пример проверки названия через ваш будущий /validate-project.php. Он не вызывается живыми демонстрациями статьи. Тайм-аут ограничивает ожидание; закрытие окна само по себе не отменяет запрос и не откатывает действие на сервере.

async function validateProjectTitle() {
  return Swal.fire({
    titleText: 'Проверить название',
    input: 'text', inputLabel: 'Название проекта',
    inputValidator: value => !value.trim() && 'Введите название',
    showCancelButton: true,
    confirmButtonText: 'Проверить', cancelButtonText: 'Отмена',
    showLoaderOnConfirm: true,
    allowOutsideClick: () => !Swal.isLoading(),
    allowEscapeKey: () => !Swal.isLoading(),
    preConfirm: async value => {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 8000);
      try {
        const response = await fetch('/validate-project.php', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
          body: JSON.stringify({ title: value.trim() }),
          signal: controller.signal
        });
        if (response.status === 422) {
          Swal.showValidationMessage('Проверьте название: от 1 до 60 символов.');
          return false;
        }
        if (!response.ok) throw new Error('HTTP error');
        const body = await response.json();
        if (body.valid !== true || typeof body.title !== 'string') {
          throw new Error('Unexpected response');
        }
        return body;
      } catch (error) {
        Swal.showValidationMessage(error.name === 'AbortError'
          ? 'Сервер не ответил вовремя. Повторите проверку.'
          : 'Не удалось проверить название. Попробуйте позже.');
        return false;
      } finally {
        clearTimeout(timeout);
      }
    }
  });
}

Минимальный PHP-обработчик ниже только проверяет строку и возвращает её нормализованное значение. Он не создаёт проект и не пишет в базу. Потребуются PHP 8.1+ и расширение mbstring. Сохраните файл под указанным именем и запускайте вместе со своей HTML-страницей на одном локальном сервере.

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');

function respond(int $status, array $body): never {
    http_response_code($status);
    echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Allow: POST');
    respond(405, ['error' => 'method_not_allowed']);
}
try {
    $body = json_decode(file_get_contents('php://input'), true, 32, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
    respond(400, ['error' => 'invalid_json']);
}
$title = is_array($body) && is_string($body['title'] ?? null)
    ? trim($body['title']) : '';
if ($title === '' || mb_strlen($title, 'UTF-8') > 60) {
    respond(422, ['error' => 'invalid_title']);
}
respond(200, ['valid' => true, 'title' => $title]);

Для настоящего сохранения добавляются аутентификация, права, защита от CSRF при cookie-сессии и операция в хранилище. Только после подтверждённого сервером изменения можно писать «Сохранено». Если PHP обращается к другому сервису, полезен Guzzle; установка серверных пакетов разобрана в руководстве по Composer. Самой SweetAlert2 Composer не нужен.

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

Источник: проверка HTTP-ответа и отмена fetch.

Источник: разбор JSON в PHP.

Безопасный текст, свой DOM и жизненный цикл окна

Название проекта, имя файла и ответ API считайте данными. Передавайте их через titleText и text. Параметр html предназначен для разметки и не обещает очистку произвольного ввода. Конкатенация пользовательской строки с тегами создаёт ненужный риск и ломает отображение символов.

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

function showProjectInfo(project) {
  const content = document.createElement('div');
  const name = document.createElement('p');
  const status = document.createElement('p');
  name.textContent = project.name;
  status.textContent = `Статус: ${project.status}`;
  content.append(name, status);
  return Swal.fire({ titleText: 'Карточка проекта', html: content });
}

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

async function showTimedNotice() {
  let interval;
  const remaining = document.createElement('p');
  remaining.textContent = 'Сообщение закроется через 5 секунд.';
  return Swal.fire({
    titleText: 'Сообщение закроется автоматически',
    html: remaining,
    timer: 5000,
    timerProgressBar: true,
    confirmButtonText: 'Понятно',
    didOpen: () => {
      interval = setInterval(() => {
        const left = Swal.getTimerLeft();
        if (left !== undefined) {
          remaining.textContent = `Осталось: ${Math.ceil(left / 1000)} с`;
        }
      }, 250);
    },
    didDestroy: () => clearInterval(interval)
  });
}

Не создавайте ещё один Swal.fire внутри незавершённого preConfirm только ради ошибки: текущий диалог может быть уничтожен вместе с вводом. Покажите сообщение в нём, верните false и дайте человеку продолжить. Отдельный экран успеха открывайте после завершения первого Promise.

Источник: обработка preConfirm и ошибок подтверждения.

Мобильный экран и клавиатура: окно должно помогать

На телефоне особенно заметны длинные подписи, низкие кнопки и поле, которое скрывает клавиатура. Ограничьте ширину окном браузера, разрешите кнопкам переноситься и оставьте читаемый размер ввода. Проверяйте не только первый экран, но и ошибку, открытое меню выбора, длинное название и появление экранной клавиатуры.

/* Класс добавляется через customClass: { popup: 'service-dialog' }. */
.service-dialog { width: min(32rem, calc(100vw - 2rem)); }
.service-dialog .swal2-actions { flex-wrap: wrap; gap: .5rem; }
.service-dialog button { min-height: 44px; }
.service-dialog input,
.service-dialog select,
.service-dialog textarea { font-size: 16px; }
@media (prefers-reduced-motion: reduce) {
  .service-dialog { animation: none !important; }
}

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

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

Избегайте нескольких модальных слоёв. При совместном использовании с Bootstrap modal проверьте обработку Escape и фокус: для перехвата клавиши у SweetAlert2 есть keydownListenerCapture, но сам по себе этот параметр не исправляет запутанный сценарий. В демонстрациях статьи диалоги доступны в области экрана; при переносе в чужой iframe это ограничение нужно проверить отдельно.

На узком экране проверьте длинную кнопку отмены и сообщение об ошибке в несколько строк. На настольном — масштаб страницы 200% и последовательность Tab/Shift+Tab. Не полагайтесь только на цвет: опасное действие должно быть понятно из текста, а успех — из фактического изменения карточки. Настройка ARIA у библиотеки помогает, но качество подписей и доступность собственных полей остаются ответственностью автора интерфейса.

Частые ошибки и как проверить готовый сценарий

Симптом Почему возникает Что изменить
Действие выполняется сразу Promise принят за ответ пользователя Дождаться await и проверить isConfirmed
Отмена считается подтверждением Проверяется весь объект result Разделить Confirm, Deny и Dismiss
Ошибка закрывает окно preConfirm вернул объект ошибки Показать сообщение и вернуть false
Успех появился при HTTP 500 Не проверен response.ok Проверить статус и формат ответа
Вместо названия появилась разметка Данные подставлены в html Использовать text/titleText или textContent
Повторно открылись два шага Инициатор не защищён от повторов Busy-флаг и очистка в finally
Toast исчезает во время чтения Пауза учитывает только мышь Учитывать фокус и сохранить важное действие на странице
После закрытия потерялся фокус Исходная кнопка удалена или отключена Вернуть фокус к доступному элементу после обновления DOM

Проверяйте отмену на каждом шаге, а не только успешный путь. Для ввода нужны пустая строка, пробелы, максимальная длина и необычные символы. Для запроса — медленный ответ, ошибка HTTP, некорректный JSON и повтор. Для удаления — нулевой выбор, изменение списка и восстановление. Эти проверки объясняют поведение гораздо лучше, чем десяток вариантов цвета одной кнопки.

Не переносите старые настройки без сверки с версией. Например, allowEnterKey в используемой ветке помечен устаревшим. У modal и toast разные роли и совместимые параметры: общую конфигурацию нужно собирать с учётом режима, а не передавать всё сразу.

Источник: параметры, устаревшие настройки и ограничения toast.

Исходники и следующий шаг в своём проекте

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

Скачать мастерскую и все шесть интерфейсов. Сохранённый самостоятельный HTML позволяет продолжить эксперименты вне статьи. Общую страницу из исходников запускайте по инструкции README; отдельные автономные примеры открываются без внешних зависимостей.

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