8

I want to show a Add more button that adds an extra text field and the corresponding Remove button.

I followed ajax_example_add_more() to achieve this in Drupal 7. For Drupal 8, I tried with the following code.

class XmlfeedsSettingsForm extends FormBase {
  public function getFormId() {
    return 'formid';
  }


  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
   // Because we have many fields with the same values, we have to set
  // #tree to be able to access them.
  $form['#tree'] = TRUE;
  $form['names_fieldset'] = array(
    '#type' => 'fieldset',
    '#title' => t('People coming to the picnic'),
    // Set up the wrapper so that AJAX will be able to replace the fieldset.
    '#prefix' => '<div id="names-fieldset-wrapper">',
    '#suffix' => '</div>',
  );

  // Build the fieldset with the proper number of names. We'll use
  // $form_state['num_names'] to determine the number of textfields to build.
  $form_state->getValue['num_names_parent'] = '';
  if (empty($form_state->getValue['num_names_parent'])) {
    $form_state->getValue['num_names_parent'] = 1;
  }
  for ($i = 0; $i < $form_state->getValue['num_names_parent']; $i++) {
    $form['names_fieldset']['name'][$i] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
    );
  }
  $form['names_fieldset']['add_name'] = array(
    '#type' => 'submit',
    '#value' => t('Add one more'),
    '#submit' => array('::ajax_example_add_more_add_one'),
    // See the examples in ajax_example.module for more details on the
    // properties of #ajax.
    '#ajax' => array(
      'callback' => '::ajax_example_add_more_callback',
      'wrapper' => 'names-fieldset-wrapper',
    ),
  );
  if ($form_state->getValue['num_names_parent'] > 1) {
    $form['names_fieldset']['remove_name'] = array(
      '#type' => 'submit',
      '#value' => t('Remove one'),
      '#submit' => array('::ajax_example_add_more_remove_one'),
      '#ajax' => array(
        'callback' => '::ajax_example_add_more_callback',
        'wrapper' => 'names-fieldset-wrapper',
      ),
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );

  // This simply allows us to demonstrate no-javascript use without
  // actually turning off javascript in the browser. Removing the #ajax
  // element turns off AJAX behaviors on that element and as a result
  // ajax.js doesn't get loaded.
  // For demonstration only! You don't need this.
 // if ($no_js_use) {
    // Remove the #ajax from the above, so ajax.js won't be loaded.
//    if (!empty($form['names_fieldset']['remove_name']['#ajax'])) {
//      unset($form['names_fieldset']['remove_name']['#ajax']);
//    }
//    unset($form['names_fieldset']['add_name']['#ajax']);
 // }

  return $form;
  }
  public function ajax_example_add_more_add_one(array &$form, FormStateInterface $form_state) {
      print_R($form_state);die;
  //$form_state->getValue['num_names_parent']++;
  //$form_state->setRebuild(TRUE);


}
public function ajax_example_add_more_callback(array &$form, FormStateInterface $form_state) {
  return $form['names_fieldset'];

}
public function ajax_example_add_more_remove_one(array &$form, FormStateInterface $form_state) {
  if ($form_state->getValue['num_names_parent'] > 1) {
    $form_state->getValue['num_names_parent']--;
  }
  $form_state['rebuild'] = TRUE;
}

print_r($form_state) in buildForm() outputs the following.

[getValue] => Array
        (
            [num_names_parent] => 1
        )

In ajax_example_add_more_add_one(), the num_names_parent index is missing.

How can I correctly add the Add more button?

4
  • Ok, which form you want this 'add_more' behavior? If you talking about Fields, just change storage setting to anything higher than 1.
    – Vagner
    May 3, 2016 at 18:47
  • I have edit in the questions, It is basically any custom form, so field storage is not required here I think. Any possible way to achieve it? May 4, 2016 at 7:10
  • Looking at the core, I found how Drupal implements this behavior, take a look in WidgetBase.php. If you need more help just ask. (there is no remove button, empty fields will be ignored).
    – Vagner
    May 4, 2016 at 12:31
  • @see api.drupal.org/api/examples/…
    – leymannx
    Mar 11, 2019 at 22:01

5 Answers 5

13

It works !! Try this

<?php
  
namespace Drupal\fapi_example\Form;
   
use Drupal\Core\Form\FormStateInterface;
  
class AjaxAddMore extends FormBase {
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['description'] = array(
      '#markup' => '<div>'. $this->t('This example shows an add-more and a remove-last button.').'</div>',
    );

    $i = 0;
    $name_field = $form_state->get('num_names');
    $form['#tree'] = TRUE;
    $form['names_fieldset'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('People coming to picnic'),
      '#prefix' => '<div id="names-fieldset-wrapper">',
      '#suffix' => '</div>',
    ];
    if (empty($name_field)) {
      $name_field = $form_state->set('num_names', 1);
    }
    for ($i = 0; $i < $name_field; $i++) {
      $form['names_fieldset']['name'][$i] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name'),
      ];
    }
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['names_fieldset']['actions']['add_name'] = [
      '#type' => 'submit',
      '#value' => $this->t('Add one more'),
      '#submit' => array('::addOne'),
      '#ajax' => [
      'callback' => '::addmoreCallback',
      'wrapper' => 'names-fieldset-wrapper',
      ],
    ];
    if ($name_field > 1) {
      $form['names_fieldset']['actions']['remove_name'] = [
      '#type' => 'submit',
      '#value' => $this->t('Remove one'),
      '#submit' => array('::removeCallback'),
       '#ajax' => [
        'callback' => '::addmoreCallback',
        'wrapper' => 'names-fieldset-wrapper',
      ]
      ];
    }
    $form_state->setCached(FALSE);
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];
  
    return $form;
  }
  
  public function getFormId() {
    return 'fapi_example_ajax_addmore';
  }

  public function addOne(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    $add_button = $name_field + 1;
    $form_state->set('num_names', $add_button);
    $form_state->setRebuild();
  }

  public function addmoreCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    return $form['names_fieldset'];
  }

  public function removeCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    if ($name_field > 1) {
      $remove_button = $name_field - 1;
      $form_state->set('num_names', $remove_button);
    }
    $form_state->setRebuild();
  }
  
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValue(array('names_fieldset', 'name'));
  
    $output = t('These people are coming to the picnic: @names', array(
      '@names' => implode(', ', $values),
      )
    );
    drupal_set_message($output);
  }
}
2
  • What's the usage of $form_state->setCached(FALSE); this line? What if I don't write this line? Also, we generally write form like $form['foo'] = array(); but you didn't use this format, instead you simply used $form['foo'] = []; What's teh difference b/w these two formats? May 16, 2016 at 6:18
  • stackoverflow.com/questions/17772534/… and if you have any other question please post it as a question rather than discussing it in this comment thread . May 16, 2016 at 6:48
5

If reporting error is turned on then the code Shreya Shetty returns error - might be related to changes in Drupal 8 ... but it is related to objects not being able to be used as ints... the amended version would be:

namespace Drupal\dynamic_forms\Form;


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

  class test3 extends FormBase {

public function buildForm(array $form, FormStateInterface $form_state) {
    $form['description'] = array(
        '#markup' => '<div>'. t('This example shows an add-more and a remove-last button.').'</div>',
    );
    $i = 0;
    $name_field = $form_state->get('num_names');
    $form['#tree'] = TRUE;
    $form['names_fieldset'] = [
        '#type' => 'fieldset',
        '#title' => $this->t('People coming to picnic'),
        '#prefix' => '<div id="names-fieldset-wrapper">',
        '#suffix' => '</div>',
    ];
    if (empty($name_field)) {
        $name_field = $form_state->set('num_names', 1);
    }

    if ($form_state->get('num_names')>0) {
        $value = $form_state->get('num_names');
    }
    else {
        $value=1;
    }
        for ($i = 0; $i < $value; $i++) {
            $form['names_fieldset']['name'][$i] = [
                '#type' => 'textfield',
                '#title' => t('Name'),
            ];
        }

    $form['actions'] = [
        '#type' => 'actions',
    ];
    $form['names_fieldset']['actions']['add_name'] = [
        '#type' => 'submit',
        '#value' => t('Add one more'),
        '#submit' => array('::addOne'),
        '#ajax' => [
            'callback' => '::addmoreCallback',
            'wrapper' => 'names-fieldset-wrapper',
        ],
    ];
    //        if ($name_field > 1) {
    if ($value > 1) {
        $form['names_fieldset']['actions']['remove_name'] = [
            '#type' => 'submit',
            '#value' => t('Remove one'),
            '#submit' => array('::removeCallback'),
            '#ajax' => [
                'callback' => '::addmoreCallback',
                'wrapper' => 'names-fieldset-wrapper',
            ]
        ];
    }
    $form_state->setCached(FALSE);
    $form['actions']['submit'] = [
        '#type' => 'submit',
        '#value' => $this->t('Submit'),
    ];

    return $form;
}

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

public function addOne(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    $add_button = $name_field + 1;
    $form_state->set('num_names', $add_button);
    $form_state->setRebuild();
}

public function addmoreCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    return $form['names_fieldset'];
}

public function removeCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    if ($name_field > 1) {
        $remove_button = $name_field - 1;
        $form_state->set('num_names', $remove_button);
    }
    $form_state->setRebuild();
}

public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValue(array('names_fieldset', 'name'));

    $output = t('These people are coming to the picnic: @names', array(
            '@names' => implode(', ', $values),
        )
    );
    drupal_set_message($output);
}

}

Thought the amended version might help someone, especially newcomers... don't forget to say thanks to Shreya for original solution.

1

Another solution (use the get method instead of relying on the return value of the set method for $form_state):

if (empty($name_field)) {
  $form_state->set('num_names', 1);
}
$name_field = $form_state->get('num_names');
for ($i = 0; $i < $name_field; $i++) {
  $form['names_fieldset']['name'][$i] = [
    '#type' => 'textfield',
    '#title' => t('Name'),
  ];
}
1

For Drupal 8:

namespace Drupal\form_api_example\Form;

use Drupal\Core\Form\FormStateInterface;

/**
 * Implements the ajax demo form controller.
 *
 * This example demonstrates using ajax callbacks to add people's names to a
 * list of picnic attendees.
 *
 * @see \Drupal\Core\Form\FormBase
 * @see \Drupal\Core\Form\ConfigFormBase
 */
class AjaxAddMore {

  /**
   * Form with 'add more' and 'remove' buttons.
   *
   * This example shows a button to "add more" - add another textfield, and
   * the corresponding "remove" button.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('This example shows an add-more and a remove-last button.'),
    ];

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

    $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_names; $i++) {
      $form['names_fieldset']['name'][$i] = [
        '#type' => 'textfield',
        '#title' => $this->t('Name'),
      ];
    }

    $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',
      ],
    ];
    // If there is more than one name, add the remove button.
    if ($num_names > 1) {
      $form['names_fieldset']['actions']['remove_name'] = [
        '#type' => 'submit',
        '#value' => $this->t('Remove one'),
        '#submit' => ['::removeCallback'],
        '#ajax' => [
          'callback' => '::addmoreCallback',
          'wrapper' => 'names-fieldset-wrapper',
        ],
      ];
    }
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'form_api_example_ajax_addmore';
  }

  /**
   * 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) {
    $name_field = $form_state->get('num_names');
    $add_button = $name_field + 1;
    $form_state->set('num_names', $add_button);
    // Since our buildForm() method relies on the value of 'num_names' to
    // generate 'name' form elements, we have to tell the form to rebuild. If we
    // don't do this, the form builder will not call buildForm().
    $form_state->setRebuild();
  }

  /**
   * Submit handler for the "remove one" button.
   *
   * Decrements the max counter and causes a form rebuild.
   */
  public function removeCallback(array &$form, FormStateInterface $form_state) {
    $name_field = $form_state->get('num_names');
    if ($name_field > 1) {
      $remove_button = $name_field - 1;
      $form_state->set('num_names', $remove_button);
    }
    // Since our buildForm() method relies on the value of 'num_names' to
    // generate 'name' form elements, we have to tell the form to rebuild. If we
    // don't do this, the form builder will not call buildForm().
    $form_state->setRebuild();
  }

  /**
   * Final submit handler.
   *
   * Reports what values were finally set.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValue(['names_fieldset', 'name']);

    $output = $this->t('These people are coming to the picnic: @names', [
      '@names' => implode(', ', $values),
    ]
    );
    $this->messenger()->addMessage($output);
  }
}

Source: https://api.drupal.org/api/examples/form_api_example%21src%21Form%21AjaxAddMore.php/class/AjaxAddMore/8.x-1.x

0

Tri this add more field with remove button in custom form, I have updated. and work for me.

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

  public function buildForm(array $form, FormStateInterface $form_state) {
    // Build your form here.
    // Add a wrapper for dynamic fields.
    $form['field_wrapper'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'field-wrapper'],
      '#tree' => TRUE, // Separate values for each field.
    ];

    // Add an "Add Field" button.
    $form['add_field'] = [
      '#type' => 'submit',
      '#value' => t('Add Field'),
      '#submit' => ['::addField'],
      '#ajax' => [
        'callback' => '::addFieldsAjaxCallback',
        'wrapper' => 'field-wrapper',
      ],
    ];

    // Check if there are fields in the form_state and add them.
    $fields = $form_state->get('num_fields') ?: 1;
    for ($i = 0; $i < $fields; $i++) {
      $form['field_wrapper'][$i] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['field-container']],
      ];
      $form['field_wrapper'][$i]['field'] = [
        '#type' => 'textfield',
        '#title' => t('Field @index', ['@index' => $i + 1]),
        '#default_value' => $form_state->getValue(['field_wrapper', $i, 'field']),
      ];

      // Add a "Remove" button for each field.
      $form['field_wrapper'][$i]['remove_field'] = [
        '#type' => 'submit',
        '#value' => t('Remove Field'),
        '#submit' => ['::removeField'],
        '#name' => 'remove_field_' . $i,
        '#ajax' => [
          'callback' => '::addFieldsAjaxCallback',
          'wrapper' => 'field-wrapper',
        ],
        '#attributes' => ['class' => ['remove-field-button']],
        '#field_index' => $i, // Pass field index to submit handler.
        '#limit_validation_errors' => [
          ['field_wrapper', $i, 'field'],
        ],
      ];

      // Use #prefix and #suffix to hide or show the field based on removal.
      if ($form_state->get(['field_wrapper', $i, 'field_removed'])) {
        $form['field_wrapper'][$i]['#access'] = FALSE;
      }
    }

    return $form;
  }

  public function addField(array &$form, FormStateInterface $form_state) {
    $fields = $form_state->get('num_fields') ?: 1;
    $form_state->set('num_fields', $fields + 1);
    $form_state->setRebuild();
  }

  public function removeField(array &$form, FormStateInterface $form_state) {
    $triggering_element = $form_state->getTriggeringElement();
    $field_index = $triggering_element['#field_index'];

    // Debugging: Check if removeField is called and gets the correct field index.
    \Drupal::logger('my_custom_module')->debug('Remove field called for index: ' . $field_index);

    // Mark the field as removed in the form state.
    $form_state->set(['field_wrapper', $field_index, 'field_removed'], TRUE);

    // Rebuild the form.
    $form_state->setRebuild();
  }

  public function addFieldsAjaxCallback(array &$form, FormStateInterface $form_state) {
    return $form['field_wrapper'];
  }

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.