Problem/Motivation

When is used the ajax api for alter the property #options of a field with the type "checkboxes", the changes aren't reflected.

Steps to reproduce it:

A field with an #ajax property, and a field with type checkboxes.

$form['target'] = [
      '#type' => 'select',
      '#title' => t('Test'),
      '#options' => ["option1" => "option 1", "option2" => "option 2"],
      '#ajax' => [
        'callback' => [$this, 'updateCheckboxes'],
        'wrapper' => 'edit-options',
        'method' => 'replace',
        'event' => 'change',
      ],
];

 $form['test'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Options'),
      '#options' => ["test1" => "Test 1", "test2" => "Test2],
      '#prefix' => '<div id="edit-options">',
      '#suffix' => '</div>',
    ];

The callback for modify the #options property

  public function updateCheckboxes($form, FormStateInterface $form_state) {
    $form['test']['#options'] = ["test3" => "Test 3", "test4" => "Test4"];
    return $form['test'];
  }

When is triggered the javascript event the result will be the old values.

Internally the checkboxes type field render the #options list into fields with the type "checkbox" and when the ajax callback return the field the #options property is ignored and render old values.

Comments

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Uncle_Drewbie’s picture

I'm on 8.5.3, facing the same issue exactly. @gnuget did you ever find a solution?

sgurlt’s picture

Yep same here, D8.5.3.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andrezstar’s picture

Same here D8.5

maseyuk’s picture

Same here I've just run into this too. It looks like each options is in its own section in the array e.g:
$form['test'][0]
$form['test'][1]

So it looks like setting the options has no effect because the form is now using these renderable elements. e.g.:
$form['test'][0] = ['#type' => 'checkbox'..... ]

So looks something needs to be triggered to rebuild those options but unfortunately I cant see what. And manually recreating them isn't really an option as all the labels and input attributes are all lost if you try and recreate them with:
$form['test'][0] = ['#type' => 'checkbox'..... ]

gcb’s picture

I was able to get this to work by fighting really hard. Here's a work-around, which I think makes it pretty clear that this needs a fix. I'm doing this in a custom ajax callback ($form['my_field']['#ajax'] = `_my_custom_callback`:

use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Checkboxes;

function _my_custom_callback(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $renderer = \Drupal::service('renderer');
  foreach (Element::children($form['my_field']) as $i) {
     unset($form['my_field'][$i]);
  }
  Checkboxes::processCheckboxes($form['my_field'], $form_state, $form);
  $response->addCommand(new ReplaceCommand("#my_field_id", $renderer->render($form['my_field'])));
  return $response;
}
bdimaggio’s picture

I discovered that with the above code, my options were indeed renewed to match what I had put into #options, but if they had been checked--either when the form was initially built or by the user in the time since then--those defaults were lost once the ajax callback had rebuilt the field. Using FormBuilder->doBuildForm() instead of Checkboxes::processCheckboxes()addressed that.

use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Checkboxes;

function _my_custom_callback(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $renderer = \Drupal::service('renderer');
  foreach (Element::children($form['my_field']) as $i) {
     unset($form['my_field'][$i]);
  }
  \Drupal::formBuilder()->doBuildForm($form['#form_id'], $field, $form_state);
  $response->addCommand(new ReplaceCommand("#my_field_id", $renderer->render($form['my_field'])));
  return $response;
}
sgurlt’s picture

@ #12

May you please post your full code as an example ? :)
When I am trying to use your code, getValue() on the checkbox field is empty.

bdimaggio’s picture

Hey @sgurlt - sure! Hope this helps.

use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Checkboxes;


/**
 * Implements hook_form_alter().
 */
function mymodule_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  if ($form_id == 'the_form_id_i_care_about') {
    // This is a multi-value field with a "check boxes/radio buttons" widget.
    $form['my_field']['widget']['#ajax'] = [
      'callback' => '_my_custom_callback',
    ];
  }
}

function _my_custom_callback(array &$form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $renderer = \Drupal::service('renderer');
  foreach (Element::children($form['my_field']) as $i) {
     unset($form['my_field'][$i]);
  }
  \Drupal::formBuilder()->doBuildForm($form['#form_id'], $field, $form_state);
  $response->addCommand(new ReplaceCommand("#my_field_id", $renderer->render($form['my_field'])));
  return $response;
}
tetranz’s picture

I think you might be going about this the wrong way.

My experience with D8 ajax forms has lead me to stick with several strict rules. Two of them are:

The only good place to build or modify the form is in the buildForm method. Don't do that anywhere else.
The ajax callback function should do nothing but return the form or a part of it. Don't do anything else in the callback.

Violating those is the path to pain and frustration :)

For the example discussed here, you can achieve what you want by doing something like this in buildForm.

    if ($form_state->getValue('target') === 'option2') {
      $options = ['test3' => 'Test 3', 'test4' => 'Test4'];
    }
    else {
      $options = ['test1' => 'Test 1', 'test2' => 'Test2'];
    }

    $form['test'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Options'),
      '#options' => $options,
      '#prefix' => '<div id="edit-options">',
      '#suffix' => '</div>',
    ];

That works with a triggering element (i.e., target here) which is not a submit button. When the ajax happens, validateForm is called and then the form is rebuilt so that inserted code knows the new value of target.

To add some unsolicited advice:
I tend to use submit buttons as the triggering element where possible (they can be styled to look like links). That's obviously not always possible like in this case but when a submit button is used a custom submit callback should be added with #submit.

In the custom submit callback, my technique is to set flags or whatever in $form_state->set(), and then call $form_state->setRebuild(). The data you set using $form_state->set() is available in buildForm with $form_state->get(). Use that to dynamically modify the form.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

anthonyf’s picture

I ran into the same issue today, trying to change options on a checkboxes element in an Ajax callback. @tetranz, your advice rings a bell, and I think it was the same situation in D7, but I'd forgotten how it worked. For me the uncertainty came from not knowing that it bounces back into the buildForm method while processing the Ajax call. Sure enough, when stepping with the debugger I can see it doing that. The one thing I would add to your example is that in the Ajax callback method you would just return $form['test']. In my testing that worked for rebuilding the checkboxes form element.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

ryanrobinson_wlu’s picture

I'm having a similar problem except that it is a dropdown select instead of checkboxes. Drupal 9.5.9, PHP 8.0. I have one select field that has an AJAX callback which restricts the allowed options of another dropdown field. It will work once, showing the message that it is running and then updating the values of the second field. But then if I try to change the first field again, it will show the message that it is running and afterward the options of the second field have not changed at all.

I tried adding a couple of the options mentioned above that did not help:

\Drupal::formBuilder()->doBuildForm($form['#form_id'], $field, $form_state);

and

\Drupal\Core\Render\Element\Select::processSelect($element,$form_state,$form);

Here's some more context of the code:

class ReservoBookingForm extends ContentEntityForm {

  /** AJAX fired function to determine allowed values of end time */
  public function updateEndTimes(array &$form, FormStateInterface $form_state) {
    $element = $form['booking_end_time'];
    $options = [different complicated function to get valid options];
    $element['widget']['#options'] = $options;
    $element['widget']['#empty_option'] = [different function that returns different text whether there are any remaining options or not]
    /* Note: empty option requires patch for a bug discussed here: https://www.drupal.org/project/drupal/issues/3180011 */ 
    return $element;

  }

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

    $form['booking_start_time']['widget']['#ajax'] = [
      'wrapper' => 'edit-booking-end-time-wrapper',
      'callback' => [$this, 'updateEndTimes'],
      'event' => 'change',
      'progress' => [
        'type' => 'throbber',
        'message' => $this->t('Checking available end times'),
      ],
    ];

    return $form;

  }

}

Version: 9.5.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.