Использование AJAX с Drupal Forms API
Всем привет! Решил я нарушить полугодовое молчание блога и написать что-нибудь интересное. Давно хотел поделиться наработками и рекомендациями по добавлению AJAX в формы Drupal’а. AJAX + Forms API – это удобная и порой незаменимая вещь в юзабилити вашего сайта. Материал предназначен для разработчиков среднего звена, которые уже более ли менее работали с AJAX’ом в Drupal.
Для начала я рассмотрю пример простой формы и поделюсь своими наработками, которые должны помочь избежать набивания шишек на первых парах работы с AJAX и Forms API. Также постараюсь объяснить, какую логику должны содержать ajax, submit и validation callback-функции, чтобы не возникало ошибок и "странного" поведения формы.
Добавление AJAX к существующей форме
Допустим, вы хотите разместить в футере вашего сайта форму обратной связи. Для этих целей на одном из проектов я использовал модуль ядра Contact. Однако форма выполнялась с перезагрузкой страницы, что дико раздражало и выглядело топорно. Чтобы сделать конфетку из этой формы, подбираемся к ней через hook_form_FORM_ID_alter():
- /**
- * Implements hook_form_FORM_ID_alter().
- */
- function MY_MODULE_form_contact_site_form_alter(&$form, &$form_state, $form_id) {
-
- $form['#ajax-class'] = drupal_html_class('my-module-contact-site-wrapper');
- $form['#prefix'] = '<div class="' . $form['#ajax-class'] . '">';
- $form['#suffix'] = '</div>';
-
- // AJAX form submitting.
- $form['actions']['submit']['#ajax'] = array(
- 'effect' => 'fade',
- 'callback' => 'MY_MODULE_contact_form_ajax_callback'
- );
-
- $form['#submit'][] = 'MY_MODULE_contact_form_submit_callback';
- form_load_include($form_state, 'pages.inc', 'contact');
- // Some code...
- }
-
- /**
- * AJAX callback for Quick form.
- */
- function MY_MODULE_contact_form_ajax_callback($form, &$form_state) {
- $commands = array();
- $selector = '.' . $form['#ajax-class'];
- $commands[] = ajax_command_replace($selector, drupal_render($form));
- $commands[] = ajax_command_prepend($selector, theme('status_messages'));
-
- return array('#type' => 'ajax', '#commands' => $commands);
- }
-
- /**
- * Submit callback for Quick form.
- */
- function MY_MODULE_contact_form_submit_callback($form, &$form_state) {
- $form_state['rebuild'] = TRUE;
- }
Итак, теперь по порядку обо всех нюансах:
Селектор (или селекторы), с помощью которого мы будем обновлять содержимое страницы лучше всего сразу вынести в отдельную переменную формы, чтобы избавится от дублирования кода. Далее везде, где потребуется, в нашем примере будем использовать $form['#ajax-class'] – для добавления класса, для создания селектора.
Не используйте привязку к ID-шникам элементов форм, так как после каждого AJAX’а они изменяются! Поэтому я взял за правило использовать атрибут class вместо id в качестве селектора. В данном примере это конечно не критично, однако если попытаетесь привязаться к ID какого-нибудь input’a будьте готовы, что не получите желаемого результата. Это же самое касается и CSS.
Если необходимо, чтобы во время AJAX-запроса подключались какие-либо inc-файлы, содержащие дополнительную логику – используйте form_load_include().
В AJAX callback функции не должно быть никакой логики работы с БД, изменений формы – запомните это! Это и является одним из ключевых посылов, который я хотел донести в рамках этого материала. Данная функция должна отвечать только за формирование контента и изменений, которые будут возвращены клиенту. Нужно что-либо положить в базу или изменить в форме? Добавляйте это в submit или validation функции, которых можете создать сколько пожелаете.
Собственно в продолжение предыдущего пункта: даже если вам надо изменить какую-то мелочь в форме – создавайте submit-функцию. В примере функция содержит всего лишь одну строчку однако, если это добавить в AJAX callback – корректного выполнения AJAX вам не видать:
.$form_state['rebuild'] = TRUE;
Обратите внимание, что помимо формы, я еще возвращаю Drupal messages. Казалось бы, мелочь, а на самом деле штука полезная: сообщения выводятся возле нужной формы (а не где-то там вверху страницы при перезагрузке), информируя об этом пользователя.
Небольшая рекомендация: если у вас возникнут непонятные ситуации с AJAX, когда все, казалось бы, «должно работать!», или же есть желание ближе разобраться с обработкой Drupal форм, то настройте отладчик и поковыряйте функцию drupal_process_form() – это поможет понять что за чем следует при отправке формы.
Мы рассмотрели сейчас небольшой пример, решающий практически классическую задачу, однако, не все разработчики придерживаются указанных выше рекомендаций и на выходе получаются всякого рода «косяки». Ну а, если описанный пример оказался для вас слишком банальным – вы молодец, берите пирожок и читайте дальше.
Field Widgets с использованием AJAX
Теперь рассмотрим немного сложнее ситуацию: допустим, нам нужно написать виджет поля, реализующий какую-нибудь хитрую логику с использованием AJAX’a, и заставить все это работать в форме добавления/редактирования ноды.
Не буду подробно задерживаться на имплементациях хука hook_field_info(), hook_field_widget_info() и тому подобных. Остановлюсь сразу на hook_field_widget_form(), который и описывает новый элемент виджета, добавляемый к форме редактирования ноды.
- /**
- * Implements hook_field_widget_form().
- */
- function MY_MODULE_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
- // Основные параметры поля.
- $field_name = $field['field_name'];
- $field_lang = $element['#language'];
- $parents = array($field_name, $field_lang);
- $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state);
-
- // Настройки AJAX для всего виджета.
- $ajax = array(
- 'callback' => 'MY_MODULE_widget_ajax_callback'
- );
-
- // Получение введенных данных из сохраненных значений или значений $field_state.
- // Значения не хранятся в $form_state['values'] по причине того, что будут слетать
- // при любом AJAX'e другого элемента формы ноды.
- $events_form = array();
- $field_state_values = !empty($field_state['items']) ? $field_state['items'] : NULL;
- $values = !empty($field_state_values) ? $field_state_values : $items;
-
- // Прочий код, который выводит значения виджета...
-
- // Fieldset с элементом добавления новых данных.
- $events_form['add_new_event'] = array(
- '#type' => 'fieldset',
- '#title' => t('Add match events'),
- '#attributes' => array('class' => array('container-inline', 'clearfix')),
- );
-
- $events_form['add_new_event']['events_list'] = array(
- '#type' => 'select',
- '#title' => t('Events'),
- '#options' => MY_MODULE_get_events_list(),
- );
-
- $events_form['add_new_event']['add_button'] = array(
- '#type' => 'submit',
- '#limit_validation_errors' => array(array($field_name)),
- '#value' => t('Add another item'),
- '#submit' => array('MY_MODULE_add_new_event_submit'),
- '#ajax' => $ajax,
- );
-
- $element += $events_form;
-
- // Прочий код...
-
- return $element;
- }
-
- /**
- * AJAX callback for widget.
- */
- function MY_MODULE_widget_ajax_callback($form, &$form_state) {
- $field_name = $form_state['triggering_element']['#parents'][0];
- $selector = '.field-name-' . drupal_html_class($field_name);
- $commands[] = ajax_command_replace($selector, drupal_render($form[$field_name]));
- $commands[] = ajax_command_prepend($selector, theme('status_messages'));
-
- return array('#type' => 'ajax', '#commands' => $commands);
- }
-
- /**
- * Submit callback for widget.
- */
- function MY_MODULE_add_new_event_submit($form, &$form_state) {
- // Прочий код...
-
- $form_state['rebuild'] = TRUE;
- }
Вот такой наглядный пример кода, на котором я постараюсь объяснить еще 2 важных момента использования AJAX'a в формах.
Валидация и свойство #limit_validation_errors
Начнем с простого – с валидации. Допустим, у нас multiple value поле и наш виджет состоит из select и submit-кнопки, реализующими следующую логику: с помощью select необходимо выбрать какое-то значение, нажать submit – выполнится AJAX-запрос, в ходе которого значение сохранится и будет выведено пользователю. Однако будьте готовы к тому, что после AJAX’a вы получите кучу сообщений с ошибками валидации всех полей формы ноды. В Drupal любой submit-элемент по умолчанию работает именно так: запускает валидацию всех элементов и сабмитит всю форму. Собственно упираемся в новую задачу: как сделать так, чтобы при нажатии нашего submit’a валидацию проходили лишь значения, введенные в форму виджета?
Это достигается путем использования свойства #limit_validation_errors для submit-элемента. Свойство достаточно редко используемое на практике, отчего про него знают не все разработчики. В нашем случае данное свойство будет определено как:
- '#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 при создании сложных форм. Если я пропустил какие-то интересные моменты или у вас остались вопросы – пишите в комментариях.
Комментарии
Спасибо за статью.
+500 Очень полезная статья. Как раз сейчас напоролся на отработку второго аякса на форме, который валил данные в динамических полях. СПАСИБО за статью!!!
+100500 Полезности. Большое спасибо за статью. Очень кстати и во время.
Добавить комментарий