5

This form consists of 1 textfield, and 1 ajax button.

I'm trying to make it add another text field with each click.

It works for the first one, to get a total of 2 text fields. I cannot get more than 2 to appear though.

I suspect the dom is being updated, but not the form. So the subsequent callbacks add an element based on the original form build.

I'm not sure how to approach this in Drupal 8. Any insight would be appreciated.

Here is my form control

<?php
...
class MyModuleForm extends FormBase {
  ...
  public function buildForm(array $form, array &$form_state) {
    $form['foo'] = array(
      '#type' => 'container',
      '#tree' => TRUE,
      '#prefix' => '<div id="foo-replace">',
      '#suffix' => '</div>'
    );
    $form['foo'][0] = array(
      '#type' => 'textfield'
    );
    $form['add'] = array(
      '#type' => 'submit',
      '#name' => 'add',
      '#value' => t('Add'),
      '#ajax' => array(
        'callback' => array($this, 'addMore'),
        'wrapper' => 'foo-replace',
        'effect' => 'fade',
      ),
    );
    return $form;
  }

  public function addMore(array $form, array &$form_state) {
    $form['foo'][] = array(
      '#type' => 'textfield'
    );
    return $form['foo'];
  }
}
4

You made a common mistake. In Form API, ['#ajax']['callback'] has only 2 purposes:

  • Make one-time changes to form, that are not meant to persist form reload, submit or second Ajax request. Good example is adding some classes to freshly modified element.
  • Return proper part of the form.

To have persistent changes, you need to keep the number of elements you want somewhere, for example in $form_state, and add them in form builder function, buildForm in your case.

My approach would be to check $form_state['triggering_element'] to see if it was your Ajax button to invoke form building, and if so, increase counter. Then simply for($i = 0; $i < $element_counter; ++$i) to add required number of elements.

  • Are you 100% on your answer? I tried your suggestions before asking my question here. buildForm only seemed to execute when the page loads. I also tried processForm, which did execute when the callback was triggered, but I did not have access to triggering_element from that one. In D7, the equivalent to buildForm() executes with the callback. I'm just trying to figure out how to accomplish this in D8. – Coder1 Sep 26 '13 at 7:37
  • 1
    @Coder1 I am sure about approach, for exact sample see Mathankumar's answer as I don't have Drupal 8 at hand. I admit that buildForm not executed on ajax calls is a bit of mystery to me, but I'm 100% sure callback is not the place for persistent changes. – Mołot Sep 26 '13 at 7:46
  • 1
    Understood, and not how it works in D7 either. That's just where I ended up because buildForm isn't triggering with the callback and code samples are a great place to start. – Coder1 Sep 26 '13 at 7:53
4

I spent more time with WidgetBase from the field module.

The solution was to define a #submit handler on the ajax button. In the submit handler, set $form_state['rebuild'] = TRUE. This will allow the form to rebuild with buildForm.

Based on core code comments, this is the proper way to gracefully degrade so it works without JS as well. I like it, no need read $form_state for triggering_element.

Note that adding $form_state['rebuild'] to submitForm will not trigger a rebuild from an ajax button. (from my testing anyway)

Once the form is rebuilding, it's like D7. You can add the handler logic in buildForm as Mołot pointed out, and return just the target element in your callback.

Here's the final working form:

<?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) {
  }
}
  • 1
    PHP Fatal error: Cannot use object of type Drupal\\Core\\Form\\FormState as array – AnD Jun 5 '16 at 6:01
  • 2
    @AnD the error is because it needs to be FormStateInterface $form_state not array $form_state – Matt Aug 8 '16 at 20:59
3

Some of the nuts and bolts of this answer have changed as the Drupal 8 APIs have evolved and "solidified". While the general approach of manipulating the form works. The $form_state param can not be used as an array, and must be an object of type FormStateInterface. The method signatures should look like:

public function buildForm(array &$form, FormStateInterface &$form_state)

The $form_state class also has methods getting for setting arbitrary data. Simply, $form_state->get(property, value) and $form_state->set(property, value) in this context. Here's a link to get started with the set method: FormStateInterface::set. And a link to a Gist

  • Unfortunately, I can't get that to work. It seems like it's not supposed to. See this question. – Chris Happy Sep 18 '17 at 7:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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