11

I'm trying to create a multistep/wizard form for Drupal 8.

  • User fills in a first name, last name fields
  • Clicks on the next button
  • Fills out more information
  • Clicks on the submit button

There are currently many resources devoted to multistep or wizard forms for Drupal 7 like this one and this.

On the other hand, I've had some trouble figuring out which is "Drupal" way of creating Drupal 8 multistep/wizard forms.

I did some research and figured there are several approaches:

Are those valid approaches for Drupal 8?

3 Answers 3

13

The easiest way to do this is to use $form_state. In your formBuild() method, you have an if/else or switch based on something like $form_state['step'] and display different form elements. Then you either have the same in your submit callback or have multiple submit callbacks, that do something to an object in $form_state that you're building, change the step and set the $form_state['rebuild'] flag to TRUE.

There are a few downsides to that approach, which is why (among other reasons) the ctools form wizard was created. It can get complicated if you have multiple steps and have to define all of that in a single form function/class and everything happens in POST requests.

What the ctools form wizard does is group multiple, separate forms together and control the navigation from one to the other. You also use the ctools object cache to store your object instead of $form_state, because that is no longer shared across your forms.

While that system doesn't exist yet, the ctools object cache has been ported to 8.x and is now called user tempstore, available as a service: \Drupal::service('user.private_tempstore') (before 8.0-beta8 called user.tempstore). This is a layer on top of the expirable key value store that introduces ownership of stored data in there. So this is what powers the well known message in views that a different user is currently editing that view and it's locked for that reason. Another advantage over using $_SESSION for that is that your data only has to be loaded when needed, when you are editing 3 views, then using $_SESSION would mean that you have to load and carry them around on every single page request.

If you don't need that, then you could rely on the session or also directly put it in an expirable key value store ($form_state is stored there now as well, not a pseudo-cache as it was in 7.x).

The configuration system however isn't a good match. That is not meant for per-user content (or content at all) as it doesn't really scale to store thousands or tens of thousands of records and might make some assumptions to pre-load everything it might need on a given page request (not yet, but there is an issue to make that happen)

2
  • One more question about your answer. This might be a silly question: Is \Drupal::service('user.tempstore') available for anonymous users as well?
    – chrisjlee
    Nov 10, 2013 at 19:31
  • Yes, it falls back to the session id for anoymous users. See api.drupal.org/api/drupal/…
    – Berdir
    Nov 10, 2013 at 21:24
4

Normally you can store form values between steps using either cTools object cache (similar to Multistep forms in Drupal 7), or through the $form_state (as per this tutorial).

In Drupal 8 you can inherit FormBase class to create a new multistep class.

In the article How to Build Multi-step Forms in Drupal 8 you can find a simple way to create a multistep form in Drupal 8.

First of all, you would need to create the base class which will be in charge of injecting the necessary dependencies.

We will group all the form classes together and place them inside a new folder called Multistep located within the Form plugin directory of our demo module. This is purely for having a clean structure and being able to quickly tell which forms are part of our multistep form process.

Here is the demo code (for MultistepFormBase.php file):

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepFormBase.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;

abstract class MultistepFormBase extends FormBase {

  /**
   * @var \Drupal\user\PrivateTempStoreFactory
   */
  protected $tempStoreFactory;

  /**
   * @var \Drupal\Core\Session\SessionManagerInterface
   */
  private $sessionManager;

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
  private $currentUser;

  /**
   * @var \Drupal\user\PrivateTempStore
   */
  protected $store;

  /**
   * Constructs a \Drupal\demo\Form\Multistep\MultistepFormBase.
   *
   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
   * @param \Drupal\Core\Session\AccountInterface $current_user
   */
  public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, AccountInterface $current_user) {
    $this->tempStoreFactory = $temp_store_factory;
    $this->sessionManager = $session_manager;
    $this->currentUser = $current_user;

    $this->store = $this->tempStoreFactory->get('multistep_data');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('user.private_tempstore'),
      $container->get('session_manager'),
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Start a manual session for anonymous users.
    if ($this->currentUser->isAnonymous() && !isset($_SESSION['multistep_form_holds_session'])) {
      $_SESSION['multistep_form_holds_session'] = true;
      $this->sessionManager->start();
    }

    $form = array();
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
      '#button_type' => 'primary',
      '#weight' => 10,
    );

    return $form;
  }

  /**
   * Saves the data from the multistep form.
   */
  protected function saveData() {
    // Logic for saving data goes here...
    $this->deleteStore();
    drupal_set_message($this->t('The form has been saved.'));

  }

  /**
   * Helper method that removes all the keys from the store collection used for
   * the multistep form.
   */
  protected function deleteStore() {
    $keys = ['name', 'email', 'age', 'location'];
    foreach ($keys as $key) {
      $this->store->delete($key);
    }
  }
}

Then you can create the actual forms class inside a file called MultistepOneForm.php:

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepOneForm.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormStateInterface;

class MultistepOneForm extends MultistepFormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'multistep_form_one';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['name'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
      '#default_value' => $this->store->get('name') ? $this->store->get('name') : '',
    );

    $form['email'] = array(
      '#type' => 'email',
      '#title' => $this->t('Your email address'),
      '#default_value' => $this->store->get('email') ? $this->store->get('email') : '',
    );

    $form['actions']['submit']['#value'] = $this->t('Next');
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->store->set('email', $form_state->getValue('email'));
    $this->store->set('name', $form_state->getValue('name'));
    $form_state->setRedirect('demo.multistep_two');
  }
}

In the buildForm() method we are defining our two dummy form elements. Do notice that we are retrieving the existing form definition from the parent class first. The default values for these fields are set as the values found in the store for those keys (so that users can see the values they filled in at this step if they come back to it). Finally, we are changing the value of the action button to Next (to indicate that this form is not the final one).

In the submitForm() method we save the submitted values to the store and then redirect to the second form (which can be found at the route demo.multistep_two). Keep in mind that we are not doing any sort of validation here to keep the code light. But most use cases will call for some input validation.

And update your routing file in the demo module (demo.routing.yml):

demo.multistep_one:
  path: '/demo/multistep-one'
  defaults:
    _form: '\Drupal\demo\Form\Multistep\MultistepOneForm'
    _title: 'First form'
  requirements:
    _permission: 'access content'
demo.multistep_two:
  path: '/demo/multistep-two'
  defaults:
    _form: '\Drupal\demo\Form\Multistep\MultistepTwoForm'
    _title: 'Second form'
  requirements:
    _permission: 'access content'

Finally, create the second form (MultistepTwoForm):

/**
 * @file
 * Contains \Drupal\demo\Form\Multistep\MultistepTwoForm.
 */

namespace Drupal\demo\Form\Multistep;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

class MultistepTwoForm extends MultistepFormBase {

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'multistep_form_two';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form = parent::buildForm($form, $form_state);

    $form['age'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your age'),
      '#default_value' => $this->store->get('age') ? $this->store->get('age') : '',
    );

    $form['location'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Your location'),
      '#default_value' => $this->store->get('location') ? $this->store->get('location') : '',
    );

    $form['actions']['previous'] = array(
      '#type' => 'link',
      '#title' => $this->t('Previous'),
      '#attributes' => array(
        'class' => array('button'),
      ),
      '#weight' => 0,
      '#url' => Url::fromRoute('demo.multistep_one'),
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->store->set('age', $form_state->getValue('age'));
    $this->store->set('location', $form_state->getValue('location'));

    // Save the data
    parent::saveData();
    $form_state->setRedirect('some_route');
  }
}

Inside the submitForm() method we again save the values to the store and defer to the parent class to persist this data in any way it sees fit. We then redirect to whatever page we want (the route we use here is a dummy one).

We should now have a working multistep form that uses the PrivateTempStore to keep data available across multiple requests. If we need more steps, all we have to do is create some more forms, add them in between the existing ones and make a couple of adjustments.

1

The multistep wizard that you've mentioned, it's already integrated with CTools, see: Wizard Support for 8.x-3.x, so you may consider extending it in your_module.services.yml, e.g.

services:
  ctools.wizard.form:
    class: Drupal\MyModuleMultistep\Controller\MyWizardForm

then extend the class in src/Controller/MyWizardForm.php:

<?php

/**
 * @file
 * Contains \Drupal\MyModuleMultistep\Controller\MyWizardForm
 */

namespace Drupal\MyModuleMultistep\Controller;

/**
 * Wrapping controller for wizard forms that serve as the main page body.
 */
class MyWizardForm extends WizardFormController {

}
2
  • do you know of an example which could help understand how to use CTools multistep wizard?
    – Duncanmoo
    Jul 7, 2017 at 13:53
  • 1
    @Duncanmoo I don't have, but feel free to ask another question with specific issue which you're having, or look in Tests/Wizard/CToolsWizard* files where you can find some tests (e.g. testWizardSteps).
    – kenorb
    Jul 7, 2017 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.