javascriptjqueryjquery-select2

Select2 throws too many events when clearing the selection


I am using Select2 for a multi select. This select is used to filter a list.

Selecting one item rebuilds the list with that filter(s). Removing one item, again rebuild the list without that specific item. You can select more than one items.

I have attached a behaviour on the "on change" event of the select. This is going well. The list is properly rebuild when individual items are selected/deselected.

My problem is when I click on the "clear handle", I get too many events. For example, when I have two items selected in the selector, if I click the clear button, I get 3 change events and the list is rebuild 3 times! I would like to have only one event that I can rely on when clearing the selection.

I could use the clearing or clear event, but would I need a way to rebuild the list when individual items are selected or unselected. The problem is I cannot use both... a select2:unselect/change event is triggered for every individual items that is removed from the list whether you remove individual item manually or use the clear button that removes all items automatically...

Using the clear button should trigger the change event only once!

The select2:unselect could be triggered once per item removed though...

const $field = $("[name=color]");
$field.select2({
    closeOnSelect : false,
    placeholder: "Please select",
    allowHtml: true,
    allowClear: true,
    tags: true
});

$field.on("change", function(e) {
  console.log("pulldown change");
  // Use $this.val() to rebuild the list with the new filters
});
$field.on("change.select2", function(e) {
  console.log("change.select2");
});
$field.on("select2:select", function(e) {
  console.log("select2:select");
});
$field.on("select2:unselect", function(e) {
  console.log("select2:unselect");
});
$field.on("select2:clearing", function(e) {
  console.log("select2:clearing");
});
$field.on("select2:clear", function(e) {
  console.log("select2:clear");
});
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
<select class="form-control" style="width: 100%" name="color" multiple>
  <option value="blue">blue</option>
  <option value="red">red</option>
  <option value="yellow">yellow</option>
</select>

Browser console after clicking the clear button. (The X on the input that replaces the select box)

select2:clearing
select2:clear
pulldown change
change.select2
select2:unselect
pulldown change
change.select2
select2:unselect
pulldown change
change.select2

In short, clicking on the clear button will trigger the clearing and the clear events, it will then trigger a change, change.select2 and a select2:unselect event for each items that was selected. It then triggers a change and a change.select2 event once more.


Solution

  • You can wrap your update function in a debounce. You will need a flag to reset each time the function gets called, so it does not get spammed.

    const debounce = (func, wait) => {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    };
    
    function fetchChildList(selectedValues) {
      // Logic to fetch/update the child-list based on selected values
      console.log("Fetching child list for values:", JSON.stringify(selectedValues));
    }
    
    $(document).ready(function() {
      const $field = $("[name=color]");
      const debouncedFetchChildList = debounce(fetchChildList, 300);
      let isClearing = false;
    
      $field.select2({
        closeOnSelect: false,
        placeholder: "Please select color(s)",
        allowHtml: true,
        allowClear: true,
        tags: true
      });
    
      $field.on("select2:clearing", function(e) {
        console.log("select2:clearing");
        isClearing = true;
      });
    
      $field.on("select2:clear", function(e) {
        console.log("select2:clear");
        // Set timeout to wait for the change events to complete
        setTimeout(() => {
          debouncedFetchChildList([]);
          isClearing = false; // Reset the clearing flag after fetch
        }, 0);
      });
    
      $field.on("change", function(e) {
        if (isClearing) {
          return; // Skip change event handling during clearing
        }
        const selectedValues = $field.val();
        console.log("Selected values:", JSON.stringify(selectedValues));
        debouncedFetchChildList(selectedValues);
      });
    });
    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
    <select class="form-control" name="color" multiple="multiple" style="width: 100%">
      <option value="blue" selected="true">Blue</option>
      <option value="red" selected="true">Red</option>
      <option value="yellow" selected="true">Yellow</option>
    </select>


    As a jQuery plugin

    I wrapped all this logic into a plugin called $.fn.select3:

    Note: I used the $.debounce function from the jquery-throttle-debounce rather than my hand-rolled version.

    function fetchChildList(selectedValues) {
      console.log("Fetching child list for values:", JSON.stringify(selectedValues));
    }
    
    $(document).ready(function() {
      $("[name=color]").select3(fetchChildList, {
        closeOnSelect: false,
        placeholder: "Please select color(s)",
        allowHtml: true,
        allowClear: true,
        tags: true
      });
    });
    
    (function($) {
      $.fn.select3 = function(updateFn, options) {
        const debouncedUpdateFn = $.debounce(300, updateFn);
        let isClearing = false;
    
        function onClearAll(e) {
          isClearing = true;
        }
        function onClear(e) {
          setTimeout(() => {
            debouncedUpdateFn([]);
            isClearing = false; // Reset the clearing flag after fetch
          }, 0);
        }
        function onChange(e) {
          if (isClearing) return;
          debouncedUpdateFn($(e.target).val());
        }
        
        return this.select2(options)
          .on({
            'select2:clearing': onClearAll,
            'select2:clear': onClear,
            'change': onChange
          });
      };
    })(jQuery);
    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js"></script>
    <select class="form-control" name="color" multiple="multiple" style="width:100%">
      <option value="blue" selected="true">Blue</option>
      <option value="red" selected="true">Red</option>
      <option value="yellow" selected="true">Yellow</option>
    </select>