0

I'm currently trying to add javascript via the settings tab for a webform that is supposed to dynamically update the displayed totals at the bottom of the webform page for the user.

A part of this involves me targeting a custom webform composite I created myself for this webform so I can hook onto an event that runs every time a value within this composite element changes.

The composite element looks like this:

enter image description here

Here's the JavaScript I have for selecting the product select element, summarized:

jQuery('table[data-drupal-selector|="edit-product-list-items"]').on('change', 'select.form-select[required]', updateValues);

function updateValues(event){
//perform updates here
}

As it is, The event is called whenever a value changes as long as the user just fills in the fields in the currently displayed table rows and nothing else. However, once they add a new item to the table, this event no longer gets called.

I've tried using the data-drupal-selector and ID attributes, but both give the same outcome. Not to mention, every time a row is added to the composite table, the ID of the table has a random string appended onto it.

Is there a better way to select the elements in this table so that my function runs consistently?

2 Answers 2

3

My heart is bleeding each time a developer starts working with Drupal and adds code to a Drupal site that doesn't take advantage of its features.

When using JavaScript with Drupal, please ALWAYS familiarize yourself with the concept of libraries (to manage JavaScript dependencies and inclusion to your site), as well as the relatively simple but powerful Drupal JavaScript behaviors (and while you're at it, the once feature, as you'll very often use it as well).

In brief, a Drupal JavaScript behavior is a JavaScript object provided by your module or theme JavaScript that features an attach and/or a detach method. Attach is called whenever a page load completed. That is the initial page load, as well as after dynamically loading page content using AJAX requests. It receives a context parameter that holds the wrapper DOM element of the changed part.

That said, your JavaScript didn't fire for dynamically added content, because you didn't use Drupal behaviors and thus the change event handler won't be bound to dynamically added page content.

The "workaround" you posted in your own answer on the other hand will cause the updateValues callback to fire on every change of a select element of your site. Regardless of whether it's part of your webform or not.

A solution that works better and conforms with the Drupal JavaScript API (for simplicity of code, I kept your jQuery dependency):

js/my-update-values.js:

(function (Drupal, $, once) {

  'use strict';

  Drupal.behaviors.myUpdateValues = {
    attach: function (context) {
      // Apply once to all required select elements within the
      // current context.
      once('myUpdateValues', 'select.form-select[required]', context)).forEach(function (element) {
        var $element = $(element);
        // Ensure the select element is part of the custom webform.
        // A CSS class selector would be better here.
        if ($element.closest('table[data-drupal-selector|="edit-product-list-items"]').length === 0) {
          return;
        }
        // Bind the change handler.
        $element.on('change.myUpdateValues', updateValues);
      });
    },
    detach: function (context) {
      // Process all required select elements of the detaching context
      // that where previously processed by our attach function, and
      // unbind the change handler.
      $(once.remove('myUpdateValues', 'select.form-select[required]', context)).off('change.myUpdateValues');
    }
  };

  function updateValues(event) {
    // Your code here.
  }

}(Drupal, jQuery, once));

This example depends on the Drupal 9.2+, jQuery and Drupal once libraries. So your library definition may look as such:

libraries.yml:

myUpdateValues:
  js:
    js/my-update-values.js: {}
  dependencies:
    - core/drupal
    - core/jquery
    - core/once

On older versions of Drupal, you may have to use jquery.once instead.

3
  • Apologies for your bleeding heart, I'm fairly new to Drupal so I'm still learning how to integrate its features with JS. This solution worked great, I realize now that my posted solution is pretty sloppy and leads to way more function calls then needed.
    – Riley Lutz
    Nov 23, 2022 at 23:29
  • Question: If I wanted to include multiple DOM elements as the selector parameter for the once() function, how would that have to look? Because I get a TypeError when just including the selector strings in a normal array.
    – Riley Lutz
    Nov 23, 2022 at 23:59
  • If you provide an array, it must be an array of DOM elements/jQuery objects. If you want to use selectors, it will work the same as with the document.querySelectorAll function. A comma separated list of selectors will do. You can check the examples mentioned here: npmjs.com/package/@drupal/once#readme Nov 24, 2022 at 1:14
-2

I found a solution: using bubbling to attach the event to something other than the table stops the listeners from being removed every time a new row is added.

jQuery('body').on('change', 'select.form-select[required]', updateValues);
1
  • 1
    This is not recommended. Depending on where that code is placed, it might get injected and executed multiple times. Use a proper Drupal Javascript Behavior instead
    – Hudri
    Nov 21, 2022 at 22:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.