When a dependent element is added to a form via AJAX, #states works as expected (at least in simple cases) when the triggering element (dependee) is inside the AJAX wrapper. If the dependee is outside the wrapper however, the dependent is not properly initialized and keeps its default state until the dependee changes.

To reproduce, download and enable the attached module derived from the examples project. This module adds a dependent "First Name" textfield via AJAX. The dependee is the checkbox "Disable first name" outside the "textfields" wrapper.

Then, visit scratch/states_issue

- Check "Ask me my first name"
- The "First Name" textfield appears
- Check "Disable first name textfield"
- The "First Name" textfield is now, as expected, disabled

Revisit scratch/states_issue

- Check "Disable first name textfield"
- Check "Ask me my first name"
- The "First Name" textfield appears. Expected state: disabled. Actual state: enabled.

Toggling the "Disable first name textfield" box will update the "First Name" textfield correctly. It's just the triggering upon ajax completion that appears to not work as *I* expect.

Members fund testing for the Drupal project. Drupal Association Learn more

Comments

nod_’s picture

Issue tags: +JavaScript

Leaving on D7 for now.

nod_’s picture

Component: forms system » javascript
Status: Active » Closed (duplicate)
Related issues: +#1592688: #states can cause the form "required" mark to appear more than once on the same element

IMO it's more of a UX issue but there is indeed a bug. In the sample module, the #states behavior is initialized several times and pretty much confuse itself with default values and all.

Applying the patch from #1592688-32: #states can cause the form "required" mark to appear more than once on the same element to states.js should solve your issue. seems like it soved it for me in any case. It'd be great if you could roll the patch for the other issue :)

Heine’s picture

Title: FAPI #states: dependent element added via AJAX initializes incorrectly if dependee is outside of wrapper » FAPI #states: dependent element added via AJAX initializes incorrectly if dependee has been initialized earlier
Status: Closed (duplicate) » Active

Thanks!

#1592688: #states can cause the form "required" mark to appear more than once on the same element provided real progress and uncovered another issue that's already hinted at in the former issue, but I'll split this off here, thus reopening

The use case demonstrated by the scratch example module is one where the user checks the Dependee, only to obtain, or remove the Dependent, AJAX loaded fields later.

With the patch in #1592688-42: #states can cause the form "required" mark to appear more than once on the same element, the following workflow works:

- Check Dependee
- Check Ask me my first name (this also loads states.js)
- The Dependent is disabled as expected

Now, stuff breaks down if the field is removed:

- Uncheck Ask me my first name
- Check Ask me my first name
- The Dependent is enabled (expected: disabled).

This happens because the Dependee has a data flag set that indicates it already been initialized.

states.Trigger = function (args) {
  $.extend(this, args);

  if (this.state in states.Trigger.states) {
    this.element = $(this.selector);

    // Only call the trigger initializer when it wasn't yet attached to this
    // element. Otherwise we'd end up with duplicate events.
    if (!this.element.data('trigger:' + this.state)) {
      this.initialize();
    }
  }
};

Possible solutions:

  • Initialization could be kept per Dependent
  • The Event handlers should properly toggle on e.value . This would make it possible to simply call the event again without worrying about earlier events, save for performance. This probably constitutes an API change for new states that are offered by modules.
  • ? (my JS knowledge is extremely limited)
lmeurs’s picture

Our problem seems similar: we have a custom form with two managed file fields (which upload files using AJAX) and a text field. When either of the managed file fields has a file selected, the text field should appear. Though the text field does appear, after removing both files it does not disappear.

We use a visible state with OR condition that checks whether one of the managed files' values is a file ID other than 0:

$form['my_text_field']['#states']['visible'] = array(
  array('[name="my_managed_file_1[fid]"]' => array('!value' => '0')),
  array('[name="my_managed_file_2[fid]"]' => array('!value' => '0')),
);

First time Drupal.behaviors.states.attach is called

A data flag that is set on dependees that indicates the dependee has been initialized. On initialization amongst other things an event is bound to the dependee, but also it's value is being cached at DrupalStatesDependentInstance.values[selector][stateName].

Second time Drupal.behaviors.states.attach is called

After ie. uploading a file Drupal.behaviors.states.attach is called again.

Data flags have already been set on existing dependees, so they are not being initialized again. This prevents events from being bound twice, but also causes their values cached at DrupalStatesDependentInstance.values[selector][stateName] to remain at their initial values null as set in states.Dependent.prototype.initializeDependee().

Newly inserted elements by AJAX do not have the data flag set and are getting initialized.

Back to our example

After a file upload Drupal.behaviors.states.attach is called for the second time. The cached values of both managed file fields are initially set to null. The existing managed file field already had the data flag and is not being initialized, so it's cached value remains null and according to our state's conditions null !== '0' the text field wrongly(!) stays visible.

Data flag

The data flag is being used to prevent double events being bound to a dependee, the comments state:

// Only call the trigger initializer when it wasn't yet attached to this
// element. Otherwise we'd end up with duplicate events.

but this also causes the values cache not being initialized properly.

If the sole purpose is preventing double bound events, could we not A) remove this data flag completely and B) unbind possible earlier bound events before binding new ones?

In Drupal.states.Trigger (unpatched Drupal 7.28) I commented out the if statement (though javascript, I use PHP formatting for syntax highlighting):

states.Trigger = function (args) {
  $.extend(this, args);

  if (this.state in states.Trigger.states) {
    this.element = $(this.selector);

    // Only call the trigger initializer when it wasn't yet attached to this
    // element. Otherwise we'd end up with duplicate events.
//    if (!this.element.data('trigger:' + this.state)) {
      this.initialize();
//    }
  }
};

and in Drupal.states.prototype.defaultTrigger I commented out the old event binding and added a new line that first unbinds possible events using event namespacing:

  defaultTrigger: function (event, valueFn) {
    var oldValue = valueFn.call(this.element);
    // Attach the event callback.
//    this.element.bind(event, $.proxy(function (e) {
    this.element.unbind(event + '.statesnamespace').bind(event + '.statesnamespace', $.proxy(function (e) {
      var value = valueFn.call(this.element, e);
      // Only trigger the event if the value has actually changed.
      if (oldValue !== value) {
        this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: oldValue });
        oldValue = value;
      }
    }, this));

    states.postponed.push($.proxy(function () {
      // Trigger the event once for initialization purposes.
      this.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });
    }, this));
  }

I am pretty familiar with js / jQuery, the states script is pretty hard to get my head around. This fix works for me so far, does it help you?

jgalletta’s picture

FileSize
972 bytes
PASSED: [[SimpleTest]]: [MySQL] 41,172 pass(es). View

The solution proposed by lmeurs corrects the issue in my case.
Here's a patch corresponding to his modifications.

David_Rothstein’s picture

Status: Active » Needs review
larowlan’s picture

Status: Needs review » Reviewed & tested by the community

Patch at #5 fixes similar issues encountered, #states not applied to dependant elements returned in an #ajax callback

lmeurs’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
1.65 KB
PASSED: [[SimpleTest]]: [MySQL] 41,461 pass(es). View

Thanks for the patch and the review! If this is the way to go, than:

  1. We no longer have to set the 'trigger:' + this.state data variable,
  2. Alter the comments accordingly to the changes we have made, ie. "Attach the event callback." can become "Detach possibly earlier attached event callback using event namespacing and attach a new event callback.",
  3. Use a more descriptive name for the event namespace, ie. "statesnamespace" can become "statesDefaultTrigger".

See patch attached.

rooby’s picture

DimitriV’s picture

Version: 7.x-dev » 8.0.x-dev
FileSize
1.29 KB

The same problem exists in Drupal 8.0.X. I updated the patch against 8.0.X

David_Rothstein’s picture

Issue tags: +needs backport to D7
radiancebox’s picture

The scratch.tar.gz is a 7 module.

m.lebedev’s picture

$dependee can be empty also...

Status: Needs review » Needs work

The last submitted patch, 13: D7-dependent_element_init-2226405-13.patch, failed testing.

DimitriV’s picture

Status: Needs work » Needs review

We need first to fix the issues in D8 before we can start backporting. The test will fail because the issue is tagged to D8.

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.

droplet’s picture

Status: Needs review » Needs work
Issue tags: +Needs JS testing, +JavaScriptTest
Eyal Shalev’s picture

I fixed the issue by adding a behavior detach function to the states object, that will set the this.element.data('trigger:' + this.state) to false.

  var original_states_detach = function() {};
  if (Drupal.behaviors.states.hasOwnProperty('detach')) {
    original_states_detach = Drupal.behaviors.states.detach;
  }

  Drupal.behaviors.states.detach = function(context, settings) {
    var $states = $(context).find('[data-drupal-states]');
    var config;
    var state;
    var il = $states.length;
    for (var i = 0; i < il; i++) {
      config = JSON.parse($states[i].getAttribute('data-drupal-states'));
      for (state in config) {
        if (config.hasOwnProperty(state)) {
          for (var selector in config[state]) {
            if (config[state].hasOwnProperty(selector)) {
              for (var trigger in config[state][selector]) {
                var $element = $(selector);
                var sanitized = Drupal.states.State.sanitize(trigger);
                if ($element.data('trigger:' + sanitized)) {
                  $element.data('trigger:' + sanitized, false);
                }
              }
            }
          }
        }
      }
    }
    original_states_detach(context, settings);
  };
Eyal Shalev’s picture

Status: Needs work » Needs review
FileSize
1.1 KB

Status: Needs review » Needs work

The last submitted patch, 19: 2226405_states_detach.patch, failed testing.

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.

filsterjisah’s picture

Patch from #19 breaks javascript (missing ",").

woprrr’s picture

Status: Needs work » Needs review
FileSize
461 bytes

This patch fixe the problem about duplicate call of Ajax when we trigger "add_more" button. This possibly fixe your problem too. I don't provide an interdiff because the previous patch is to D7 and break D8.

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.