Fixes #4955: Replaced callbacks with events for ActiveForm

This commit is contained in:
Qiang Xue
2014-09-09 22:37:27 -04:00
parent 071c2e55a0
commit f50f840a55
5 changed files with 235 additions and 268 deletions

View File

@ -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');
},
validate: function () {
var $form = $(this),
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;
}
}
// 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;
}
}
}
});
// 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];
}
}
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) {
$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);
}
});
},
submitForm: 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;
}
}
// 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;
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;
}
validate($form, function (messages) {
var errors = [];
$.each(data.attributes, function () {
if (updateInput($form, this, messages)) {
errors.push(this.input);
}
});
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);
}
} 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();
}
return;
}
data.submitting = false;
}, function () {
data.submitting = false;
});
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];
$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);
}
}
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);
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) {