Использование AJAX с Drupal Forms API

Drupal AJAX и Forms API

Всем привет! Решил я нарушить полугодовое молчание блога и написать что-нибудь интересное. Давно хотел поделиться наработками и рекомендациями по добавлению AJAX в формы Drupal’а. AJAX + Forms API – это удобная и порой незаменимая вещь в юзабилити вашего сайта. Материал предназначен для разработчиков среднего звена, которые уже более ли менее работали с AJAX’ом в Drupal.

Для начала я рассмотрю пример простой формы и поделюсь своими наработками, которые должны помочь избежать набивания шишек на первых парах работы с AJAX и Forms API. Также постараюсь объяснить, какую логику должны содержать ajax, submit и validation callback-функции, чтобы не возникало ошибок и "странного" поведения формы.

Добавление AJAX к существующей форме

Допустим, вы хотите разместить в футере вашего сайта форму обратной связи. Для этих целей на одном из проектов я использовал модуль ядра Contact. Однако форма выполнялась с перезагрузкой страницы, что дико раздражало и выглядело топорно. Чтобы сделать конфетку из этой формы, подбираемся к ней через hook_form_FORM_ID_alter():

  1. /**
  2.  * Implements hook_form_FORM_ID_alter().
  3.  */
  4. function MY_MODULE_form_contact_site_form_alter(&$form, &$form_state, $form_id) {
  5.  
  6. $form['#ajax-class'] = drupal_html_class('my-module-contact-site-wrapper');
  7. $form['#prefix'] = '<div class="' . $form['#ajax-class'] . '">';
  8. $form['#suffix'] = '</div>';
  9.  
  10. // AJAX form submitting.
  11. $form['actions']['submit']['#ajax'] = array(
  12. 'effect' => 'fade',
  13. 'callback' => 'MY_MODULE_contact_form_ajax_callback'
  14. );
  15.  
  16. $form['#submit'][] = 'MY_MODULE_contact_form_submit_callback';
  17. form_load_include($form_state, 'pages.inc', 'contact');
  18. // Some code...
  19. }
  20.  
  21. /**
  22.  * AJAX callback for Quick form.
  23.  */
  24. function MY_MODULE_contact_form_ajax_callback($form, &$form_state) {
  25. $commands = array();
  26. $selector = '.' . $form['#ajax-class'];
  27. $commands[] = ajax_command_replace($selector, drupal_render($form));
  28. $commands[] = ajax_command_prepend($selector, theme('status_messages'));
  29.  
  30. return array('#type' => 'ajax', '#commands' => $commands);
  31. }
  32.  
  33. /**
  34.  * Submit callback for Quick form.
  35.  */
  36. function MY_MODULE_contact_form_submit_callback($form, &$form_state) {
  37. $form_state['rebuild'] = TRUE;
  38. }

Итак, теперь по порядку обо всех нюансах:

  1. Селектор (или селекторы), с помощью которого мы будем обновлять содержимое страницы лучше всего сразу вынести в отдельную переменную формы, чтобы избавится от дублирования кода. Далее везде, где потребуется, в нашем примере будем использовать $form['#ajax-class'] – для добавления класса, для создания селектора.

  2. Не используйте привязку к ID-шникам элементов форм, так как после каждого AJAX’а они изменяются! Поэтому я взял за правило использовать атрибут class вместо id в качестве селектора. В данном примере это конечно не критично, однако если попытаетесь привязаться к ID какого-нибудь input’a будьте готовы, что не получите желаемого результата. Это же самое касается и CSS.

  3. Если необходимо, чтобы во время AJAX-запроса подключались какие-либо inc-файлы, содержащие дополнительную логику – используйте form_load_include().

  4. В AJAX callback функции не должно быть никакой логики работы с БД, изменений формы – запомните это! Это и является одним из ключевых посылов, который я хотел донести в рамках этого материала. Данная функция должна отвечать только за формирование контента и изменений, которые будут возвращены клиенту. Нужно что-либо положить в базу или изменить в форме? Добавляйте это в submit или validation функции, которых можете создать сколько пожелаете.

  5. Собственно в продолжение предыдущего пункта: даже если вам надо изменить какую-то мелочь в форме – создавайте submit-функцию. В примере функция содержит всего лишь одну строчку однако, если это добавить в AJAX callback – корректного выполнения AJAX вам не видать: $form_state['rebuild'] = TRUE;.

  6. Обратите внимание, что помимо формы, я еще возвращаю Drupal messages. Казалось бы, мелочь, а на самом деле штука полезная: сообщения выводятся возле нужной формы (а не где-то там вверху страницы при перезагрузке), информируя об этом пользователя.

  7. Небольшая рекомендация: если у вас возникнут непонятные ситуации с AJAX, когда все, казалось бы, «должно работать!», или же есть желание ближе разобраться с обработкой Drupal форм, то настройте отладчик и поковыряйте функцию drupal_process_form() – это поможет понять что за чем следует при отправке формы.

Мы рассмотрели сейчас небольшой пример, решающий практически классическую задачу, однако, не все разработчики придерживаются указанных выше рекомендаций и на выходе получаются всякого рода «косяки». Ну а, если описанный пример оказался для вас слишком банальным – вы молодец, берите пирожок и читайте дальше.

Field Widgets с использованием AJAX

Теперь рассмотрим немного сложнее ситуацию: допустим, нам нужно написать виджет поля, реализующий какую-нибудь хитрую логику с использованием AJAX’a, и заставить все это работать в форме добавления/редактирования ноды.

Не буду подробно задерживаться на имплементациях хука hook_field_info(), hook_field_widget_info() и тому подобных. Остановлюсь сразу на hook_field_widget_form(), который и описывает новый элемент виджета, добавляемый к форме редактирования ноды.

  1. /**
  2.  * Implements hook_field_widget_form().
  3.  */
  4. function MY_MODULE_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  5. // Основные параметры поля.
  6. $field_name = $field['field_name'];
  7. $field_lang = $element['#language'];
  8. $parents = array($field_name, $field_lang);
  9. $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
  10.  
  11. // Настройки AJAX для всего виджета.
  12. $ajax = array(
  13. 'callback' => 'MY_MODULE_widget_ajax_callback'
  14. );
  15.  
  16. // Получение введенных данных из сохраненных значений или значений $field_state.
  17. // Значения не хранятся в $form_state['values'] по причине того, что будут слетать
  18. // при любом AJAX'e другого элемента формы ноды.
  19. $events_form = array();
  20. $field_state_values = !empty($field_state['items']) ? $field_state['items'] : NULL;
  21. $values = !empty($field_state_values) ? $field_state_values : $items;
  22.  
  23. // Прочий код, который выводит значения виджета...
  24.  
  25. // Fieldset с элементом добавления новых данных.
  26. $events_form['add_new_event'] = array(
  27. '#type' => 'fieldset',
  28. '#title' => t('Add match events'),
  29. '#attributes' => array('class' => array('container-inline', 'clearfix')),
  30. );
  31.  
  32. $events_form['add_new_event']['events_list'] = array(
  33. '#type' => 'select',
  34. '#title' => t('Events'),
  35. '#options' => MY_MODULE_get_events_list(),
  36. );
  37.  
  38. $events_form['add_new_event']['add_button'] = array(
  39. '#type' => 'submit',
  40. '#limit_validation_errors' => array(array($field_name)),
  41. '#value' => t('Add another item'),
  42. '#submit' => array('MY_MODULE_add_new_event_submit'),
  43. '#ajax' => $ajax,
  44. );
  45.  
  46. $element += $events_form;
  47.  
  48. // Прочий код...
  49.  
  50. return $element;
  51. }
  52.  
  53. /**
  54.  * AJAX callback for widget.
  55.  */
  56. function MY_MODULE_widget_ajax_callback($form, &$form_state) {
  57. $field_name = $form_state['triggering_element']['#parents'][0];
  58. $selector = '.field-name-' . drupal_html_class($field_name);
  59. $commands[] = ajax_command_replace($selector, drupal_render($form[$field_name]));
  60. $commands[] = ajax_command_prepend($selector, theme('status_messages'));
  61.  
  62. return array('#type' => 'ajax', '#commands' => $commands);
  63. }
  64.  
  65. /**
  66.  * Submit callback for widget.
  67.  */
  68. function MY_MODULE_add_new_event_submit($form, &$form_state) {
  69. // Прочий код...
  70.  
  71. $form_state['rebuild'] = TRUE;
  72. }

Вот такой наглядный пример кода, на котором я постараюсь объяснить еще 2 важных момента использования AJAX'a в формах.

Валидация и свойство #limit_validation_errors

Начнем с простого – с валидации. Допустим, у нас multiple value поле и наш виджет состоит из select и submit-кнопки, реализующими следующую логику: с помощью select необходимо выбрать какое-то значение, нажать submit – выполнится AJAX-запрос, в ходе которого значение сохранится и будет выведено пользователю. Однако будьте готовы к тому, что после AJAX’a вы получите кучу сообщений с ошибками валидации всех полей формы ноды. В Drupal любой submit-элемент по умолчанию работает именно так: запускает валидацию всех элементов и сабмитит всю форму. Собственно упираемся в новую задачу: как сделать так, чтобы при нажатии нашего submit’a валидацию проходили лишь значения, введенные в форму виджета?

Field Widget с AJAX

Это достигается путем использования свойства #limit_validation_errors для submit-элемента. Свойство достаточно редко используемое на практике, отчего про него знают не все разработчики. В нашем случае данное свойство будет определено как:

  1. '#limit_validation_errors' => array(array($field_name)),

Если объяснять словами, то #limit_validation_errors содержит массивы секций $form_state['values'], которые должны проходить валидацию при тригере данного элемента формы. Вы можете указать различную вложенность, а также несколько секций через запятую. Данный прием активно используется в мультистеп формах. Если вы хотите полностью отключить валидацию формы, то установить значением пустой массив. В нашем же случае будет проходить проверку все значения элементов, которые относятся к виджету поля.

Несколько AJAX-элементов в форме

Вот тут начинается самое интересное. Из подзаголовка уже ясна проблема: что делать, если в форме ноды (а в целом, и в любой другой форме) оказалось несколько полей (или попросту элементов), использующих AJAX?

При построении формы мы привыкли использовать $form_state['values'] для получения уже введенных значений значений. Однако данный метод здесь не поможет, так как вы будете терять значения при поочередном заполнении AJAX-элементов формы. Дело в том, что значения $form_state['values'] не кешируются (не сохраняются в БД) – обратите внимание на функцию form_state_keys_no_cache(). Также значения могут быть перезаписаны в drupal_validate_form(), если мне не изменяет память.

Поэтому напрашивается только одно решение – хранить значения в $form_state под каким-нибудь кастомным ключом, которое не будет обнуляться и начнет попадать в БД. Возможно, кто-то сейчас удивится, но Drupal именно так и работает с полями ноды. Данный прием встречается во многих популярных модулях – так что не надо думать, что я предлагаю вам «костыль».

В случае с формой ноды все значения полей хранятся в $from_state['fields'] и извлекаются функцией ядра – field_form_get_state(). Другие модули, например Webform, используют $from_state['storage'] – данный ключ рекомендован даже документацией Drupal (см. описание к функции drupal_build_form()). Будем считать, что с хранением введенных значений мы разобрались. Задачей разработчика остается грамотно организовать запись и извлечение этих самых значений. Могу посоветовать заглянуть в функции ядра, которые в свое время помогли мне разобраться:

  • @see file_field_widget_form();
  • @see file_managed_file_submit();
  • @see file_field_widget_submit();

На этом мой ликбез по использованию AJAX’a в Drupal формах будем считать законченным. Надеюсь, материал поможет довести до совершенства ваши формы и избавит вас от лишних шишек на граблях Forms API при создании сложных форм. Если я пропустил какие-то интересные моменты или у вас остались вопросы – пишите в комментариях.

Комментарии

Аватар пользователя Алексей.
Алексей.

Спасибо за статью.

Аватар пользователя Kirill
Kirill

+500 Очень полезная статья. Как раз сейчас напоролся на отработку второго аякса на форме, который валил данные в динамических полях. СПАСИБО за статью!!!

Аватар пользователя AlexMazaltov
AlexMazaltov

+100500 Полезности. Большое спасибо за статью. Очень кстати и во время.

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

 888b     d888  888    d8P   888b     d888           
8888b d8888 888 d8P 8888b d8888
88888b.d88888 888 d8P 88888b.d88888
888Y88888P888 888d88K 888Y88888P888 888 888
888 Y888P 888 8888888b 888 Y888P 888 `Y8bd8P'
888 Y8P 888 888 Y88b 888 Y8P 888 X88K
888 " 888 888 Y88b 888 " 888 .d8""8b.
888 888 888 Y88b 888 888 888 888


Зарегистрируйтесь для добавления материалов без проверки.