AJAX callback on form item generated by AJAX

I'm trying to add AJAX callback to form element that was previously rendered by AJAX but with no luck. Code below.

A bit of description. There are three main methods here:

  1. buildForm - base form
  2. extraField - AJAX callback attached to ['month']
  3. choosePerson - AJAX callback attached to: ['week_day']['hour']

In the buildForm method I create base form with AJAX callback on 'month' select element. In the AJAX callback you can find an extraField method. This method contains form element called 'hour' ($form['week_day']['hour']) and this element renders well. But this element also contains AJAX callback, which doesn't work, it does not generate 'person_container' element and the 'person' select.

Any idea how to ajaxify elements that previously were rendered by ajax?

class ReservationForm extends FormBase {

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

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

    $form['month'] = array(
      '#type' => 'select',
      '#title' => $this->t('Select month'),
      '#options' => array($this->t('January'), $this->t('February'), $this->t('March')),
      '#ajax' => array(
        'callback' => array($this, 'extraField'),
        'event' => 'change',
        'wrapper' => 'week-day',
      ),
    );

    // Disable caching on this form.
    $form_state->setCached(FALSE);

    $form['week_day'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'week-day'],
    ];
  }

  public function extraField(array &$form, FormStateInterface $form_state) {
    $month = $form_state->getValue('month');

    $hour_options = array();

    if ($month == '0') {
      $hour_options[0] = $this->t('08:00');
      $hour_options[1] = $this->t('09:00');
      $hour_options[2] = $this->t('10:00');
    }

    if ($month == '1') {
      $hour_options[0] = $this->t('12:00');
      $hour_options[1] = $this->t('13:00');
      $hour_options[2] = $this->t('14:00');
    }

    if ($month == '2') {
      $hour_options[0] = $this->t('18:00');
      $hour_options[1] = $this->t('19:00');
      $hour_options[2] = $this->t('20:00');
    }

    $form['week_day']['hour'] = array(
      '#type' => 'select',
      '#title' => $this->t('Choose hours @month', array('@month' => $month)),
      '#options' => $hour_options,
      '#ajax' => array(
        // tried with that callback too: [ $this, 'choosePerson']
        'callback' => array($this, 'choosePerson'),
        'event' => 'change',
        'wrapper' => 'person',
      ),
    );

    $form['week_day']['person_container'] = array(
      '#type' => 'container',
      '#attributes' => ['id' => 'person-container'],
    );

    $form['week_day']['person_container']['person'] = array(
      '#type' => 'select',
      '#title' => $this->t('Choose person'),
      '#options' => ['John', 'Sarah', 'Peter'],
    );

    return $form['week_day'];
  }

  public function choosePerson(array &$form, FormStateInterface $form_state) {
    $form['week_day']['person_container']['person'] = array(
      '#type' => 'select',
      '#title' => $this->t('Choose person'),
      '#options' => ['John', 'Sarah', 'Peter'],
    );

   return $form['week_day']['person_container'];
  }
}  

Answers 2

  • The Link 4k4 posted in the discussion of the question gives the solution:

    Create a callback function (named by the #ajax['callback']). This is generally a very simple function which does nothing but select and return the portion of the form that is to be replaced on the original page. Note, that as part of the Form APIs security system, you cannot create new form elements in the callback function, as they will throw errors upon submission, and any #ajax on elements created in the callback will also not work. If you need to create new elements on ajax submit, they must be added in the form definition. You can use the values in $form_state to determine whether the form build process is the initial form load, or an #ajax initiated load.

    So the following code example is working.

    class ReservationForm extends FormBase {
    
      public function getFormId() {
        return 'reservation_form';
      }
    
      public function buildForm(array $form, FormStateInterface $form_state) {
        $form['month'] = [
          '#type' => 'select',
          '#title' => $this->t('Select month'),
          '#options' => [
            $this->t('January'),
            $this->t('February'),
            $this->t('March'),
          ],
          '#ajax' => [
            'callback' => [$this, 'extraField'],
            'event' => 'change',
            'wrapper' => 'week-day',
          ],
        ];
    
        // Disable caching on this form.
        $form_state->setCached(FALSE);
    
        $form['week_day'] = [
          '#type' => 'container',
          '#attributes' => ['id' => 'week-day'],
        ];
    
        if ($form_state->getUserInput()['_triggering_element_name'] == 'month') {
          $month = $form_state->getValue('month');
          $hour_options = [];
    
          if ($month == '0') {
            $hour_options[0] = $this->t('08:00');
            $hour_options[1] = $this->t('09:00');
            $hour_options[2] = $this->t('10:00');
          }
    
          if ($month == '1') {
            $hour_options[0] = $this->t('12:00');
            $hour_options[1] = $this->t('13:00');
            $hour_options[2] = $this->t('14:00');
          }
    
          if ($month == '2') {
            $hour_options[0] = $this->t('18:00');
            $hour_options[1] = $this->t('19:00');
            $hour_options[2] = $this->t('20:00');
          }
    
          $form['week_day']['hour'] = [
            '#type' => 'select',
            '#title' => $this->t('Choose hours @month', ['@month' => $month]),
            '#options' => $hour_options,
            '#ajax' => [
              'callback' => [$this, 'choosePerson'],
              'event' => 'change',
              'wrapper' => 'person-container',
            ],
          ];
    
          $form['week_day']['person_container'] = [
            '#type' => 'container',
            '#attributes' => ['id' => 'person-container'],
          ];
    
          $form['week_day']['person_container']['person'] = [
            '#type' => 'select',
            '#title' => $this->t('Choose person'),
            '#options' => ['John', 'Sarah', 'Peter'],
          ];
        }
    
        if ($form_state->getUserInput()['_triggering_element_name'] == 'hour') {
          $form['week_day']['person_container']['person'] = [
            '#type' => 'select',
            '#title' => $this->t('Choose person'),
            '#options' => ['Hans', 'Frank', 'Emma'],
          ];
        }
    
        return $form;
      }
    
      public function submitForm(array &$form, FormStateInterface $form_state) {
      }
    
      public function extraField(array &$form, FormStateInterface $form_state) {
        return $form['week_day'];
      }
    
      public function choosePerson(array &$form, FormStateInterface $form_state) {
        return $form['week_day']['person_container'];
      }
    
    }
    

  • You can't change elements on callback or validateForm(), you need to put it on buildForm() method.

    Here is a full example:

        /method buildForm
        $result = \Drupal\my_module\Controller\Contaminante::list(); //database logic
        foreach ($result as $it){
            $arrSelectContaminantes[$it["id_contaminante"]] = t($it["descripcion"]);
        }
    
        $form['select_contaminantes'] = array(
            '#type' => 'select',
            '#options' => $arrSelectContaminantes,
            '#ajax' => [
                'callback' => '::seleccionContaminantes',
                'disable-refocus' => false,
                'event' => 'change',
                'wrapper' => 'el-select-estaciones',
                'method' => 'replaceWith'
            ]
        );
    
        $selected_value = $form_state->getValue('select_contaminantes');
        $opciones = array();
    
        if(isset(selected_value)){
             $resultado;
             if($form_state->getValue("select_contaminantes") != 0){
                $opciones["0"] = t("ALL");
                $resultado = \Drupal\montar_mapa\Controller\Estacion::lista($form_state->getValue("select_contaminantes")); //DB LOGIC
            }else{
                $resultado = \Drupal\montar_mapa\Controller\Estacion::lista(); //DB LOGIC
            }
    
            foreach ($resultado as $it){
                $opciones[$it["id"]] = t($it["nombre"]);
            }
        }
    
        $form['select_estaciones'] = array(
            '#type' => 'select',
            '#id' => "el-select-estaciones",
            '#options' => $opciones,
            '#empty_option' => $this->t('-Select contaminante first'),
            '#validated' => TRUE,
        );
    

    Really important:

    Each time you print the dependent field ($form['select_estaciones') in example), must be validated ('#validated' => TRUE in options)

    You need to compare the selected value of triggering field ($form['select_contaminante] in example) if is null or not, if you compare directly, in case you get 0 value, interpretates as "no selection".


Related Questions