0
votes

I have an observable array of formFields which is rendered to a list of input / select fields on my form.

Each field is a separate viewModel and rendered according to its own fieldType property (for example - firstName, lastName, creditCardNumber etc...).

In the database, I keep formTypes definition. For each formType, there is a mapping of all the fields that are displayed for this form type (for example - formType=Address will have this list of formFields - Street, City, Country, ZipCode etc..).

This way, I can create different forms dynamically depending on the form definition and its corresponding formFields.

Now, my problem is that I want to use knockout validation to validate the observableArray itself of the formFields (in addition to each specific formField separate validation which works fine). I mean a validation for the entire form with inter-field dependencies. For example:

Bank Account Form:

Fields: Branch Number, Account Number, Account Holder Name, Bank Name Validation: (Branch Number + Account Number) * 13 % 6 == 3 (just an example).

The fields BranchNumber and AccountNumber are field viewModels inside the formFields observableArray and I need the validation to run for the form each time an item inside the observableArray changes.

My problem is that when the field values inside the array change, the inter-field validation is not re-evaluating the errors() property.

Can anybody help?

My own sample code is way more complex than what I described, this is why I didn't post it here. I hope that was clear enough as is. Thanks!


Short example (the validate is only called on form initialization, how do I make it run each time the values of inner items inside the observableArray are changed):

function FormField(formField, parentForm) {
var self = this;

self.detail = ko.observable().extend({
    validation: {
        validator: function(val) {
            isValid = false;
            if (typeof self.requiredFieldType === "undefined") {
                isValid = true;
            } else if ([3, 5, 22].indexOf(self.requiredFieldType) > -1) { // Email, Product Account Email, Giftcard Delivery Email
                isValid = val !== "" && emailPattern.test($.trim(val));
            } else {
                isValid = $.trim(val) !== "";
            }
            return isValid;
        },
        message: function() {
            if (self.requiredFieldType === 2) {
                return 'Please enter a valid Zip Code.';
            } else if (self.requiredFieldType === 3 || self.requiredFieldType === 22) {
                return 'Invalid Email.';
            } else if (self.requiredFieldType === 5) {
                return 'Invalid Account Email.';
            } else if (self.requiredFieldType === 6) {
                return 'Please select your iTunes store.';
            } else if (self.requiredFieldType === 7) {
                return 'Please enter a phone number.';
            } else if (self.requiredFieldType === 9) {
                return 'Please enter frequent flyer number.';
            } else if (self.requiredFieldType === 10) {
                return 'Please enter street.';
            } else if (self.requiredFieldType === 11) {
                return 'Please enter house #.';
            } else if (self.requiredFieldType === 12) {
                return 'Please enter city.';
            } else if (self.requiredFieldType === 13 || self.requiredFieldType === 23) {
                return 'Please enter country.';
            } else if (self.requiredFieldType === 15) {
                return 'Please enter bank account number.';
            } else if (self.requiredFieldType === 16) {
                return 'Please enter bank account holder name.';
            } else if (self.requiredFieldType === 17) {
                return 'Please enter bank name.';
            } else if (self.requiredFieldType === 18) {
                return 'Please enter bank branch number.';
            } else if (self.requiredFieldType === 19) {
                return 'Please enter first name.';
            } else if (self.requiredFieldType === 20) {
                return 'Please enter last name.';
            } else if (self.requiredFieldType === 21) {
                return 'Please select a currency.';
            }
        }
    }
}).extend({
    required: {
        message: 'This field is required.'
    }
});


self.requiredFieldType = formField.RequiredFieldType;
self.errors = ko.validation.group(self);
}
}

function Form(form) {
var self = this;

self.formName = form.ProductName;
self.formFields = ko.observableArray($.map(form.FormFields, function(jsonFormField) {
    return new FormField(jsonFormField, self)
})).extend({
    validation: {
        validator: function(val) {
            return false; // this is being called only once
        },
        message: function() {}
    }
});

self.errors = ko.validation.group(self);
}
1
I'm afraid we're going to have to see your data structure to understand how it works. Can you distill one example down (like your Bank Account Form) to a minimal demonstration of the problem? Sometimes in making these minimal examples, the solution jumps right out.Roy J
@RoyJ Is it better now? Thanks.Uri Abramson

1 Answers

0
votes

The validator for the observableArray will only be called when there is a subscribable event on it, which does not include modifying elements of the contained array. You can tell the array that it has been updated with

self.formFields.valueHasMutated();

You will need to attach subscriptions to each of the elements, and have the subscription call valueHasMutated, maybe something like:

function validateParent () {
  self.formFields.valueHasMutated();
}
ko.utils.arrayForEach(self.formFields(), function (item) {
  item.detail.subscribe(validateParent);
});

Update: Investigating further, I found this link, which mentions ko.validation.validateObservable(), which seems like a more to-the-point choice than valueHasMutated.

Neither of them will cause all the fields to be re-evaluated. Here's a snippet that includes labels and a "last validated" field, to illustrate what changes when. Notice that the individual fields' labels update each time they change, but not when the other does, even though the last validated time is updated with every change.

I've commented out valueHasMutated in favor of validateObservable, but they work exactly the same.

vm = {
    lastValidated: ko.observable(0)
};

vm.v = ko.observableArray([]).extend({
    validation: {
        validator: function (val) {
            vm.lastValidated((new Date()).getMilliseconds());
        }
    }
});

function addMember(name) {
    var item = ko.observable(name);
    item.subscribe(function (newValue) {
        ko.validation.validateObservable(vm.v);
        //vm.v.valueHasMutated();
    });
    vm.v.push({
        data: item
    });
}

addMember('one');
addMember('two');

ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js"></script>
<div data-bind="foreach:v">
    <input data-bind="value:data, valueUpdate:'input'" />
</div>
<div data-bind="foreach:v">
    <div data-bind="text:data()+((new Date()).getMilliseconds())"></div>
</div>
<div data-bind="text: lastValidated"></div>