1

Once again i am having a problem with the JS called more than once. the code here transform the select/options of the exposed filters to ul/li. The problem is that the

console.log("enter init")

is called many times , when i would need it to be called only once on load :

(function($, Drupal) {
  /**
   *
   * Retourne la valeur correspondante du localstorage
   *
   * @param {string} key
   * @return {string}
   */
  function getFromLocalStorage(key) {
    if (localStorage.getItem(key) === null) {
      localStorage.setItem(key, "All");
    }
    return localStorage.getItem(key);
  }

  const variables = {
    selectElement /** @type={HTMLElement} */: document.querySelector(
      ".views-exposed-form .form-select"
    ),
    optionElements /** @type={HTMLCollection} */: document.querySelector(
      ".views-exposed-form .form-select"
    ).children,
    inputElement /** @type={HTMLElement} */: document.querySelector(
      ".views-exposed-form .form-submit"
    ),
    selectedFilter: getFromLocalStorage("data-value"),
    ul: document.createElement("ul")
  };

  /**
   * Stocke le li clické
   *
   * @param {Event } e
   * @return {void}
   */
  function setToLocalStorageOnClick(e) {
    variables.selectedFilter = e.target.dataset.value;
    localStorage.setItem("data-value", variables.selectedFilter);
  }

  /**
   * Convertit select/option to ul/li
   * Affranchit de l'usage du submit bouton
   * Mémoire des clicks
   *
   * @return {void}
   */
  function selectToUlFilter() {
    variables.inputElement.classList.add("visually-hidden");
    variables.selectElement.hidden = true;
    variables.ul.setAttribute("class", "selectlist");
    Array.from(variables.optionElements).forEach(item => {
      const li = document.createElement("li");
      li.setAttribute("class", "selectlist-option");
      li.dataset.value = item.value;
      li.innerHTML = item.innerHTML;
      variables.ul.append(li);
    });
    variables.selectElement.after(variables.ul);
  }

  /**
   * Transpose le li sélectionné au select/option avant le submit
   *
   * @return {void}
   */
  function transposeModifToSelect() {
    Array.from(variables.optionElements).forEach(item => {
      if (item.value === variables.selectedFilter) {
        item.setAttribute("selected", "selected");
      } else {
        item.removeAttribute("selected");
      }
    });
  }

  /**
   * Ajout d'un attribut à l'element sélectionné pour le css
   *
   * @return {void}
   */
  function addSelectedAttribute() {
    document.querySelector(
      `li[data-value="${variables.selectedFilter}"]`
    ).dataset.selected = "true";
  }

  /**
   * Default controller appelé au chargement de la page
   *
   * @return {void}
   */
  function init() {
    console.log(drupalSettings.amu_bef_selectors);
    console.log("enter init");

    selectToUlFilter();
    addSelectedAttribute();
    transposeModifToSelect();

    variables.inputElement.click();

    const newUl = document.querySelector(".selectlist");
    const liDefaultEN = document.querySelector(
      '.lang--en li[data-value="All"]'
    );
    if (liDefaultEN) {
      liDefaultEN.innerHTML("All");
    }
  }

  Drupal.behaviors.bef = {
    attach(newUl) {
      document.addEventListener("DOMContentLoaded", init(), { once: true });

      newUl.addEventListener(
        "click",
        e => {
          setToLocalStorageOnClick(e);
          transposeModifToSelect();
          variables.inputElement.click();
        },
        { once: true }
      );
    }
  };
})(jQuery, Drupal);

EDIT:

  • if (!initialised) added

  • what happen then is eventlistener is not working on : variables.inputElement.click() : error Form submission canceled because the form is not connected.

    (function($, Drupal) {
    /**
     *
     * Retourne la valeur correspondante du localstorage
     *
     * @param {string} key
     * @return {string}
     */
    function getFromLocalStorage(key) {
      if (localStorage.getItem(key) === null) {
        localStorage.setItem(key, "All");
      }
      return localStorage.getItem(key);
    }
    
    const variables = {
      selectElement /** @type={HTMLElement} */: document.querySelector(
        ".views-exposed-form .form-select"
      ),
      optionElements /** @type={HTMLCollection} */: document.querySelector(
        ".views-exposed-form .form-select"
      ).children,
      inputElement /** @type={HTMLElement} */: document.querySelector(
        ".views-exposed-form .form-submit"
      ),
      selectedFilter: getFromLocalStorage("data-value"),
      ul: document.createElement("ul")
    };
    
    /**
     * Stocke le li clické
     *
     * @param {Event } e
     * @return {void}
     */
    function setToLocalStorageOnClick(e) {
      variables.selectedFilter = e.target.dataset.value;
      localStorage.setItem("data-value", variables.selectedFilter);
    }
    
    /**
     * Convertit select/option to ul/li
     * Affranchit de l'usage du submit bouton
     * Mémoire des clicks
     *
     * @return {void}
     */
    function selectToUlFilter() {
      console.log("enter selectToul");
      // variables.inputElement.classList.add("visually-hidden");
      // variables.selectElement.hidden = true;
      variables.ul.setAttribute("class", "selectlist");
      Array.from(variables.optionElements).forEach(item => {
        const li = document.createElement("li");
        li.setAttribute("class", "selectlist-option");
        li.dataset.value = item.value;
        li.innerHTML = item.innerHTML;
        variables.ul.append(li);
      });
      variables.selectElement.after(variables.ul);
    }
    
    /**
     * Transpose le li sélectionné au select/option avant le submit
     *
     * @return {void}
     */
    function transposeModifToSelect() {
      Array.from(variables.optionElements).forEach(item => {
        if (item.value === variables.selectedFilter) {
          item.setAttribute("selected", "selected");
        } else {
          item.removeAttribute("selected");
        }
      });
    }
    
    /**
     * Ajout d'un attribut à l'element sélectionné pour le css
     *
     * @return {void}
     */
    function addSelectedAttribute() {
      const selectedLi = document.querySelector(
        `li[data-value="${variables.selectedFilter}"]`
      );
      if (selectedLi !== null) {
        selectedLi.dataset.selected = "true";
      }
    }
    
    /**
     * Default controller appelé au chargement de la page
     *
     * @return {void}
     */
    function init() {
      console.log(drupalSettings.amu_bef_selectors);
      console.log("enter init");
    
      selectToUlFilter();
      addSelectedAttribute();
      transposeModifToSelect();
      // variables.inputElement.click();
    
      const liDefaultEN = document.querySelector(
        '.lang--en li[data-value="All"]'
      );
      if (liDefaultEN) {
        liDefaultEN.innerHTML("All");
      }
    }
    
    const form = document.querySelector(".views-exposed-form");
    form.addEventListener("click", e => {
      setToLocalStorageOnClick(e);
      transposeModifToSelect();
      // works but no ajax
      form.submit();
      // when this does not work : Form submission canceled because the form is not connected
      // variables.inputElement.click();
    });
    
    let initialised = false;
    Drupal.behaviors.bef = {
      attach() {
        if (!initialised) {
          initialised = true;
          init();
        }
      }
    };
    

    })(jQuery, Drupal);

EDIT 2 (following Danny Englander's reply):

i have removed the JQuery dependency and add the @drupal/once dependency. Although my individuals selectors are now theorically protected from re-processing eg

const selectElement = /** @type={HTMLElement} */ once( "my-once-id", document.querySelector(${formSelector} select) ).shift();

, my function init() and eventlistener is still called many times, still generating errors. the

console.log(selectElement)

returns the select element but then undefined on each subsequent inadequate call. What's wrong with my code logic ??

(function(Drupal, once) {
  const initialised = false;
  Drupal.behaviors.bef = {
    attach(context) {
      let formSelector;
      const config = drupalSettings.amu_bef_selectors;
      const formSelectors = config.split("\r\n");
      formSelectors.forEach((value, index) => {
        if (document.querySelector(`${value}`)) {
          // Un exposed filter par page uniquement
          formSelector = value;
        }
      });

      /**
       *
       * Retourne la valeur correspondante du localstorage
       *
       * @param {string} key
       * @return {string}
       */
      function getFromLocalStorage(key) {
        if (localStorage.getItem(key) === null) {
          localStorage.setItem(key, "All");
        }
        return localStorage.getItem(key);
      }
      let selectedFilter = getFromLocalStorage("data-value");

      const form = document.querySelector(`${formSelector}`);
      const selectElement = /** @type={HTMLElement} */ once(
        "my-once-id",
        document.querySelector(`${formSelector} select`)
      ).shift();
      console.log(selectElement);
      const optionElements /** @type={HTMLCollection} */ = once(
        "my-once-id2",
        selectElement.children
      );
      const inputElement /** @type={HTMLElement} */ = once(
        "my-once-id3",
        `${formSelector} .form-submit`
      ).shift();

      const ul = document.createElement("ul");

      /**
       * Stocke le li clické
       *
       * @param {Event } e
       * @return {void}
       */
      function setToLocalStorageOnClick(e) {
        selectedFilter = e.target.dataset.value;
        localStorage.setItem("data-value", selectedFilter);
      }

      /**
       * Convertit select/option to ul/li
       * Affranchit de l'usage du submit bouton
       * Mémoire des clicks
       *
       * @return {void}
       */
      function selectToUlFilter() {
        inputElement.classList.add("visually-hidden");
        selectElement.hidden = true;
        ul.setAttribute("class", "selectlist");
        Array.from(optionElements).forEach(item => {
          const li = document.createElement("li");
          li.setAttribute("class", "selectlist-option");
          li.dataset.value = item.value;
          li.innerHTML = item.innerHTML;
          ul.append(li);
        });
        selectElement.after(ul);
      }

      /**
       * Transpose le li sélectionné au select/option avant le submit
       *
       * @return {void}
       */
      function transposeModifToSelect() {
        Array.from(optionElements).forEach(item => {
          if (item.value === selectedFilter) {
            item.setAttribute("selected", "selected");
          } else {
            item.removeAttribute("selected");
          }
        });
      }

      /**
       * Ajout d'un attribut à l'element sélectionné pour le css
       *
       * @return {void}
       */
      function addSelectedAttribute() {
        const selectedLi = document.querySelector(
          `li[data-value="${selectedFilter}"]`
        );
        if (selectedLi !== null) {
          selectedLi.dataset.selected = "true";
        }
      }

      /**
       * Default controller appelé au chargement de la page
       *
       * @return {void}
       */
      function init() {
        selectToUlFilter();
        addSelectedAttribute();
        transposeModifToSelect();
        // inputElement.click();

        const liDefaultEN = document.querySelector(
          '.lang--en li[data-value="All"]'
        );
        if (liDefaultEN) {
          liDefaultEN.innerHTML("All");
        }
      }

      form.addEventListener("click", e => {
        setToLocalStorageOnClick(e);
        transposeModifToSelect();
        inputElement.click();
      });

      init();
    }
  };
})(Drupal, once);


bef:
  version: 1.x
  js:
    https://unpkg.com/@drupal/once: { type: external, minified: true }
    js/amu_bef.js: {}
  dependencies:
    - core/drupalSettings
9
  • Does this answer your question? jquery once onload
    – leymannx
    Mar 25 at 17:10
  • Do you try to avoid jQuery? If not add - core/jquery.once to the dependency of the library and then use as outlined in the other answer, it has a snippet for D8&+ in the bottom. Also explained in more detail in the official docs.
    – leymannx
    Mar 25 at 17:13
  • 1
    You don't put the once on every line of code. You just put it once. 😅 Okay, so is the question actually "How can I refactor jQuery once with Vanilla JS"? Then I guess this is off-topic...
    – leymannx
    Mar 25 at 17:29
  • 1
    Once is applied to the context, it is not needed multiple times in a behavior. A reason to follow the official docs in this respect is so you do not get bit by ajax or bigpipe etc later. drupal.org/docs/drupal-apis/javascript-api/…
    – Kevin
    Mar 25 at 17:31
  • 2
    Oh, and document.addEventListener("DOMContentLoaded", init(), { once: true }); is wrong - you shouldn't invoke init, just pass it. You don't even need that line, the DOM is guaranteed to be loaded when behaviors are invoked so you can replace it with init();
    – Clive
    Mar 25 at 19:49
2

There is now a vanilla version of Drupal once (https://www.drupal.org/project/once) and I've been using it successfully on a Drupal 9 project with vanilla JS in my theme. You can look at the change log here and the original issue that was the impetus for converting to vanilla. The source file in the project from which you can build sheds some insight on its use.

In your code above, I see you are using document.querySelector and you'll want to pay attention to that as I found a bug as noted here. "Error when context.querySelector() doesn't find an element on the page".

The documentation says:

 // once methods always return an array, to simplify the use with a single
 *   // element use destructuring or the shift method.
 *   const [myElement] = once('my-once-id', document.body);
 *   const myElement = once('my-once-id', document.body).shift();

So in the end, it's probably best for a single element to use an id with the patch linked to herein rather than document.querySelector. For example, I have a single element I want to target so I do it like this:

const glossary_view_conditions = once('glossary-view-once', '#views-exposed-form-scs-conditions-block');

For multiple elements being targeted, querySelectorAll works great. You'll also need your main function to target once, beginning and end. So an example would be:

(function (Drupal, once) {
  Drupal.behaviors.customCheckboxes = {
    attach(context) {
      // Find checkboxes, add markup.
      let outer_checkboxes = once('yay-only-once', context.querySelectorAll('.js-form-type-checkbox'));
      // Loop through each matched element.
      for (let i = 0; i < outer_checkboxes.length; i++) {
        // This is similar to jquery $(this).find('input.form-checkbox).
        let inner_inputs = outer_checkboxes[i].querySelectorAll("input.form-checkbox");
        // This is similar to jquery .after().
        // Since querySelectorAll returns a collection, we target with [0].
        inner_inputs[0].insertAdjacentHTML('afterend', '<span class="checkbox-toggle"><span class="checkbox-toggle__inner"></span></span>');
      }
    }
  };
}(Drupal, once));

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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