0

In this snippet, located under src/Form/DocumentCreationForm.php, I create an AJAX callback that adds more section elements to the form when the add section button is clicked.
Now I'm trying to read those values in submitForm or validateForm.

I read somewhere (sorry, I can't remember where) that it was impossible to submit AJAX-added form elements via submit Form, so I'm thinking I might have to intercept the submit button, add a Controller and submit the form to the controller via javascript when the submit button is pushed.

Does anyone know of a better way?

<?php

namespace Drupal\mc_documents\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\cloud\CloudAdapter;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Url;

class DocumentCreationForm extends FormBase {

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

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, $param1=NULL){
    // $form['attached']['library'][] = 'mc_documents/mc_documents';

    // ... populate the form here. see https://api.drupal.org/api/drupal/elements for options
    $form_state->set('num_sections', 0);

    $form['document_name'] = [
      '#weight' => 0,
      '#title' => t('Document Name'),
      '#type' => 'textfield',
      '#attributes' => [
        'id' => 'document_name'
      ]
    ];

    $cloud = new CloudAdapter();

    $document_types = $cloud->query(
      "select * from MC_Selection_V where Category = 'MC_Documents_Document_Type';",
      []
    );

    $formatted_document_types = [];
    $len = count($document_types);
    for($i = 0; $i < $len; $i++){
      $formatted_document_types[$document_types[$i]['Value']] = $document_types[$i]['Caption'];
    }

    $form['document_type'] = [
      '#weight' => 1,
      '#title' => t('Document Type'),
      '#type' => 'select',
      '#options' => $formatted_document_types
    ];

    $form['sections'] = [
      '#weight' => 2,
      '#type' => 'container',
      '#title' => t('Sections'),
      '#attributes' => [
        'id' => ['sections']
      ]
    ];

    $form['add_section'] = [
      '#weight' => 3,
      '#value' => t('Add Section'),
      '#type' => 'button',
      '#ajax' => [
        'callback' => '::addSection',
        'event' => 'click',
        'wrapper' => 'sections'
      ],
    ];

    $form['submit'] = [
      '#weight' => 4,
      '#type' => 'submit',
      '#value' => t('Submit'),
      '#attributes' => [
        // 'onclick' => 'function(){ submit_document(); return false; }'
      ],
    ];

    return $form;
  }

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

    $form_state->setValue('num_sections', $form_state->getValue('num_sections') + 1);

    $cloud = new CloudAdapter();
    $field_types = $cloud->query(
      "select * from MC_Selection_V where Category = 'MC_Documents_Field_Type';",
      []
    );
    $formatted_field_types = [];
    $len = count($field_types);
    for($i = 0; $i < $len; $i++){
      $field_type = $field_types[$i];
      $formatted_field_types[$field_type['Value']] = $field_type['Caption'];
    }

    $elems = [
      [
        '#type' => 'html_tag',
        '#tag' => 'hr'
      ],
      [
        '#type' => 'select',
        '#title' => t('Field Type'),
        '#options' => $formatted_field_types,
        '#attributes' => [
          'class' => ['field-type']
        ]
      ],
      [
        '#type' => 'textarea',
        '#title' => t('Section'),
        '#placeholder' => 'Enter a placeholder here...',
        '#attributes' => [
          'class' => 'section-markup'
        ],
        '#value' => ''
      ]
    ];

    $renderer = \Drupal::service('renderer');
    $response = new AjaxResponse();
    $response->addCommand(new AppendCommand('#sections', $renderer->render($elems)));
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state){

  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state){
    drupal_set_message($form_state->getValues('sections'));
    // insert document
    // insert document field
    // insert document field data
  }
}

?>

1 Answer 1

1

You can't create form elements in an ajax callback and can't modify $form_state either.

This is a working example for the same task, which creates the form elements in buildForm() and modifies $form_state in submit handlers:

<?php

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 extends DemoBase {

  /**
   * 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://www.drupal.org/project/examples

2
  • Is there a reason both the add_name and remove_name buttons specify the addmoreCallback instead of a separate add- and remove- callback? Commented Oct 22, 2018 at 20:04
  • The ajax callback is the same for both buttons, because it returns only the changed wrapper part of the form build. The submit handlers are different though, the add button increments and the remove button decrements the number of elements.
    – 4uk4
    Commented Oct 22, 2018 at 20:36

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.