2

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'];
  }
}  
4
  • I have the same problem, is there a solution for that?
    – schneidolf
    Commented Sep 7, 2017 at 8:34
  • 1
    @Wolf, there are hundreds of these questions on DA, you just picked one. Not all are answered, because there are so many. You can't build form elements in an ajax callback. See Ajax Forms in Drupal 7, which is still valid for D8.
    – 4uk4
    Commented Sep 7, 2017 at 13:39
  • Thank you, for the link, now I got it. I will post the right solution in an answer.
    – schneidolf
    Commented Sep 8, 2017 at 6:26
  • @schneidolf can you share the link for the answer where you've shared your findings? Commented Aug 5, 2019 at 20:19

2 Answers 2

9

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'];
  }

}
4
  • Ok - my answer lacked the callback and show hide part. Was a quicky ;)
    – rémy
    Commented Sep 8, 2017 at 11:45
  • For the triggering element name see drupal.stackexchange.com/a/223290/47547
    – 4uk4
    Commented Jun 30, 2019 at 6:37
  • This is almost working for me. The only problem was the week_day field is not updated with new value. But on the xhr it return correct html, the selector was empty. What did I miss?
    – Permana
    Commented May 20, 2021 at 14:26
  • I forgot to make the callback function as static, but the problem now is I can't submit the form, it always return 'The value you selected is not a valid choice.' error.
    – Permana
    Commented May 20, 2021 at 15:04
1

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".

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.