How to create a multi step custom form in Drupal 8 & 9
Here we are going create custom form with three steps, each steps having form fields and at the final step we are capturing this field values and displaying it in same page.
So the final page will look like as below.
Here dn_subscribe is our custom module.
Create custom path in rout in dn_subscribe.routing.yml as below.
n_subscribe.subscribe_form: path: '/subscribe-form' defaults: _form: '\Drupal\dn_subscribe\Form\SubscribeForm' _title: 'Subscribe form' requirements: _permission: 'TRUE'
So in the below steps we are going to create a single page Ajax form which has multiple steps.
Create below SubscribeForm.php
/dn_subscribe/src/Form/SubscribeForm.php
Include below classes and interfaces
use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface;
Before starting, our SubscribeForm.php will be as below.
<?php namespace Drupal\dn_subscribe\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; class SubscribeForm extends FormBase { /** * {@inheritdoc} */ public function getFormId() { return 'form_subscribe_form'; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { } }
So here we are going to create form elements functions in this file.
Here submitForm function will be the final submit handler where we are capturing form fields values of all steps.
In all steps we are attaching style.css using below line.
$form['#attached']['library'][] = 'dn_subscribe/dn_subscribe';
We have dn_subscribe.libraries.yml with below content.
dn_subscribe: css: theme: css/style.css: {}
Style.css file placed in below path.
/dn_subscribe/css/style.css
Before starting to build form we are creating below function.
public function getFormPrefix($step){ switch ($step) { case 1: return '<div class="my-form-wrapper"> <ul id="progressbar"> <li class="active" id="account"><span><strong>Account</strong></span></li> <li id="personal"><span><strong>Personal</strong></span></li> <li id="confirm"><span><strong>Finish</strong></span></li> </ul> </div>'; break; case 2: return '<div class="my-form-wrapper"> <ul id="progressbar"> <li id="account"><span><strong>Account</strong></span></li> <li class="active" id="personal"><span><strong>Personal</strong></span></li> <li id="confirm"><span><strong>Finish</strong></span></li> </ul> </div>'; break; case 3: return '<div class="my-form-wrapper"> <ul id="progressbar"> <li id="account"><span><strong>Account</strong></span></li> <li id="personal"><span><strong>Personal</strong></span></li> <li id="confirm" class="active"><span><strong>Finish</strong></span></li> </ul> </div>'; break; default: return ''; } }
Step – 1 First form
As a first step we are creating fields in buildForm function for first form, here we are creating below First name and Last name fields.
$form['#attached']['library'][] = 'dn_subscribe/dn_subscribe'; $form['description'] = [ '#type' => 'item', '#title' => $this->t('A basic multistep form (page 1)'), ]; $form['first_name'] = [ '#type' => 'textfield', '#title' => $this->t('First Name'), '#description' => $this->t('Enter your first name.'), '#default_value' => $form_state->getValue('first_name', ''), '#required' => TRUE, ]; $form['last_name'] = [ '#type' => 'textfield', '#title' => $this->t('Last Name'), '#default_value' => $form_state->getValue('last_name', ''), '#description' => $this->t('Enter your last name.'), ];
So here we are adding below line for setting page number variable.
$form_state->set(‘page_num’, 1);
Add below action for submit button and handlers for first form.
$form['actions'] = [ '#type' => 'actions', ]; $form['actions']['next'] = [ '#type' => 'submit', '#button_type' => 'primary', '#value' => $this->t('Next'), // Custom submission handler for page 1. '#submit' => ['::subscribeFirstNextSubmit'], // Custom validation handler for page 1. //'#validate' => ['::fapiExampleMultistepFormNextValidate'], ];
Here subscribeFirstNextSubmit’ function will be called when click on Next button, addition to this you can add validations in #validate handler.
In this buildForm function we are adding below if conditions for loading other steps forms based page number value.
if ($form_state->has('page_num') && $form_state->get('page_num') == 2) { //return $this->fapiExamplePageTwo($form, $form_state); return $this->subscribePageTwo($form, $form_state); } if ($form_state->has('page_num') && $form_state->get('page_num') == 3) { return $this->subscribePageThree($form, $form_state); }
Here subscribePageTwo function will load the second form and the subscribePageThree function will load the third page. We will create this functions in next steps.
Also add below line , which will load steps at the top of the form.
$form[‘#prefix’] = $this->getFormPrefix(1);
Here we are passing 1 to switch condition in getFormPrefix function, So this will return the first step as an active step in returned ul element.
So finally we have buildForm function as below.
Step 2 – First Form submission and loading second form
So here we are creating a submit handler for first form, as mentioned in action submit handler, create subscribeFirstNextSubmit function as below.
public function subscribeFirstNextSubmit(array &$form, FormStateInterface $form_state) { $form_state ->set('page_values', [ // Keep only first step values to minimize stored data. 'first_name' => $form_state->getValue('first_name'), 'last_name' => $form_state->getValue('last_name'), //'birth_year' => $form_state->getValue('birth_year'), ]) ->set('page_num', 2) // Since we have logic in our buildForm() method, we have to tell the form // builder to rebuild the form. Otherwise, even though we set 'page_num' // to 2, the AJAX-rendered form will still show page 1. ->setRebuild(TRUE); }
So here we are setting first form values in page_values variable and also updating page_num as 2,
So when set rebuild , rebuildFunction will again execute and load second form 2.
So in build function, check condition for page_num== 2 , below function will load second form elements ,
public function subscribePageTwo(array &$form, FormStateInterface $form_state) { $form['#attached']['library'][] = 'dn_subscribe/dn_subscribe'; $form['description'] = [ '#type' => 'item', '#title' => $this->t('A basic multistep form (page 2)'), ]; $form['address'] = [ '#type' => 'textfield', '#title' => $this->t('Address'), '#required' => TRUE, '#default_value' => $form_state->getValue('address', ''), ]; $form['city'] = [ '#type' => 'textfield', '#title' => $this->t('City'), '#required' => TRUE, '#default_value' => $form_state->getValue('city', ''), ]; $form['back'] = [ '#type' => 'submit', '#value' => $this->t('Back'), // Custom submission handler for 'Back' button. '#submit' => ['::subscribePageTwoBack'], // We won't bother validating the required 'color' field, since they // have to come back to this page to submit anyway. '#limit_validation_errors' => [], ]; $form['submit'] = [ '#type' => 'submit', '#button_type' => 'primary', '#value' => $this->t('Next'), '#submit' => ['::subscribeSecondNextSubmit'] ]; $form['#prefix'] = $this->getFormPrefix(2); return $form; }
See below submit handler of second form.
public function subscribeSecondNextSubmit(array &$form, FormStateInterface $form_state) { $name = $form_state->get('page_values'); //print_r($fname);exit; $form_state ->set('page_values', [ // Keep only first step values to minimize stored data. 'address' => $form_state->getValue('address'), 'city' => $form_state->getValue('city'), 'first_name' => $name['first_name'], 'last_name' => $name['last_name'], ]) ->set('page_num', 3) // Since we have logic in our buildForm() method, we have to tell the form // builder to rebuild the form. Otherwise, even though we set 'page_num' // to 2, the AJAX-rendered form will still show page 1. ->setRebuild(TRUE); }
Here we are first retrieving first form elements from page_values and then setting values again in page_values, so we can transfer first and second form values to third form.
Above setRebuild with page_num=3 , loads the third form.
Step 3 Third form
Below provided the third form function subscribePageThree.
public function subscribePageThree(array &$form, FormStateInterface $form_state) { $form['#attached']['library'][] = 'dn_subscribe/dn_subscribe'; $form['description'] = [ '#type' => 'item', '#title' => $this->t('A basic multistep form (page 3)'), ]; $form['declaration'] = [ '#type' => 'checkboxes', '#required' => TRUE, '#options' => array('option-1' => t('Confirm details provided')), ]; $form['back'] = [ '#type' => 'submit', '#value' => $this->t('Back'), // Custom submission handler for 'Back' button. '#submit' => ['::subscribePageThreeBack'], // We won't bother validating the required 'color' field, since they // have to come back to this page to submit anyway. '#limit_validation_errors' => [], ]; $form['submit'] = [ '#type' => 'submit', '#button_type' => 'primary', '#value' => $this->t('Submit'), ]; $form['#prefix'] = $this->getFormPrefix(3); return $form; }
So here we are not providing a separate submit handler. Hence the third form submission directs the submit to the below submitForm function.
public function submitForm(array &$form, FormStateInterface $form_state) { $page_values = $form_state->get('page_values'); $this->messenger()->addMessage($this->t('The form has been submitted. name="@first @last", address="@address". city="@city"', [ '@first' => $page_values['first_name'], '@last' => $page_values['last_name'], '@address' => $page_values['address'], '@city' => $page_values['city'], ])); }
Here we are displaying all three form values and displays in message.
Also we are providing below back button handlers.
public function subscribePageTwoBack(array &$form, FormStateInterface $form_state) { $form_state // Restore values for the first step. ->setValues($form_state->get('page_values')) ->set('page_num', 1) // Since we have logic in our buildForm() method, we have to tell the form // builder to rebuild the form. Otherwise, even though we set 'page_num' // to 1, the AJAX-rendered form will still show page 2. ->setRebuild(TRUE); } public function subscribePageThreeBack(array &$form, FormStateInterface $form_state) { $form_state // Restore values for the first step. ->setValues($form_state->get('page_values')) ->set('page_num', 2) // Since we have logic in our buildForm() method, we have to tell the form // builder to rebuild the form. Otherwise, even though we set 'page_num' // to 1, the AJAX-rendered form will still show page 2. ->setRebuild(TRUE); }
Complete source code you can download here.