Mike
Mike

Reputation: 715

When using knockout validation, isValid reporting incorrectly when bound inside ko.foreach

When using knockout.validation the isValid property will incorrectly report its status but only when it is "inside" a ko.foreach.

Using the code snippet below, if you run and enter the following values (q,z,10) in the same textbox,you will get the following output:

As you can see, the validation rule is for a number and q should not report true as well as 10 should not report false.

It appears the order of execution is messed up because InvoiceAmount.subcribe is executed prior to InvoiceAmount.isValid.subscribe.

When the view model is just a simple property, the results are as expected:

Anyone know why this might be happening?


<Edit>

I'm really bad at asking the right question so I'll try to make it a little more clear what I'm after. Sorry ;)

I need to know if the value of the input is valid when inside the "subscribe" function of the observable that is bound to that input.

I am using the isValid value inside the subscribe function to determine if execution can continue (isValid is true) or if execution must stop (isValid is false).

When the input is bound to an observable inside a ko.foreach, the value of isValid is not updated until AFTER the subscribe function of the observable is executed. This means that I am getting a stale value for isValid when I need to decide if execution can continue or if execution must stop.

</Edit>


ko.bindingHandlers.numeral = {
            init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
            },
            update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                var value = ko.utils.unwrapObservable(valueAccessor()),
                    format = ko.utils.unwrapObservable(allBindingsAccessor().format) || ko.bindingHandlers.numeral.defaultFormat,
                    formattedValue = Number(value) ? numeral(value).format(format) : value;
                $(element).val(formattedValue);
            },
            defaultFormat: "0,0.00"
        };

        var viewModel = function(param) {

            var self = this;
          
            self.item = ko.observable().extend({ number: true });
            self.item.subscribe(function(value) {
                $("#log").append("<p>self.item is " + value + " and self.item.isValid is " + self.item.isValid() + "</p>");
            });

            self.items = ko.mapping.fromJS([]);

            ko.mapping.fromJS(param, {
                key: function(data) {
                    return data.CustomerId;
                },
                create: function(options) {
                    return new itemViewModel(options.data);
                }
            }, self.items);

        };

        var itemViewModel = function(item) {

            var self = this;

            ko.mapping.fromJS(item, {}, self);

            self.InvoiceAmount.extend({ number: true });

            self.InvoiceAmount.subscribe(function(value) {
                $("#log").append("<p>self.InvoiceAmount is " + value + " and self.InvoiceAmount.isValid is " + self.InvoiceAmount.isValid() + "</p>");
            });
        }

        $(function() {
            var vm = new viewModel([{ "InvoiceAmount": 10 }, { "InvoiceAmount": 20 }, { "InvoiceAmount": 30 }, { "InvoiceAmount": 40 }]);
            ko.applyBindings(vm);
        });
.error {
  color: #FF0000;
}
p {
  margin: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/numeral.js/1.5.3/numeral.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.js"></script>

<div>
  single item:<br/>
<input type="text" data-bind="numeral: item"/>
  </div>

<div>
  foreach:<br/>
<table>
    <tbody data-bind="foreach: items">
    <tr>
        <td><input type="text" data-bind="numeral: InvoiceAmount"/></td>
    </tr>
    </tbody>
</table>
</div>

<div id="log"></div>

Upvotes: 0

Views: 664

Answers (1)

Roy J
Roy J

Reputation: 43899

Completely revised reply:

It is most likely a mistake to depend on the order of subscriptions. There are states of transition for a system of dependencies, during which the overall state is inconsistent. The system is designed to get to a consistent state, not to be in one at every instant.

A common trick to give the system time to reach a consistent state is to wrap a setTimeout around code that might otherwise be executed in mid-change. I've modified the code below to do that, and you can see that the reported states are correct.

ko.bindingHandlers.numeral = {
  init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  },
  update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    var value = ko.utils.unwrapObservable(valueAccessor()),
      format = ko.utils.unwrapObservable(allBindingsAccessor().format) || ko.bindingHandlers.numeral.defaultFormat,
      formattedValue = Number(value) ? numeral(value).format(format) : value;
    $(element).val(formattedValue);
  },
  defaultFormat: "0,0.00"
};

var viewModel = function(param) {

  var self = this;

  self.item = ko.observable().extend({
    number: true
  });
  self.item.subscribe(function(value) {
    $("#log").append("<p>self.item is " + value + " and self.item.isValid is " + self.item.isValid() + "</p>");
  });

  self.items = ko.mapping.fromJS([]);

  ko.mapping.fromJS(param, {
    key: function(data) {
      return data.CustomerId;
    },
    create: function(options) {
      return new itemViewModel(options.data);
    }
  }, self.items);

};

var itemViewModel = function(item) {

  var self = this;

  ko.mapping.fromJS(item, {}, self);

  self.InvoiceAmount.extend({
    number: true
  });

  self.InvoiceAmount.subscribe(function(value) {
    setTimeout(function () {
      $("#log").append("<p>self.InvoiceAmount is " + value + " and self.InvoiceAmount.isValid is " + self.InvoiceAmount.isValid() + "</p>");
      }, 0);
  });
}

$(function() {
  var vm = new viewModel([{
    "InvoiceAmount": 10
  }, {
    "InvoiceAmount": 20
  }, {
    "InvoiceAmount": 30
  }, {
    "InvoiceAmount": 40
  }]);
  ko.applyBindings(vm);
});
.error {
  color: #FF0000;
}
p {
  margin: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/numeral.js/1.5.3/numeral.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.js"></script>

<div>
  single item:
  <br/>
  <input type="text" data-bind="numeral: item" />
</div>

<div>
  foreach:
  <br/>
  <table>
    <tbody data-bind="foreach: items">
      <tr>
        <td>
          <input type="text" data-bind="numeral: InvoiceAmount" />
        </td>
      </tr>
    </tbody>
  </table>
</div>

<div id="log"></div>

Upvotes: 2

Related Questions