Commit f50f840a by Qiang Xue

Fixes #4955: Replaced callbacks with events for `ActiveForm`

parent 071c2e55
......@@ -258,6 +258,9 @@ Yii Framework 2 Change Log
- Chg #4591: `yii\helpers\Url::to()` will no longer prefix relative URLs with the base URL (qiangxue)
- Chg #4595: `yii\widgets\LinkPager`'s `nextPageLabel`, `prevPageLabel`, `firstPageLabel`, `lastPageLabel` are now taking `false` instead of `null` for "no label" (samdark)
- Chg #4911: Changed callback signature used in `yii\base\ArrayableTrait::fields()` from `function ($field, $model) {` to `function ($model, $field) {` (samdark)
- Chg #4955: Replaced callbacks with events for `ActiveForm` (qiangxue)
- Removed `beforeValidate()`, `beforeValidateAll()`, `afterValidate()`, `afterValidateAll()`, `ajaxBeforeSend()` and `ajaxComplete()` from `ActiveForm`.
- Added `beforeValidate`, `afterValidate`, `beforeSubmit`, `ajaxBeforeSend` and `ajaxComplete` events to `yii.activeForm`.
- Chg: Replaced `clearAll()` and `clearAllAssignments()` in `yii\rbac\ManagerInterface` with `removeAll()`, `removeAllRoles()`, `removeAllPermissions()`, `removeAllRules()` and `removeAllAssignments()` (qiangxue)
- Chg: Added `$user` as the first parameter of `yii\rbac\Rule::execute()` (qiangxue)
- Chg: `yii\grid\DataColumn::getDataCellValue()` visibility is now `public` to allow accessing the value from a GridView directly (cebe)
......
......@@ -213,3 +213,18 @@ new ones save the following code as `convert.php` that should be placed in the s
* `Html::radio()`, `Html::checkbox()`, `Html::radioList()`, `Html::checkboxList()` no longer generate the container
tag around each radio/checkbox when you specify labels for them. You should manually render such container tags,
or set the `item` option for `Html::radioList()`, `Html::checkboxList()` to generate the container tags.
* `beforeValidate()`, `beforeValidateAll()`, `afterValidate()`, `afterValidateAll()`, `ajaxBeforeSend()` and `ajaxComplete()`
are removed from `ActiveForm`. The same functionality is now achieved via JavaScript event mechanism. For example,
if you want to do something before performing validation on the client side, you can write the following
JavaScript code:
```js
$('#myform').on('beforeValidate', function (event, messages, deferreds, attribute) {
if (attribute === undefined) {
// the event is triggered when submitting the form
} elseif (attribute.id === 'something') {
// the event is triggered before validating "something"
}
});
```
......@@ -22,6 +22,66 @@
}
};
var events = {
/**
* beforeValidate event is triggered before validating the whole form and each attribute.
* The signature of the event handler should be:
* function (event, messages, deferreds, attribute)
* where
* - event: an Event object. You can set event.isValid to be false to stop validating the form or attribute
* - messages: error messages. When attribute is undefined, this parameter is an associative array
* with keys being attribute IDs and values being error messages for the corresponding attributes.
* When attribute is given, this parameter is an array of the error messages for that attribute.
* - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation.
* - attribute: an attribute object. Please refer to attributeDefaults for the structure.
* If this is undefined, it means the event is triggered before validating the whole form.
* Otherwise it means the event is triggered before validating the specified attribute.
*/
beforeValidate: 'beforeValidate',
/**
* afterValidate event is triggered after validating the whole form and each attribute.
* The signature of the event handler should be:
* function (event, messages, attribute)
* where
* - event: an Event object.
* - messages: error messages. When attribute is undefined, this parameter is an associative array
* with keys being attribute IDs and values being error messages for the corresponding attributes.
* When attribute is given, this parameter is an array of the error messages for that attribute.
* If the array length is greater than 0, it means the attribute has validation errors.
* - attribute: an attribute object. Please refer to attributeDefaults for the structure.
* If this is undefined, it means the event is triggered before validating the whole form.
* Otherwise it means the event is triggered before validating the specified attribute.
*/
afterValidate: 'afterValidate',
/**
* beforeSubmit event is triggered before submitting the form (after all validations pass).
* The signature of the event handler should be:
* function (event)
* where event is an Event object.
*/
beforeSubmit: 'beforeSubmit',
/**
* ajaxBeforeSend event is triggered before sending an AJAX request for AJAX-based validation.
* The signature of the event handler should be:
* function (event, jqXHR, settings)
* where
* - event: an Event object.
* - jqXHR: a jqXHR object
* - settings: the settings for the AJAX request
*/
ajaxBeforeSend: 'ajaxBeforeSend',
/**
* ajaxComplete event is triggered after completing an AJAX request for AJAX-based validation.
* The signature of the event handler should be:
* function (event, jqXHR, textStatus)
* where
* - event: an Event object.
* - jqXHR: a jqXHR object
* - settings: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror").
*/
ajaxComplete: 'ajaxComplete'
};
// NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well
var defaults = {
// whether to encode the error summary
......@@ -41,28 +101,7 @@
// the type of data that you're expecting back from the server
ajaxDataType: 'json',
// the URL for performing AJAX-based validation. If not set, it will use the the form's action
validationUrl: undefined,
// a callback that is called before submitting the form. The signature of the callback should be:
// function ($form) { ...return false to cancel submission...}
beforeSubmit: undefined,
// a callback that is called before validating each attribute. The signature of the callback should be:
// function ($form, attribute, messages) { ...return false to cancel the validation...}
beforeValidate: undefined,
// a callback that is called before validation starts (This callback is only called when the form is submitted). This signature of the callback should be:
// function($form, data) { ...return false to cancel the validation...}
beforeValidateAll: undefined,
// a callback that is called after an attribute is validated. The signature of the callback should be:
// function ($form, attribute, messages)
afterValidate: undefined,
// a callback that is called after all validation has run (This callback is only called when the form is submitted). The signature of the callback should be:
// function ($form, data, messages)
afterValidateAll: undefined,
// a pre-request callback function on AJAX-based validation. The signature of the callback should be:
// function ($form, jqXHR, textStatus)
ajaxBeforeSend: undefined,
// a function to be called when the request finishes on AJAX-based validation. The signature of the callback should be:
// function ($form, jqXHR, textStatus)
ajaxComplete: undefined
validationUrl: undefined
};
// NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well
......@@ -151,7 +190,7 @@
var $form = $(this),
attributes = $form.data('yiiActiveForm').attributes,
index = -1,
attribute;
attribute = undefined;
$.each(attributes, function (i) {
if (attributes[i]['id'] == id) {
index = i;
......@@ -168,7 +207,8 @@
// find an attribute config based on the specified attribute ID
find: function (id) {
var attributes = $(this).data('yiiActiveForm').attributes, result;
var attributes = $(this).data('yiiActiveForm').attributes,
result = undefined;
$.each(attributes, function (i) {
if (attributes[i]['id'] == id) {
result = attributes[i];
......@@ -189,66 +229,120 @@
return this.data('yiiActiveForm');
},
submitForm: function () {
validate: function () {
var $form = $(this),
data = $form.data('yiiActiveForm');
if (data.validated) {
if (data.settings.beforeSubmit !== undefined) {
if (data.settings.beforeSubmit($form) == false) {
data.validated = false;
data.submitting = false;
return false;
}
data = $form.data('yiiActiveForm'),
needAjaxValidation = false,
messages = {},
deferreds = deferredArray();
if (data.submitting) {
var event = $.Event(events.beforeValidate, {'isValid': true});
$form.trigger(event, [messages, deferreds]);
if (!event.isValid) {
data.submitting = false;
return;
}
// continue submitting the form since validation passes
return true;
}
if (data.settings.timer !== undefined) {
clearTimeout(data.settings.timer);
}
data.submitting = true;
if (data.settings.beforeValidateAll && !data.settings.beforeValidateAll($form, data)) {
data.submitting = false;
return false;
}
validate($form, function (messages) {
var errors = [];
$.each(data.attributes, function () {
if (updateInput($form, this, messages)) {
errors.push(this.input);
// client-side validation
$.each(data.attributes, function () {
// perform validation only if the form is being submitted or if an attribute is pending validation
if (data.submitting || this.status === 2 || this.status === 3) {
var msg = messages[this.id];
if (msg === undefined) {
msg = [];
messages[this.id] = msg;
}
var event = $.Event(events.beforeValidate, {'isValid': true});
$form.trigger(event, [msg, deferreds, this]);
if (event.isValid) {
if (this.validate) {
this.validate(this, getValue($form, this), msg, deferreds);
}
if (this.enableAjaxValidation) {
needAjaxValidation = true;
}
}
});
if (data.settings.afterValidateAll) {
data.settings.afterValidateAll($form, data, messages);
}
updateSummary($form, messages);
if (errors.length) {
var top = $form.find(errors.join(',')).first().offset().top;
var wtop = $(window).scrollTop();
if (top < wtop || top > wtop + $(window).height) {
$(window).scrollTop(top);
});
// ajax validation
$.when.apply(this, deferreds).always(function() {
// Remove empty message arrays
for (var i in messages) {
if (0 === messages[i].length) {
delete messages[i];
}
} else {
data.validated = true;
var $button = data.submitObject || $form.find(':submit:first');
// TODO: if the submission is caused by "change" event, it will not work
if ($button.length) {
$button.click();
} else {
// no submit button in the form
$form.submit();
}
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');
}
return;
$.ajax({
url: data.settings.validationUrl,
type: $form.prop('method'),
data: $form.serialize() + extData,
dataType: data.settings.ajaxDataType,
complete: function (jqXHR, textStatus) {
$form.trigger(events.ajaxComplete, [jqXHR, textStatus]);
},
beforeSend: function (jqXHR, settings) {
$form.trigger(events.ajaxBeforeSend, [jqXHR, settings]);
},
success: function (msgs) {
if (msgs !== null && typeof msgs === 'object') {
$.each(data.attributes, function () {
if (!this.enableAjaxValidation) {
delete msgs[this.id];
}
});
updateInputs($form, $.extend(messages, msgs));
} else {
updateInputs($form, messages);
}
},
error: function () {
data.submitting = false;
}
});
} else if (data.submitting) {
// delay callback so that the form can be submitted without problem
setTimeout(function () {
updateInputs($form, messages);
}, 200);
} else {
updateInputs($form, messages);
}
data.submitting = false;
}, function () {
data.submitting = false;
});
return false;
},
submitForm: function () {
var $form = $(this),
data = $form.data('yiiActiveForm');
if (data.validated) {
var event = $.Event(events.beforeSubmit, {'isValid': true});
$form.trigger(event, [$form]);
if (!event.isValid) {
data.validated = false;
data.submitting = false;
return false;
}
return true; // continue submitting the form since validation passes
} else {
if (data.settings.timer !== undefined) {
clearTimeout(data.settings.timer);
}
data.submitting = true;
methods.validate.call($form);
return false;
}
},
resetForm: function () {
......@@ -275,31 +369,6 @@
}
};
var watchAttributes = function ($form, attributes) {
$.each(attributes, function (i, attribute) {
var $input = findInput($form, attribute);
if (attribute.validateOnChange) {
$input.on('change.yiiActiveForm',function () {
validateAttribute($form, attribute, false);
});
}
if (attribute.validateOnBlur) {
$input.on('blur.yiiActiveForm', function () {
if (attribute.status == 0 || attribute.status == 1) {
validateAttribute($form, attribute, !attribute.status);
}
});
}
if (attribute.validateOnType) {
$input.on('keyup.yiiActiveForm', function () {
if (attribute.value !== getValue($form, attribute)) {
validateAttribute($form, attribute, false);
}
});
}
});
};
var watchAttribute = function ($form, attribute) {
var $input = findInput($form, attribute);
if (attribute.validateOnChange) {
......@@ -356,14 +425,7 @@
$form.find(this.container).addClass(data.settings.validatingCssClass);
}
});
validate($form, function (messages) {
var hasError = false;
$.each(data.attributes, function () {
if (this.status === 2 || this.status === 3) {
hasError = updateInput($form, this, messages) || hasError;
}
});
});
methods.validate.call($form);
}, data.settings.validationDelay);
};
......@@ -379,88 +441,52 @@
};
return array;
};
/**
* Performs validation.
* @param $form jQuery the jquery representation of the form
* @param successCallback function the function to be invoked if the validation completes
* @param errorCallback function the function to be invoked if the ajax validation request fails
* Updates the error messages and the input containers for all applicable attributes
* @param $form the form jQuery object
* @param messages array the validation error messages
*/
var validate = function ($form, successCallback, errorCallback) {
var data = $form.data('yiiActiveForm'),
needAjaxValidation = false,
messages = {},
deferreds = deferredArray();
var updateInputs = function ($form, messages) {
var data = $form.data('yiiActiveForm');
$.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, deferreds);
}
if (this.enableAjaxValidation) {
needAjaxValidation = true;
}
if (data.submitting) {
var errorInputs = [];
$.each(data.attributes, function () {
if (updateInput($form, this, messages)) {
errorInputs.push(this.input);
}
}
});
});
$.when.apply(this, deferreds).always(function() {
//Remove empty message arrays
for (var i in messages) {
if (0 === messages[i].length) {
delete messages[i];
}
}
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');
$form.trigger(events.afterValidate, [messages]);
updateSummary($form, messages);
if (errorInputs.length) {
var top = $form.find(errorInputs.join(',')).first().offset().top;
var wtop = $(window).scrollTop();
if (top < wtop || top > wtop + $(window).height) {
$(window).scrollTop(top);
}
$.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);
data.submitting = false;
} else {
successCallback(messages);
data.validated = true;
var $button = data.submitObject || $form.find(':submit:first');
// TODO: if the submission is caused by "change" event, it will not work
if ($button.length) {
$button.click();
} else {
// no submit button in the form
$form.submit();
}
}
});
} else {
$.each(data.attributes, function () {
if (this.status === 2 || this.status === 3) {
updateInput($form, this, messages);
}
});
}
};
/**
......@@ -475,12 +501,14 @@
$input = findInput($form, attribute),
hasError = false;
if (data.settings.afterValidate) {
data.settings.afterValidate($form, attribute, messages);
if (!$.isArray(messages[attribute.id])) {
messages[attribute.id] = [];
}
$form.trigger(events.afterValidate, [messages[attribute.id], attribute]);
attribute.status = 1;
if ($input.length) {
hasError = messages && $.isArray(messages[attribute.id]) && messages[attribute.id].length;
hasError = messages[attribute.id].length > 0;
var $container = $form.find(attribute.container);
var $error = $container.find(attribute.error);
if (hasError) {
......
......@@ -15,7 +15,6 @@ use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\web\JsExpression;
/**
* ActiveForm is a widget that builds an interactive HTML form for one or multiple data models.
......@@ -146,79 +145,6 @@ class ActiveForm extends Widget
*/
public $ajaxDataType = 'json';
/**
* @var string|JsExpression a JS callback that will be called when the form is being submitted.
* The signature of the callback should be:
*
* ~~~
* function ($form) {
* ...return false to cancel submission...
* }
* ~~~
*/
public $beforeSubmit;
/**
* @var string|JsExpression a JS callback that is called before validating an attribute.
* The signature of the callback should be:
*
* ~~~
* function ($form, attribute, messages) {
* ...return false to cancel the validation...
* }
* ~~~
*/
public $beforeValidate;
/**
* @var string|JsExpression a JS callback that is called before any validation has run (Only called when the form is submitted).
* The signature of the callback should be:
*
* ~~~
* function ($form, data) {
* ...return false to cancel the validation...
* }
* ~~~
*/
public $beforeValidateAll;
/**
* @var string|JsExpression a JS callback that is called after validating an attribute.
* The signature of the callback should be:
*
* ~~~
* function ($form, attribute, messages) {
* }
* ~~~
*/
public $afterValidate;
/**
* @var string|JsExpression a JS callback that is called after all validation has run (Only called when the form is submitted).
* The signature of the callback should be:
*
* ~~~
* function ($form, data, messages) {
* }
* ~~~
*/
public $afterValidateAll;
/**
* @var string|JsExpression a JS pre-request callback function on AJAX-based validation.
* The signature of the callback should be:
*
* ~~~
* function ($form, jqXHR, textStatus) {
* }
* ~~~
*/
public $ajaxBeforeSend;
/**
* @var string|JsExpression a JS callback to be called when the request finishes on AJAX-based validation.
* The signature of the callback should be:
*
* ~~~
* function ($form, jqXHR, textStatus) {
* }
* ~~~
*/
public $ajaxComplete;
/**
* @var array the client validation options for individual attributes. Each element of the array
* represents the validation options for a particular attribute.
* @internal
......@@ -282,11 +208,6 @@ class ActiveForm extends Widget
if ($this->validationUrl !== null) {
$options['validationUrl'] = Url::to($this->validationUrl);
}
foreach (['beforeSubmit', 'beforeValidate', 'beforeValidateAll', 'afterValidate', 'afterValidateAll', 'ajaxBeforeSend', 'ajaxComplete'] as $name) {
if (($value = $this->$name) !== null) {
$options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value);
}
}
// only get the options that are different from the default ones (set in yii.activeForm.js)
return array_diff_assoc($options, [
......
......@@ -140,7 +140,7 @@ class HtmlTest extends TestCase
public function testButton()
{
$this->assertEquals('<button>Button</button>', Html::button());
$this->assertEquals('<button type="button">Button</button>', Html::button());
$this->assertEquals('<button name="test" value="value">content<></button>', Html::button('content<>', ['name' => 'test', 'value' => 'value']));
$this->assertEquals('<button type="submit" class="t" name="test" value="value">content<></button>', Html::button('content<>', ['type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"]));
}
......
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