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:
- `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 <<<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
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
- 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)
......
......@@ -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);
}
}
});
};
/**
......
......@@ -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) . '}');
}
}
......
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