From 6df21b1575cf7448e5254375ec4e2f7e7a2e7050 Mon Sep 17 00:00:00 2001 From: jordan kapelner Date: Fri, 14 Mar 2014 01:11:05 -0400 Subject: [PATCH] added asyncFuncCall - Validates a field using an asynchronous third party function call --- README.md | 25 +++ demos/demoValidators.html | 37 ++++- js/jquery.validationEngine.js | 289 +++++++++++++++++++++++++--------- tests/asyncfunccall.html | 80 ++++++++++ 4 files changed, 352 insertions(+), 79 deletions(-) create mode 100644 tests/asyncfunccall.html diff --git a/README.md b/README.md index 8e2bbdc..f7f6319 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ The following attribute's value will be loaded for the relative validation rule: * custom * ajax * funcCall +* asyncFuncCall ##### data-errormessage * a generic fall-back error message @@ -500,6 +501,30 @@ The following declaration will do ``` +### asyncFuncCall[methodName] + +Validates a field using an asynchronous third party function call. This behaves similarly to funcCall, but the external function is an asynchronous function - it does not return anything. Instead it must call a callback with the validation result (true if success, false if failure) and an optional message to display. This allows you to validate anything asynchronously (i.e. call ajax, call some 3rd party library, use websockets, etc.) + +```js +function checkHELLO(field, rules, i, options, callback){ + setTimeout(function(){ + if (field.val() === "HELLO") { + callback(true); + } + else { + var msg = options.allrules.validate2fields.alertText; // this allows to use i18 for the error msgs + callback(false, msg); + } + },2000); +} +``` + +The following declaration will do + +```html + +``` + ### ajax[selector] Delegates the validation to a server URL using an asynchronous Ajax request. The selector is used to identify a block of properties in the translation file, take the following for example. diff --git a/demos/demoValidators.html b/demos/demoValidators.html index 6a3a6ea..07b4eb4 100755 --- a/demos/demoValidators.html +++ b/demos/demoValidators.html @@ -31,6 +31,29 @@ return options.allrules.validate2fields.alertText; } } + + /** + * + * @param {jqObject} the field where the validation applies + * @param {Array[String]} validation rules for this field + * @param {int} rule index + * @param {Map} form options + * @returns nothing - call the callback with the result + * function must call the callback with the following parameters: + * @param {boolean} status - true (success) or false (failure) + * @param {string} msg - optional message to display + */ + function asyncCheckHELLO(field, rules, i, options, callback){ + setTimeout(function(){ + if (field.val() === "HELLO") { + callback(true); + } + else { + var msg = options.allrules.validate2fields.alertText; // this allows to use i18 for the error msgs + callback(false, msg); + } + },2000); + } @@ -136,7 +159,19 @@ validate[required,funcCall[checkHELLO]] - + +
+ + Asynchronous Function + + +
+
Conditional required diff --git a/js/jquery.validationEngine.js b/js/jquery.validationEngine.js index ef88e0f..697455d 100644 --- a/js/jquery.validationEngine.js +++ b/js/jquery.validationEngine.js @@ -303,6 +303,11 @@ // cancel form auto-submission - process with async call onAjaxFormComplete return false; } + + //prevent form submission if there are any asynchronous function calls that are in progress (when they all complete, form will get re-submitted) + if (methods._checkAsyncInProgress(options)) { + return false; + } if(options.onValidationComplete) { // !! ensures that an undefined return is interpreted as return false but allows a onValidationComplete() to possibly return true and have form continue processing @@ -441,7 +446,7 @@ } else if(options.focusFirstField) first_err.focus(); return false; - } + } return true; }, /** @@ -687,6 +692,14 @@ case "funcCall": errorMsg = methods._getErrorMessage(form, field, rules[i], rules, i, options, methods._funcCall); break; + case "asyncFuncCall": + // asynchronous function defaults to returning it's loading message - same as ajax + errorMsg = methods._asyncFuncCall(field, rules, i, options); + if (errorMsg) { + promptType = "load"; + } + isAjaxValidator = true; //async function is similar to ajax - the result's not ready yet - so we don't want to handle it yet + break; case "creditCard": errorMsg = methods._getErrorMessage(form, field, rules[i], rules, i, options, methods._creditCard); break; @@ -780,28 +793,40 @@ if (!isAjaxValidator) { field.trigger("jqv.field.result", [field, options.isError, promptText]); - } - - /* Record error */ - var errindex = $.inArray(field[0], options.InvalidFields); - if (errindex == -1) { - if (options.isError) - options.InvalidFields.push(field[0]); - } else if (!options.isError) { - options.InvalidFields.splice(errindex, 1); - } - - methods._handleStatusCssClasses(field, options); - - /* run callback function for each field */ - if (options.isError && options.onFieldFailure) - options.onFieldFailure(field); - - if (!options.isError && options.onFieldSuccess) - options.onFieldSuccess(field); + methods._handleValidationResult(field, options); + } + return options.isError; }, + /** + * Handling css classes and callbacks of fields indicating result of validation + * + * @param {jqObject} + * field + * @param {Array[String]} + * field's validation rules + * @private + */ + _handleValidationResult: function(field, options) { + /* Record error */ + var errindex = $.inArray(field[0], options.InvalidFields); + if (errindex == -1) { + if (options.isError) + options.InvalidFields.push(field[0]); + } else if (!options.isError) { + options.InvalidFields.splice(errindex, 1); + } + + methods._handleStatusCssClasses(field, options); + + /* run callback function for each field */ + if (options.isError && options.onFieldFailure) + options.onFieldFailure(field); + + if (!options.isError && options.onFieldSuccess) + options.onFieldSuccess(field); + }, /** * Handling css classes of fields indicating result of validation * @@ -930,6 +955,7 @@ "minCheckbox": "range-underflow", "equals": "pattern-mismatch", "funcCall": "custom-error", + "asyncFuncCall": "custom-error", "funcCallRequired": "custom-error", "creditCard": "pattern-mismatch", "condRequired": "value-missing" @@ -1088,6 +1114,94 @@ return methods._funcCall(field,rules,i,options); }, /** + * Return true if there are any asynchronous function calls in progress + * @param {Object} options + * @return true if any asynchronous function is in progress, false if not + */ + _checkAsyncInProgress: function(options) { + var status = false; + $.each(options.asyncInProgress, function(key, value) { + if (value) { + status = true; + // break the each + return false; + } + }); + return status; + }, + /** + * Validate custom asynchronous function outside of the engine scope + * @param {jqObject} field + * @param {Array[String]} rules + * @param {int} i rules index + * @param {Map} + * user options + * @return just some "load" text - same as ajax + * This function is similar to funcCall where it calls a custom external function - however, it does not return + * a result right away. Instead it passes a callback to the external function, which must + * call it with the following parameters: + * @param {boolean} status - true (success) or false (failure) + * @param {string} msg - optional message to display + */ + _asyncFuncCall: function(field, rules, i, options) { + var functionName = rules[i + 1]; + var fn; + var rule = { + alertText: 'Field is invalid', + alertTextOk: null, + alertTextLoad: 'Validating...' + }; + + $.extend(rule, options.allrules[functionName]); + + if(functionName.indexOf('.') >-1) + { + var namespaces = functionName.split('.'); + var scope = window; + while(namespaces.length) + { + scope = scope[namespaces.shift()]; + } + fn = scope; + } + else + fn = window[functionName] || options.customFunctions[functionName]; + + if (typeof(fn) == 'function') { + // If a field change event triggered this we want to clear the cache for this ID + if ((options.eventTrigger == "field") || !options.binded) { + delete(options.ajaxValidCache[field.attr("id")]); + } + + // If there is an error or if the the field is already validated, do not re-execute the function + if (!options.isError && (options.ajaxValidCache[field.attr("id")] === undefined)) { + options.asyncInProgress[field.attr("id")] = true; + + fn(field, rules, i, options, function(status, msg) { + options.asyncInProgress[field.attr("id")] = false; + methods._ajaxSuccess(field, status, msg, options, rule); //this gets called from the external function's callback, so we'll do the same thing that ajax does when it completes + + if ((options.eventTrigger == 'submit') && !methods._checkAsyncInProgress(options)) { //if a form submission triggered this and there are no other asynchronous functions in progress + if (methods._checkAjaxStatus(options)) { //if all asynchronous (including ajax) validations passed + field.closest("form").submit(); //resubmit the form + } + else { + //trigger callback events if necessary for displaying errors + if (options.onValidationComplete) { + options.onValidationComplete(field.closest("form"), false); + } + } + } + }); + + return rule.alertTextLoad; + } + else if (options.ajaxValidCache[field.attr("id")] === false) { //if validation previously failed, but did not change + return {status: '_error_no_prompt'}; //force our caller to fail and bail (don't change the prompt) + } + } + }, + /** * Field match * * @param {jqObject} field @@ -1463,70 +1577,85 @@ } }, success: function(json) { - - // asynchronously called on success, data is the json answer from the server - var errorFieldId = json[0]; - //var errorField = $($("#" + errorFieldId)[0]); - var errorField = $("#"+ errorFieldId).eq(0); - - // make sure we found the element - if (errorField.length == 1) { - var status = json[1]; - // read the optional msg from the server - var msg = json[2]; - if (!status) { - // Houston we got a problem - display an red prompt - options.ajaxValidCache[errorFieldId] = false; - options.isError = true; - - // resolve the msg prompt - if(msg) { - if (options.allrules[msg]) { - var txt = options.allrules[msg].alertText; - if (txt) { - msg = txt; - } - } - } - else - msg = rule.alertText; - - if (options.showPrompts) methods._showPrompt(errorField, msg, "", true, options); - } else { - options.ajaxValidCache[errorFieldId] = true; - - // resolves the msg prompt - if(msg) { - if (options.allrules[msg]) { - var txt = options.allrules[msg].alertTextOk; - if (txt) { - msg = txt; - } - } - } - else - msg = rule.alertTextOk; - - if (options.showPrompts) { - // see if we should display a green prompt - if (msg) - methods._showPrompt(errorField, msg, "pass", true, options); - else - methods._closePrompt(errorField); - } - - // If a submit form triggered this, we want to re-submit the form - if (options.eventTrigger == "submit") - field.closest("form").submit(); - } - } - errorField.trigger("jqv.field.result", [errorField, options.isError, msg]); + // asynchronously called on success, data is the json answer from the server + var errorFieldId = json[0]; + var status = json[1]; + var msg = json[2]; + //var errorField = $($("#" + errorFieldId)[0]); + var errorField = $("#"+ errorFieldId).eq(0); + + methods._ajaxSuccess(errorField, status, msg, options, rule); + + // If ajax was successful and a submit form triggered this and there are no asynchronous functions in progress, we want to re-submit the form + if ((options.eventTrigger == "submit") && methods._checkAjaxFieldStatus(errorFieldId, options) && !methods._checkAsyncInProgress(options)) { + errorField.closest("form").submit(); + } } }); return rule.alertTextLoad; } }, + + /** + * Common method to handle ajax success + * + * @param {string} errorFieldId + * @param {boolean} status + * @param {string} msg + * @param {object} options + */ + _ajaxSuccess: function(errorField, status, msg, options, rule){ + var errorFieldId = errorField.attr("id"); + + // make sure we found the element + if (errorField.length == 1) { + if (!status) { + // Houston we got a problem - display an red prompt + options.ajaxValidCache[errorFieldId] = false; + options.isError = true; + + // resolve the msg prompt + if(msg) { + if (options.allrules[msg]) { + var txt = options.allrules[msg].alertText; + if (txt) { + msg = txt; + } + } + } + else + msg = rule.alertText; + + if (options.showPrompts) methods._showPrompt(errorField, msg, "", true, options); + } else { + options.ajaxValidCache[errorFieldId] = true; + + // resolves the msg prompt + if(msg) { + if (options.allrules[msg]) { + var txt = options.allrules[msg].alertTextOk; + if (txt) { + msg = txt; + } + } + } + else + msg = rule.alertTextOk; + + if (options.showPrompts) { + // see if we should display a green prompt + if (msg) + methods._showPrompt(errorField, msg, "pass", true, options); + else + methods._closePrompt(errorField); + } + } + methods._handleValidationResult(errorField, options); + } + errorField.trigger("jqv.field.result", [errorField, options.isError, msg]); + }, + /** * Common method to handle ajax errors * @@ -2098,6 +2227,10 @@ // Caches field validation status, typically only bad status are created. // the array is used during ajax form validation to detect issues early and prevent an expensive submit ajaxValidCache: {}, + + // keeps track of asyncronous function calls that are pending to prevent form submission before they complete + asyncInProgress: {}, + // Auto update prompt position after window resize autoPositionUpdate: false, diff --git a/tests/asyncfunccall.html b/tests/asyncfunccall.html new file mode 100644 index 0000000..b013b50 --- /dev/null +++ b/tests/asyncfunccall.html @@ -0,0 +1,80 @@ + + + + + JQuery Validation Engine + + + + + + + + +

+ Evaluate form +

+

+ Test using an asynchronous function +
+

+
This is a div element
+
+
+ + Asynchronous Function + + + +
+
+
+ +