JS #states behavior does not have a detach method

Created on 19 August 2024, 3 months ago
Updated 3 September 2024, 2 months ago

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.

🐛 Bug report
Status

Needs review

Version

11.0 🔥

Component
Javascript 

Last updated about 20 hours ago

Created by

🇫🇮Finland Alexander Tallqvist

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @Alexander Tallqvist
  • 🇫🇮Finland Alexander Tallqvist

    Adding the mentioned patch.

  • 🇨🇷Costa Rica yuvania

    Hello alexander tallqvist , I tested the patch, and it worked for me.

  • Status changed to RTBC 3 months ago
  • Status changed to Needs work 3 months ago
  • 🇫🇷France nod_ Lille

    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.

  • 🇮🇳India sukr_s

    @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 of field_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.

  • 🇫🇷France nod_ Lille
  • 🇫🇮Finland Alexander Tallqvist

    Hi @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_browser module 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_short is always displayed wheter or not the field_main_media field 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 of field_main_media[target_id], just as you said. I triple checked that the selector is indeed field_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.

  • 🇫🇷France pbonnefoi

    I 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 :-)

  • 🇫🇷France nod_ Lille

    The patch that exist is not the appropriate fix, it's broken in a different way :)

  • 🇫🇷France pbonnefoi

    Thanks for the info @nod_ any idea which fix to apply yet ?

  • Merge request !930011.x → (Open) created by sukr_s
  • Pipeline finished with Failed
    3 months ago
    Total: 1119s
    #262277
  • Pipeline finished with Failed
    3 months ago
    Total: 155s
    #262567
  • Pipeline finished with Failed
    3 months ago
    Total: 673s
    #262576
  • Pipeline finished with Failed
    3 months ago
    Total: 993s
    #266831
  • Pipeline finished with Failed
    3 months ago
    Total: 153s
    #266852
  • Pipeline finished with Failed
    3 months ago
    Total: 181s
    #266874
  • Pipeline finished with Success
    3 months ago
    Total: 433s
    #266888
  • Status changed to Needs review 3 months ago
  • 🇫🇷France pbonnefoi

    Works for me :-)

  • 🇫🇷France byacoubi

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

  • Status changed to Needs work 2 months ago
  • 🇫🇷France nod_ Lille

    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.

  • Pipeline finished with Failed
    2 months ago
    Total: 196s
    #272229
  • Pipeline finished with Failed
    2 months ago
    Total: 250s
    #272233
  • Pipeline finished with Failed
    2 months ago
    Total: 165s
    #272238
  • Pipeline finished with Failed
    2 months ago
    Total: 155s
    #272249
  • Pipeline finished with Canceled
    2 months ago
    Total: 67s
    #272253
  • Pipeline finished with Failed
    2 months ago
    Total: 2958s
    #272254
  • Pipeline finished with Failed
    2 months ago
    Total: 2042s
    #272317
  • Pipeline finished with Success
    2 months ago
    Total: 689s
    #272351
  • Status changed to Needs review 2 months ago
  • 🇮🇳India sukr_s

    changes as per MR comments

  • 🇨🇦Canada tame4tex

    Thank 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

    1. Create with form with Ajax and states as below.
    2.     $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,
          ];
          if ($form_state->getValue('reload')) {
            $form['js_states_test']['dependent_checkbox'] = [
              '#type' => 'checkbox',
              '#title' => 'Only visible if reload is checked',
            ];
          }
          $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],
              ],
            ],
          ];
          $form['js_checkbox_field_textfield'] = [
            '#type' => 'textfield',
            '#title' => 'Field should hide when dependent_checkbox is checked after ajax',
            '#states' => [
              'invisible' => [
                ':input[name="dependent_checkbox"]' => ['checked' => TRUE],
              ],
            ],
          ];
      
    3. Click on "Check me to reload Select Field field" checkbox
    4. Select value 1 for "Select field"
    5. BUG: 'Select should show when 1 is selected in select_field after ajax' is not visible when it should be
    6. Click on "Only visible if reload is checked" checkbox
    7. BUG: "Field should hide when dependent_checkbox is checked after ajax" is not hidden when it should be

    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.


    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.

  • Pipeline finished with Failed
    about 2 months ago
    #295067
  • 🇨🇦Canada tame4tex

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

  • Pipeline finished with Failed
    about 2 months ago
    Total: 168s
    #295083
  • Pipeline finished with Success
    about 2 months ago
    Total: 839s
    #295092
  • 🇨🇦Canada tame4tex

    I 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:

    1. Renamed the statesObjects property to processedDependees to hopefully be more descriptive of what it is.
    2. Modified the states.Dependent function 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.
    3. Fixed issue in Drupal.states.Dependent.destroy where it was removing the state:${dependeeStates[i]} event associated with other Dependents
    4. Fixed issue where duplicate entires were being added to statesObjects property when states were reprocessed on ajax
    5. Fixed issue in Drupal.states.Trigger.destroy where trigger related event listeners were not being removed.
    6. Added functionality to the state of the attach function to ensure states are reprocessed for dependee elements that are added to the DOM during ajax
  • Pipeline finished with Failed
    about 2 months ago
    Total: 453s
    #295515
  • Pipeline finished with Failed
    about 2 months ago
    Total: 157s
    #295520
  • 🇨🇦Canada tame4tex

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

  • Pipeline finished with Failed
    about 2 months ago
    Total: 4811s
    #295524
  • Pipeline finished with Failed
    about 2 months ago
    Total: 498s
    #295586
  • Pipeline finished with Success
    about 2 months ago
    Total: 541s
    #295603
  • 🇨🇦Canada tame4tex

    All failing tests are fixed. Good for review.

  • 🇨🇦Canada tame4tex

    So 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_message in \Drupal\Core\Entity\ContentEntityForm::addRevisionableFormFields() is now being correctly handled because the revision checkbox 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 Don't show the revision log message on creation of content, media, etc. Needs work . 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!

  • 🇫🇷France nod_ Lille

    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.

  • The 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.

  • 🇧🇷Brazil andre.bonon

    I'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

  • 🇮🇳India mdsohaib4242

    You need to make sure that JavaScript behaviours are reattached after the AJAX callback. You can do this by ensuring that the ajax['callback'] method returns the form correctly.

    use Drupal\Core\Form\FormBase;
    use Drupal\Core\Form\FormStateInterface;
    
    class AjaxStatesForm extends FormBase {
    
      /**
       * {@inheritdoc}
       */
      public function getFormId() {
        return 'ajax_states_form';
      }
    
      /**
       * {@inheritdoc}
       */
      public function buildForm(array $form, FormStateInterface $form_state) {
        $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' => $this->t('Check me to reload Select Field field'),
          '#ajax' => [
            'callback' => '::buildAjax',
            'wrapper' => 'js_states_test_wrapper',
          ],
        ];
    
        $form['js_states_test']['select_field'] = [
          '#type' => 'select',
          '#title' => $this->t('Select Field'),
          '#options' => [0 => 0, 1 => 1],
          '#default_value' => 0,
          '#ajax' => [
            'callback' => '::buildAjax',
            'wrapper' => 'js_states_test_wrapper',
            'event' => 'change', 
          ],
        ];
    
        $form['js_select_field_textfield'] = [
          '#type' => 'textfield',
          '#title' => $this->t('This field should show when 1 is selected in Select Field'),
          '#states' => [
            'visible' => [
              ':input[name="select_field"]' => ['value' => 1],
            ],
          ],
        ];
    
        $form['#attached']['library'][] = 'core/drupal.ajax';
    
        return $form;
      }
    
      /**
       * AJAX callback to rebuild the form section.
       */
      public function buildAjax(array &$form, FormStateInterface $form_state) {
        return $form['js_states_test'];
      }
    
      /**
       * {@inheritdoc}
       */
      public function submitForm(array &$form, FormStateInterface $form_state) {
      }
    }
    
    

    This approach ensures that #states are reattached correctly after the form is reloaded via AJAX.

  • 🇨🇦Canada tame4tex

    @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 🐛 Form API #states property/states should use .once() to apply its rules (Can cause failures with BigPipe and possibly other situations) Needs work 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!

Production build 0.71.5 2024