Так вот, multi-step формы в Друпале вещь совсем не сложная, хотя раньше я думал иначе. Поэтому, чтобы реализовать работающий вариант пришлось немного по-трудиться. Все ниже изложенное будем рассматривать на примере трех-шаговой формы. В Друпале уже заложен механизм multi-step форм. Для этого есть массив $form_state['storage']. В нем сохраняются данные из предыдущих шагов, которые вам могут понадобиться в следующих шагах. Итак, дорогие друзья, на повестке дня у нас шикарные мультистеп формы. Не написано ни строки js, однако всё шустро работает, ещё и с сохранением состояния формы. Великий и могучий Друпал. Теперь от слов переходим к делу.
Шаг первый. Создаём страницу с формой.
function multistep_example_menu() { $items = array(); $items['multistep_example'] = array( 'title' => 'Multi-step ajax form example', 'page callback' => 'drupal_get_form', 'page arguments' => array('multistep_example_form'), 'access callback' => TRUE, ); return $items; }
Шаг второй. Создаём форму.
Наша форма будет состоять из трёх последовательных шагов. В любой момент можно вернуться на предыдущий шаг или перейти к следующему. Итак, код формы с подробными комментариями кода:
function multistep_example_form($form, &$form_state) { // Обёртка для формы. Каждый раз в неё будет передаваться новая форма через ajax. $form['#prefix'] = '<div id="multistep-example-form-wrapper">'; $form['#suffix'] = '</div>'; // Данные в форме будут представлены в виде дерева, т.е. // сохранять ключи родительских элементов. $form['#tree'] = TRUE; // Если форма только что была создана, то мы окажемся на первом шаге. // Если же пользователь уже "полазил" по форме, то забираем текущий шаг формы. $step = empty($form_state['storage']['step']) ? 1 : $form_state['storage']['step']; $form_state['storage']['step'] = $step; // Смотрим, на каком шаге мы сейчас находимся, и в зависимости // от этого показываем или скрываем часть формы. switch ($step) { case 1: // Если пользователь находится на первом шаге, // то показываем ему форму для первого шага. $form['step1'] = array( '#type' => 'fieldset', '#title' => 'Шаг первый. Выбор возраста и пола.', ); $form['step1']['age'] = array( '#type' => 'select', '#title' => 'Выберите ваш возраст', '#options' => drupal_map_assoc(array('10-25', '26-50', '51-76', '77-123')), '#field_suffix' => 'лет', ); // Если этот шаг уже был пройден, то в форме должно сохраниться значение, которое выбрал пользователь. // Если же такого значения нет, то указываем возраст по умолчанию. if (isset($form_state['values']['step1']['age'])) { $form['step1']['age']['#default_value'] = $form_state['values']['step1']['age']; } $form['step1']['sex'] = array( '#type' => 'select', '#title' => 'Укажите ваш пол', '#options' => drupal_map_assoc(array('Не определён', 'Мужской', 'Женский')), ); // То же самое, что и для возраста. Если значение уже указывалось - подтягиваем его. // Если нет - задаём своё. if (isset($form_state['values']['step1']['sex'])) { $form['step1']['sex']['#default_value'] = $form_state['values']['step1']['sex']; } break; case 2: // Задаём форму для второго шага. $form['step2'] = array( '#type' => 'fieldset', '#title' => t('Шаг второй. Предпочтения.'), ); $default_value = empty($form_state['values']['step2']['module']) ? '' : $form_state['values']['step2']['module']; $form['step2']['module'] = array( '#type' => 'textfield', '#title' => 'Укажите ваш любимый модуль в Друпале', '#default_value' => $default_value, '#required' => TRUE, ); $default_value = empty($form_state['values']['step2']['blog']) ? '' : $form_state['values']['step2']['blog']; $form['step2']['blog'] = array( '#type' => 'textfield', '#title' => 'Введите адрес любимого блога о Друпале', '#default_value' => $default_value, '#field_prefix' => 'http://', '#description' => 'Правильный ответ: drupalace.ru', ); break; case 3: // Задаём форму для третьего шага. $form['step3'] = array( '#type' => 'fieldset', '#title' => 'Шаг третий. Восторг.', ); $form['step3']['drupal'] = array( '#type' => 'checkboxes', '#title' => 'А вы уже успели оценить всю мощь Друпала?', '#options' => drupal_map_assoc(array('Да', 'Да, конечно', 'Да, и я в восторге', 'Да, и это прекрасно', 'В процессе')), '#required' => TRUE, ); if (isset($form_state['values']['step3']['drupal'])) { $form['step3']['drupal']['#default_value'] = $form_state['values']['step3']['drupal']; } break; } // После того, как создали форму, надо позаботиться о том, чтобы // пользователь видел правильные кнопки. В зависимости от текущего шага, // будут отображаться кнопки "Следующий шаг", "Предыдущий шаг" и "Хватит". // Создаём обёртку для кнопок. По фэн-шую в седьмом Друпале так надо. // Верстальщики вам скажут спасибо. $form['actions'] = array('#type' => 'actions'); // Если мы на последнем шаге - то показываем кнопку "Хватит". if ($step == 3) { $form['actions']['submit'] = array( '#type' => 'submit', '#value' => 'Хватит', ); } // Если мы не достигли последнего шага, то у нас обязательно // будет присутствовать кнопка "Следующий шаг". if ($step < 3) { $form['actions']['next'] = array( '#type' => 'submit', '#value' => 'Следующий шаг', // На кнопку вешаем ajax-обработчик, который будет возвращать форму // в ранее созданный <div id="multistep-example-form-wrapper"></div> '#ajax' => array( 'wrapper' => 'multistep-example-form-wrapper', 'callback' => 'multistep_example_ajax_callback', ), ); } // Если мы ушли с первого шага, то покажем кнопку "Предыдущий шаг". if ($step > 1) { $form['actions']['prev'] = array( '#type' => 'submit', '#value' => 'Предыдущий шаг', // Это хороший трюк - не валидируем форму, если нажимаем кнопку "Предыдущий шаг". '#limit_validation_errors' => array(), '#submit' => array('multistep_example_form_submit'), '#ajax' => array( 'wrapper' => 'multistep-example-form-wrapper', 'callback' => 'multistep_example_ajax_callback', ), ); } // С чувством выполненного долга показываем пользователю форму. return $form; }
Шаг третий. Создаём AJAX обработчик.
Самый сложный этап разработки. Пишем мощнейщий обработчик:
function multistep_example_ajax_callback($form, $form_state) { // Указываем, что хотим перезагрузить всю форму, // просто вернув её целиком обратно. return $form; }
Шаг четвёртый. Пишем обработчик состояний формы.
Сразу переходим к коду - там все комментарии.
function multistep_example_form_submit($form, &$form_state) { // Сохраняем состояние формы, полученное при переходе на новый шаг. $current_step = 'step' . $form_state['storage']['step']; if (!empty($form_state['values'][$current_step])) { $form_state['storage']['values'][$current_step] = $form_state['values'][$current_step]; } // Если перешли на следующий шаг - то увеличиваем счётчик шагов. if (isset($form['actions']['next']['#value']) && $form_state['triggering_element']['#value'] == $form['actions']['next']['#value']) { $form_state['storage']['step']++; // Если данные для следующего шага были уже введены пользователем, // то восстанавливаем их и передаём в форму. $step_name = 'step' . $form_state['storage']['step']; if (!empty($form_state['storage']['values'][$step_name])) { $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name]; } } // Если вернулись на шаг назад - уменьшаем счётчик шагов. if (isset($form['actions']['prev']['#value']) && $form_state['triggering_element']['#value'] == $form['actions']['prev']['#value']) { $form_state['storage']['step']--; // Забираем из хранилища данные по предыдущему шагу и возвращаем их в форму. $step_name = 'step' . $form_state['storage']['step']; $form_state['values'][$step_name] = $form_state['storage']['values'][$step_name]; } // Если пользователь прошёл все шаги и нажал на кнопку "Хватит", // то обрабатываем полученные данные со всех шагов. if (isset($form['actions']['submit']['#value']) && $form_state['triggering_element']['#value'] == $form['actions']['submit']['#value']) { // Показываем сообщение с введёнными данными. $message = 'Введённые данные: <br/>'; foreach ($form_state['storage']['values'] as $step => $values) { $message .= "<br/>$step: <br/>"; foreach ($values as $key => $value) { $output = ''; if (is_array($value)) { foreach ($value as $val) { $output .= $val ? $val . '; ' : ''; } $value = implode(', ', $value); } else { $output = $value; } $message .= "$key = $output<br/>"; } } drupal_set_message($message); $form_state['rebuild'] = FALSE; return; } // Указываем, что форма должна быть построена заново. $form_state['rebuild'] = TRUE; }
Вот и всё. Постоение хорошей формы на ajax в седьмом Друпале будет состоять как минимум из этих трёх этапов: постоение формы, ajax-обработчик и обработчик самой формы. Обычно к этому списку добавляется ещё и валидация формы, но в этом примере она не нужна.
По итогу мы получили сложную (на первый взгляд) мультистеп форму, которая работает через AJAX, без написания какого-либо javascript'a, оформленную по всем правилам, абсолютно безопасную с точки зрения пробиваемости, и созданную с помощью инструментов Друпала.