Drupal 8: AJAX отправка и валидация формы

На всех моих последних Drupal сайтах (D7) все формы выполняются через AJAX, так как это имеет множество преимуществ, от банального быстрого получения результата заполнения формы, ошибок или сообщения об успешном выполнении, до небольшого увеличения производительности за счет обработки одной формы, вместе рендера всей страницы с проверкой формы. В 8-ке никуда эта особенность не делась, но, к сожалению, пока что это лишь срезает одну крутую штуку, появившуюся в Drupal 8 - это html5 required элементы формы, которые проверяются на стороне клиента и сразу выдают ошибку что форма обязательна к заполнению или не соответствует нужному формату. Это удобно тем что даже нет необходимости слать запрос на сервер для элементарнейших проверок, но, к сожалению, обратная сторона медали - отказ от этого функционала, он просто перестает работать и форма субмитится игнорируя это. Но нет худа без добра, в 8.1 уже есть эксперементальный модуль для inline form error, который может очень даже решить проблему и даже добавить крутости, так как это уже будет контролироваться кодом, и можно будет менять сообщение, оформление, положение, чего html5 предоставить не может.

Что же, приступим. Для начала нам нужна какая-то форма. Как делать формы я уже писал, и приводил не один пример, поэтому я сразу приведу листинг формы над которой мы будем шаманить.

dummy.routing.yml
dummy.ajax_form_submit_example:
  path: '/dummy/ajax-form-submit-example'
  defaults:
    _form: '\Drupal\dummy\Form\AjaxFormSubmitExample'
    _title: 'Form with AJAX submit'
  requirements:
    _permission: 'access content'
/src/Form/AjaxFormSubmitExample.php
<?php
/**
 * @file
 * Contains \Drupal\dummy\Form\AjaxFormSubmitExample.
 */

namespace Drupal\dummy\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Form with modal window.
 */
class AjaxFormSubmitExample extends FormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'ajax_form_submit_example';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['email'] = [
      '#title' => 'Email',
      '#type' => 'email',
      '#required' => TRUE,
    ];

    $form['select'] = [
      '#title' => 'Select some fruit',
      '#type' => 'select',
      '#options' => [
        'apple' => 'Apple',
        'banana' => 'Banana',
        'orange' => 'Orange',
      ],
      '#empty_option' => '- Select -',
      '#required' => TRUE,
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#name' => 'submit',
      '#value' => 'Submit this form',
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message('Form submitted! Hooray!');
  }

}

Валидация

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

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

Сначала разберемся с полем для email, будем проверять, если почта на example.com домене, то будем писать что письмо может не дойти. Для начала давайте добавить к элементу формы email #ajax параметр со всеми настройками.


$form['email'] = [
  '#title' => 'Email',
  '#type' => 'email',
  '#required' => TRUE,
  '#ajax' => [
    # Если валидация находится в другом классе, то необходимо указывать
    # в формате Drupal\modulename\ClassName::methodName.
    'callback' => '::validateEmailAjax',
    # Событие, на которое будет срабатывать наш AJAX.
    'event' => 'change',
    # Настройки прогресса. Будет показана гифка с анимацией загрузки.
    'progress' => array(
      'type' => 'throbber',
      'message' => t('Verifying email..'),
    ),
  ],
  # Элемент, в который мы будем писать результат в случае необходимости.
  '#suffix' => '<div class="email-validation-message"></div>'
];
/**
 * {@inheritdoc}
 */
public function validateEmailAjax(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  if (substr($form_state->getValue('email'), -11) == 'example.com') {
    $response->addCommand(new HtmlCommand('.email-validation-message', 'This provider can lost our mail. Be care!'));
  }
  else {
    # Убираем ошибку если она была и пользователь изменил почтовый адрес.
    $response->addCommand(new HtmlCommand('.email-validation-message', ''));
  }
  return $response;
}
При попытке указать почту в домене @example.com выводится наше предупреждение.

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

$form['select'] = [
  '#title' => 'Select some fruit',
  '#type' => 'select',
  '#options' => [
    'apple' => 'Apple',
    'banana' => 'Banana',
    'orange' => 'Orange',
  ],
  '#empty_option' => '- Select -',
  '#required' => TRUE,
  '#ajax' => [
    'callback' => '::validateFruitAjax',
    'event' => 'change',
  ],
  '#prefix' => '<div id="fruit-selector">',
  '#suffix' => '</div>',
];
/**
 * {@inheritdoc}
 */
public function validateFruitAjax(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  switch ($form_state->getValue('select')) {
    case 'apple':
      $style = ['border' => '2px solid green'];
      break;

    case 'banana':
      $style = ['border' => '2px solid yellow'];
      break;

    case 'orange':
      $style = ['border' => '2px solid orange'];
      break;

    default:
      $style = ['border' => '2px solid transparent'];
  }
  $response->addCommand(new CssCommand('#fruit-selector select', $style));
  return $response;
}
Выбранный фрукт теперь влияет на цвет рамки селекта.

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

AJAX отправка (субмит) формы

Ну и осталось самое, наверное, полезное и главное из данной статьи. Хотя, если вы хоть раз делали аякс формы на 7-ке, вам не составит труда догадаться что в данном случае нужно сделать для этого. Всё супер просто и намного легче 7-ки.

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

Добавляем новый элемент формы
# Добавляем в форму новый элемент где будем выводить системные сообщения.
$form['system_messages'] = [
  '#markup' => '<div id="form-system-messages"></div>',
  '#weight' => -100,
];
Добавляем AJAX субмит к форме
$form['submit'] = [
  '#type' => 'submit',
  '#name' => 'submit',
  '#value' => 'Submit this form',
  '#ajax' => [
    'callback' => '::ajaxSubmitCallback',
    'event' => 'click',
    'progress' => [
      'type' => 'throbber',
    ],
  ],
];
AJAX callback
/**
 * {@inheritdoc}
 */
public function ajaxSubmitCallback(array &$form, FormStateInterface $form_state) {
  $ajax_response = new AjaxResponse();
  $message = [
    '#theme' => 'status_messages',
    '#message_list' => drupal_get_messages(),
    '#status_headings' => [
      'status' => t('Status message'),
      'error' => t('Error message'),
      'warning' => t('Warning message'),
    ],
  ];
  $messages = \Drupal::service('renderer')->render($message);
  $ajax_response->addCommand(new HtmlCommand('#form-system-messages', $messages));
  return $ajax_response;
}
Вывод ошибки о не успешной отправке формы. Успешная отправка формы.

Вот и всё! Дальше лишь фантазия и необходимость.

AJAX отправка сторонней формы

Мы рассмотрели как работать с AJAX в собственной форме. Но что если нужно добавить то же самое но к сторонней форме? Например у вас есть форма от модуля Contact, и вы хотите ей пользоваться, но вам не хватает AJAX отправки. Что же, это не проблема, делается это примерно точно также, с одним лишь отличием, что вы будете работать с чужой формой и поэтому тут появляется hook_form_alter(), в остальном ситуация точно такая же.

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

Листинг /src/AjaxContactSubmit.php
<?php

/**
 * @file
 * Contains \Drupal\dummy\AjaxContactSubmit.
 */

namespace Drupal\dummy;

use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class AjaxContactSubmit
 * @package Drupal\dummy\AjaxContactSubmit
 */
class AjaxContactSubmit {

  /**
   * Ajax form submit callback.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   * @return \Drupal\Core\Ajax\AjaxResponse
   */
  public function ajaxSubmitCallback(array &$form, FormStateInterface $form_state) {
    $ajax_response = new AjaxResponse();
    $message = [
      '#theme' => 'status_messages',
      '#message_list' => drupal_get_messages(),
      '#status_headings' => [
        'status' => t('Status message'),
        'error' => t('Error message'),
        'warning' => t('Warning message'),
      ],
    ];
    $messages = \Drupal::service('renderer')->render($message);
    $ajax_response->addCommand(new HtmlCommand('#' . Html::getClass($form['form_id']['#value']) . '-messages', $messages));
    return $ajax_response;
  }

}
Добавляем hook_form_BASE_FORM_ID_alter().
use Drupal\Component\Utility\Html;

...

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function dummy_form_contact_message_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  # Добавляем элемент куда будем выводить сообщения об ошибках или успешном
  # отправлении формы.
  $form['system_messages'] = [
    '#markup' => '<div id="' . Html::getClass($form_id) . '-messages"></div>',
    '#weight' => -100,
  ];

  # Добавляем наш AJAX колбек для кнопки. Так как он находится в отдельном
  # объекте, то указывать нужно полный путь с пространством имен.
  $form['actions']['submit']['#ajax'] = [
    'callback' => 'Drupal\dummy\AjaxContactSubmit::ajaxSubmitCallback',
    'event' => 'click',
    'progress' => [
      'type' => 'throbber',
    ],
  ];
}

Теперь Contact формы будут отправляться при помощи AJAX.

На этом всё, модуль с примерами прилагаю.

Ссылки

Комментарии

pr0g
пт, 07/10/2016 - 13:40

Спасибо за статью. В примере не прописаны 2 или 3 необходимые библиотеки, но это помогает научится анализировать логи PHP, чтобы понять почему некоторые функции не работают. При условии конечно что вы пользуетесь хдебагом и IDE инструментом. :-)

Николай
вс, 18/06/2017 - 11:47

А как сделать не для контактов а для формы добавления ноды? Пробовал заменить на dummy_form_node_form_alter но не сработало. Спасибо за отличній материал.

Николай
чт, 22/06/2017 - 02:05

Ага всё получилось! Только не понял почему у меня в колбеке не получается $form_state->getValue('field_body') - выводится пустота. То есть я хочу получить назад содержимое отправленного текста. Пол дня копаю и ничего не понял.

Дмитри
пт, 07/07/2017 - 06:45

А как использовать уже готовую форму, к примеру мне нужно написать кастомный ajax login?

Niklan
пт, 07/07/2017 - 10:05

$form['form_id']['#value'] — хранит ID формы. Например my_form_name. Html::getClass() — преобразует строку в валидный CSS класс. Т.е. переводит в нижний регистр, нижние подчеркивания заменяет на тире и т.д.. И my_form_name превращается в my-form-name. А чуть левее там конкатинация с #, в итоге превращается в #my-form-name — который является селектором для формы.

Владислав
чт, 15/02/2018 - 17:42

Добрый день. Если нужно в D8 добавить ajax обработчик для элемента формы ноды, как это сделать правильно?

Также как в D7 в хуке hook_form_alter добавил к полю ['#ajax'] => ..., но не работает. 'callback' может быть здесь обычной функцией ? если нет в каком объекте тогда должен быть определен метод обработчика в таком случае?

Lex Misiuro
Домашняя страница чт, 24/05/2018 - 19:58
'#message_list' => drupal_get_messages(),

Можно и нужно заменить на

'#message_list' => $this->messenger()->all(),

В Drupal 8.5 мессенджер был перенесен в сервис. А в FormBase сервис подключается через трейт MessengerTrait

rem
ср, 31/10/2018 - 10:07

А как сделать проверку формы через аякс, но отправить в случае успеха её обычным образом?

rem
ср, 28/11/2018 - 12:59

Не выходит заставить форму, если отправляешь её через ajax, проходить стандартную валидацию через #states required. Стандартно если поставить('#required' => TRUE) - то работает. Притом submit то стандартный проходит. Вот тут подробнее описал https://drupal.ru/node/138531 Может знаете как решить, или такое нерешаемо?

Denis Dorosh
пт, 02/08/2019 - 15:01

Спасибо за статью, оч полезно! Проблема с которой столкнулся, если включен inline form error при аяксе ошибки не выводятся. Сталкивался с этим?

Denis Dorosh
пт, 02/08/2019 - 15:10

нужно как то создать контейнер для каждого поля в которое будет выводиться сообщение. хз как такое сделать. Подскажет кто?

Harry Potter
Домашняя страница вт, 13/08/2019 - 12:51

Если есть необходимость скрыть throbber и выводимое рядом сообщение, то достаточно в 'progress' передать пустой массив - 'progress' => []. Ели progress вообще не указывать, то будет выводится дефолтный.

Alex
вт, 02/06/2020 - 18:04

Важный момент выключайте возврат фокуса после получения ответа ajax очень разражает, когда вводишь уже другое поле а ajax меняет фокус на предыдущее '#ajax' => [ 'callback' => '::validateEmailAjax', 'event' => 'change', 'disable-refocus' => TRUE, ],

Ilya
ср, 25/08/2021 - 13:53

В друпале 8.5 + функций по типу drupal_get_messages больше нет, вроде как вместо нее теперь MessengerInterface::all, но вот я не понимаю как нам ее использовать чтобы работала? Я подключил use Drupal\Core\Messenger\MessengerInterface; и вместо drupal_get_messages написал MessengerInterface::all(), но не помогло. Может кто знает решение?

Добавить комментарий

Поддерживается Markdown
Поделиться