Steps to reproduce:

1. Define a Drupal behaviour. For example:

Drupal.behaviors.testBehavior = {
	attach: function (context) {
		if (jQuery(context).find('.js-form-type-email').length > 0) {
			console.log('Attach behaviours to the email field');
		};
	}
};

2. Enable Ajax, using the Contact form for example which works with the JS above.

3. As an end user click the "Send message" button to submit the form. Form is submitted with Ajax, and then validation errors are rendered on the empty required fields, as expected.

However, if you look at the JavaScript console, not all is as it should be.

Expected output:

After the Ajax request, Drupal behaviours JavaScript should be attached once.

Actual output:

After the Ajax request, Drupal behaviours JavaScript is attached twice.

This is because the Drupal.Ajax.prototype.success method attaches behaviours after a successful Ajax request. The Drupal.AjaxCommands.prototype.insert method, which is called by the Drupal.AjaxCommands.prototype.webformInsert also attaches behaviours after inserting content into the DOM.

screen recording showing defect reproduced

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

keeganstreet created an issue. See original summary.

keeganstreet’s picture

A workaround is possible because the context parameter is different in each case. In the first call, context is the <div> surrounding the <form> element, and in the second call, context is the <form> element itself.

jrockowitz’s picture

I am not a JavaScript expert and I am very open to suggestions on how to fix this.

I am not seeing Drupal.AjaxCommands.prototype.webformInsert behavior being called during a webform submission Ajax request.

jrockowitz’s picture

Hmm... I think we are running into this issue #736066: ajax.js insert command sometimes wraps content in a div, potentially producing invalid HTML and other bugs which would explain the wrapper div.

keeganstreet’s picture

Hmm yeah you're right, the Drupal.AjaxCommands.prototype.webformInsert command isn't there, its actually the insert command which must be triggering the second call.

keeganstreet’s picture

Does that insert command originate from the webform or core? Maybe this is actually a core issue.

jrockowitz’s picture

The insert command is from core but called from the \Drupal\webform\Form\WebformAjaxFormTrait::replaceForm

jrockowitz’s picture

Status: Active » Postponed (maintainer needs more info)
niknak’s picture

FileSize
263.94 KB
263.94 KB

I'm working for integrate in my website, a solr search engine with search api and ajax facets.

If adding a webform block, in my search result page, the drupal behaviors for ajax facets is called for each block in page loading (see in attached images) off that if I remove the form block, it is called only once.

Can you fix this issue ?

Thx

jrockowitz’s picture

I was unable to fix it. I need someone who is more knowledgeable about JavaScript to help me figure out the problem.

bucefal91’s picture

Status: Postponed (maintainer needs more info) » Active

Let's see if I can be of any use here.

First of all, dear niknak, you've uploaded the same image twice in #9. So I do not really understand your issue.

Secondly, back to the original behaviors problem. So I took a simple webform with a single radios element on it and enabled ajax on the webform as in the steps to reproduce.

On plain page load (when you just open the page and haven't submitted the webform yet), I get 11 times, in the following order:

  • drupal.init.js - the generic on page load fire where context is the whole document.
  • [repeated x10 times]: webform.form.js - extra attach behaviors are invoked for each contextual link. In webform.form.js, look at the very bottom, where it listens to the event drupalContextualLinkAdded. Each time it has a different context - a different contextual link. Let me return in further detail to this piece of code down below.

Then I submit a form without values (so I would get a validation error) and this time I get 5 times, in the following order:

  • ajax.js Drupal.AjaxCommands.prototype.insert function. The context is the new (updated) and rendered into html webform. That's apparent and obvious, you're supposed to invoke attachBehaviors on a context you've just inserted into the DOM.
  • ajax.js Drupal.Ajax.prototype.success function. It invokes attachBehaviors on the whole <form>..</form> DOM object.
  • [repeated x3 times]: webform.form.js - same thing as in the initial page load. attachBehaviors() is invoked for contextual links.

Now, let me say a few words about attachBehaviors(). One is wrong to expect attachBehaviors() to be invoked exclusively once for every single DOM element on a page. It is a very difficult task to achieve. There is a much better alternative to it - make sure to run your code only once for a given DOM object by using .once(). https://www.drupal.org/docs/8/api/javascript-api/javascript-api-overview - this page has necessary insights about it. So overall this ticket smells to me as "works as designed".

Let me return at this moment back to this webform.form.js. By using git blame I've identified that jrockowitz has introduced that piece of code as part of #2866554: Add Quick Edit off canvas form.. Which in turn seems like a forced step because of a core bug: #2764931: Contextual links don't work with 'use-ajax' links. As a matter of fact, that core bug is fixed and committed into Drupal core 8.5.x.

To summarize, the behavior reported in the main description of this ticket is expected and perfectly fine. You, as a custom module developer, are expected to make your behavior look like the following (I am just taking your behavior and wrapping things with .once()):

Drupal.behaviors.testBehavior = {
	attach: function (context) {
		if (jQuery(context).find('.js-form-type-email').once('name-of-my-behavior').length > 0) {
			console.log('Attach behaviours to the email field');
		};
	}
};
Nigel Cunningham’s picture

Just confirming what @bucefal91 wrote - .once() is the correct thing to do to avoid a behaviour being applied to a DOM element more than once.

bucefal91’s picture

Status: Active » Closed (works as designed)

Alrightie, I will close this issue then.