Sorting Observable Array in Knockouts JS

2019-08-04 09:46发布

问题:

Ok... so I'm using Twitter Bootstrap. I have an observable array which represents a set of groups. In the UI, this observableArray is being used to render a "tab" and "tab-pane" for each group. I can sort and display these groups by their name property, no probs, doddle right!

<ul data-bind="foreach: myArray().sort(function (l, r) { return l.name() > r.name() ? 1 : -1 })" class="nav nav-tabs" role="tablist">
    <li data-bind="text: name"></li>
</ul>

That's great... however, I have an 'All' object in that same array which needs to be at the beginning of the set of tabs being displayed.

Tabs currently looks like this... A | All | B | C | D

Need to look like this... All | A | B | C | D

Any ideas? :-/

回答1:

Tweak your sort function. In OO fashion, the "All" object would have a property that indicates that it should be ordered at the top. Alternatively, the quick and dirty way to go is to tweak your sort function to something like this:

function (l, r) {
    if (l.name === "All") return 1;
    if (r.name === "All") return -1;
    return l.name().localeCompare(r.name());
}

I think I got the +1/-1 logic right, but you've got unit tests to work out those details, right? ;)

As a side note, I'd use the localeCompare function to compare strings.


Seconding @MattBurland's comment, you should move the sorting logic to your View Model too (this would be required anyways to unit test it). In addition, note that sort will sort the array itself, and you can also call sort on the observable (without invoking it as a function to get the observable's value), to sort the contents of the observable.

Here's what that would look like:

function ViewModel(items) {
  var self = this;
  self.myArray = (items);
  self.myArray.sort(function (l, r) {
      if (l.name() === "All") return -1;
      if (r.name() === "All") return 1;
      return l.name().localeCompare(r.name());
  });
};

var vm = new ViewModel([
  { name: ko.observable("B") },
  { name: ko.observable("A") },
  { name: ko.observable("All") }
]);

ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul data-bind="foreach: myArray" class="nav nav-tabs" role="tablist">
    <li data-bind="text: name"></li>
</ul>

Or like this, if you use the OO approach:

function ViewModel(items) {
  var self = this;
  self.myArray = (items);
  self.myArray.sort(function (l, r) {
      if (!!l.isSpecialOption) return -1;
      if (!!r.isSpecialOption) return 1;
      return l.name().localeCompare(r.name());
  });
};

var vm = new ViewModel([
  { name: ko.observable("B") },
  { name: ko.observable("A") },
  { name: ko.observable("All"), isSpecialOption: true }
]);

ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<ul data-bind="foreach: myArray" class="nav nav-tabs" role="tablist">
    <li data-bind="text: name"></li>
</ul>



回答2:

I'd handle all of this in the ViewModel, rather than doing it in the template. The ideal solution is that your allTab is already separate from your other tabs, but if not you might need to loop through the other tabs and find it.

vm.sortedTabs = ko.computed(function () {
    var allTab = ?;
    var otherTabs = ?; //I don't know enough about your VM to know where you get these

    return [allTab].concat(otherTabs.sort(sortFunction)); 
});

Then your template is just

<ul data-bind="foreach: sortedTabs" class="nav nav-tabs" role="tablist">
    <li data-bind="text: name"></li>
</ul>

But this sort of sorting logic is far easier to deal with in the VM than inline in the template.

And compared to Jeroen's answer, I think it's far clearer to handle the special tab or tabs THEN sort the rest, rather than trying to put logic for handling the special tabs into your sorting alogorithm.