Drupal 8: Change the user registration form to a multi-step form.

Profile picture for user Phil Frilling
By Phil Frilling, 5 September, 2018
I was tasked with breaking the user registration form of a Drupal 8 installation into a multi-step form. This was necessary because depending on what a user selected in step one, they were shown different form fields in step two. To begin, I added all of the fields I needed to the user profile using the GUI. (/admin/config/people/accounts/fields). Then, I added the fields I needed to the manage form display/register tab (/admin/config/people/accounts/form-display/register). This gave me a working, one page registration form. So, the tricky part, how to break it apart into pages. First, I searched for any contributed modules that are already doing this, but I couldn't find any that did the trick. So, I used Drupal Console and generated a custom module.
drupal generate:module
Next, I began writing a form alter implementation.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_user_register_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
}
I needed to defined what page number I was starting with, so I added a $form_state variable to begin with:

// If the form doesn't have a page_num defined, define it here.
if (!$form_state->has('page_num')) {
  $form_state->set('page_num', 1);
}
Then, I created a multidimensional array of fields that I wanted on each page that looked like this:

$pages = [
  1 => [
    'field_first_name',
    'field_last_name',
    'field_phone',
    'mail',
    'name',
    'account',
  ],
  2 => [
    'field_employer_address',
    'field_employer_job_title',
    'field_employer_type',
    'field_seeker_interests',
    'field_seeker_introduction',
    'field_seeker_resume',
    'actions',
  ],
];
Once I defined the fields I wanted and on which page, I could then loop through that array and set the #access field to FALSE for the fields that are not on my current page. I also needed to add a validation array to pass to our custom submit button later. This array contains the field names that are on the current page that need validated. Without validation of these fields, the submitted values are not in the $form_state variable upon submission.


// Validate the fields of the appropriate steps.
$validation = [];

// Disable all the fields to begin.
foreach ($pages as $page => $fields) {
  if ($page != $form_state->get('page_num')) {
    foreach ($fields as $field) {
      $form[$field]['#access'] = FALSE;
    }
  }
  else {
    foreach ($fields as $field) {
      array_push($validation, [$field]);
    }
  }
}
Then, I added a custom validation function and my next/back buttons to the form. More on the custom validation function later. The next/back buttons are pretty standard. Both have a custom submit handler. Notice the #limit_validation_errors on the next button. This allows us to inform the form to validate the fields that are submitted in this step.


// Add our custom validation function to the start of the array.
array_unshift($form['#validate'], 'my_module_user_register_pre_submit');

// Remove access to the roles selection.
$form['job_seeker']['#access'] = FALSE;
$form['employer']['#access'] = FALSE;

$form['my_module_actions'] = [
  '#type' => 'actions',
  '#weight' => 100,
];

$form['my_module_actions']['next'] = [
  '#type' => 'submit',
  '#value' => t('Next'),
  '#submit' => [
    'my_module_register_next_previous_form_submit',
  ],
  '#limit_validation_errors' => $validation,
  '#access' => FALSE,
  '#weight' => 1,
];

$form['my_module_actions']['previous'] = [
  '#type' => 'submit',
  '#value' => t('Back'),
  '#submit' => [
    'my_module_register_next_previous_form_submit',
  ],
  '#access' => FALSE,
  '#limit_validation_errors' => [],
];

Then, I added a switch statement to handle any page specific form alterations:

switch ($form_state->get('page_num')) {
  case 1:
    // Allow access to the pages on this page.
    foreach ($pages[$form_state->get('page_num')] as $form_key) {
      $form[$form_key]['#access'] = TRUE;
    }

    $form['my_module_roles'] = [
      '#type' => 'radios',
      '#options' => $new_role_options,
      '#title' => t('What type of account would you like to create'),
      '#required' => TRUE,
      '#weight' => 49,
    ];

    // Enable our next button.
    $form['my_module_actions']['next']['#access'] = TRUE;

    break;

  case 2:
    $page_values = $form_state->get('page_values');

    // Add our previous buttons.
    $form['my_module_actions']['next']['#access'] = FALSE;
    $form['my_module_actions']['previous']['#access'] = TRUE;

    $role_selected = $page_values[1]['my_module_roles'];

    foreach ($pages[2] as $formkey) {
      if (strpos($formkey, 'field_employer') !== FALSE) {
        // Remove access from all field_employer fields if the role selected is
        // not 'employer'.
        $form[$formkey]['#access'] = ($role_selected == 'employer' ? TRUE : FALSE);
      }
      else if (strpos($formkey, 'field_seeker') !== FALSE) {
        // Remove access from all field_seeker fields if the role selected is
        // not 'job_seeker'.
        $form[$formkey]['#access'] = ($role_selected == 'job_seeker' ? TRUE : FALSE);
      }
    }

    break;
}

That pretty well takes care of the form_alter function. The submit function to manage our next/back buttons is below. I started by getting the current page that was submitted and loading our $pages variable from the form_alter function. We also define a $page_values array that will store our paged submissions while we step through the form. We then act on the form based on which button the user selected. If next, we loop through the fields that should be on page one and save them to the $page_values array. Then we increment the current page. Lastly, we save the $page_values and $page_num to the $form_state and set the rebuild parameter to true.

/**
 * Custom form submit for the user registration form.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 */
function my_module_register_next_previous_form_submit($form, Drupal\Core\Form\FormStateInterface $form_state) {

  // Get the current page that was submitted.
  $current_page = $form_state->get('page_num');

  // Get the fields for the pages.
  // These are the same as the $pages variable in the form_alter.
  // I've excluded them here for brevity.
  $fields = [...];

  // Setup our page values variable.
  if (!$form_state->get('page_values')){
    $page_values = [];
  }

  if ($form_state->getValue('next')) {
    // Add the values to the page_value array.
    foreach ($fields[$current_page] as $key) {
      $page_values[$current_page][$key] = $form_state->getValue($key);
    }
    // Increment the page number.
    $current_page++;
  }
  else if ($form_state->getValue('previous')) {
    // Discard the values the page_value array.
    foreach ($fields[$current_page] as $key) {
      $form_state->setValue($key, $page_values[$current_page][$key]);
      unset($page_values[$current_page][$key]);
    }
    // Decrement the page number.
    $current_page--;
  }

  $form_state
    ->set('page_num', $current_page)
    ->set('page_values', $page_values)
    ->setRebuild(TRUE);
}

Lastly, we add our custom validation function for the entire form. Notice we named the function user_register_pre_submit. This was done because we needed to alter the $form_state variables prior to the other validation functions getting run. Without this step, the fields that were on the previous pages would not be in the $form_state and would throw a validation error. This function puts all of the fields back into the existing $form_state, causing the validation functions to think it was a one page form submission again. Tricky!


/**
 * Custom function to re-add the form state values back from the page values.
 *
 * @param $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 */
function my_module_user_register_pre_submit($form, Drupal\Core\Form\FormStateInterface $form_state) {
  // Load the submitted values.
  $page_values = $form_state->get('page_values');
  $submitted_values = $form_state->getValues();

  // Put all the paged values back into the form_state values.
  foreach ($page_values as $page_num => $fields) {
    foreach ($fields as $key => $value) {
      $submitted_values[$key] = $value;
    }
  }

  // Save the form_state values for further processing.
  $form_state->setValues($submitted_values);
}
There you have it, a working two page registration form in Drupal 8. This could certainly be expanded to as many pages as necessary (but I only needed two). Please let me know if you have any questions/comments.