Dynamic addition of fields using ajax in Drupal custom form

Here we are discussing how to add infinite number fields in a custom form using Ajax, Also we can remove the specific fields added in form.

We are creating a custom form in path –  /src/Form/AddRemoveForm.php

Here we are going to create a form as below  which has field set  as students joining and we can add first name fields as many we need for each student  by click on Add one more button. Finally our form will be looking as below.

We have to create a form with storing value for number of lines and to keep which line need to be removed.

So in build function of form we are keeping one variable  num_lines to track number of lines, this variable will be incremented when add one more button clicked. 

One more variable we have is an array removed_fields. This variable keeps array of removed lines indexes to omit  field lines   removed while rendering form.We can create this form in three steps.

  1. Create form build function with for loop for rendering fields
  1. Add one more button call back for increment num_lines variable 
  1. Remove button to populate remove_fields array

1.Create form build function with for loop for rendering fields

In build function first get the num_lines value set or not. If it not null, save to the $num_lines variable.

// 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');
   }

If $num_lines is null,  set value to 1 , this make sure one name fields available in form. This $num_lines variable we will use in for loop to display first name field.

Next in our build function get the value from  removed_fields, if it is null, initialize with an array.

// 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', []);
     $removed_fields = $form_state->get('removed_fields');
   }

So $removed_fields will be having indexes of the removed lines, that we will use in for loop to omit those lines.

Next in build function, create fields set as below.

$form['names_fieldset'] = [
     '#type' => 'fieldset',
     '#title' => $this->t('Students joining'),
     '#prefix' => '<div id="names-fieldset-wrapper">',
     '#suffix' => '</div>',
   ];

Here prefix div is important, we will later using this div as wrapper in our ajax Add and remove calls.

Above field set is a common field set. In below for loop we are creating dynamically creating a student fields set with first name field as below.

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 student
      * where we can add first  name
      */
     // Fieldset title.
     $form['names_fieldset'][$i] = [
       '#type' => 'fieldset',
       '#title' => $this->t('Student') . ' ' . ($i + 1),
     ];
     // Date.
     $form['names_fieldset'][$i]['firstname'] = [
       '#type' => 'textfield',
       '#title' => $this->t('First name'),
     ];


$form['names_fieldset'][$i]['actions'] = [
       '#type' => 'submit',
       '#value' => $this->t('Remove'),
       '#name' => $i,
       '#submit' => ['::removeCallback'],
       '#ajax' => [
         'callback' => '::addmoreCallback',
         'wrapper' => 'names-fieldset-wrapper',
       ],
     ];
   }

So this for loop print the student fields set with First name field based on count in $num_lines.

Also skips the index if $i values indexes we stored in $removed_fields, so that removed student fields set is not going to display.

  2. Remove button to populate remove_fields array

At the last of the for loop on same student field set, we are keeping remove button so that we can remove each line item.

$form['names_fieldset'][$i]['actions'] = [
       '#type' => 'submit',
       '#value' => $this->t('Remove'),
       '#name' => $i,
       '#submit' => ['::removeCallback'],
       '#ajax' => [
         'callback' => '::addmoreCallback',
         'wrapper' => 'names-fieldset-wrapper',
       ],
     ];

Here you can see two call backs, on submit action,  removeCallback where we are going to unset the the selected line item and addmoreCallback just return the field set , this is used with the both add and remove buttons.

So implement removeCallback as  below.

/**
  * 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();
 }

So mentioned in above code comments, we are first getting name element where this remove click triggered, then removing that line item and again setting the names_fieldset. We have to keep names_fieldset first in a variable before removing, then set again names_field set again after removing element.

Finally save removed lines indexes in removed_fields with other removed indexes. Then rebuild the form.

3. Add one more button call back for increment num_lines variable

Add below Add more and submit button at end of form build function.

$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'),
   ];

Add below callback for add more button outside form build function.

/**
  * 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();
 }

Here we are incrementing num_lines value and the rebuild the form.

So finally we are having a form where we can add multiple fields using ajax using add more button.

Download complete source code here.

Get Free E-book
Get a free Ebook on Drupal 8 -theme tutorial
I agree to have my personal information transfered to MailChimp ( more information )
if you like this article Buy me a Coffee this will be an ispiration for me to write articles like this.

You may also like...