diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index f43d98d..8de93c8 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -25,18 +25,6 @@ var defaults = { // the jQuery selector for the error summary errorSummary: undefined, - // whether to enable client-side (JavaScript) validation - enableClientValidation: true, - // whether to enable AJAX-based validation - enableAjaxValidation: false, - // the URL for performing AJAX-based validation. If not set, it will use the the form's action - validationUrl: undefined, - // number of milliseconds of validation delay. This is used when validateOnType is true. - validationDelay: 200, - // whether to perform validation when a change is detected on the input. - validateOnChange: true, - // whether to perform validation when the user is typing. - validateOnType: false, // whether to perform validation before submitting the form. validateOnSubmit: true, // the container CSS class representing the corresponding attribute has validation error @@ -45,40 +33,50 @@ successCssClass: 'success', // the container CSS class representing the corresponding attribute is being validated validatingCssClass: 'validating', - // a callback that is called before validating any attribute + // 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 validating every attribute + beforeValidateAttribute: undefined, + // a callback that is called after validating every attribute + afterValidateAttribute: undefined, + // a callback that is called before validating ALL attributes when submitting the form beforeValidate: undefined, - // a callback that is called after validating any attribute + // a callback that is called after validating ALL attributes when submitting the form afterValidate: undefined, // the GET parameter name indicating an AJAX-based validation ajaxVar: 'ajax' }; + var attributeDefaults = { + // attribute name or expression (e.g. "[0]content" for tabular input) + name: undefined, + // the jQuery selector of the container of the input field + container: undefined, + // the jQuery selector of the input field + input: undefined, + // the jQuery selector of the error tag + error: undefined, + // whether to perform validation when a change is detected on the input + validateOnChange: false, + // whether to perform validation when the user is typing. + validateOnType: false, + // number of milliseconds that the validation should be delayed when a user is typing in the input field. + validationDelay: 200, + // whether to enable AJAX-based validation. + enableAjaxValidation: false, + // function (attribute, value, messages), the client-side validation function. + validate: undefined, + // callback called before validating the attribute + beforeValidate: undefined, + // callback called after validating an attribute. + afterValidate: undefined, + // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating + status: 0, + // the value of the input + value: undefined + }; + var methods = { - /** - * Initializes the plugin. - * @param attributes array attribute configurations. Each attribute may contain the following options: - * - * - name: string, attribute name or expression (e.g. "[0]content" for tabular input) - * - container: string, the jQuery selector of the container of the input field - * - input: string, the jQuery selector of the input field - * - error: string, the jQuery selector of the error tag - * - value: string|array, the value of the input - * - validateOnChange: boolean, whether to perform validation when a change is detected on the input. - * If not set, it will take the value of the corresponding global setting. - * - validateOnType: boolean, defaults to false, whether to perform validation when the user is typing. - * If not set, it will take the value of the corresponding global setting. - * - enableAjaxValidation: boolean, whether to enable AJAX-based validation. - * If not set, it will take the value of the corresponding global setting. - * - enableClientValidation: boolean, whether to enable client-side validation. - * If not set, it will take the value of the corresponding global setting. - * - validate: function (attribute, value, messages), the client-side validation function. - * - beforeValidate: function ($form, attribute), callback called before validating an attribute. If it - * returns false, the validation will be cancelled. - * - afterValidate: function ($form, attribute, data, hasError), callback called after validating an attribute. - * - status: integer, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating - * - * @param options object the configuration for the plugin. The following options can be set: - */ init: function (attributes, options) { return this.each(function () { var $form = $(this); @@ -86,38 +84,33 @@ return; } - var settings = $.extend(defaults, options || {}); + var settings = $.extend({}, defaults, options || {}); if (settings.validationUrl === undefined) { settings.validationUrl = $form.attr('action'); } $.each(attributes, function (i) { - attributes[i] = $.extend({ - validateOnChange: settings.validateOnChange, - validateOnType: settings.validateOnType, - enableAjaxValidation: settings.enableAjaxValidation, - enableClientValidation: settings.enableClientValidation, - value: getValue($form, this) - }, this); + attributes[i] = $.extend({}, attributeDefaults, this); }); $form.data('yiiActiveForm', { settings: settings, attributes: attributes, - submitting: false + submitting: false, + validated: false }); - bindAttributes($form, attributes); + watchAttributes($form, attributes); /** * Clean up error status when the form is reset. * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE. */ - $form.bind('reset.yiiActiveForm', resetForm); + $form.bind('reset.yiiActiveForm', methods.resetForm); if (settings.validateOnSubmit) { $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () { $form.data('yiiActiveForm').submitObject = $(this); }); - $form.on('submit', submitForm); + $form.on('submit', methods.submitForm); } }); }, @@ -127,6 +120,73 @@ $(window).unbind('.yiiActiveForm'); $(this).removeData('yiiActiveForm'); }); + }, + + submitForm: function () { + var $form = $(this), + data = $form.data('yiiActiveForm'); + if (data.validated) { + // continue submitting the form since validation passes + data.validated = false; + return true; + } + + if (data.settings.timer !== undefined) { + clearTimeout(data.settings.timer); + } + data.submitting = true; + if (!data.settings.beforeValidate || data.settings.beforeValidate($form)) { + validate($form, function (messages) { + var hasError = false; + $.each(data.attributes, function () { + hasError = updateInput($form, this, messages) || hasError; + }); + updateSummary($form, messages); + if (!data.settings.afterValidate || data.settings.afterValidate($form, data, hasError)) { + if (!hasError) { + 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; + }); + } else { + data.submitting = false; + } + return false; + }, + + resetForm: function () { + var $form = $(this); + var data = $form.data('yiiActiveForm'); + // Because we bind directly to a form reset event instead of a reset button (that may not exist), + // when this function is executed form input values have not been reset yet. + // Therefore we do the actual reset work through setTimeout. + setTimeout(function () { + $.each(data.attributes, function () { + // Without setTimeout() we would get the input values that are not reset yet. + this.value = getValue($form, this); + this.status = 0; + var $container = $form.find(this.container); + $container.removeClass( + data.settings.validatingCssClass + ' ' + + data.settings.errorCssClass + ' ' + + data.settings.successCssClass + ); + $container.find(this.error).html(''); + }); + $form.find(data.settings.summary).hide().find('ul').html(''); + }, 1); } }; @@ -150,7 +210,7 @@ } }; - var bindAttributes = function ($form, attributes) { + var watchAttributes = function ($form, attributes) { $.each(attributes, function (i, attribute) { var $input = findInput($form, attribute); if (attribute.validateOnChange) { @@ -202,7 +262,7 @@ $form.find(this.container).addClass(data.settings.validatingCssClass); } }); - validateForm($form, function (messages) { + validate($form, function (messages) { var hasError = false; $.each(data.attributes, function () { if (this.status === 2 || this.status === 3) { @@ -218,141 +278,71 @@ }; /** - * Performs the ajax validation request. - * This method is invoked internally to trigger the ajax validation. - * @param $form jquery the jquery representation of the form - * @param successCallback function the function to be invoked if the ajax request succeeds - * @param errorCallback function the function to be invoked if the ajax request fails + * 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 */ - var validateForm = function ($form, successCallback, errorCallback) { + var validate = function ($form, successCallback, errorCallback) { var data = $form.data('yiiActiveForm'), needAjaxValidation = false, messages = {}; $.each(data.attributes, function () { - var msg = []; - if (this.validate && (data.submitting || this.status === 2 || this.status === 3)) { - this.validate(this, getValue($form, this), msg); - if (msg.length) { - messages[this.name] = msg; + if (data.submitting || this.status === 2 || this.status === 3) { + var msg = []; + if (this.validate) { + this.validate(this, getValue($form, this), msg); + if (msg.length) { + messages[this.name] = msg; + } + } + if (this.enableAjaxValidation && !msg.length) { + needAjaxValidation = true; } - } - if (this.enableAjaxValidation && !msg.length && (data.submitting || this.status === 2 || this.status === 3)) { - needAjaxValidation = true; } }); - if (!needAjaxValidation || data.submitting && !$.isEmptyObject(messages)) { - if (data.submitting) { - // delay callback so that the form can be submitted without problem - setTimeout(function () { - successCallback(messages); - }, 200); - } else { - successCallback(messages); + if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { + var $button = data.submitObject, + extData = '&' + data.settings.ajaxVar + '=' + $form.attr('id'); + if ($button && $button.length && $button.attr('name')) { + extData += '&' + $button.attr('name') + '=' + $button.attr('value'); } - return; - } - - var $button = data.submitObject, - extData = '&' + data.settings.ajaxVar + '=' + $form.attr('id'); - if ($button && $button.length && $button.attr('name')) { - extData += '&' + $button.attr('name') + '=' + $button.attr('value'); - } - - $.ajax({ - url: data.settings.validationUrl, - type: $form.attr('method'), - data: $form.serialize() + extData, - dataType: 'json', - success: function (data) { - if (data !== null && typeof data === 'object') { - $.each(data.attributes, function () { - if (!this.enableAjaxValidation) { - delete data[this.name]; - } - }); - successCallback($.extend({}, messages, data)); - } else { - successCallback(messages); - } - }, - error: errorCallback - }); - }; - - var validated = false; - var submitForm = function () { - var $form = $(this), - data = $form.data('yiiActiveForm'); - if (validated) { - validated = false; - return true; - } - if (data.settings.timer !== undefined) { - clearTimeout(data.settings.timer); - } - data.submitting = true; - if (!data.settings.beforeValidate || data.settings.beforeValidate($form)) { - validateForm($form, function (messages) { - var hasError = false; - $.each(data.attributes, function () { - hasError = updateInput($form, this, messages) || hasError; - }); - updateSummary($form, messages); - if (!data.settings.afterValidate || data.settings.afterValidate($form, data, hasError)) { - if (!hasError) { - 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; + $.ajax({ + url: data.settings.validationUrl, + type: $form.attr('method'), + data: $form.serialize() + extData, + dataType: 'json', + success: function (msgs) { + if (msgs !== null && typeof msgs === 'object') { + $.each(data.attributes, function () { + if (!this.enableAjaxValidation) { + delete msgs[this.name]; + } + }); + successCallback($.extend({}, messages, msgs)); + } else { + successCallback(messages); } - } - data.submitting = false; + }, + error: errorCallback }); + } else if (data.submitting) { + // delay callback so that the form can be submitted without problem + setTimeout(function () { + successCallback(messages); + }, 200); } else { - data.submitting = false; + successCallback(messages); } - return false; - }; - - var resetForm = function () { - var $form = $(this); - var data = $form.data('yiiActiveForm'); - /** - * because we bind directly to a form reset event, not to a reset button (that could or could not exist), - * when this function is executed form elements values have not been reset yet, - * because of that we use the setTimeout - */ - setTimeout(function () { - $.each(data.attributes, function () { - this.status = 0; - $form.find(this.container).removeClass( - data.settings.validatingCssClass + ' ' + - data.settings.errorCssClass + ' ' + - data.settings.successCssClass - ); - $form.find(this.error).html(''); - /* - * without the setTimeout() we would get here the current entered value before the reset instead of the reset value - */ - this.value = getValue($form, this); - }); - $form.find(data.settings.summary).hide().find('ul').html(''); - }, 1); }; /** - * updates the error message and the input container for a particular attribute. - * @param attribute object the configuration for a particular attribute. - * @param messages array the json data obtained from the ajax validation request + * Updates the error message and the input container for a particular attribute. * @param $form the form jQuery object + * @param attribute object the configuration for a particular attribute. + * @param messages array the validation error messages * @return boolean whether there is a validation error for the specified attribute */ var updateInput = function ($form, attribute, messages) { @@ -364,23 +354,26 @@ if ($input.length) { hasError = messages && $.isArray(messages[attribute.name]) && messages[attribute.name].length; var $container = $form.find(attribute.container); - $container.removeClass( - data.settings.validatingCssClass + ' ' + - data.settings.errorCssClass + ' ' + - data.settings.successCssClass - ); - + var $error = $container.find(attribute.error); if (hasError) { - $container.find(attribute.error).html(messages[attribute.name][0]); - $container.addClass(data.settings.errorCssClass); - } else if (attribute.enableAjaxValidation || attribute.enableClientValidation && attribute.validate) { - $container.addClass(data.settings.successCssClass); + $error.html(messages[attribute.name][0]); + $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) + .addClass(data.settings.errorCssClass); + } else { + $error.html(''); + $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') + .addClass(data.settings.successCssClass); } attribute.value = getValue($form, attribute); } return hasError; }; + /** + * Updates the error summary. + * @param $form the form jQuery object + * @param messages array the validation error messages + */ var updateSummary = function ($form, messages) { var data = $form.data('yiiActiveForm'), $summary = $form.find(data.settings.errorSummary), @@ -388,8 +381,8 @@ if ($summary.length && messages) { $.each(data.attributes, function () { - if ($.isArray(messages[this.name])) { - content += '
  • ' + messages[this.name].join('
  • ') + '
  • '; + if ($.isArray(messages[this.name]) && messages[this.name].length) { + content += '
  • ' + messages[this.name][0] + '
  • '; } }); $summary.toggle(content !== '').find('ul').html(content); diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index 5ca8e4e..fd098be 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -18,7 +18,7 @@ yii.validation = (function ($) { return { required: function (value, messages, options) { - var valid = false; + var valid = false; if (options.requiredValue === undefined) { if (options.strict && value !== undefined || !options.strict && !isEmpty(value, true)) { valid = true; @@ -27,7 +27,7 @@ yii.validation = (function ($) { valid = true; } - if(!valid) { + if (!valid) { messages.push(options.message); } }, @@ -39,7 +39,7 @@ yii.validation = (function ($) { var valid = !options.strict && (value == options.trueValue || value == options.falseValue) || options.strict && (value === options.trueValue || value === options.falseValue); - if(!valid) { + if (!valid) { messages.push(options.message); } }, @@ -90,7 +90,7 @@ yii.validation = (function ($) { var valid = !options.not && $.inArray(value, options.range) || options.not && !$.inArray(value, options.range); - if(!valid) { + if (!valid) { messages.push(options.message); } }, @@ -112,7 +112,7 @@ yii.validation = (function ($) { var valid = value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)); - if(!valid) { + if (!valid) { messages.push(options.message); } }, @@ -147,7 +147,7 @@ yii.validation = (function ($) { for (var i = v.length - 1, h = 0; i >= 0; --i) { h += v.charCodeAt(i); } - if(h != hash) { + if (h != hash) { messages.push(options.message); } }, @@ -190,7 +190,7 @@ yii.validation = (function ($) { break; } - if(!valid) { + if (!valid) { messages.push(options.message); } } diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index 060fe7e..bc7d696 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -4,10 +4,10 @@ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ - namespace yii\widgets; use yii\base\Component; +use yii\db\ActiveRecord; use yii\helpers\Html; use yii\base\Model; use yii\helpers\JsExpression; @@ -79,8 +79,8 @@ class ActiveField extends Component */ public $validateOnType; /** - * @var integer number of milliseconds that the validation should be delayed when a user is typing in an input field. - * This property is used only when [[validateOnType]] is true. + * @var integer number of milliseconds that the validation should be delayed when the input field + * is changed or the user types in the field. * If not set, it will take the value of [[ActiveForm::validationDelay]]. */ public $validationDelay; @@ -121,28 +121,13 @@ class ActiveField extends Component public function begin() { - $inputID = Html::getInputId($this->model, $this->attribute); - $attribute = Html::getAttributeName($this->attribute); - - $validators = array(); - foreach ($this->model->getActiveValidators($attribute) as $validator) { - /** @var \yii\validators\Validator $validator */ - if (($js = $validator->clientValidateAttribute($this->model, $attribute)) != '') { - $validators[] = $js; - } - } - $jsOptions = array( - 'name' => $this->attribute, - 'container' => ".field-$inputID", - 'input' => "#$inputID", - 'error' => '.help-inline', - ); - if ($validators !== array()) { - $jsOptions['validate'] = new JsExpression("function(attribute, value, messages) {" . implode('', $validators) . '}'); + $options = $this->getClientOptions(); + if ($options !== array()) { + $this->form->attributes[$this->attribute] = $options; } - $this->form->attributes[$this->attribute] = $jsOptions; - + $inputID = Html::getInputId($this->model, $this->attribute); + $attribute = Html::getAttributeName($this->attribute); $options = $this->options; $class = isset($options['class']) ? array($options['class']) : array(); $class[] = "field-$inputID"; @@ -152,9 +137,8 @@ class ActiveField extends Component if ($this->model->hasErrors($attribute)) { $class[] = $this->form->errorCssClass; } - - $options['class'] = implode(' ', $class); + return Html::beginTag($this->tag, $options); } @@ -163,6 +147,65 @@ class ActiveField extends Component return Html::endTag($this->tag); } + protected function getClientOptions() + { + if ($this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation) { + $attribute = Html::getAttributeName($this->attribute); + $validators = array(); + foreach ($this->model->getActiveValidators($attribute) as $validator) { + /** @var \yii\validators\Validator $validator */ + $js = $validator->clientValidateAttribute($this->model, $attribute); + if ($validator->enableClientValidation && $js != '') { + $validators[] = $js; + } + } + if ($validators !== array()) { + $options['validate'] = new JsExpression("function(attribute,value,messages){" . implode('', $validators) . '}'); + } + } + + if ($this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation) { + $options['enableAjaxValidation'] = 1; + } + + if (isset($options['validate']) || isset($options['enableAjaxValidation'])) { + $inputID = Html::getInputId($this->model, $this->attribute); + $options['name'] = $inputID; + if ($this->model instanceof ActiveRecord && !$this->model->getIsNewRecord()) { + $option['status'] = 1; + } + + $names = array( + 'enableAjaxValidation', + 'validateOnChange', + 'validateOnType', + 'validationDelay', + ); + foreach ($names as $name) { + $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; + } + $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; + $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; + if (isset($this->errorOptions['class'])) { + $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); + } else { + $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; + } + + foreach (array('beforeValidate', 'afterValidate') as $callback) { + $value = $this->$callback; + if ($value instanceof JsExpression) { + $options[$callback] = $value; + } elseif (is_string($value)) { + $options[$callback] = new JsExpression($value); + } + } + return $options; + } else { + return array(); + } + } + /** * Generates a label tag for [[attribute]]. * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 80de64b..a735982 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -88,7 +88,7 @@ class ActiveForm extends Widget * @var boolean whether to perform validation when an input field loses focus and its value is found changed. * If [[ActiveField::validateOnChange]] is set, its value will take precedence for that input field. */ - public $validateOnChange = false; + public $validateOnChange = true; /** * @var boolean whether to perform validation while the user is typing in an input field. * If [[ActiveField::validateOnType]] is set, its value will take precedence for that input field. @@ -96,12 +96,16 @@ class ActiveForm extends Widget */ public $validateOnType = false; /** - * @var integer number of milliseconds that the validation should be delayed when a user is typing in an input field. - * This property is used only when [[validateOnType]] is true. + * @var integer number of milliseconds that the validation should be delayed when an input field + * is changed or the user types in the field. * If [[ActiveField::validationDelay]] is set, its value will take precedence for that input field. */ public $validationDelay = 200; /** + * @var string the name of the GET parameter indicating the validation request is an AJAX request. + */ + public $ajaxVar = 'ajax'; + /** * @var JsExpression|string a [[JsExpression]] object or a JavaScript expression string representing * the callback that will be invoked BEFORE validating EACH attribute on the client side. * The signature of the callback should be like the following: @@ -192,6 +196,7 @@ class ActiveForm extends Widget 'errorCssClass' => $this->errorCssClass, 'successCssClass' => $this->successCssClass, 'validatingCssClass' => $this->validatingCssClass, + 'ajaxVar' => $this->ajaxVar, ); if ($this->validationUrl !== null) { $options['validationUrl'] = Html::url($this->validationUrl); @@ -204,11 +209,10 @@ class ActiveForm extends Widget ); foreach ($callbacks as $callback) { $value = $this->$callback; - if (is_string($value)) { - $value = new JsExpression($value); - } if ($value instanceof JsExpression) { $options[$callback] = $value; + } elseif (is_string($value)) { + $options[$callback] = new JsExpression($value); } }