Многошаговые (мультистеп) формы на AJAX в Drupal 7

Многошаговые (мультистеп) формы на AJAX в Drupal 7Так вот, 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, оформленную по всем правилам, абсолютно безопасную с точки зрения пробиваемости, и созданную с помощью инструментов Друпала.