Hello!

Many years ago (2015 to be exact), we published an article on how to create self-populating dropdown forms using the Drupal 7 Webform API. Now that the year is 2019 and Drupal 8 has been “Released” for quite some time now, with 8.7.1 as of May 2019, we thought it might be a good idea to update the strategy to do the same or similar action in Drupal 8.

What are we trying to do anyways? Well we want a way for people to interact with a Webform in an interactive way. This means we want subsequent dropdown selections to be populated by previous choices.

This logic doesn’t have to be restricted to dropdowns, it can be input boxes, checkboxes or radio buttons. Anything, really.

In the example above, you can see “Beverage” is chosen for “Industries”. The “Products” dropdown underneath has the options that you see populated based on the first choice. This can be done with larger amounts of successive dependent dropdowns (if you wanted). Hopefully you can see the potential here with this sort of Webform interaction – you can make “smart” and dynamic forms for your end users.

Create a Drupal 8 module

There is quite a few resources already to guide you towards creating your own Drupal 8 module, so we won’t get into the basics, but if you want to jump to using a Drupal 8 module skeleton, you can check out this github repository that you can clone.

Once you have a module skeleton set up, you will want to focus on a main controller, defined in the module.routing.yml file :

_controller: 'Drupal\shift8module\Controller\Shift8Controller::content' 1 _controller : 'Drupal\shift8module\Controller\Shift8Controller::content'

In the main controller for your module, you want to reference your new form, which we are aptly going to name Shift8Form :

namespace Drupal\shift8module\Controller; use Drupal\Core\Controller\ControllerBase; class Shift8Controller extends ControllerBase { public function content() { $shift8form = \Drupal::formBuilder()->getForm('Drupal\shift8module\Form\Shift8Form'); // If you want modify the form: return [ '#theme' => 'shift8module', '#form' => $shift8form, ]; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace Drupal \ shift8module \ Controller ; use Drupal \ Core \ Controller \ ControllerBase ; class Shift8Controller extends ControllerBase { public function content ( ) { $ shift8form = \ Drupal :: formBuilder ( ) -> getForm ( 'Drupal\shift8module\Form\Shift8Form' ) ; // If you want modify the form: return [ '#theme' = > 'shift8module' , '#form' = > $ shift8form , ] ; } }

Now that we have our controller and it is referencing our (soon-to-be-created) form, we can turn our attention to the Form code.

Create a Form in your Drupal 8 Module

What we want to do is create a form with two dropdowns. The first dropdown will take the first level hierarchy of a custom taxonomy. The second dropdown will populate after the first is selected and will populate a list of the children of that selected taxonomy hierarchy. Again this can be anything, it doesnt have to be taxonomies. You can have a hard coded list of choices or you can create taxonomies or custom content to pull this data from – the possibilities are endless. Once you can wrap your head around the potential database queries to pull the necessary data, then everything else should fall into place.

namespace Drupal\shift8module\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; // Use for Ajax. use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\InvokeCommand; use Drupal\Core\Ajax\AppendCommand; use Drupal\Core\Ajax\HtmlCommand; /** * ProductForm controller. */ class Shift8Form extends FormBase { /** * Returns a unique string identifying the form. * * The returned ID should be a unique string that can be a valid PHP function * name, since it's used in hook implementation names such as * hook_form_FORM_ID_alter(). * * @return string * The unique string identifying the form. */ public function getFormId() { return 'shift8module_form'; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 namespace Drupal \ shift8module \ Form ; use Drupal \ Core \ Form \ FormBase ; use Drupal \ Core \ Form \ FormStateInterface ; // Use for Ajax. use Drupal \ Core \ Ajax \ AjaxResponse ; use Drupal \ Core \ Ajax \ InvokeCommand ; use Drupal \ Core \ Ajax \ AppendCommand ; use Drupal \ Core \ Ajax \ HtmlCommand ; /** * ProductForm controller. */ class Shift8Form extends FormBase { /** * Returns a unique string identifying the form. * * The returned ID should be a unique string that can be a valid PHP function * name, since it's used in hook implementation names such as * hook_form_FORM_ID_alter(). * * @return string * The unique string identifying the form. */ public function getFormId ( ) { return 'shift8module_form' ; }

In the above code snippet, first and foremost we want to use the right elements. FormBase and FormStateInterface are key to building the form we need. Next to that, we want to be able to utilize Ajax to (transparently to the end user) submit the selected information and perform the necessary queries based on the selections made. We should now be ready to build our form now.

public function buildForm(array $form, FormStateInterface $form_state) { // Get values of selected dropdowns to aid in filtering and db queries $options_first = $this->get_first_dropdown_options(); $form['description'] = [ '#type' => 'item', ]; // Product Category $form['dropdown_first'] = array( '#type' => 'select', '#title' => 'Industries', '#prefix' => '<div id="dropdown-first-replace" class="shift8-form-select">', '#suffix' => '</div>', '#options' => $options_first, '#attributes' => array('style' => 'display:inline-block;'), '#default_value' => '_none', '#empty_option' => t('- None -'), '#empty_value' => '_none', '#validated' => TRUE, '#ajax' => array( 'callback' => [$this, 'shift8_dependent_dropdown_callback'], 'event' => 'change', 'wrapper' => 'dropdown-second-replace', ), ); // Product Sub Category $form['dropdown_second'] = array( '#type' => 'select', '#title' => 'Products', '#prefix' => '<div id="dropdown-second-replace" class="shift8-form-select">', '#suffix' => '</div>', '#options' => array(), '#attributes' => array('style' => 'display:inline-block;'), '#default_value' => '_none', '#empty_option' => t('- None -'), '#empty_value' => '_none', '#validated' => TRUE, '#ajax' => array( 'callback' => [$this, 'shift8_dependent_dropdown_callback_second'], 'event' => 'change', 'wrapper' => 'dropdown-third-replace', ), ); // Group submit handlers in an actions element with a key of "actions" so // that it gets styled correctly, and so that other modules may add actions // to the form. This is not required, but is convention. $form['actions'] = [ '#type' => 'actions', ]; $form['reset'] = array( '#type' => 'button', '#button_type' => 'reset', '#value' => t('Reset'), '#validate' => array(), '#attributes' => array( 'onclick' => 'this.form.reset(); return false;', ), '#ajax' => array( 'callback' => [$this, 'shift8_reset'], ), '#prefix' => '', '#sufix' => '', ); $form['message'] = [ '#type' => 'markup', '#markup' => '<div class="result_message"></div>', ]; return $form; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 public function buildForm ( array $ form , FormStateInterface $ form_state ) { // Get values of selected dropdowns to aid in filtering and db queries $ options_first = $ this -> get_first_dropdown_options ( ) ; $ form [ 'description' ] = [ '#type' = > 'item' , ] ; // Product Category $ form [ 'dropdown_first' ] = array ( '#type' = > 'select' , '#title' = > 'Industries' , '#prefix' = > '<div id="dropdown-first-replace" class="shift8-form-select">' , '#suffix' = > '</div>' , '#options' = > $ options_first , '#attributes' = > array ( 'style' = > 'display:inline-block;' ) , '#default_value' = > '_none' , '#empty_option' = > t ( '- None -' ) , '#empty_value' = > '_none' , '#validated' = > TRUE , '#ajax' = > array ( 'callback' = > [ $ this , 'shift8_dependent_dropdown_callback' ] , 'event' = > 'change' , 'wrapper' = > 'dropdown-second-replace' , ) , ) ; // Product Sub Category $ form [ 'dropdown_second' ] = array ( '#type' = > 'select' , '#title' = > 'Products' , '#prefix' = > '<div id="dropdown-second-replace" class="shift8-form-select">' , '#suffix' = > '</div>' , '#options' = > array ( ) , '#attributes' = > array ( 'style' = > 'display:inline-block;' ) , '#default_value' = > '_none' , '#empty_option' = > t ( '- None -' ) , '#empty_value' = > '_none' , '#validated' = > TRUE , '#ajax' = > array ( 'callback' = > [ $ this , 'shift8_dependent_dropdown_callback_second' ] , 'event' = > 'change' , 'wrapper' = > 'dropdown-third-replace' , ) , ) ; // Group submit handlers in an actions element with a key of "actions" so // that it gets styled correctly, and so that other modules may add actions // to the form. This is not required, but is convention. $ form [ 'actions' ] = [ '#type' = > 'actions' , ] ; $ form [ 'reset' ] = array ( '#type' = > 'button' , '#button_type' = > 'reset' , '#value' = > t ( 'Reset' ) , '#validate' = > array ( ) , '#attributes' = > array ( 'onclick' = > 'this.form.reset(); return false;' , ) , '#ajax' = > array ( 'callback' = > [ $ this , 'shift8_reset' ] , ) , '#prefix' = > '' , '#sufix' = > '' , ) ; $ form [ 'message' ] = [ '#type' = > 'markup' , '#markup' = > '<div class="result_message"></div>' , ] ; return $ form ; }

The above snippet, while a bit long, should be fairly straightforward. The key elements we are declaring is the dropdown_first and dropdown_second elements. Furthermore, the important items to note are the ajax callback functions under the #ajax heading in each dropdown declaration.

What this does is, on a detected change of the dropdown (i.e. when a new selection is made), Drupal will trigger either the shift8_dependent_dropdown_callback or the shift8_dependent_dropdown_callback_second functions.

Before that even happens, when the form is first rendered, we want to populate the first dropdown with default options. That is why you see the $options_first variable get declared, variables pulled with the function and populated into the #options field of dropdown_first. The get_first_dropdown_options function can return anything, so long as it makes sense in the logical flow of how things are supposed to be populating. You can build this whole form with static info and worry about database queries later. This will allow you to functionally build out your form first in smaller pieces.

Hopefully you can see how things are playing out here , and its not super different with how things were laid out in the Drupal 7 Webform API interactions.

How to use an Ajax Callback for a Drupal 8 Webform

Rather than lay out all the code we put together at once, it makes more sense to show you the first callback. Its relatively simple to take this example below and put together a series of callbacks for your Webform.

public function shift8_dependent_dropdown_callback(array &$form, FormStateInterface $form_state) { // This is where you will build your form options and call other functions to generate options $this->first_selected = $form_state->getValue('dropdown_first'); // Get entity_id of first dropdown selection $query = db_select('taxonomy_term_field_data'); $query->fields('taxonomy_term_field_data', array('tid',)); $query->condition(db_and() ->condition('name', $this->first_selected, '=') ); $query->range(0, 1); $results = $query->execute(); $firstchoiceTID = $results->fetchField(); // Build list of options based on first selection $query_2 = db_select('taxonomy_term__parent'); $query_2->fields('taxonomy_term__parent', array('entity_id',)); $query_2->condition(db_and() ->condition('bundle', 'shift8_products', '=') ->condition('parent_target_id', $firstchoiceTID, '=') ); $results_2 = $query_2->execute(); $result_ids = array(); foreach ($results_2 as $result) { $result_ids[] = $result->entity_id; } $entities = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadMultiple($result_ids); $result_names = array(); $result_names[] = array( '_none' => '- None -'); $result_options = null; foreach ($entities as $single_entity) { $result_names[] = array( $single_entity->get('name')->getValue()[0]['value'] => $single_entity->get('name')->getValue()[0]['value'] ); $result_options .= '<option value="' . $single_entity->get('name')->getValue()[0]['value'] . '">' . $single_entity->get('name')->getValue()[0]['value'] . '</option>'; } $result_names = call_user_func_array('array_merge', $result_names); $form['dropdown_second']['#options'] = $result_names; // Result Handling $response = new AjaxResponse(); // Populate the dropdown $response->addCommand(new AppendCommand('#edit-dropdown-second',$result_options)); return $response; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public function shift8_dependent_dropdown_callback ( array & $ form , FormStateInterface $ form_state ) { // This is where you will build your form options and call other functions to generate options $ this -> first_selected = $ form_state -> getValue ( 'dropdown_first' ) ; // Get entity_id of first dropdown selection $ query = db_select ( 'taxonomy_term_field_data' ) ; $ query -> fields ( 'taxonomy_term_field_data' , array ( 'tid' , ) ) ; $ query -> condition ( db_and ( ) -> condition ( 'name' , $ this -> first_selected , '=' ) ) ; $ query -> range ( 0 , 1 ) ; $ results = $ query -> execute ( ) ; $ firstchoiceTID = $ results -> fetchField ( ) ; // Build list of options based on first selection $ query_2 = db_select ( 'taxonomy_term__parent' ) ; $ query_2 -> fields ( 'taxonomy_term__parent' , array ( 'entity_id' , ) ) ; $ query_2 -> condition ( db_and ( ) -> condition ( 'bundle' , 'shift8_products' , '=' ) -> condition ( 'parent_target_id' , $ firstchoiceTID , '=' ) ) ; $ results_2 = $ query_2 -> execute ( ) ; $ result_ids = array ( ) ; foreach ( $ results_2 as $ result ) { $ result_ids [ ] = $ result -> entity_id ; } $ entities = \ Drupal :: entityTypeManager ( ) -> getStorage ( 'taxonomy_term' ) -> loadMultiple ( $ result_ids ) ; $ result_names = array ( ) ; $ result_names [ ] = array ( '_none' = > '- None -' ) ; $ result_options = null ; foreach ( $ entities as $ single_entity ) { $ result_names [ ] = array ( $ single_entity -> get ( 'name' ) -> getValue ( ) [ 0 ] [ 'value' ] = > $ single_entity -> get ( 'name' ) -> getValue ( ) [ 0 ] [ 'value' ] ) ; $ result _ options . = '<option value="' . $ single_entity -> get ( 'name' ) -> getValue ( ) [ 0 ] [ 'value' ] . '">' . $ single_entity -> get ( 'name' ) -> getValue ( ) [ 0 ] [ 'value' ] . '</option>' ; } $ result_names = call_user_func_array ( 'array_merge' , $ result_names ) ; $ form [ 'dropdown_second' ] [ '#options' ] = $ result_names ; // Result Handling $ response = new AjaxResponse ( ) ; // Populate the dropdown $ response -> addCommand ( new AppendCommand ( '#edit-dropdown-second' , $ result_options ) ) ; return $ response ; }

There is quite a lot happening in the above snippet, but perhaps just ignore all the MySQL database queries that are happening. All that is happening there is we are pulling taxonomy IDs and subsequently a list of child taxonomy based on the parent’s selection. This means that with the first dropdown, which is prepopulated at the loading of the form by the way, we want to populate the second dropdown options once the first dropdown is chosen.

So we build a list of child taxonomies and create an AjaxResponse result with a callback command that targets the selector for the second dropdown with the result_options variable that we populated with all those database queries. Again if you strip out the database queries and just build your own static data to start with, you may find building the form logic easier to handle before making the data truly dynamic.

You can also read more about Ajax Callbacks to learn about all the options that you can use to interact with your form and page using Ajax. One thing not covered here (which might be covered in a future blog post) is not only populating Webform elements in the ways described above, but populating content on the page (think search results, but with Ajax and avoiding the need for a page refresh).

I hope this helps you on your form-building journey!