Problem/Motivation
If a field having states is depended on a field loaded in or modified in Ajax, the the states has no effect on the field when the Ajax is loaded.
Steps to reproduce
1. Create with form with Ajax and states as below.
$form['js_states_test'] = [
'#type' => 'details',
'#open' => TRUE,
'#prefix' => '<div id="js_states_test_wrapper">',
'#suffix' => '</div>',
];
$form['js_states_test']['reload'] = [
'#type' => 'checkbox',
'#title' => 'Check me to reload Select Field field',
'#ajax' => [
'callback' => '::buildAjax',
'wrapper' => 'js_states_test_wrapper',
],
];
$form['js_states_test']['select_field'] = [
'#type' => 'select',
'#title' => 'Select Field',
'#options' => [0 => 0, 1 => 1],
'#default_value' => 0,
];
$form['js_select_field_textfield'] = [
'#type' => 'select',
'#title' => 'Select should show when 1 is selected in select_field after ajax',
'#states' => [
'visible' => [
':input[name="select_field"]' => ['value' => 1],
],
],
];
2. Click on "Check me to reload Select Field field" checkbox
3. Select value 1 for "Select field"
4. 'Select should show when 1 is selected in select_field after ajax' is not visible
Proposed resolution
Detach the states semaphore on unload.
Original report by alexander tallqvist
After upgrading from Drupal 10.2.7 to 10.3.2, I encountered an issue with conditional field visibility in my project. I’ve implemented conditional visibility using form #states, where certain text fields should only be displayed once specific entity reference fields are filled. However, following the changes introduced in issue #3347144 or #3386191, these conditional behaviors no longer function as expected. Now, when an entity reference field is populated, the dependent text field does not become visible.
I suspect the issue I’m experiencing is similar to the one described in #3416398, although that issue has already been closed.
Original Steps to reproduce
Create a content type and add both a text field and an entity reference field (in this case, the entity reference field is used to reference Media).
Develop a custom module and use the field_widget_single_element_form_alter hook in the .module file to add a visibility condition to the text field. For this example, the text field has the machine name field_image_caption_short, and the entity reference field is field_main_media.
function test_conditional_fields_field_widget_single_element_form_alter(&$element, &$form_state, $context): void {
$field_definition = $context['items']->getFieldDefinition();
$field_name = $field_definition->getName();
if ($field_name !== 'field_image_caption_short') {
return;
}
// Create a selector for the "depended_field" field.
$fieldSelector = sprintf(':input[name="%s[%s]"]', 'field_main_media', 'target_id');
// Alter the widget state.
$element['value']['#states'] = [
'visible' => [
$fieldSelector => ['filled' => TRUE],
],
];
}
Confirm that the element names are correct, as the state change should work under these conditions. Notably, the visibility state functions correctly when the text field depends on another text field, rather than an entity reference field.
Original Proposed resolution
After further investigation, I found that the introduction of the once() function in issues #3347144 and #3386191 appears to be the root cause of the problem. To address this, I re-rolled patch #5, which was originally created for #3416398, to ensure compatibility with Drupal 10.3.2. Applying this updated patch resolved the issue for me.
| Comment | File | Size | Author |
|---|---|---|---|
| #26 | 3468860-nr-bot.txt | 2.62 KB | needs-review-queue-bot |
Issue fork drupal-3468860
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 11.x
changes, plain diff MR !9300
- 11.x-states-api-alt
changes, plain diff MR !12091
- 3468860-js-states-behavior
compare
Comments
Comment #2
alexander tallqvist commentedAdding the mentioned patch.
Comment #3
yuvaniaHello alexander tallqvist , I tested the patch, and it worked for me.
Comment #4
yuvaniaComment #5
nod_The main issue here is that we do not have a detach method for the states script, which causes the various issues we can see here and the linked issue.
Haven't looked in details what's needed but we might need some extensive refactor to make everything work. First step here would be to make a failing test showing the issue so that we're sure we don't break it again. We have pretty extensive test coverage for this functionality so refactor will be safe as long as the tests are green.
Comment #6
sukr_s commented@alexander tallqvist I'm unable to reproduce the problem. I tried with Entity reference - media and with media reference. With entity reference it worked fine, though I had to make changes in the alter function. The field selector needed to be
field_main_media[0][target_id]instead offield_main_media[target_id]With media reference, it did not work. The field selector generated with target_id doesn't exists.
Can you share some more details on how to reproduce the problem.
Comment #7
nod_Comment #8
alexander tallqvist commentedHi @sukr_s. Turns out that my "Steps to reproduce" instructions were not thorough enough. In order to replicate the situation I'm experiencing, you need to have the
media_entity_browsermodule installed together with its dependencies. These were my exact steps today when I reproduces the issue:1. Install a fresh version of Drupal 10.3.2.
2. Enable the media and media_library modules that come with core.
3. Install and enable the media_entity_browser module together with its dependencies:
composer require 'drupal/media_entity_browser:^2.0'; drush en media_entity_browser.4. Disable the "Auto open entity browser" setting at:
/admin/config/content/entity_browser/media_entity_browser_modal/edit.5. Edit the article content type and add the previously mentioned fields:
field_image_caption_short(short text field) andfield_main_media(entity reference to media, bundle=image, limit=1).6. Edit the form display for the article content type and set the widget for the Main media field to: Entity browser -> Media entity browser (modal).
7. Create a custom module and add the previously posted hook to it.
8. Add an new article. Not that the
field_image_caption_shortis never displayed whether or not thefield_main_mediafield is referencing an image or not.9. Apply the patch I posted in #2 and repeat step 7. Everything should work as expected.
I'm guessing you we're trying to replicate my issue with the "Autocomplete" widget, which indeed seems to work as intended. Also, said widget formats the selector name to
field_main_media[0][target_id]instead offield_main_media[target_id], just as you said. I triple checked that the selector is indeedfield_main_media[target_id]when using the media entity browser. My guess is that anytime any kind of JS functionality is involved, the #states break because of the missing detach method.Comment #9
pbonnefoi commentedI had the problem with states visibility on a custom form (which was in a modal with ajax reload) and I confirm that the patch fixes my problem :-)
Comment #10
nod_The patch that exist is not the appropriate fix, it's broken in a different way :)
Comment #11
pbonnefoi commentedThanks for the info @nod_ any idea which fix to apply yet ?
Comment #13
sukr_s commentedComment #14
sukr_s commentedComment #15
pbonnefoi commentedWorks for me :-)
Comment #16
byacoubi commentedwith the patch it will work however between the version of drupal 10.3.0 and 10.3.1 there were quite a few changes to the files:
-state.php
-state.js
- ...
see the release note for 10.3.1 here
https://git.drupalcode.org/project/drupal/-/commit/8905837f693c4f0341dd9...
I agree with nod_ that you will have other problems later.
Comment #17
nod_Thanks for starting that code and writing a test! Haven't gone through in details but I can spot a few things already, see comments in MR.
Comment #18
sukr_s commentedchanges as per MR comments
Comment #19
tame4tex commentedThank you for your efforts so far but this doesn't completely fix the issue. I still have an issue with an element that is added by ajax and is the states dependee of an element that is not affected by ajax.
Here is a modified version of the form in the issue description that will highlight the bug.
Steps to reproduce
NOTE: This is a very simplified version of the problem we are experiencing. I know the "dependent_checkbox" doesn't have to be added via ajax and can easily be made visible via states based on "reload" and then there is no bug, but that is not possible in our situation. This is just an easy way to demonstrate the bug.
Proposed resolutions
In addition to the current MR !9300 changes which fixes #4 above, I suggest removing the dupal-once attribute if one or more of the dependees are not present in the DOM. This will ensure the states of that element get processed again when the DOM is updated.
Thoughts?
I will add suggested code as comments to the MR !9300.
Comment #20
tame4tex commentedI have updated the MR and added more comprehensive testing to highlight the ajax related bugs that still exist.
I moved the testing added to its own new method.
These new tests should cause the MR to fail as expected.
I have a working solution to fix all remaining issues that I am just doing final cleanup on and will commit and comment on shortly.
Comment #21
tame4tex commentedI have updated the MR and added to the great start by sukr_s. I was going to make code suggestions to the MR but there were a lot of changes so decided to commit to expedite things. I hope you don't mind.
All tests are now passing and its ready for review.
Summary of changes made to the MR:
statesObjectsproperty toprocessedDependeesto hopefully be more descriptive of what it is.states.Dependentfunction to keep track of any dependees not in the DOM so they can be rechecked on attach. Also ensured Dependents are re-evaluated if none of their dependees are in the DOM to ensure states are reprocessed when dependees are removed via ajax.Drupal.states.Dependent.destroywhere it was removing thestate:${dependeeStates[i]}event associated with other DependentsstatesObjectsproperty when states were reprocessed on ajaxDrupal.states.Trigger.destroywhere trigger related event listeners were not being removed.attachfunction to ensure states are reprocessed for dependee elements that are added to the DOM during ajaxComment #22
tame4tex commentedI found another bug.
If the ajax added dependee includes the form in its selector it doesn't work. It is completely valid for the dependee selector to include a form identifier if there are multiple forms on a page that have the same fields.
This bug is caused by using the
find()method on the context when checking if the dependee is now present in the dom. This won't work when the selector contains form because on ajax reload the form will be the context root .We need to instead use the jQuery Selector
$(), which seems valid given that is what is used to apply the event listeners and also what was used to determine if the element is in the dom in the first place.I have added testing for this bug and fixed it in the latest MR commits. But it looks like the changes has caused other failing tests. I'm investigating now.
Comment #23
tame4tex commentedAll failing tests are fixed. Good for review.
Comment #24
tame4tex commentedSo this fix has an unexpected affect in that the revision log message textfield is no longer displayed on node create forms.
I think what is happening with the fix is the states added to the
revision_log_messagein\Drupal\Core\Entity\ContentEntityForm::addRevisionableFormFields()is now being correctly handled because therevisioncheckbox is not present in the DOM so the field shouldn't be visible.I wonder if it was intended to be shown or if this is a bug that slipped through. I found this possible related issue https://www.drupal.org/project/drupal/issues/3096906. Either way it would need to be addressed before this fixed is merged in.
Would be great to get a maintainers input on this. Thanks!
Comment #25
nod_thanks for the work, just wanted to say that the issue is on my plate but it needs a good chunk of time to review. It's on my todo but can't say exactly when I'll have the focus to tackle it.
Comment #26
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #27
andre.bononI've got a similar situation with a webform where one field controls the visibility of another and it is replaced by an Ajax call. I tried out both Patch #2 and the MR, and I can confirm that the MR still hasn't fixed the issue, but Patch #2 has.
Drupal version: 10.3.1
Comment #28
mdsohaib4242 commentedComment #29
tame4tex commented@andre.bonon are you able to provide code example of your problem as I am trying to ascertain how it is different to the current test.
Unfortunately Patch #2 is not the correct solution due to #3347144: Form API #states property/states should use .once() to apply its rules (Can cause failures with BigPipe and possibly other situations) and as nod_ has explained in previous comments. Therefore we need to work on an alternate solution and getting more info on your problem would be extremely helpful.
@mdsohaib4242 was your comment for @andre.bonon or does it relate to the current MR in some way? Thanks!
Comment #30
tame4tex commentedUgh I have found another bug in the code.
The testing code is also becoming quite unwieldy. I am going to spend some time of the weekend to refactor the testing code and add testing and hopefully a fix for the new bug.
Comment #31
tame4tex commentedI was experiencing a bug where states were not applied to fields that are added via ajax when their trigger also targets another field that is not added via ajax.
I have added testing for this bug and a fix.
Back to Need Review!
Comment #32
sophie.skWhat a gnarly bug :D
Just to say this patch has applied cleanly to Drupal 10.3.10 and resolved our client's issue:
primary_reasonhas an Ajax callback that populates & returns thesecondary_reasonfield.additional_informationfield should show depending on the value chosen in thesecondary_reasonfield, determined using the#statesarray. This was not working.Now the patch is applied, the
additional_informationfield is correctly showing/hiding.Comment #33
smustgrave commentedJS test appears to consistently be failing, re ran twice.
Resolved the threads
Comment #34
dswier commentedApologies that I can't contribute more to this, but I at least want to report that the MR does not fix the issue in my case. We're dealing with the version of the issue that has been reported in the Webform module queue(https://www.drupal.org/project/webform/issues/3481569). In the meantime we implemented the patch that reverts the patch that broke all of this. Extremely frustrating that such a breaking issue has existed in core for so long. My team was stuck on 10.3.0 waiting for this to get fixed, and finally had to settle for a horrible revert just to get core up to date.
Comment #35
nod_Thanks for moving this forward @tame4tex, i checked the tests they make sense so +1 for them. Webform overrides some of the states API methods so any patch here will need an update on webform side
I replicated #3481569: Using a computed twig as a value for a visibility condition breaks in Drupal 10.3 and made a test locally and the MR fixes the issue. all good here.
For the JS I think it's solid, elements and event handlers are cleaned up so memory shouldn't leak. I wish we could make the code simpler but I don't have a better way (:
Comment #36
smustgrave commentedRan the test 3 times and the javascript fail each time. Moving to NW for that.
Comment #37
tame4tex commentedThanks for the review @sophie.sk, @nod_, @smustgrave!
@nod: I too wish the code could be simpler. I have a hunch there might be a simpler approach but I think it would require major refactor of the existing code. I think it best to commit a fix for these Ajax bugs with as minimal change to existing code as possible and then we could potentially revisit
states.jsas a whole in future issue.On that note, I have found another situation that is not correctly handled by the code which I have added a test and fix for.
If a trigger includes a form identifier, which would be necessary if multiples of the same form are on the page, states is no longer correctly applied when the trigger value is affected by Ajax. It was a simple fix in the detach method. The fix also highlighted an error in a test that needed to be corrected.
I will address the failing tests mentioned by @smustgrave in a separate comment as they are still failing and I am investigating.
Comment #38
tame4tex commentedSo the failing tests are similar to the
\Drupal\Core\Entity\ContentEntityForm::addRevisionableFormFields()issue mentioned in comment #24.The tests are failing because form elements in
\Drupal\migrate_drupal_ui\Form\CredentialFormare not visible when they are expected to be.In
\Drupal\migrate_drupal_ui\Form\CredentialFormnumerous elements have a visible states declared based on thesource_connectioninput having an empty value. Here is an example:This problem is the
source_connectioninput is not always present in the DOM due to its #access setting:With the new code to better handle ajax added and removed elements, if an element has visible states declared and the associated triggering element is not present in the DOM then the visible state is not applied. The
this.reevaluate();code at line 226 is the cause of this change.Previously, if the triggering element wasn't present, the states declaration was essentially ignored, so the element would always be visible.
The current fix feels more correct to me, in that for the state to be applied, the trigger element must first present in the DOM and then the trigger condition must be TRUE. Otherwise, the inverse state is applied. However, this is a change in states behaviour, so it would be good to get maintainer feedback.
I did try and refactor the code to ignore states if the triggering element wasn't present, but I was unable to find a solution that also correctly handled the states associated with a trigger element being added and removed via ajax.
In an attempt to keep this issue progressing forward I have refactored
\Drupal\migrate_drupal_ui\Form\CredentialFormto fix the failing tests and all tests are now passing.Comment #39
tame4tex commentedComment #40
mikelutzThe changes to the migration form make sense. The tests in Drupal\Tests\migrate_drupal_ui\FunctionalJavascript\SettingsTest all pass, so that looks good to me as a migration maintainer.
Comment #41
smustgrave commentedThanks @mikelutz for that
Checking the MR appears all threads are resolved
However when I run the test-only feature it passes when it should fail so not sure if the bug is being covered.
Comment #42
tame4tex commentedThanks @mikelutz and @smustgrave
It looks like the test only feature wasn't failing because of #3494332: Functional Javascript tests are silently skipped on Gitlab CI due to selenium standalone configuration.
I have rebased the MR and it is now correctly failing.
Comment #43
darvanenI applied the changes in MR9300 to a site running 10.4.3 and now an element that has both ajax and states dependees only runs its ajax handler once. I'm going to keep digging but if that sparks any ideas please let me know.
Comment #44
darvanenCommenting out line 624 of the adjusted states.js stops the ajax event from being removed.
Comment #45
darvanenI was unable to get the test form to repeat the behaviour I'm seeing - possibly because it uses a checkbox as the ajax trigger rather than a select element, however I did discover the cause of our issue. The trigger events needed to be namespaced so that other events weren't stripped off along with them.
I have pushed a very small amendment to that effect.
Comment #46
darvanenAh, got it.
Doing this to the test form does demonstrate the problem. wasn't sure if I should push the change though
Yes that means there is a dependent/dependee pair that share both ajax and states relationships. It comes into play (at least for us) when ajax is used to update large sections of form that can have portions which use state to manage visibility based on the ajax trigger. And the form in question *did* work in 10.2
Comment #47
seanbJust found an issue using this patch. When you have 2 forms on the same page, the states no longer seem to work.
When I remove the patch everything works as expected.
When I remove one of the forms, everything also works as expected.
I was not yet able to pinpoint the exact issue, but it seems to be caused by the patch I created from the MR.
Comment #48
smustgrave commentedSounds like not fully ready based on #47
Comment #49
nitesh624I don't see any issue on using the patch from MR having 2 forms on same page.
@seanb if possible can you add code snippet to demonstrate the issue.
Comment #50
seanbIt seems my issues were caused by an AJAX element in the form, that did not have proper arguments to make it unique. Which caused issues updating the form and caused the states of the wrong form to be detached. So it seemed related but the actual cause was something else. Sorry I didn't report back earlier, kinda forgot about this comment.
Comment #51
seanbSetting back to needs review, functionally it works fine for me :)
Comment #52
maheshv commentedThanks for the update @seanb
Comment #53
nitesh624Comment #54
nod_Yes so about the code, it works, I don't see any memory leak (with a moderate usage) so that's good. The touchy subject is from #38:
I'm a bit uncomfortable with that change, it does feel like a big change and people will wonder what's going on. I'd like to have some webform or other heavy users of #states involved here because I don't want to break all #states forms that are not correctly written. Like if you have a typo in the states config, it used to work and now it'll be hidden because of the typo.
I agree it's better this way, but given how old this thing is, the less than ideal way might be the way to go.
Comment #55
nod_We need a change record with code examples to show when the condition in #38 is likely to occur and code sample to show how to fix it.
Comment #57
nod_Tried to simplify some things.
For the credential forms we don't need to change too much logic, we need to reverse some conditions and it works as expected.Famous last words, didn't work as expected. Seems to work with manual testing, not in tests.Comment #58
drupalite1411 commentedhttps://git.drupalcode.org/project/drupal/-/merge_requests/9300/diffs#no...
I have used this patch in my project. I am facing below issue which was not there earlier:
I have a form 1 in which email field is required only when 'email' is selected.
$form['contact_option'] = [
'#type' => 'radios',
'#options' => [
"email" => $this->t("E-Mail"),
"phone" => $this->t("Phone"),
],
'#title' => $this->t('Contact me'),
'#required' => TRUE,
'#weight' => 15,
'#attributes' => [
'class' => [
'contact-option',
],
],
];
$form['email'] = [
'#type' => 'email',
'#title' => $this->t('Email Address'),
'#default_value' => "",
'#weight' => 20,
'#states' => [
// Show this field as required if the contact option is email.
'required' => [
':input[name="contact_option"]' => ['value' => 'email'],
],
],
];
form 2 extends form 1 and make it's email field required. Also unset contact options. Below is the code.
$form = parent::buildForm($form, $form_state, $email_destination);
unset($form['contact_option']);
$form['email']['#required'] = TRUE;
but after appling this patch email required field is not working.
Comment #59
tame4tex commented@drupalite1411 the issue is with your code and related to #38. The
#statesdeclaration (which is now invalid) is conflicting with your#requireddeclaration.The states code has become more robust and is more sensitive to coding errors. Given
contact_optionno longer exists on the form, the#statesdeclaration is invalid and should be removed. If the triggering element cannot be found on the form (e.g.contact_option), the inverse state is applied (e.g. optional).The code in your child form should be