truckingsim / Ajax-Bootstrap-Select

This uses the bootstrap-select plugin and extends it so that you can use a remote source to search.
MIT License
280 stars 97 forks source link

multiple + preserveSelected + default <options> groups unselected options into 'Currently Selected' #199

Open Nitrodist opened 3 years ago

Nitrodist commented 3 years ago

I am using ajax-bootstrap-select to search against people in my business's application. Up to this point I was using it without the multiple option, so I wasn't taking advantage of a few features.

Remote search works great. Prepopulating the list doesn't work great. It thinks I am 'selecting' the initial list of options that I've provided.

With the non-multiple select tag in use, I was able to initialize the search list with a few preferred items using ajax-bootstrap-select. When I switched this new tag to use multiple, I've run into an issue where ajax-bootstrap-select groups the initial list into an <optgroup> off 'Currently Selected' items even though none of them were selected.

Steps to reproduce:

  1. Initialize html/javascript as depicted below
  2. Observe that list does not have 'currently selected' items
  3. Type a letter to trigger remote searching
  4. The ajax-bootstrap-select dropdown flashes white + refreshes
  5. Every item provided as an initial set of <option> tags is grouped into 'Currently Selected'
  6. The ajax-bootstrap-select dropdown flashes white + refreshes
  7. Remove ajax call is completed + processed — server-driven items show up
  8. Observe that I have the remote items where the initial items were before grouped into 'Currently Selected' and I have the remote items as well.

Desired Behaviour

Assuming that we have preserveSelected: true, that it only preserves the ones that were actually selected instead of all options in the list.

I believe this is a problem where we rely on $obj.find(':selected') on this line of code in src/js/classes/AjaxBootstrapSelectList.js #L69

Code

HAML:

= select_tag :dummy_client_id, options_for_select(BpSearcher.person_search_preset_options(current_account, @controller_instance, "options_for_select"), selected: nil), {class: 'form-control custom-select selectpicker', id: 'js-calendar-filter-by-person', data: {}, title: "Filter by Person/Pet", multiple: true }

Generated HTML:

<select name="dummy_client_id[]" id="js-calendar-filter-by-person" class="form-control custom-select selectpicker" title="Filter by Person/Pet" multiple="multiple">
  <option data-content="&lt;h4 class=&#39;text-secondary pb-0 mb-0&#39;&gt;Pops  -  Front Desk&lt;/h4&gt;Pops NYC Ltd." data-k="User" data-v="92" data-name="Pops  -  Front Desk" data-remove-preset="true" preserved="false" value="User|92">Pops  -  Front Desk</option>
  <option data-content="&lt;h4 class=&#39;text-secondary pb-0 mb-0&#39;&gt;Nina Knutson&lt;/h4&gt;Pops NYC Ltd." data-k="User" data-v="98" data-name="Nina Knutson" data-remove-preset="true" preserved="false" value="User|98">Nina Knutson</option>
  <option data-content="&lt;h4 class=&#39;text-secondary pb-0 mb-0&#39;&gt;Polly Flak&lt;/h4&gt;Pops NYC Ltd." data-k="User" data-v="4" data-name="Polly Flak" data-remove-preset="true" preserved="false" value="User|4">Polly Flak</option>
  <option data-content="<h4 class='text-primary pb-0 mb-0'>Irene McDons</h4><small><img class=&quot;rounded-circle float-left mr-2&quot; src=&quot;/assets/default/dog-8f91cad3b1e288c0202d05a7a49e15dbab8dde4de8fd5ff75364ba85e87ba879.jpg&quot; width=&quot;24&quot; height=&quot;24&quot; />&nbsp;Arthur</small>" data-k="Client" data-v="103" data-name="Irene McDons" data-remove-preset="true" preserved="false" value="Client|103">Irene McDons - Arthur</option>
  <option data-content="<h4 class='text-primary pb-0 mb-0'>Krista Lamont &  Liam Pirlott</h4><small><img class=&quot;rounded-circle float-left mr-2&quot; src=&quot;/assets/default/dog-8f91cad3b1e288c0202d05a7a49e15dbab8dde4de8fd5ff75364ba85e87ba879.jpg&quot; width=&quot;24&quot; height=&quot;24&quot; />&nbsp;Akira</small>" data-k="Client" data-v="105" data-name="Krista Lamont &amp;  Liam Pirlott" data-remove-preset="true" preserved="false" value="Client|105">Krista Lamont &amp;  Liam Pirlott - Akira</option>
  <option data-content="<h4 class='text-primary pb-0 mb-0'>Kathryn Zaragoza & Tyler Miskinski</h4><small><img class=&quot;rounded-circle float-left mr-2&quot; src=&quot;/assets/default/dog-8f91cad3b1e288c0202d05a7a49e15dbab8dde4de8fd5ff75364ba85e87ba879.jpg&quot; width=&quot;24&quot; height=&quot;24&quot; />&nbsp;Arya</small>" data-k="Client" data-v="109" data-name="Kathryn Zaragoza &amp; Tyler Miskinski" data-remove-preset="true" preserved="false" value="Client|109">Kathryn Zaragoza &amp; Tyler Miskinski - Arya</option>
  <option data-content="<h4 class='text-primary pb-0 mb-0'>Meg Casaszgarcia & Peter Runzinurnik</h4><small><img class=&quot;rounded-circle float-left mr-2&quot; src=&quot;/assets/default/dog-8f91cad3b1e288c0202d05a7a49e15dbab8dde4de8fd5ff75364ba85e87ba879.jpg&quot; width=&quot;24&quot; height=&quot;24&quot; />&nbsp;Baba Looey</small>" data-k="Client" data-v="113" data-name="Meg Casaszgarcia &amp; Peter Runzinurnik" data-remove-preset="true" preserved="false" value="Client|113">Meg Casaszgarcia &amp; Peter Runzinurnik - Baba Looey</option>
  <option data-content="<h4 class='text-primary pb-0 mb-0'>Test  Booking</h4><small>&nbsp;</small>" data-k="Client" data-v="83" data-name="Test  Booking" data-remove-preset="true" preserved="false" value="Client|83">Test  Booking - </option>
</select>
  onmount("#js-calendar-filter-by-person", function () {
    $(this)
      .selectpicker({
        liveSearch: true,
        liveSearchPlaceholder: "Filter by Person",
      })
      .ajaxSelectPicker({
        ajax: {
          url: "/business/bookings/person_search",
          beforeSend: function (xhr) {
            xhr.setRequestHeader(
              "X-CSRF-Token",
              $('meta[name="csrf-token"]').attr("content")
            );
          },
          data: function () {
            var params = {
              q: "{{{q}}}",
            };
            return params;
          },
        },
        clearOnEmpty: true,
        preserveSelectedPosition: "before",
        log: 4,
        emptyRequest: true,
        cache: false,
        preserveSelected: true,
        //preprocessData: function (data) {
        //  return data;
        //},
      });
Nitrodist commented 3 years ago

I think I have fixed it with a few lines that removes the options programmatically upon keydown of the search input (ajax-bootstrap-select listens to keyup, thankfully).

My workaround involves marking the preset options, removing them from the select, notifying bootstrap-selectpicker to refresh the list, and then emptying out the internal selected list that Ajax-Bootstrap-Select uses:

    // Fixes an issue where we want to display an initial list of options but
    // the current behaviour of AjaxBootstrapSelect with preserveSelected set
    // to true is to on keyup after typing into the live search show that
    // initial list as 'currently selected'.
    //
    // This is visually incorrect since the user did not click on any of the
    // initial list of options.
    //
    // See this issue that I reported:
    //
    // https://github.com/truckingsim/Ajax-Bootstrap-Select/issues/199
    $(
      ".js-calendar-filter-by-person-container .bootstrap-select input"
    )[0].addEventListener(
      "keydown",
      (event) => {
        $(
          '.js-calendar-filter-by-person-container [data-remove-preset="true"]'
        ).remove(); // bootstrap-select: remove options and then invoke selectpicker("refresh") on the next line - these are the standard instructions from the bootstrap-select lib.
        $(".selectpicker").selectpicker("refresh"); // bootstrap-select: list is visually removed from UI at this point
        $(".selectpicker").data("AjaxBootstrapSelect").list.selected = []; // Ajax-Bootstrap-Select: this line is needed so that the list is not restored into the `Currently Selected` optgroup upon keydown
      },
      { once: true }
    );
Nitrodist commented 3 years ago

Mmm, I may have fixed it from sticking around after searching, but I also broke the functionality where they actually select an item from the initial list they're presented.

I've made some adjustments to respect when they're actually selected, see below for the code where I use lodash to filter out items that are not selected.

Speaking of which, while I was code spelunking inside the codebase I saw that we rely on jquery's :selected pseudo-filter to know what the preserved list should be. Is it possible to replace that logic going forward with something like this? What do you reckon'?

-      (event) => {
+      (_event) => {
+        // bootstrap-select: remove options and then invoke selectpicker("refresh") on
+        // the next line - these are the standard instructions from the
+        // bootstrap-select lib.
+        //
+        // Also, I think I may be a little bit aggressive here in removing
+        // options against options that may actually be selected, however,
+        // nothing visually or otherwise indicates a problem with this code as
+        // it is and the effect of this code is limited to only one invocation,
+        // so I'm comfortable with this as is.
+        //
+        // Versions:
+        //  "ajax-bootstrap-select": "^1.4.5",
+        //  "bootstrap-select": "^1.13.18",
         $(
           '.js-calendar-filter-by-person-container [data-remove-preset="true"]'
-        ).remove(); // bootstrap-select: remove options and then invoke selectpicker("refresh") on the next line - these are the standard instructions from the bootstrap-select lib.
+        ).remove();
         $(".selectpicker").selectpicker("refresh"); // bootstrap-select: list is visually removed from UI at this point
-        $(".selectpicker").data("AjaxBootstrapSelect").list.selected = []; // Ajax-Bootstrap-Select: this line is needed so that the list is not restored into the `Currently Selected` optgroup upon keydown
+        var actually_selected_for_us_to_keep = _.filter(
+          $(".selectpicker").data("AjaxBootstrapSelect").list.selected,
+          (item) => {
+            return item.selected;
+          }
+        );
+
+        $(".selectpicker").data(
+          "AjaxBootstrapSelect"
+        ).list.selected = actually_selected_for_us_to_keep; // Ajax-Bootstrap-Select: this line is needed so that the list is not restored into the `Currently Selected` optgroup upon keydown