Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validating observableArray against a condition

I am trying to experiment and see if there are any clever solutions for creating custom validators that can be abstracted for neatness and reuse.

In the jsfiddle below, I just put together a simple parent model that stores and array of Measurements (just a value and a date). In this example, I put in two requirements.

  1. Each Measurement has either both fields supplied, or neither must be supplied.
  2. There has to be at least one valid (meets previous condition) measurement in parent array.

    Ideally, I want the validation logic for what defines being valid, stored within the Measurement object as I have done below. But, the thing I am very distastful of is the "hands-on" validation I am having to perform in the parent model in atLeastOne().

Knockout validation will automatically validate the individual fields for numbers and dates BUT, I have to step in and perform validation against the rule for the array.

Question: Are the any approaches that allow me to setup KO validation to check the array for this the required condition while still having the HasValues method still reside in the Measurement Model??? I.e I want to abstract the concept of searching for "at least one" into a custom validator of some sort that can handle the job for me, and then just tell this validator "hey, here is the function I want you to use to validate each item in the array."

Thanks in advance!

    function Model(data) 
    {
       var self = this;
        self.Measurements = ko.observableArray();

       for(var i = 0; i < data.length; i++)
           self.Measurements.push(new Measurement(data[i]));

        function hasAtLeastOne(){
           var atLeastOne = false;
            $.each(self.Measurements(), function(i, item) { 
                if (item.HasValues()) { 
                   atLeastOne = true; 
                   return; 
                 } 
            });
            return atLeastOne;
        }

        self.Save = function() {               
            if (self.canSave() && atLeastOne())
                alert('save');
            else
                alert('arg!');
        };

        self.errors = ko.validation.group(self);
        self.canSave = ko.computed(function() {
            return self.errors().length == 0;
        });
    }

    function Measurement(data)
    {
       var self = this;
       self.Value = ko.observable(data.val);
       self.Date = ko.observable(data.date);

       self.Value.extend({ required: { onlyIf: isRequired }, number: true });
       self.Date.extend({ required: { onlyIf: isRequired }, date: true });

        self.HasValues = function() {
            return ko.utils.isNotNullUndefOrEmpty(self.Value()) && 
                   self.Date() && self.Date().length > 0;
        };

        function isRequired() {
            return ko.utils.isNotNullUndefOrEmpty(self.Value()) || 
                   (self.Date() && self.Date().length > 0);
        }
    }

    ko.utils.isNotNullUndefOrEmpty = function (value) {
        return (typeof value === 'string' && value.length > 0) || 
               (typeof value !== 'string' && value);
    };

Here is a jsfiddle to play with that has my example: http://jsfiddle.net/cACZ9/

like image 480
Matthew Cox Avatar asked Nov 28 '12 17:11

Matthew Cox


1 Answers

I have been mucking around the library source to see if I could discover something that would be a viable option. Here is what I found so far.

Two different options both with pros/cons (of course):

Using Custom Validator:

 ko.validation.rules['AtLeastOne'] = {
        validator: function (array, predicate) {
            var self = this;
            self.predicate = predicate;
            return ko.utils.arrayFirst(array, function (item) {
                return self.predicate.call(item);
            }) != null; 
        },
        message: 'The array must contain at least one valid element.'
    };


  function Modal() {
     var self = this;
     self.Measurements = ko.observableArray().extend({ AtLeastOne: function () {
        return this && this.HasValues();
     }

     ...//refer to OP
     ...
     ...

     self.Save() = function() {
         if (self.errors().length == 0)
           alert('Everything is valid, we can save!');
       else if (!self.Measurements.isValid())
           alert('You must have at least one valid item in the pot!');
     };
  });

This approach pretty much takes the validation out of the programmer's hands and is very reusable. However, I noticed that one potential con is that it will call the custom validation rule every time any of the values stored inside the array (objects or otherwise) have mutated. May not be an issue for most.

Using a validator factory:

 var KoValidationFactory = {
        AtLeastOne: function (measurements, validator) {
            return function () {
                var self = this;
                self.validator = validator;
                return ko.utils.arrayFirst(measurements, function (measurement) {
                            return self.validator.call(measurement);
                        }) != null; 
            };
        }

   };

   function Modal() {
      var self = this;
      self.Measurements = ko.observableArray();

     ...//refer to OP
     ...
     ...

      self.Save = function () {
          var atLeastOneArrayValidator = KoValidationFactory.AtLeastOne(self.Measurements(), function () {
               return this && this.HasValues();
       });
          var arrayWasValid = atLeastOneArrayValidator();
          if (arrayWasValid && self.errors() == 0)
             alert('everything is good, we can save');
          else if (!arrayWasValid)
             alert('You must have at least one item in the pot!');
       };
   }

This approach can ensure you only validate the entire array when you explicitly choose to do so. Down-side is that you have more hands-on work and it doesn't take full advantage of the knockout validation library. You must specifically be validating the array and any/all other observables that take this approach which could potentially become messy if there were many of them.

I encourage edits and suggestions on these approaches.

like image 145
Matthew Cox Avatar answered Oct 14 '22 12:10

Matthew Cox