Commit f74e2081 by Qiang Xue

Merge branch 'deferred-validation' of github.com:Alex-Code/yii2 into Alex-Code-deferred-validation

Conflicts: framework/CHANGELOG.md
parents 201f0abb b6071007
...@@ -487,6 +487,7 @@ predefined variables: ...@@ -487,6 +487,7 @@ predefined variables:
- `attribute`: the name of the attribute being validated. - `attribute`: the name of the attribute being validated.
- `value`: the value being validated. - `value`: the value being validated.
- `messages`: an array used to hold the validation error messages for the attribute. - `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 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. against the existing status data. The validator supports both server side and client side validation.
...@@ -535,6 +536,57 @@ JS; ...@@ -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 <<<JS
var def = $.Deferred();
var img = new Image();
img.onload = function() {
if (this.width > 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 ### Ajax validation
Some kind of validation can only be done on server side because only the server has the necessary information Some kind of validation can only be done on server side because only the server has the necessary information
......
...@@ -165,6 +165,7 @@ Yii Framework 2 Change Log ...@@ -165,6 +165,7 @@ Yii Framework 2 Change Log
- Enh #4317: Added `absoluteAuthTimeout` to yii\web\User (ivokund, nkovacs) - Enh #4317: Added `absoluteAuthTimeout` to yii\web\User (ivokund, nkovacs)
- Enh #4360: Added client validation support for file validator (Skysplit) - Enh #4360: Added client validation support for file validator (Skysplit)
- Enh #4436: Added callback functions to AJAX-based form validation (thiagotalma) - 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 #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: 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) - Enh: Supported adding a new response formatter without the need to reconfigure existing formatters (qiangxue)
......
...@@ -270,7 +270,20 @@ ...@@ -270,7 +270,20 @@
}); });
}, data.settings.validationDelay); }, 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. * Performs validation.
* @param $form jQuery the jquery representation of the form * @param $form jQuery the jquery representation of the form
...@@ -280,70 +293,78 @@ ...@@ -280,70 +293,78 @@
var validate = function ($form, successCallback, errorCallback) { var validate = function ($form, successCallback, errorCallback) {
var data = $form.data('yiiActiveForm'), var data = $form.data('yiiActiveForm'),
needAjaxValidation = false, needAjaxValidation = false,
messages = {}; messages = {},
deferreds = deferredArray();
$.each(data.attributes, function () { $.each(data.attributes, function () {
if (data.submitting || this.status === 2 || this.status === 3) { if (data.submitting || this.status === 2 || this.status === 3) {
var msg = []; var msg = [];
messages[this.id] = msg;
if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) { if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) {
if (this.validate) { if (this.validate) {
this.validate(this, getValue($form, this), msg); this.validate(this, getValue($form, this), msg, deferreds);
} }
if (msg.length) { if (this.enableAjaxValidation) {
messages[this.id] = msg;
} else if (this.enableAjaxValidation) {
needAjaxValidation = true; needAjaxValidation = true;
} }
} }
} }
}); });
if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { $.when.apply(this, deferreds).always(function() {
// Perform ajax validation when at least one input needs it. //Remove empty message arrays
// If the validation is triggered by form submission, ajax validation for (var i in messages) {
// should be done only when all inputs pass client validation if (0 === messages[i].length) {
var $button = data.submitObject, delete messages[i];
extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id'); }
if ($button && $button.length && $button.prop('name')) {
extData += '&' + $button.prop('name') + '=' + $button.prop('value');
} }
$.ajax({ if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) {
url: data.settings.validationUrl, // Perform ajax validation when at least one input needs it.
type: $form.prop('method'), // If the validation is triggered by form submission, ajax validation
data: $form.serialize() + extData, // should be done only when all inputs pass client validation
dataType: data.settings.ajaxDataType, var $button = data.submitObject,
complete: function (jqXHR, textStatus) { extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id');
if (data.settings.ajaxComplete) { if ($button && $button.length && $button.prop('name')) {
data.settings.ajaxComplete($form, jqXHR, textStatus); extData += '&' + $button.prop('name') + '=' + $button.prop('value');
} }
}, $.ajax({
beforeSend: function (jqXHR, textStatus) { url: data.settings.validationUrl,
if (data.settings.ajaxBeforeSend) { type: $form.prop('method'),
data.settings.ajaxBeforeSend($form, jqXHR, textStatus); data: $form.serialize() + extData,
} dataType: data.settings.ajaxDataType,
}, complete: function (jqXHR, textStatus) {
success: function (msgs) { if (data.settings.ajaxComplete) {
if (msgs !== null && typeof msgs === 'object') { data.settings.ajaxComplete($form, jqXHR, textStatus);
$.each(data.attributes, function () { }
if (!this.enableAjaxValidation) { },
delete msgs[this.id]; beforeSend: function (jqXHR, textStatus) {
} if (data.settings.ajaxBeforeSend) {
}); data.settings.ajaxBeforeSend($form, jqXHR, textStatus);
successCallback($.extend({}, messages, msgs)); }
} else { },
successCallback(messages); success: function (msgs) {
} if (msgs !== null && typeof msgs === 'object') {
}, $.each(data.attributes, function () {
error: errorCallback if (!this.enableAjaxValidation) {
}); delete msgs[this.id];
} else if (data.submitting) { }
// delay callback so that the form can be submitted without problem });
setTimeout(function () { 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); successCallback(messages);
}, 200); }
} else { });
successCallback(messages);
}
}; };
/** /**
......
...@@ -703,7 +703,7 @@ class ActiveField extends Component ...@@ -703,7 +703,7 @@ class ActiveField extends Component
} }
} }
if (!empty($validators)) { if (!empty($validators)) {
$options['validate'] = new JsExpression("function (attribute, value, messages) {" . implode('', $validators) . '}'); $options['validate'] = new JsExpression("function (attribute, value, messages, deferred) {" . implode('', $validators) . '}');
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment