Warm tip: This article is reproduced from stackoverflow.com, please click
knockout.js

Filter selectlist options based on user input while retaining items that were already selected

发布于 2020-04-23 15:12:58

Html Code

<input type="text" class="radius" placeholder="SerialNo" data-bind="textInput: fromSerialNo" />
<br/>
<select data-bind="options: filteredInventoryList,
                   optionsText: function(item) {
                     return item.Id + ' (' + item.SerialNo + ')';
                   },
                   selectedOptions: selectedEquipment                   
                   " size="5" multiple="multiple" style="width: 300px;"></select>

Sample data (simplified):

  var inventory = [{
      Id: "1",
      SerialNo: "00001"
    },
    {
      Id: "2",
      SerialNo: "00002"
    },
    {
      Id: "3",
      SerialNo: "10003"
    },
    {
      Id: "4",
      SerialNo: "10004"
    }
  ];

Knockout Code:

function viewModel() {
  var _root = this;
  // User input serialNo for filtering
  _root.fromSerialNo = ko.observable();
  // selectedOptions of the select list
  _root.selectedEquipment = ko.observableArray();
  // parent list of all equipment
  _root.fromInventoryList = ko.observableArray(inventory);

  // filtered list based on serialNo user input (should including the previously selected items)
  _root.filteredInventoryList = ko.computed(function() {
      var filteredList = ko.observableArray(null);

      if (!_root.fromSerialNo()) {
        // This works perfect, allows the user to select one or more item from the list.
        return _root.fromInventoryList();
      }
      else {
        // The following works and allow users to filter the parent list of equipment

        // Only show items that begin with the SerialNo entered
        filteredList(ko.utils.arrayFilter(_root.fromInventoryList(), function (item) {
                    return item.SerialNo.startsWith(_root.fromSerialNo());
                }));

        return filteredList();
      }
  });
}

Everything works fine in terms of filtering the list based on the serial number the user inputs. Example here https://jsfiddle.net/JohnnyCodes/5h9pnqLg/.

Use Case:

  • User selected the first item in (Id: 1, Serial 00001)
  • User then enters 1 into the SerialNumber filter
    • I'd like the list to contain the selected item (Id: 1, Serial 00001) as well as the two items whose serial numbers begin with 1

Problem is, it's like there's some kind of recursive reference and the list starts to get wonky.

Enter 1 to filter then change it to 0 then change it back to 1

Here's an example, https://jsfiddle.net/JohnnyCodes/cs4z9xpg/5/

Pretty some this is something really simple and stupid I've overlooked but I've been going around in circles for a while now. Wouldn't mind another pair of eyes.

Thanks

Questioner
Tim Leclair
Viewed
22
Jason Spake 2020-02-11 06:40

The problem is with the following line

// Default the list to include the selected items.
filteredList(_root.selectedEquipment());

Observables use a standard array as their underlying data, but when you initialize an observable using an existing array the observable uses that array as the underlying array instead of creating a new array, and all references to it are preserved. This means that on the next loop through even though your filteredList var has a new scope, it then gets pointed back to the same existing array (selectedEquipment) and that array still has values in it from the last recalc.

Changing the line so that items are added to the observable created at the top of the function without re-using the root level selectedEquipment array should solve the problem:

ko.utils.arrayPushAll(filteredList, _root.selectedEquipment());