I really like how Eric Barnard's knockout validation lib integrates with observables, allows grouping, & offers custom validator pluggability (including on-the-fly validators). There are a couple of places where it could be more UX flexible/friendly, but overall it's reasonably well-documented... except, imo, when it comes to async validators.
I wrestled with this for a few hours today before doing a search and landing on this. I think I have the same issues/questions as the original author, but agree it wasn't clear exactly what duxa was asking for. I want to bring the question more attention so I am also asking here.
function MyViewModel() {
var self = this;
self.nestedModel1.prop1 = ko.observable().extend({
required: { message: 'Model1 Prop1 is required.' },
maxLength: {
params: 140,
message: '{0} characters max please.'
}
});
self.nestedModel2.prop2 = ko.observable().extend({
required: { message: 'Model2 Prop2 is required' },
validation: {
async: true,
validator: function(val, opts, callback) {
$.ajax({ // BREAKPOINT #1
url: '/validate-remote',
type: 'POST',
data: { ...some data... }
})
.success(function(response) {
if (response == true) callback(true); // BREAKPOINT #2
else callback(false);
});
},
message: 'Sorry, server says no :('
}
});
}
ko.validation.group(self.nestedModel1);
ko.validation.group(self.nestedModel2);
A couple of notes about the code above: There are 2 separate validation groups, one for each nested model. Nested model #1 has no async validators, and nested model #2 has both a sync (required) and an async. The async invokes a server call to validate the inputs. When the server responds, the callback
argument is used to tell ko.validation
whether the user input is good or bad. If you put breakpoints on the lines indicated and trigger validation using a known invalid value, you end up with an infinite loop where the ajax success
function causes the validator
function to be called again. I cracked open the ko.validation
source to see what was going on.
ko.validation.validateObservable = function(observable) {
// set up variables & check for conditions (omitted for brevity)
// loop over validators attached to the observable
for (; i < len; i++) {
if (rule['async'] || ctx['async']) {
//run async validation
validateAsync();
} else {
//run normal sync validation
if (!validateSync(observable, rule, ctx)) {
return false; //break out of the loop
}
}
}
//finally if we got this far, make the observable valid again!
observable.error = null;
observable.__valid__(true);
return true;
}
This function is in a subscription chain attached to the user input observable so that when its value changes, the new value will be validated. The algorithm loops over each validator attached to the input and executes separate functions depending on whether or not the validator is async or not. If sync validation fails, the loop is broken and the whole validateObservable
function exits. If all sync validators pass, the last 3 lines are executed, essentially telling ko.validation
that this input is valid. The __valid__
function in the library looks like this:
//the true holder of whether the observable is valid or not
observable.__valid__ = ko.observable(true);
Two things to take away from this: __valid__
is an observable, and it is set to true
after the validateAsync
function exits. Now let's take a look at validateAsync
:
function validateAsync(observable, rule, ctx) {
observable.isValidating(true);
var callBack = function (valObj) {
var isValid = false,
msg = '';
if (!observable.__valid__()) {
// omitted for brevity, __valid__ is true in this scneario
}
//we were handed back a complex object
if (valObj['message']) {
isValid = valObj.isValid;
msg = valObj.message;
} else {
isValid = valObj;
}
if (!isValid) {
//not valid, so format the error message...
observable.error = ko.validation.formatMessage(...);
observable.__valid__(isValid);
}
// tell it that we're done
observable.isValidating(false);
};
//fire the validator and hand it the callback
rule.validator(observable(), ctx.params || true, callBack);
}
It's important to note that only the first and last lines of this function are executed before ko.validation.validateObservable
sets the __valid__
observable to true and exits. The callBack
function is what gets passed as the 3rd parameter to the async validator
function declared in MyViewModel
. However before this happens, an isValidating
observable's subscribers are invoked to notify that async validation has begun. When the server call is complete, the callback is invoked (in this case just passing either true or false).
Now here's why the breakpoints in MyViewModel
are causing an infinite ping pong loop when server-side validation fails: In the callBack
function above, notice how the __valid__
observable is set to false when validation fails. Here's what happens:
- The invalid user input changes the
nestedModel2.prop2
observable. - The
ko.validation.validateObservable
is notified via subscription of this change. - The
validateAsync
function is invoked. - The custom async validator is invoked, which submits an async
$.ajax
call to the server and exits. - The
ko.validation.validateObservable
sets the__valid__
observable totrue
and exits. - The server returns an invalid response, and
callBack(false)
is executed. - The
callBack
function sets__valid__
tofalse
. - The
ko.validation.validateObservable
is notified of the change to the__valid__
observable (callBack
changed it fromtrue
tofalse
) This essentially repeats step 2 above. - Steps 3, 4, and 5 above are repeated.
- Since the observable's value has not changed, the server returns another invalid response, triggering steps 6, 7, 8, & 9 above.
- We have ourselves a ping pong match.
So it seems like the problem is that the ko.validation.validateObservable
subscription handler is listening to changes not just to the user input value, but also changes to its nested __valid__
observable. Is this a bug, or am I doing something wrong?
A secondary question
You can see from the ko.validation
sources above that a user input value with an async validator is treated as valid while the server is validating it. Because of this, calling nestedModel2.isValid()
cannot be relied on for "the truth". Instead, it looks like we have to use the isValidating
hooks to create subscriptions to the async validators, and only make these decisions after they notify a value of false
. Is this by design? Compared to the rest of the library this seems the most counter intuitive because non async validators don't have an isValidating
to subscribe to, and can rely on .isValid()
to tell the truth. Is this also by design, or am I doing something wrong here as well?