Drupal 8 AJAX button

I needed to implement "Add another" functionality on an entity form for a module I'm working on. The AJAX changes in D8 are subtle, but this was enough to derail me for a bit.

Objective: Show 1 text field on load. Provide a button that adds another text field with each click.

Note: If you prefer to just read the code, you can reference the entire class at the bottom of this post.

1) Setup a variable to maintain an item count in $form_state

I looked at the field module, WidgetBase specifically, for this convention. The array nests so deep to avoid conflicts with other modules/fields which are doing the same thing.

snippets from: MyModuleForm->buildForm()

   // Initialize the counter if it hasn't been set.
    if (!isset($form_state['fields'])) {
      // Nested this deep to avoid conflicts with other modules
      $form_state['fields'] = array(
        'mymodule' => array(
          'foo' => array(
            'items_count' => 1
          )
        )
      );
    }

2) Build the element that contains the text fields

This element will be replaced when the button is clicked. Notice we're wrapping it with a dev defined in #prefix and #suffix. This is what the AJAX callback will target.

Nothing very new here so far.

    $max = $form_state['fields']['mymodule']['foo']['items_count'];
    $form['foo'] = array(
      '#tree' => TRUE,
      '#prefix' => '<div id="foo-replace">',
      '#suffix' => '</div>'
    );

    // Add elements that don't already exist
    for ($delta = 0; $delta < $max; $delta++) {
      if (!isset($form['foo'][$delta])) {
        $element = array(
          '#type' => 'textfield'
        );
        $form['foo'][$delta] = $element;
      }
    }

3) The AJAX submit button

The format of the submit button is similar to D7, with a different format for defining local methods vs a function in the .module file.

i.e. array($this, 'addMoreSubmit') refers to MyModuleForm->addMoreSubmit() and array($this, 'addMoreCallback') refers to MyModuleForm->addMoreCallback().

    $form['add'] = array(
      '#type' => 'submit',
      '#name' => 'add',
      '#value' => t('Add'),
      '#submit' => array(array($this, 'addMoreSubmit')),
      '#ajax' => array(
        'callback' => array($this, 'addMoreCallback'),
        'wrapper' => 'foo-replace',
        'effect' => 'fade',
      ),
    );

4) Create the AJAX callback

Nothing very different here. We're just returning the element that we are replacing.

  public function addMoreCallback(array &$form, array &$form_state) {
    return $form['foo'];
  }

5) Create the AJAX submit handler

This is where the subtle difference is.

$form_state['rebuild'] = TRUE is required here for MyModuleForm->buildForm() to rebuild.

Adding the rebuild code to MyModuleForm->submitForm() will not trigger a rebuild when AJAX is handled.

Note we are also incrementing the item count here, so when the form rebuilds, it will add the additional item. We don't need to look for $form_state['triggering_element'] in the form because we're changing the state the foo element is based on right here.

  public function addMoreSubmit(array &$form, array &$form_state) {
    $form_state['fields']['mymodule']['foo']['items_count']++;
    $form_state['rebuild'] = TRUE;
  }

That's all there is too it. Not much change, but a little difficult to pick up just from going through core code.

Full Class Example

<?php
namespace Drupal\mymodule\Form;

use Drupal\Core\Form\FormBase;

class MyModuleForm extends FormBase {

  public function getFormId() {
    return 'mymodule_form';
  }

  public function buildForm(array $form, array &$form_state) {
    // Initialize the counter if it hasn't been set.
    if (!isset($form_state['fields'])) {
      // Nested this deep to avoid conflicts with other modules
      $form_state['fields'] = array(
        'mymodule' => array(
          'foo' => array(
            'items_count' => 1
          )
        )
      );
    }

    $max = $form_state['fields']['mymodule']['foo']['items_count'];
    $form['foo'] = array(
      '#tree' => TRUE,
      '#prefix' => '<div id="foo-replace">',
      '#suffix' => '</div>'
    );

    // Add elements that don't already exist
    for ($delta = 0; $delta < $max; $delta++) {
      if (!isset($form['foo'][$delta])) {
        $element = array(
          '#type' => 'textfield'
        );
        $form['foo'][$delta] = $element;
      }
    }

    $form['add'] = array(
      '#type' => 'submit',
      '#name' => 'add',
      '#value' => t('Add'),
      '#submit' => array(array($this, 'addMoreSubmit')),
      '#ajax' => array(
        'callback' => array($this, 'addMoreCallback'),
        'wrapper' => 'foo-replace',
        'effect' => 'fade',
      ),
    );
    return $form;
  }

  public function addMoreSubmit(array &$form, array &$form_state) {
    $form_state['fields']['mymodule']['foo']['items_count']++;
    $form_state['rebuild'] = TRUE;
  }

  public function addMoreCallback(array &$form, array &$form_state) {
    return $form['foo'];
  }

  public function validateForm(array &$form, array &$form_state) {
  }

  public function submitForm(array &$form, array &$form_state) {
  }
}