Search code examples
javascriptknockout.jscomputed-observable

Why is my Knockoutjs Computed Observable Not Working when my Observable Array is being Sorted?


Background Info

I have an Observable array with three item details in it. Each item detail has the following properties; Item, Group, TotalQTY and InputQty. When the user enters the Input Quantity and it matches the Total Quantity the item will be "Verified" which means it gets greyed and moves to the bottom of the list. This is done using a JavaScript sort function in the HTML (see snippet below)

    <tbody data-bind="foreach: ItemsByGroupArray().sort(function (l, r) { return l.Verified() == r.Verified() ? 0 : (l.Verified() < r.Verified() ? -1 : 1 ) } )">   
        <tr data-bind="css: {'verified-item': Verified }">
            <td data-bind="text: $index() + 1"></td>
            <td data-bind="text: ITEM"></td>
            <td data-bind="text: GROUP"></td>
            <td>
                <input type="number" data-bind="value: InputQTY, valueUpdate: ['afterkeydown', 'input']"
                       size="4" min="0" max="9999" step="1" style=" width:50px; padding:0px; margin:0px">                        
            </td>
            <td data-bind="text: TotalQTY"></td>
        </tr>
    </tbody>

As the array is being sorted there is a computed observable that gets processed. This observable is used to check that if each InputQTY in the ItemsByGroupArray matches the TotalQTY. If it does then the Item Detail gets marked as verified. (see snippet)

self.ITEMInputQTYs = ko.computed(function () {
    return ko.utils.arrayForEach(self.ItemsByGroupArray(), function (item) {
        if (item.InputQTY() == item.TotalQTY()) {
            item.Verified(true);
        } else {
            item.Verified(false);
        }
    });
});

Executable Code in which the Snippets Came:

var itemDetail = function (item, group, iQty, tQty, verified) {
    var self = this;
    self.ITEM = ko.observable(item);
    self.GROUP = ko.observable(group);
    self.InputQTY = ko.observable(iQty);
    self.TotalQTY = ko.observable(tQty);
    self.Verified = ko.observable(verified);
};

// The core viewmodel that handles much of the processing logic.
var myViewModel = function () {
    var self = this;

    self.ItemsByGroupArray = ko.observableArray([]);

    self.ITEMInputQTYs = ko.computed(function () {
        return ko.utils.arrayForEach(self.ItemsByGroupArray(), function (item) {
            if (item.InputQTY() == item.TotalQTY()) {
                item.Verified(true);
            } else {
                item.Verified(false);
            }
        });
    });
    
    self.AddItems = function() {
        var newItemData = new itemDetail("ITEM1", "GROUP1", 0, 10, false);
        var newItemData2 = new itemDetail("ITEM2", "GROUP1", 0, 10, false);
        var newItemData3 = new itemDetail("ITEM3", "GROUP1", 0, 10, false);

        self.ItemsByGroupArray.push(newItemData);
        self.ItemsByGroupArray.push(newItemData2);
        self.ItemsByGroupArray.push(newItemData3);        
    };
    
    self.AddItems();
};

ko.applyBindings(new myViewModel());
.verified-item
{
    text-decoration: line-through;
    background: Gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table style="width:90%; margin-left: auto; margin-right: auto;">
    <thead>
        <tr>
            <th></th>
            <th>ITEM</th>
            <th>GROUP</th>
            <th>InputQty</th>
            <th>TotalQty</th>
        </tr>
    </thead>
    
    <!--    <tbody data-bind="foreach: ItemsByGroupArray()">    -->
        
    <tbody data-bind="foreach: ItemsByGroupArray().sort(function (l, r) { return l.Verified() == r.Verified() ? 0 : (l.Verified() < r.Verified() ? -1 : 1 ) } )">
    
        <tr data-bind="css: {'verified-item': Verified }">
            <td data-bind="text: $index() + 1"></td>
            <td data-bind="text: ITEM"></td>
            <td data-bind="text: GROUP"></td>
            <td>
                <input type="number" data-bind="value: InputQTY, valueUpdate: ['afterkeydown', 'input']"
                       size="4" min="0" max="9999" step="1" style=" width:50px; padding:0px; margin:0px">                        
            </td>
            <td data-bind="text: TotalQTY"></td>
        </tr>
    </tbody>
</table>

The Problem

The issue here is if you run the fiddle and perform the following steps "Item3" will not get Verified.

  1. Double click InputQTY for Item1 and change it to a value of 10. Result: Item1 gets verified.
  2. Double click InputQTY for Item2 and change it to a value of 10. Result: Item2 does not get verified.
  3. Double click InputQTY for Item3 and change it to a value of 10. Result: Item2 gets verified but Item3 does not.

My Question

Why is the third item not getting computed as expected and how can I fix this? Also, is this a possible bug in Knockoutjs code?

Thanks in advance for any replies!


Solution

  • this works once because of how knockout picks up its initial computed's variables. But what's actually required is to trigger the observableArray whenever a property of an item changes. I have added the additional code in the AddItem section that fixes your issue.

    var itemDetail = function(item, group, iQty, tQty, verified) {
      var self = this;
      self.ITEM = ko.observable(item);
      self.GROUP = ko.observable(group);
      self.InputQTY = ko.observable(iQty);
      self.TotalQTY = ko.observable(tQty);
      self.Verified = ko.observable(verified);
    };
    
    // The core viewmodel that handles much of the processing logic.
    var myViewModel = function() {
      var self = this;
    
      self.ItemsByGroupArray = ko.observableArray([]);
    
      self.ITEMInputQTYs = ko.computed(function() {
        return ko.utils.arrayForEach(self.ItemsByGroupArray(), function(item) {
          if (item.InputQTY() == item.TotalQTY()) {
            item.Verified(true);
          } else {
            item.Verified(false);
          }
        });
      });
    
      self.AddItems = function() {
        var newItemData = new itemDetail("ITEM1", "GROUP1", 0, 10, false);
        var newItemData2 = new itemDetail("ITEM2", "GROUP1", 0, 10, false);
        var newItemData3 = new itemDetail("ITEM3", "GROUP1", 0, 10, false);
    
        self.ItemsByGroupArray.push(newItemData);
        self.ItemsByGroupArray.push(newItemData2);
        self.ItemsByGroupArray.push(newItemData3);
    
        ko.utils.arrayForEach(self.ItemsByGroupArray(), function(item) {
          item.InputQTY.subscribe(function(val) {
            self.ItemsByGroupArray.valueHasMutated();
          });
        });
      };
    
      self.AddItems();
    };
    
    ko.applyBindings(new myViewModel());
    .verified-item {
      text-decoration: line-through;
      background: Gray;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <table style="width:90%; margin-left: auto; margin-right: auto;">
      <thead>
        <tr>
          <th></th>
          <th>ITEM</th>
          <th>GROUP</th>
          <th>InputQty</th>
          <th>TotalQty</th>
        </tr>
      </thead>
    
      <!--    <tbody data-bind="foreach: ItemsByGroupArray()">    -->
    
      <tbody data-bind="foreach: ItemsByGroupArray().sort(function (l, r) { return l.Verified() == r.Verified() ? 0 : (l.Verified() < r.Verified() ? -1 : 1 ) } )">
    
        <tr data-bind="css: {'verified-item': Verified }">
          <td data-bind="text: $index() + 1"></td>
          <td data-bind="text: ITEM"></td>
          <td data-bind="text: GROUP"></td>
          <td>
            <input type="number" data-bind="value: InputQTY, valueUpdate: ['afterkeydown', 'input']" size="4" min="0" max="9999" step="1" style=" width:50px; padding:0px; margin:0px">
          </td>
          <td data-bind="text: TotalQTY"></td>
        </tr>
      </tbody>
    </table>