Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
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
Comment #6
Uncle_Drewbie CreditAttribution: Uncle_Drewbie commentedI'm on 8.5.3, facing the same issue exactly. @gnuget did you ever find a solution?
Comment #7
sgurlt CreditAttribution: sgurlt as a volunteer and at Bright Solutions GmbH commentedYep same here, D8.5.3.
Comment #9
andrezstar CreditAttribution: andrezstar commentedSame here D8.5
Comment #10
maseyuk CreditAttribution: maseyuk commentedSame 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'..... ]
Comment #11
gcbI 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`
:Comment #12
bdimaggioI 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. UsingFormBuilder->doBuildForm()
instead ofCheckboxes::processCheckboxes()
addressed that.Comment #13
sgurlt CreditAttribution: sgurlt as a volunteer and at Bright Solutions GmbH commented@ #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.
Comment #14
bdimaggioHey @sgurlt - sure! Hope this helps.
Comment #15
tetranz CreditAttribution: tetranz at Third and Grove commentedI 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.
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.
Comment #18
anthonyf CreditAttribution: anthonyf at Pegasystems commentedI 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.Comment #23
ryanrobinson_wlu CreditAttribution: ryanrobinson_wlu commentedI'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: