Drupal Form API - Ajax form and results on the same page

Overview

Often in Drupal, projects will come with the requirement to have a form and results on the same page. There are various ways this can be done with views, but sometimes requirements call for a custom solution when views does not fit the bill.

This tutorial will explain how to build an (optionally) Ajax-enabled form in Drupal OOP (version 8+) using the Form API, that loads the results of the form submission into a div on the same page.

The form created in this tutorial has a search field from which node titles can be searched using partial strings. In the real (Drupal) world, this functionality would be built with Views, creating much more flexible options through the Admin UI. But, for the purposes of a simple to understand example, this form fits the purpose.

Prerequisites

To follow this tutorial, you will need to understand the following concepts that will not be covered:

  • How to create a custom Drupal module
  • An understanding of how to create a custom form in Drupal, with a route at which to access it
  • An understanding of what a Drupal entity is, specifically the Node entity
  • An understanding of dependency injection in Drupal, to follow how services are injected

Steps

  1. Create a new search form
  2. Set the form to rebuild upon submission
  3. Build the search results using the submitted data
  4. (Optional) #ajax the form

Step 1: Create a new search form

The first step is to create a new search form. The first thing to do is implement buildForm(), and define a text field and a submit button:

  1. <?php
  2.  
  3. namespace Drupal\EXAMPLE\Form;
  4.  
  5. use Drupal\Core\Entity\EntityTypeManagerInterface;
  6. use Drupal\Core\Form\FormBase;
  7. use Drupal\Core\Form\FormStateInterface;
  8. use Symfony\Component\DependencyInjection\ContainerInterface;
  9.  
  10. /**
  11.  * Form example of an ajax form with results shown on the same page.
  12.  */
  13. class SearchAndResultsForm extends FormBase {
  14.  
  15. /**
  16.   * The entity type manager
  17.   *
  18.   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  19.   */
  20. protected $entityTypeManager;
  21.  
  22. /**
  23.   * {@inheritdoc}
  24.   */
  25. public function __construct(EntityTypeManagerInterface $entityTypeManager) {
  26. $this->entityTypeManager = $entityTypeManager;
  27. }
  28.  
  29. /**
  30.   * {@inheritdoc}
  31.   */
  32. public static function create(ContainerInterface $container) {
  33. return new static(
  34. $container->get('entity_type.manager')
  35. );
  36. }
  37.  
  38. /**
  39.   * {@inheritdoc}
  40.   */
  41. public function getFormId() {
  42. return 'search_and_results_form';
  43. }
  44.  
  45. /**
  46.   * {@inheritdoc}
  47.   */
  48. public function buildForm(array $form, FormStateInterface $form_state) {
  49. // The search field.
  50. $form['node_title'] = [
  51. '#type' => 'textfield',
  52. '#title' => $this->t('Content Title'),
  53. '#description' => $this->t('Enter part or all of a node title'),
  54. '#required' => TRUE,
  55. ];
  56.  
  57. // A wrapper for form buttons on Drupal forms.
  58. $form['actions'] = [
  59. '#type' => 'actions',
  60. ];
  61.  
  62. // The submit button.
  63. $form['submit'] = [
  64. '#type' => 'submit',
  65. '#value' => $this->t('Submit'),
  66. ];
  67.  
  68. return $form;
  69. }
  70.  
  71. }

Step 2: Set the form to rebuild upon submission

Somewhat counter-intuitively, nothing is done in the form submission of this form. The form is simply set to rebuild itself. Here is the entire submit handler for the form in this tutorial:

  1. /**
  2.  * {@inheritdoc}
  3.  */
  4. public function submitForm(array &$form, FormStateInterface $form_state) {
  5. // Set the form to rebuild. The submitted values are maintained in the
  6. // form state, and used to build the search results in the form definition.
  7. $form_state->setRebuild(TRUE);
  8. }

As is stated in the code comments, when the form is set to rebuild, the submitted values are passed into back into the buildForm() method, as part of the $form_state object. This leads to step three, which adds code to the buildForm() method we defined in step 1. This time, the form is rebuild the same as in step 1, but the results of a search using the submitted code are rendered below the form, including a no results found message when applicable.

  1. /**
  2.   * {@inheritdoc}
  3.   */
  4. public function buildForm(array $form, FormStateInterface $form_state) {
  5. // The search field.
  6. $form['node_title'] = [
  7. '#type' => 'textfield',
  8. '#title' => $this->t('Content Title'),
  9. '#description' => $this->t('Enter part or all of a node title'),
  10. '#required' => TRUE,
  11. ];
  12.  
  13. // A wrapper for form buttons on Drupal forms.
  14. $form['actions'] = [
  15. '#type' => 'actions',
  16. ];
  17.  
  18. // The submit button.
  19. $form['submit'] = [
  20. '#type' => 'submit',
  21. '#value' => $this->t('Submit'),
  22. ];
  23.  
  24. // The wrapper for search results.
  25. $form['search_results'] = [
  26. // Set the results to be below the form.
  27. '#weight' => 100,
  28. ];
  29.  
  30. // The triggering element is the button that triggered the form submit. This
  31. // will be empty on initial page load, as the form has not been submitted
  32. // yet. Therefore the code inside the conditional is only executed when a
  33. // value has been submitted, and there are results to be rendered.
  34. if ($form_state->getTriggeringElement()) {
  35. // Get the text submitted by the user as a search query.
  36. $node_title = $form_state->getValue('node_title');
  37. // Get the node storage.
  38. $node_storage = $this->entityTypeManager->getStorage('node');
  39. // Get a list of Node IDs for nodes whose title contains the submitted
  40. // value. Note that instead of CONTAINS you can use STARTS_WITH or
  41. // or ENDS_WITH.
  42. $query = $node_storage->getQuery()
  43. ->condition('title', $node_title, 'CONTAINS')
  44. ->groupBy('nid')
  45. ->sort('title', 'ASC');
  46.  
  47. // Execute the query.
  48. if ($nids = $query->execute()) {
  49. // Load the nodes with the given NIDs.
  50. if ($nodes = $node_storage->loadMultiple($nids)) {
  51. // Get the node view builder. This is used to build the render array
  52. // to view the node.
  53. $view_builder = $this->entityTypeManager->getViewBuilder('node');
  54. // Loop through the found nodes.
  55. foreach ($nodes as $node) {
  56. // Build the render array for the node teaser and add to the
  57. // results.
  58. $form['search_results']['result'][$node->id()] = $view_builder->view($node, 'teaser');
  59. }
  60. }
  61. }
  62.  
  63. // Check if no results were found.
  64. if (!isset($form['search_results']['result'])) {
  65. // Add a 'no results found' message.
  66. $form['search_results']['no_result'] = [
  67. '#prefix' => '<p>',
  68. '#suffix' => '</p>',
  69. '#markup' => $this->t('Sorry, no results found for this search'),
  70. ];
  71. }
  72. }
  73.  
  74. return $form;
  75. }

The process works as follows. The user first enters the base, and because the form has never been submitted, $form_state->getTriggeringElement() is empty and the whole search results section is skipped. The form is submitted, it is rebuilt, <code>$form_state-&gt;getTriggeringElement()</code> returns a value, and the search results are added to the page below the form Voila!

Step 4: (Optional) #ajax the form

The final step to this form is to add Ajax, so the search results are added to the page without requiring a page submit. This takes a few small adjustments to our form.

The first thing is to add #ajax to the submit button on the form. As such, the submit button definition is updated to be as follows:

  1. // The submit button.
  2. $form['submit'] = [
  3. '#type' => 'submit',
  4. '#value' => $this->t('Submit'),
  5. '#ajax' => [
  6. 'callback' => '::ajaxSubmit',
  7. // The ID of the <div/> into which search results should be inserted.
  8. 'wrapper' => 'set_search_results_wrapper',
  9. ],
  10. ];

In the code above, callback and wrapper have been named, but these still need to be defined. As such, the next step is to build the wrapper identified above, the container for the search results needs to be added to the form on first render, so that the Ajax has a container into which the results can be inserted. The PHP results wrapper was created in the last step, and this will be updated to do two things:

  1. Have a div wrapper with a specific ID
  2. Force render on page load, even when there are no search results.

With no input, there are no search results, and therefore the wrapper render array does not have any children. Render array elements with no children in Drupal render arrays are not rendered. In the code below, the wrapper definition is updated, adding an HTML wrapper which is forced to render by creating a child using #markup:

  1. // The wrapper for search results.
  2. $form['search_results'] = [
  3. // Set the results to be below the form.
  4. '#weight' => 100,
  5. // The prefix/suffix are the div with the ID specified as the wrapper in
  6. // the submit button's #ajax definition.
  7. '#prefix' => '<div id="set_search_results_wrapper">',
  8. '#suffix' => '</div>',
  9. // The #markup element forces rendering of the #prefix and #suffix.
  10. // Without content, the wrappers are not rendered. Therefore, an empty
  11. // string is declared, ensuring that the wrapper for the search results
  12. // is present when the page is loaded.
  13. '#markup' => '',
  14. ];

So now the button has had Ajax added, and a container exists for the search results. The last thing to do is to define the callback referred to in the submit button above. This callback is prefixed with two colons (::), meaning it is a method in the current class. So, the following method is added to the form class:

  1. /**
  2.  * Custom ajax submit handler for the form. Returns search results.
  3.  */
  4. public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
  5. // Return the search results element of the form.
  6. return $form['search_results'];
  7. }

This code is very simple. It simply takes the search results wrapper of the form definition, and returns it. The system handles the rendering into HTML and insertion into the page.

Last Consideration

While the code above defines the HTML container into which the results will be inserted, any HTML element on the page with an ID could be used instead. However, note that in the rare case the user does not have JavaScript enabled in their browser, they will not be able to view the results of the form. As such, the only adjustment to be made to the code above would be to remove the #prefix and #suffix that define the <div/> referred to by the submit button's #ajax definition. This way, if JavaScript is disabled and the form is submitted, the results will be appended to the form on page reload.

Summary

This tutorial explained how a Drupal form definition can be altered to display the search results in a container, when the form is rebuilt after initial submission. Happy Drupaling!

Complete Code

  1. <?php
  2.  
  3. namespace Drupal\testmodule\Form;
  4.  
  5. use Drupal\Core\Entity\EntityTypeManagerInterface;
  6. use Drupal\Core\Form\FormBase;
  7. use Drupal\Core\Form\FormStateInterface;
  8. use Symfony\Component\DependencyInjection\ContainerInterface;
  9.  
  10. /**
  11.  * Form example of an ajax form with results shown on the same page.
  12.  */
  13. class SearchAndResultsForm extends FormBase {
  14.  
  15. /**
  16.   * The entity type manager
  17.   *
  18.   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  19.   */
  20. protected $entityTypeManager;
  21.  
  22. /**
  23.   * {@inheritdoc}
  24.   */
  25. public function __construct(EntityTypeManagerInterface $entityTypeManager) {
  26. $this->entityTypeManager = $entityTypeManager;
  27. }
  28.  
  29. /**
  30.   * {@inheritdoc}
  31.   */
  32. public static function create(ContainerInterface $container) {
  33. return new static(
  34. $container->get('entity_type.manager')
  35. );
  36. }
  37.  
  38. /**
  39.   * {@inheritdoc}
  40.   */
  41. public function getFormId() {
  42. return 'search_and_results_form';
  43. }
  44.  
  45. /**
  46.   * {@inheritdoc}
  47.   */
  48. public function buildForm(array $form, FormStateInterface $form_state) {
  49. // The search field.
  50. $form['node_title'] = [
  51. '#type' => 'textfield',
  52. '#title' => $this->t('Content Title'),
  53. '#description' => $this->t('Enter part or all of a node title'),
  54. '#required' => TRUE,
  55. ];
  56.  
  57. // A wrapper for form buttons on Drupal forms.
  58. $form['actions'] = [
  59. '#type' => 'actions',
  60. ];
  61.  
  62. // The submit button.
  63. $form['submit'] = [
  64. '#type' => 'submit',
  65. '#value' => $this->t('Submit'),
  66. '#ajax' => [
  67. 'callback' => '::ajaxSubmit',
  68. 'wrapper' => 'set_search_results_wrapper',
  69. ],
  70. ];
  71.  
  72. // The wrapper for search results.
  73. $form['search_results'] = [
  74. // Set the results to be below the form.
  75. '#weight' => 100,
  76. // The prefix/suffix are the div with the ID specified as the wrapper in
  77. // the submit button's #ajax definition.
  78. '#prefix' => '<div id="set_search_results_wrapper">',
  79. '#suffix' => '</div>',
  80. // The #markup element forces rendering of the #prefix and #suffix.
  81. // Without content, the wrappers are not rendered. Therefore, an empty
  82. // string is declared, ensuring that the wrapper for the search results
  83. // is present when the page is loaded.
  84. '#markup' => '',
  85. ];
  86.  
  87. // The triggering element is the button that triggered the form submit. This
  88. // will be empty on initial page load, as the form has not been submitted
  89. // yet. Therefore the code inside the conditional is only executed when a
  90. // value has been submitted, and there are results to be rendered.
  91. if ($form_state->getTriggeringElement()) {
  92. // Get the text submitted by the user as a search query.
  93. $node_title = $form_state->getValue('node_title');
  94. // Get the node storage.
  95. $node_storage = $this->entityTypeManager->getStorage('node');
  96. // Get a list of Node IDs for nodes whose title contains the submitted
  97. // value. Note that instead of CONTAINS you can use STARTS_WITH or
  98. // or ENDS_WITH.
  99. $query = $node_storage->getQuery()
  100. ->condition('title', $node_title, 'CONTAINS')
  101. ->groupBy('nid')
  102. ->sort('title', 'ASC');
  103.  
  104. // Execute the query.
  105. if ($nids = $query->execute()) {
  106. // Load the nodes with the given NIDs.
  107. if ($nodes = $node_storage->loadMultiple($nids)) {
  108. // Get the node view builder. This is used to build the render array
  109. // to view the node.
  110. $view_builder = $this->entityTypeManager->getViewBuilder('node');
  111. // Loop through the found nodes.
  112. foreach ($nodes as $node) {
  113. // Build the render array for the node teaser and add to the
  114. // results.
  115. $form['search_results']['result'][$node->id()] = $view_builder->view($node, 'teaser');
  116. }
  117. }
  118. }
  119.  
  120. // Check if no results were found.
  121. if (!isset($form['search_results']['result'])) {
  122. // Add a 'no results found' message.
  123. $form['search_results']['no_result'] = [
  124. '#prefix' => '<p>',
  125. '#suffix' => '</p>',
  126. '#markup' => $this->t('Sorry, no results found for this search'),
  127. ];
  128. }
  129. }
  130.  
  131. return $form;
  132. }
  133.  
  134. /**
  135.   * {@inheritdoc}
  136.   */
  137. public function submitForm(array &$form, FormStateInterface $form_state) {
  138. // Set the form to rebuild. The submitted values are maintained in the
  139. // form state, and used to build the search results in the form definition.
  140. $form_state->setRebuild(TRUE);
  141. }
  142.  
  143. /**
  144.   * Custom ajax submit handler for the form. Returns search results.
  145.   */
  146. public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
  147. // Return the search results element of the form.
  148. return $form['search_results'];
  149. }
  150.  
  151. }