ExpressiveAnnotations
ExpressiveAnnotations copied to clipboard
AssertThat client side validation with promise
I have a dynamic form with lots of fields that are added on the fly to the form and every conditional validation works on the client side (after conditionally updating client side validators) except the one which gets results from the Ajax Call.
Requirement: Unique Email that needs verification from the database.
Here is the annotation of email field Annotation:
[AssertThat("IsUniqueEmail(email, key)", ErrorMessage = "Email address already exists.")]
public string email { get; set; }
Problem is that the expression on the client side returns false/null before the Ajax call completes.
I tried the following:
ea.addMethod('IsUniqueEmail', function (email, id) {
$.get(url, { email: email, id: id }, function (data) {
return data;
});
}
If I convert above statement so that promise is returned like the following, it won't work
return $.get(url, { email: email, id: id }, function (data) {
return data;
});
I then have tried to resolve the promise in chaining fashion like the following, it won't work too
var isUnique = false;
$.get(url, { email: email, id: id }, function (data) {
isUnique = data;
}).then(function(){
return isUnique;
});
What could be the solution ?
I've not tested that logic with asynchronous calls. It needs some investigation from my side, most likely an enhancement (any ideas would be highly appreciated). In short, I don't have a good answer right now.
You can use workaround though - return true
at initial stage (just pretend everything is ok), and after ajax call returns, invalidate field manually using showErrors()
from jquery.validate.js, i.e.
ea.addMethod('IsUnique', function(email, id) {
$.get(url, { email: email, id: id }, function(data) {
if (!data) {
var validator = $('form').validate();
validator.showErrors({ // invalidate field manually
'email': 'Email address already exists.'
});
}
});
return true; // pretend validation passed
});
You can pass more parameters to the function not to hardcode the values, e.g.
ea.addMethod('IsUnique', function(email, id, fieldName, errorMessage) {...
There is also [synchronous ajax call](http://stackoverflow.com/a/133327/270315), however it is rather useless here due to breaking UI responsiveness.
Thanks for the prompt response.
You are right the the synchronous ajax call may not work due to UI responsiveness issue. Other solution, on the other hand, may work temporarily. I'll try to look into the code and see if I can update client side validation to support async callbacks. If so, I'll send you the pull request by weekend.
I've looked at that enhancement, and it is not a trivial task.
My current thinking is to extend the api of additional function for asynchronous methods registration:
api = {
...
addAsyncMethod: function(name, func) {
toolchain.addAsyncMethod(name, func);
},
}
It could be used in the following way:
<script>
ea.addAsyncMethod('IsUnique', function (email, trigger) { // notice 'trigger' argument at last position - it is a callback used to notify ea library, that async request is done
// ! it will be provided and injected by ea library automatically
// its invocation should be executed by the user as soon as ajax request returns
$.get(url, { email: email }, function (result) {
trigger(result); // notify ea that async call is done
});
});
btw: despite the one additional argument at client-side, server-side signature and invocation stays unchanged:
[AssertThat("IsUnique(Email)")]
public string Email { get; set; }
bool IsUnique(string email) { ...
Implementation of addAsyncMethod
could be following:
toolchain = {
...
addAsyncMethod: function(name, func) {
var old = this.methods[name];
this.methods[name] = function () {
if (func.length - 1 === arguments.length) { // check over less number of args (trigger arg will be injected later)
// do stuff before calling function (something like AOP) ------------------------------------------------
var model = this;
var uuid = createGuid(); // create some unique identifier marking this async invocation - for the awaiting code to know what it should actually await for (there can be many async requests) (http://stackoverflow.com/a/2117523/270315)
var args = Array.prototype.slice.call(arguments);
args[arguments.length] = function(result) { // insert one more argument - trigger callback
console.log('async done: (' + result + ') ' + uuid);
$(model).trigger('asyncnoise', { id: uuid, status: 'done', method: name, result: result }); // notify
}
console.log('async start: ' + uuid);
$(this).trigger('asyncnoise', { id: uuid, status: 'start', method: name }); // notify any listener which is interested in such an event
// ----------------------------------------------------------------------------------------
// call the function as it would have been called normally ----------------------------------------------------------------
return func.apply(this, args);
// ----------------------------------------------------------------------------------------
}
if (typeof old === 'function') {
return old.apply(this, arguments);
}
};
},
So now we have implemented some kind of monitoring - when async method is invoked, and when it returns (in possibly least invasive (for users) way I could think of).
This was easy part, the fun begins now, mainly how to await for such async requests when expression is being evaluated...
$.validator.addMethod(method, function(value, element, params) {
...
$(model).on('asyncnoise', function (data, arg) { // catch async invocation, and what next?
async = true;
console.log('asyncnoise catched: ' + arg.id + ' ' + arg.status);
});
modelHelper.ctxEval(params.expression, model);
These are just my neutral thoughts (not suggestions) related to that issue since it seems to be really interesting.
I was able to provide some working code right now. The push is here: https://github.com/JaroslawWaliszko/ExpressiveAnnotations/commit/0bca720c0c113d654c070952d2ad62af82eab547. It is more or less a startup, needs some heavy testing etc. You can review it if you'll find some spare time and give it a try.
Regards
Thanks, I'll review it. I also have plans to work on the code (and testing) on coming weekend. I already have a created a small project that I'm going to use for testing purposes.
Thanks for your input on this question.
Just for your information - I've noticed couple of issues in that code, e.g. when nested asynchronous functions are provided to expressions, etc.
I'm going out for a weekend, so probably I'll not be able to maintain the code, but after my return, I'll most likely reset the master branch to the commit from before my async modifications - last commit will be wiped out from master, and moved to another branch I'll create especially for asynchronous work. It will be called e.g. async-work, where any contributions related to that domain will be pushed. At the same time, master branch will not be disturbed (possible fixes related to more deterministic work will be done there). Only after some time, when async functionality will be mature enough (if ever), newly created branch will be merged to master.
Regards
New async-work branch created.