Использование 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 Полезности. Большое спасибо за статью. Очень кстати и во время.

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

 888      d8888    .d8888b.     888888 
888 d8P888 d88P Y88b "88b
888 d8P 888 888 888
888 d8P 888 .d88P 888
888 d88 888 .od888P" 888
888 8888888888 d88P" 888
888 888 888" 88P
888 888 888888888 888
.d88P
.d88P"
888P"
Зарегистрируйтесь для добавления материалов без проверки.