Вызовите event.preventDefault() до первого await, выполните асинхронную проверку, а затем используйте form.requestSubmit(), чтобы начать новую попытку отправки. Одноразовый флаг позволит второй попытке продолжиться без повторной асинхронной проверки.

Это руководство предназначено для тех, кто редактирует небольшой веб-интерфейс в браузере на Mac. Вы создадите локальный пример HTML, в котором форма закрывает диалоговое окно только после успешной синтетической проверки метки. Этот же шаблон обработки событий подходит для обычных форм, которые выполняют переход или отправляют запрос.

Почему асинхронный обработчик отправки не задерживает отправку?

Браузер не ожидает выполнения промиса, возвращённого обработчиком события. Алгоритм диспетчеризации событий DOM вызывает обработчики и продолжает работу; алгоритм отправки формы HTML проверяет, было ли событие submit отменено, прежде чем продолжить. Добавление async к обработчику не вводит в этот жизненный цикл этап ожидания. Стандарт DOM: диспетчеризация событий, Стандарт HTML: алгоритм отправки формы

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

Отмена завершает исходную попытку, а не приостанавливает её для последующего возобновления. После успешной проверки код должен явно запросить новую отправку. Кроме того, preventDefault() не останавливает распространение события, поэтому другие обработчики по-прежнему могут его получить. MDN: Event.preventDefault()

Обрабатывайте событие submit на форме. Это охватывает отправку кнопкой, поддерживаемую неявную отправку, например нажатием Enter, и вызовы requestSubmit(). Обработчик щелчка только для кнопки не охватывает все способы отправки. Обычно сначала выполняется встроенная проверка ограничений: недопустимое обязательное поле блокирует отправку, не вызывая submit. MDN: событие отправки HTMLFormElement

Какой метод сохраняет поведение отправки формы?

Используйте requestSubmit(), если после успешной проверки нужно вернуться к обычному жизненному циклу формы. Прямой вызов submit() пропускает и проверку ограничений, и событие submit. Такой обход меняет поведение, которое должен сохранять асинхронный обработчик. MDN: HTMLFormElement.submit()

Подход Что происходит Подходит ли здесь
Вернуть промис из обработчика Диспетчеризация событий продолжается, не дожидаясь его выполнения Не задерживает действие по умолчанию
Отменить событие, затем вызвать submit() Отправка пропускает встроенную проверку и обработчики события отправки Меняет жизненный цикл
Отменить событие, затем вызвать requestSubmit(submitter) Встроенная проверка и событие отправки выполняются снова Сохраняет предполагаемый путь отправки

До ожидания сохраните event.submitter. Передача этой кнопки в requestSubmit() сохраняет её роль, включая переопределения параметров отправки и добавление её имени и значения, если это применимо. Если кнопки отправки не было, вызовите requestSubmit() без аргументов. Переданная кнопка должна по-прежнему принадлежать форме; недопустимая кнопка или кнопка, принадлежащая другой форме, вызовет исключение. MDN: HTMLFormElement.requestSubmit()

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

Создайте законченный локальный пример на Mac

Сохраните следующий полный документ как async-form.html в обычном текстовом редакторе кода, а затем откройте его в актуальном браузере на Mac. Библиотеки, импорты, сервер и установка не требуются. В стандарте указана поддержка requestSubmit(), начиная с Safari 16. Стандарт HTML: API форм

В примере используется method="dialog", чтобы действие по умолчанию было легко увидеть локально. При успешной отправке закрывается родительское диалоговое окно, а его returnValue задаётся значением кнопки отправки; содержимое полей формы не передаётся. Для отображения окна кнопка открытия вызывает showModal(). MDN: элемент dialog, MDN: HTMLDialogElement.showModal()

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

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Asynchronous form validation</title>
</head>
<body>
  <h1>Label preview</h1>
  <button id="open" type="button">Open form</button>
  <p id="outcome" role="status"></p>

  <dialog id="editor" aria-labelledby="dialog-title">
    <form id="label-form" method="dialog">
      <h2 id="dialog-title">Choose a label</h2>
      <label for="label">Label</label>
      <input id="label" name="label" required autofocus>
      <button type="submit" value="accepted">Continue</button>
      <button id="cancel" type="button">Cancel</button>
      <p id="status" role="status"></p>
    </form>
  </dialog>

  <script>
    const dialog = document.getElementById("editor");
    const form = document.getElementById("label-form");
    const input = document.getElementById("label");
    const status = document.getElementById("status");
    const outcome = document.getElementById("outcome");
    const openButton = document.getElementById("open");
    const cancelButton = document.getElementById("cancel");

    let pending = false;
    let allowNextSubmit = false;
    let revision = 0;

    async function validateLabel(value) {
      await new Promise(resolve => setTimeout(resolve, 700));
      const label = value.trim();
      if (label === "error") {
        throw new Error("Synthetic validation failure");
      }
      return label.length >= 3 && label !== "taken";
    }

    openButton.addEventListener("click", () => {
      revision += 1;
      dialog.returnValue = "";
      status.textContent = "";
      outcome.textContent = "";
      dialog.showModal();
    });

    input.addEventListener("input", () => {
      revision += 1;
    });

    cancelButton.addEventListener("click", () => dialog.close());

    dialog.addEventListener("close", () => {
      revision += 1;
      outcome.textContent = "Dialog closed. Return value: " +
        (dialog.returnValue || "(empty)");
    });

    form.addEventListener("submit", async event => {
      if (allowNextSubmit) {
        allowNextSubmit = false;
        return;
      }

      event.preventDefault();
      if (pending) return;

      pending = true;
      const submitter = event.submitter;
      const value = input.value;
      const checkedRevision = revision;
      status.textContent = "Checking label...";

      try {
        const valid = await validateLabel(value);
        if (!dialog.open) return;

        if (revision !== checkedRevision || input.value !== value) {
          status.textContent = "The label changed. Submit again.";
          return;
        }

        if (!valid) {
          status.textContent =
            "Use at least three characters and avoid taken.";
          return;
        }

        status.textContent = "Check passed. Submission requested.";
        allowNextSubmit = true;
        try {
          if (submitter) form.requestSubmit(submitter);
          else form.requestSubmit();
        } finally {
          allowNextSubmit = false;
        }
      } catch (error) {
        if (dialog.open && revision === checkedRevision) {
          status.textContent = "The check failed. Try again.";
        }
        console.error(error);
      } finally {
        pending = false;
      }
    });
  </script>
</body>
</html>

Поведение браузера в примере следует из документированного жизненного цикла. Небольшая проверка на Node.js с EventTarget подтвердила различие во времени: синхронная отмена изменила результат диспетчеризации, тогда как отмена после await на него уже не повлияла. Сам пример с диалоговым окном здесь не запускался в браузере на Mac.

Как флаг сохраняет порядок действий и учитывает изменения?

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

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

При успешной проверке allowNextSubmit устанавливается непосредственно перед вызовом requestSubmit(). Этот метод снова запускает встроенную проверку и путь события отправки. При повторном входе в обработчик разрешение используется, и обработчик возвращается, не отменяя событие. После этого браузер может выполнить действие по умолчанию и закрыть диалоговое окно. Такой порядок соответствует последовательности стандарта: сначала проверка, затем событие отправки. Стандарт HTML: алгоритм отправки формы

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

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

Проверьте сценарии успеха, отказа и прерывания

Выполните эти проверки в локальном примере в браузере. Ожидаемые результаты следуют из приведённого кода; они не являются результатами тестирования в браузере.

Действие Ожидаемый результат
Оставить метку пустой и выбрать Continue Встроенная проверка обязательного поля блокирует отправку до запуска асинхронного обработчика
Ввести ab и выбрать Continue Диалоговое окно остаётся открытым; после проверки отображается сообщение об отказе
Ввести taken Синтетическое правило отклоняет значение
Ввести sample Во время проверки диалоговое окно остаётся открытым, затем закрывается со значением accepted
Ввести error Появляется сообщение об ошибке, и отправку можно выполнить ещё раз
Отправить метку, затем изменить её во время проверки Результат отбрасывается, и форма предлагает отправить данные ещё раз
Несколько раз отправить форму во время проверки Асинхронная проверка запускается только для первой попытки
Выбрать Cancel во время проверки Диалоговое окно закрывается, не дожидаясь результата проверки

Также попробуйте отправить форму клавишей Enter, когда поле ввода находится в фокусе. Неявная отправка зависит от структуры формы и особенностей платформы; обработка submit нужна для охвата любого поддерживаемого способа взаимодействия, который действительно запускает отправку формы. MDN: событие отправки HTMLFormElement

Что меняется, если форма отправляет обычный запрос?

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

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

Если существуют другие обработчики отправки, они увидят и первую отменённую попытку, и следующую попытку. Согласуйте их работу, чтобы они не выполняли одно и то же дважды и не изменяли проверенные значения во время разрешённого прохода. Вызов requestSubmit() также не гарантирует завершения: другой обработчик может отменить событие, а встроенные ограничения — заблокировать отправку. Возвращаемое значение — undefined, а не промис завершения. MDN: HTMLFormElement.requestSubmit()

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

Источники