0

I followed this Drupal example: https://git.drupalcode.org/project/examples/blob/8.x-1.x/form_api_example/src/Form/AjaxAddMore.php

I've edited the code a bit to use fieldsets instead of just one field. This seems to work, though I'm not sure if I did this correctly.

I now have a custom form that allows users to create new fields and remove the last field in the list.

What I need is a remove button next to each field that allows the end-user to choose which field to remove instead of only being able to remove the last field.

I don't know how to get the selected field or fieldset in the submit handler though.

Here's the code I currently have:

<?php

namespace Drupal\xxx\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;


class DebtorForm extends FormBase {

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

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

    // Gather the number of lines in the form already.
    $num_lines = $form_state->get('num_lines');
    // We have to ensure that there is at least one line field.
    if ($num_lines === NULL) {
      $num_field = $form_state->set('num_lines', 1);
      $num_lines = 1;
    }

    $form['#tree'] = TRUE;
    $form['names_fieldset'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Je onbetaalde factuur'),
      '#prefix' => '<div id="invoices-fieldset">',
      '#suffix' => '</div>',
    ];

    for ($i = 0; $i < $num_lines; $i++) {
      // Fieldset line title
      $form['names_fieldset'][$i] = [
        '#type' => 'fieldset',
        '#title' => $this->t('Factuur') . ' ' . ($i + 1),
      ];
      // disputed
      $form['names_fieldset'][$i]['disputed'] = [
        '#type' => 'radios',
        '#title' => $this->t('Mijn factuur is'),
        '#options' => array(
          0 => $this->t('Niet betwist'),
          1 => $this->t('Betwist'),
        ),
      ];
      // date
      $form['names_fieldset'][$i]['date'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Datum factuur'),
      ];
      // amount
      $form['names_fieldset'][$i]['amount'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Bedrag factuur'),
      ];
      // invoiceId
      $form['names_fieldset'][$i]['invoiceId'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Factuurnummer'),
      ];
      $form['names_fieldset'][$i]['invoiceId'] = [
        '#type' => 'submit',
        '#value' => $this->t('Verwijder'),
        '#submit' => ['::removeCallback'],
        '#ajax' => [
          'callback' => '::addmoreCallback',
          'wrapper' => 'invoices-fieldset',
        ],
      ];
    }

    $form['names_fieldset']['actions'] = [
      '#type' => 'actions',
    ];

    $form['names_fieldset']['actions']['add_name'] = [
      '#type' => 'submit',
      '#value' => $this->t('Add one more'),
      '#submit' => ['::addOne'],
      '#ajax' => [
        'callback' => '::addmoreCallback',
        'wrapper' => 'invoices-fieldset',
      ],
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;

  }

  /**
   * Callback for both ajax-enabled buttons.
   *
   * Selects and returns the fieldset with the names in it.
   */
  public function addmoreCallback(array &$form, FormStateInterface $form_state) {
    return $form['names_fieldset'];
  }

  /**
   * Submit handler for the "add-one-more" button.
   *
   * Increments the max counter and causes a rebuild.
   */
  public function addOne(array &$form, FormStateInterface $form_state) {
    $num_field = $form_state->get('num_lines');
    $add_button = $num_field + 1;
    $form_state->set('num_lines', $add_button);
    $form_state->setRebuild();
  }

  /**
   * Submit handler for the "remove" button.
   *
   * Removes the corresponding line
   */
  public function removeCallback(array &$form, FormStateInterface $form_state) {
    $num_field = $form_state->get('num_lines');
    if ($num_field > 1) {
      $remove_button = $num_field - 1;
      $form_state->set('num_lines', $remove_button);
    }
    $form_state->setRebuild();
  }


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

  }

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

  }

}
4
  • Did you try to add a #name to the buttons and then use $form_state->getTriggeringElement() in the submit handler? See drupal.stackexchange.com/questions/179168/…
    – 4uk4
    Jan 21, 2020 at 12:44
  • That would help me identify the triggering element, but then how do I get the fieldset the button is in? Jan 21, 2020 at 12:52
  • @PascalClaes You might be able to by adding '#tree' => TRUE, to your fieldset.
    – No Sssweat
    Jan 22, 2020 at 6:27
  • I can unset the subfield from the $form object, which removes it from the visible form, but it's still present in the $form_state object, which has the values in a protected field, so I cannot unset them there. Using $form_state->unsetValue('names_fieldset') works to remove the entire fieldset, but I only want to remove a single array in names_fieldset, not the entire fieldset, which doesn't seem to work $form_state->unsetValue('names_fieldset[0]') has no effect. Jan 29, 2020 at 13:06

1 Answer 1

2

After extensive searching and testing I found the following solution:

<?php

namespace Drupal\form_api_example\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;


class AjaxAddRemoveElements extends FormBase {

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

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

    $form['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('This example shows an add-more button and a remove button for each specific element.'),
    ];

    // Get the number of names in the form already.
    $num_lines = $form_state->get('num_lines');
    // We have to ensure that there is at least one name field.
    if ($num_lines === NULL) {
      $form_state->set('num_lines', 1);
      $num_lines = $form_state->get('num_lines');
    }

    // Get a list of fields that were removed
    $removed_fields = $form_state->get('removed_fields');
    // If no fields have been removed yet we use an empty array.
    if ($removed_fields === NULL) {
      $form_state->set('removed_fields', array());
      $removed_fields = $form_state->get('removed_fields');
    }

    $form['#tree'] = TRUE;
    $form['names_fieldset'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('People coming to picnic'),
      '#prefix' => '<div id="names-fieldset-wrapper">',
      '#suffix' => '</div>',
    ];

    for ($i = 0; $i < $num_lines; $i++) {
      // Check if field was removed
      if (in_array($i, $removed_fields)) {
        // Skip if field was removed and move to the next field
        continue;
      }

      // Create a new fieldset for each person where we can add first and last name
      // Fieldset title
      $form['names_fieldset'][$i] = [
        '#type' => 'fieldset',
        '#title' => $this->t('Person ') . ' ' . ($i + 1),
      ];
      // date
      $form['names_fieldset'][$i]['firstname'] = [
        '#type' => 'textfield',
        '#title' => $this->t('First name'),
      ];
      // amount
      $form['names_fieldset'][$i]['lastname'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Last name'),
      ];
      $form['names_fieldset'][$i]['actions'] = [
        '#type' => 'submit',
        '#value' => $this->t('Remove'),
        '#name' => $i,
        '#submit' => ['::removeCallback'],
        '#ajax' => [
          'callback' => '::addmoreCallback',
          'wrapper' => 'names-fieldset-wrapper',
        ],
      ];
    }

    $form['names_fieldset']['actions'] = [
      '#type' => 'actions',
    ];

    $form['names_fieldset']['actions']['add_name'] = [
      '#type' => 'submit',
      '#value' => $this->t('Add one more'),
      '#submit' => ['::addOne'],
      '#ajax' => [
        'callback' => '::addmoreCallback',
        'wrapper' => 'names-fieldset-wrapper',
      ],
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;

  }

  /**
   * Callback for both ajax-enabled buttons.
   *
   * Selects and returns the fieldset with the names in it.
   */
  public function addmoreCallback(array &$form, FormStateInterface $form_state) {
    return $form['names_fieldset'];
  }

  /**
   * Submit handler for the "add-one-more" button.
   *
   * Increments the max counter and causes a rebuild.
   */
  public function addOne(array &$form, FormStateInterface $form_state) {
    $num_field = $form_state->get('num_lines');
    $add_button = $num_field + 1;
    $form_state->set('num_lines', $add_button);
    $form_state->setRebuild();
  }

  /**
   * Submit handler for the "remove" button.
   *
   * Removes the corresponding line
   */
  public function removeCallback(array &$form, FormStateInterface $form_state) {
    // We use the name of the remove button to find the element we want to remove
    // Line 72: '#name' => $i,
    $trigger = $form_state->getTriggeringElement();
    $indexToRemove = $trigger['#name'];

    // Remove the fieldset from $form (the easy way)
    unset($form['names_fieldset'][$indexToRemove]);

    // Remove the fieldset from $form_state (the hard way)
    // First fetch the fieldset, then edit it, then set it again
    // Form API does not allow us to directly edit the field
    $namesFieldset = $form_state->getValue('names_fieldset');
    unset($namesFieldset[$indexToRemove]);
//    $form_state->unsetValue('names_fieldset');
    $form_state->setValue('names_fieldset', $namesFieldset);

    // Keep track of removed fields so we can add new fields at the bottom
    // Without this they would be added where a value was removed
    $removed_fields = $form_state->get('removed_fields');
    $removed_fields[] = $indexToRemove;
    $form_state->set('removed_fields', $removed_fields);

    // Rebuild form_state
    $form_state->setRebuild();
  }


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

  }

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

  }

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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