Отправка формы без перезагрузки в JavaScript

Обычная HTML-форма отправляет данные на сервер и загружает новую страницу. Если нужно остаться на текущей странице, отправку перехватывают в JavaScript: отменяют стандартное действие и выполняют запрос с помощью fetch().

Основная схема выглядит так:

form.addEventListener('submit', async function (event) {
  event.preventDefault();

  const formData = new FormData(form);
  const response = await fetch('/api/feedback', {
    method: 'POST',
    body: formData,
  });
});

preventDefault() оставляет пользователя на странице, FormData собирает поля, а fetch() отправляет их на сервер.

Форма для отправки

Создадим форму обратной связи:

<form
  class="feedback-form"
  action="/api/feedback"
  method="post"
>
  <label>
    Имя
    <input name="username" type="text" required>
  </label>

  <label>
    Электронная почта
    <input name="email" type="email" required>
  </label>

  <label>
    Сообщение
    <textarea name="message" rows="5" required></textarea>
  </label>

  <button type="submit">Отправить</button>
</form>

<p class="feedback-status" aria-live="polite"></p>

Атрибут action содержит адрес серверного обработчика, а method=“post” - HTTP-метод. У каждого отправляемого поля есть name. Без него значение не попадёт в данные формы.

Атрибуты required и type=“email” включают встроенную проверку браузера. Если обязательное поле не заполнено или адрес имеет неверный формат, событие submit не произойдёт.

Полный пример отправки

Добавим обработчик, который отправляет форму, показывает состояние запроса и учитывает возможные ошибки:

const feedbackForm = document.querySelector('.feedback-form');
const submitButton = feedbackForm.querySelector('[type="submit"]');
const feedbackStatus = document.querySelector('.feedback-status');

feedbackForm.addEventListener('submit', async function (event) {
  event.preventDefault();

  submitButton.disabled = true;
  feedbackStatus.textContent = 'Отправляем сообщение...';

  try {
    const formData = new FormData(feedbackForm);

    const response = await fetch(feedbackForm.action, {
      method: feedbackForm.method,
      body: formData,
    });

    if (!response.ok) {
      throw new Error('Ошибка HTTP: ' + response.status);
    }

    const result = await response.json();

    feedbackStatus.textContent = (
      result.message ?? 'Сообщение успешно отправлено'
    );

    feedbackForm.reset();
  } catch (error) {
    console.error(error);
    feedbackStatus.textContent = (
      'Не удалось отправить сообщение. Попробуйте ещё раз.'
    );
  } finally {
    submitButton.disabled = false;
  }
});

В примере предполагается, что сервер принимает запрос методом POST по адресу /api/feedback и возвращает JSON:

{
  "message": "Спасибо! Сообщение отправлено"
}

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

submit и preventDefault()

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

Метод event.preventDefault() отменяет стандартный переход на другую страницу. Он не отправляет форму самостоятельно, поэтому после него требуется вызвать fetch() или выполнить другое предусмотренное действие.

Ключевое слово async разрешает использовать await внутри обработчика. await ожидает завершения запроса, но не блокирует всю страницу: пользователь по-прежнему может прокручивать её и работать с другими элементами.

Отправка FormData через fetch()

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

Свойства feedbackForm.action и feedbackForm.method получают значения одноимённых HTML-атрибутов. В нашем примере запрос отправляется методом POST на адрес /api/feedback.

При передаче FormData не нужно вручную задавать заголовок Content-Type. Браузер сам добавит тип multipart/form-data и специальную границу между частями данных.

Если самостоятельно установить Content-Type: multipart/form-data, в заголовке не будет автоматически созданной границы. Сервер может не суметь разобрать такой запрос.

Обработка ответа сервера

fetch() возвращает промис с объектом Response. В примере оператор await ожидает этот объект и сохраняет его в переменную response.

Свойство response.ok равно true для успешных HTTP-статусов от 200 до 299. Если свойство равно false, полный пример создаёт ошибку через throw new Error().

После проверки тело ответа читается методом, который соответствует формату сервера. Если сервер возвращает JSON, используют response.json().

Метод response.json() также возвращает промис, поэтому перед ним стоит await. Если сервер возвращает обычный текст, вместо него нужен response.text(). Выбор метода зависит от ответа конкретного серверного обработчика.

Обработка ошибок

Вызов fetch() отклоняет промис при сетевой ошибке, например когда сервер недоступен. Но ответы с HTTP-статусами 404 или 500 сами по себе не переходят в catch. Поэтому статус необходимо проверить через response.ok и при ошибке явно вызвать throw.

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

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

Состояние кнопки и сброс формы

Сетевой запрос может выполняться заметное время. На этот период кнопку отправки лучше отключить, чтобы пользователь случайно не создал несколько одинаковых запросов. В полном примере это делает присваивание submitButton.disabled = true.

Блок finally выполняется после try или catch, поэтому в нём кнопка возвращается в рабочее состояние.

Метод reset() очищает форму или возвращает полям начальные значения. Вызывать его следует только после успешного ответа.

Элемент со свойством aria-live=“polite” сообщает об изменении статуса пользователям программ чтения с экрана. Он подходит для сообщений «Отправляем», «Готово» и «Произошла ошибка».

Отправка формы в формате JSON

Некоторые серверные API ожидают JSON, а не FormData. В этом случае значения собирают в обычный объект, преобразуют через JSON.stringify() и явно указывают тип содержимого:

const data = {
  username: feedbackForm.elements.username.value.trim(),
  email: feedbackForm.elements.email.value.trim(),
  message: feedbackForm.elements.message.value.trim(),
};

const response = await fetch(feedbackForm.action, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data),
});

Здесь заголовок Content-Type нужен, потому что тело запроса действительно содержит JSON. Сервер должен быть настроен на чтение именно этого формата.

Выбор между JSON и FormData зависит от требований серверного API. Для обычных строковых данных подходят оба варианта. Если форма содержит файлы, обычно используют FormData.

Отправка файлов

Чтобы добавить файл, достаточно поместить input type=“file” внутрь формы и задать ему name:

<label>
  Вложение
  <input name="attachment" type="file">
</label>

Конструктор FormData включит выбранный файл, а код запроса останется тем же:

Файл не нужно заранее превращать в строку. Браузер передаст его как отдельную часть multipart/form-data. Ограничения размера и допустимых типов необходимо проверять не только в интерфейсе, но и на сервере.

Адрес обработчика и сервер

Адрес /api/feedback в примере условный. В реальном проекте его нужно заменить маршрутом, который существует на вашем сервере и принимает отправляемый формат.

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

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

Итого

Для отправки формы без перезагрузки обработайте событие submit, вызовите event.preventDefault(), соберите значения и передайте их в fetch(). FormData подходит для полей формы и файлов, а JSON - для API, которые ожидают объект в этом формате.

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