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:
nestedModel2.prop2
observable.ko.validation.validateObservable
is notified via subscription of this change.validateAsync
function is invoked.$.ajax
call to the server and exits.ko.validation.validateObservable
sets the __valid__
observable to true
and exits.callBack(false)
is executed.callBack
function sets __valid__
to false
.ko.validation.validateObservable
is notified of the change to the __valid__
observable (callBack
changed it from true
to false
) This essentially repeats step 2 above.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?
The async validator's validate method returns a Promise that resolves if validation passes and value is updated or rejects with an Error if validation does not pass and value is not updated. The async validator also has a hint field that returns a Promise that when resolved will return a hint.
So the question I asked really had to do with how to use async validators in ko.validation. There are 2 big takeaways that I have learned from my experience:
Do not create async
Anonymous or Single-Use Custom Rule validators. Instead, create them as Custom Rules. Otherwise you will end up with the infinite loop / ping ping match described in my question.
If you use async
validators, don't trust isValid()
until all async
validators' isValidating
subscriptions
change to false.
If you have multiple async validators, you can use a pattern like the following:
var viewModel = {
var self = this;
self.prop1 = ko.observable().extend({validateProp1Async: self});
self.prop2 = ko.observable().extend({validateProp2Async: self});
self.propN = ko.observable();
self.isValidating = ko.computed(function() {
return self.prop1.isValidating() || self.prop2.isValidating();
});
self.saveData = function(arg1, arg2, argN) {
if (self.isValidating()) {
setTimeout(function() {
self.saveData(arg1, arg2, argN);
}, 50);
return false;
}
if (!self.isValid()) {
self.errors.showAllMessages();
return false;
}
// data is now trusted to be valid
$.post('/something', 'data', function() { doWhatever() });
}
};
You can also see this for another reference with similar alternate solutions.
Here is an example of an async "custom rule":
var validateProp1Async = {
async: true,
message: 'you suck because your input was wrong fix it or else',
validator: function(val, otherVal, callback) {
// val will be the value of the viewmodel's prop1() observable
// otherVal will be the viewmodel itself, since that was passed in
// via the .extend call
// callback is what you need to tell ko.validation about the result
$.ajax({
url: '/path/to/validation/endpoint/on/server',
type: 'POST', // or whatever http method the server endpoint needs
data: { prop1: val, otherProp: otherVal.propN() } // args to send server
})
.done(function(response, statusText, xhr) {
callback(true); // tell ko.validation that this value is valid
})
.fail(function(xhr, statusText, errorThrown) {
callback(false); // tell ko.validation that his value is NOT valid
// the above will use the default message. You can pass in a custom
// validation message like so:
// callback({ isValid: false, message: xhr.responseText });
});
}
};
Basically, you use the callback
arg to the validator
function to tell ko.validation whether or not validation succeeded. That call is what will trigger the isValidating
observables on the validated property observables to change back to false
(meaning, async validation has completed and it is now known whether the input was valid or not).
The above will work if your server-side validation endpoints return an HTTP 200 (OK) status when validation succeeds. That will cause the .done
function to execute, since it is the equivalent of the $.ajax
success
. If your server returns an HTTP 400 (Bad Request) status when validation fails, it will trigger the .fail
function to execute. If your server returns a custom validation message back with the 400, you can get that from xhr.responseText
to effectively override the default you suck because your input was wrong fix it or else
message.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With