diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index 8acfb6e322..532945d211 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -487,6 +487,7 @@ predefined variables: - `attribute`: the name of the attribute being validated. - `value`: the value being validated. - `messages`: an array used to hold the validation error messages for the attribute. +- `deferred`: an array which deferred objects can be pushed into. In the following example, we create a `StatusValidator` which validates if an input is a valid status input against the existing status data. The validator supports both server side and client side validation. @@ -535,6 +536,57 @@ JS; ] ``` +### Deferred validation + +If you need to perform any asynchronous validation you can use a [deferred object](http://api.jquery.com/category/deferred-object/). + +deferred objects must be pushed to the ```deferred``` array for validation to use them. +Once any asynchronous validation has finished you must call ```resolve()``` on the Deferred object for it to complete. + +This example shows reading an image to check the dimensions client side (```file``` will be from an input of type=file). +```php +... +public function clientValidateAttribute($model, $attribute, $view) +{ + return << 150) { + messages.push('Image too wide!!'); + } + def.resolve(); + } + var reader = new FileReader(); + reader.onloadend = function() { + img.src = reader.result; + } + reader.readAsDataURL(file); + + deferred.push(def); +JS; +} +... +``` + +Ajax can also be used and pushed straight into the deferred array. +``` +deferred.push($.get("/check", {value: value}).done(function(data) { + if ('' !== data) { + messages.push(data); + } +})); +``` + +The ```deferred``` array also has a shortcut method ```add```. +``` +deferred.add(function(def) { + //Asynchronous Validation here + //The context of this function and the first argument is the Deferred object where resolve can be called. +}); +``` +> Note: `resolve` must be called on any deferred objects after the attribute has been validated or the main form validation will not complete. + ### Ajax validation Some kind of validation can only be done on server side because only the server has the necessary information diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 23138182cf..e0e8a35891 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -165,6 +165,7 @@ Yii Framework 2 Change Log - Enh #4317: Added `absoluteAuthTimeout` to yii\web\User (ivokund, nkovacs) - Enh #4360: Added client validation support for file validator (Skysplit) - Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma) +- Enh #4485: Added support for deferred validation in `ActiveForm` (Alex-Code) - Enh #4520: Added sasl support to `yii\caching\MemCache` (xjflyttp) - Enh: Added support for using sub-queries when building a DB query with `IN` condition (qiangxue) - Enh: Supported adding a new response formatter without the need to reconfigure existing formatters (qiangxue) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 9f8c6159d3..bab3fb5936 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -270,7 +270,20 @@ }); }, data.settings.validationDelay); }; - + + /** + * Returns an array prototype with a shortcut method for adding a new deferred. + * The context of the callback will be the deferred object so it can be resolved like ```this.resolve()``` + * @returns Array + */ + var deferredArray = function () { + var array = []; + array.add = function(callback) { + this.push(new $.Deferred(callback)); + }; + return array; + }; + /** * Performs validation. * @param $form jQuery the jquery representation of the form @@ -280,70 +293,78 @@ var validate = function ($form, successCallback, errorCallback) { var data = $form.data('yiiActiveForm'), needAjaxValidation = false, - messages = {}; + messages = {}, + deferreds = deferredArray(); $.each(data.attributes, function () { if (data.submitting || this.status === 2 || this.status === 3) { var msg = []; + messages[this.id] = msg; if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) { if (this.validate) { - this.validate(this, getValue($form, this), msg); + this.validate(this, getValue($form, this), msg, deferreds); } - if (msg.length) { - messages[this.id] = msg; - } else if (this.enableAjaxValidation) { + if (this.enableAjaxValidation) { needAjaxValidation = true; } } } }); - if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { - // Perform ajax validation when at least one input needs it. - // If the validation is triggered by form submission, ajax validation - // should be done only when all inputs pass client validation - var $button = data.submitObject, - extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id'); - if ($button && $button.length && $button.prop('name')) { - extData += '&' + $button.prop('name') + '=' + $button.prop('value'); + $.when.apply(this, deferreds).always(function() { + //Remove empty message arrays + for (var i in messages) { + if (0 === messages[i].length) { + delete messages[i]; + } } - $.ajax({ - url: data.settings.validationUrl, - type: $form.prop('method'), - data: $form.serialize() + extData, - dataType: data.settings.ajaxDataType, - complete: function (jqXHR, textStatus) { - if (data.settings.ajaxComplete) { - data.settings.ajaxComplete($form, jqXHR, textStatus); - } - }, - beforeSend: function (jqXHR, textStatus) { - if (data.settings.ajaxBeforeSend) { - data.settings.ajaxBeforeSend($form, jqXHR, textStatus); - } - }, - success: function (msgs) { - if (msgs !== null && typeof msgs === 'object') { - $.each(data.attributes, function () { - if (!this.enableAjaxValidation) { - delete msgs[this.id]; - } - }); - successCallback($.extend({}, messages, msgs)); - } else { - successCallback(messages); - } - }, - error: errorCallback - }); - } else if (data.submitting) { - // delay callback so that the form can be submitted without problem - setTimeout(function () { + if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { + // Perform ajax validation when at least one input needs it. + // If the validation is triggered by form submission, ajax validation + // should be done only when all inputs pass client validation + var $button = data.submitObject, + extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id'); + if ($button && $button.length && $button.prop('name')) { + extData += '&' + $button.prop('name') + '=' + $button.prop('value'); + } + $.ajax({ + url: data.settings.validationUrl, + type: $form.prop('method'), + data: $form.serialize() + extData, + dataType: data.settings.ajaxDataType, + complete: function (jqXHR, textStatus) { + if (data.settings.ajaxComplete) { + data.settings.ajaxComplete($form, jqXHR, textStatus); + } + }, + beforeSend: function (jqXHR, textStatus) { + if (data.settings.ajaxBeforeSend) { + data.settings.ajaxBeforeSend($form, jqXHR, textStatus); + } + }, + success: function (msgs) { + if (msgs !== null && typeof msgs === 'object') { + $.each(data.attributes, function () { + if (!this.enableAjaxValidation) { + delete msgs[this.id]; + } + }); + successCallback($.extend({}, messages, msgs)); + } else { + successCallback(messages); + } + }, + error: errorCallback + }); + } else if (data.submitting) { + // delay callback so that the form can be submitted without problem + setTimeout(function () { + successCallback(messages); + }, 200); + } else { successCallback(messages); - }, 200); - } else { - successCallback(messages); - } + } + }); }; /** diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 956e0d9ce8..636db97fe5 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -703,7 +703,7 @@ class ActiveField extends Component } } if (!empty($validators)) { - $options['validate'] = new JsExpression("function (attribute, value, messages) {" . implode('', $validators) . '}'); + $options['validate'] = new JsExpression("function (attribute, value, messages, deferred) {" . implode('', $validators) . '}'); } }