diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index ec13b581c2..e5146dc5d3 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -133,6 +133,7 @@ Yii Framework 2 Change Log - Enh #3380: Allow `value` in `defaultValueValidator` to be a closure (Alex-Code) - Enh #3384: Added callback-style transactions (leandrogehlen, Ragazzo, samdark) - Enh #3399, #3241: Added support for MS SQL Server older than 2012 (fourteenmeister, samdark) +- Enh #3410: yii.activeForm.js now supports adding/removing fields dynamically (qiangxue) - Enh #3459: Added logging of errors, which may occur at `yii\caching\FileCache::gc()` (klimov-paul) - Enh #3472: Added configurable option to encode spaces in dropDownLists and listBoxes (kartik-v) - Enh #3518: `yii\helpers\Html::encode()` now replaces invalid code sequences with "�" (DaSourcerer) diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index 05517fdb9f..a7a4f9013e 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -22,19 +22,24 @@ } }; + // 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 encodeErrorSummary: true, // the jQuery selector for the error summary - errorSummary: undefined, + errorSummary: '.error-summary', // whether to perform validation before submitting the form. validateOnSubmit: true, // the container CSS class representing the corresponding attribute has validation error - errorCssClass: 'error', + errorCssClass: 'has-error', // the container CSS class representing the corresponding attribute passes validation - successCssClass: 'success', + successCssClass: 'has-success', // the container CSS class representing the corresponding attribute is being validated validatingCssClass: 'validating', + // the GET parameter name indicating an AJAX-based validation + ajaxParam: 'ajax', + // 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: @@ -57,13 +62,10 @@ 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, - // the GET parameter name indicating an AJAX-based validation - ajaxParam: 'ajax', - // the type of data that you're expecting back from the server - ajaxDataType: 'json' + ajaxComplete: undefined }; + // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well var attributeDefaults = { // a unique ID identifying an attribute (e.g. "loginform-username") in a form id: undefined, @@ -71,16 +73,16 @@ name: undefined, // the jQuery selector of the container of the input field container: undefined, - // the jQuery selector of the input field + // the jQuery selector of the input field under the context of the container input: undefined, - // the jQuery selector of the error tag - error: undefined, + // the jQuery selector of the error tag under the context of the container + error: '.help-block', // whether to encode the error encodeError: true, // whether to perform validation when a change is detected on the input - validateOnChange: false, + validateOnChange: true, // whether to perform validation when the input loses focus - validateOnBlur: false, + validateOnBlur: true, // 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. @@ -107,9 +109,12 @@ if (settings.validationUrl === undefined) { settings.validationUrl = $form.prop('action'); } + $.each(attributes, function (i) { attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this); + watchAttribute($form, attributes[i]); }); + $form.data('yiiActiveForm', { settings: settings, attributes: attributes, @@ -117,8 +122,6 @@ validated: false }); - 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. @@ -134,6 +137,47 @@ }); }, + // add a new attribute to the form dynamically. + // please refer to attributeDefaults for the structure of attribute + add: function (attribute) { + var $form = $(this); + attribute = $.extend({value: getValue($form, attribute)}, attributeDefaults, attribute); + $form.data('yiiActiveForm').attributes.push(attribute); + watchAttribute($form, attribute); + }, + + // remove the attribute with the specified ID from the form + remove: function (id) { + var $form = $(this), + attributes = $form.data('yiiActiveForm').attributes, + index = -1, + attribute; + $.each(attributes, function (i) { + if (attributes[i]['id'] == id) { + index = i; + attribute = attributes[i]; + return false; + } + }); + if (index >= 0) { + attributes.splice(index, 1); + unwatchAttribute($form, attribute); + } + return attribute; + }, + + // find an attribute config based on the specified attribute ID + find: function (id) { + var attributes = $(this).data('yiiActiveForm').attributes, result; + $.each(attributes, function (i) { + if (attributes[i]['id'] == id) { + result = attributes[i]; + return false; + } + }); + return result; + }, + destroy: function () { return this.each(function () { $(this).unbind('.yiiActiveForm'); @@ -256,6 +300,33 @@ }); }; + var watchAttribute = function ($form, 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 unwatchAttribute = function ($form, attribute) { + findInput($form, attribute).off('.yiiActiveForm'); + }; + var validateAttribute = function ($form, attribute, forceValidate) { var data = $form.data('yiiActiveForm'); diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index dd58294f98..b46dd074a6 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -693,9 +693,9 @@ class ActiveField extends Component return []; } - $options = []; - $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; + $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + if ($enableClientValidation) { $validators = []; foreach ($this->model->getActiveValidators($attribute) as $validator) { @@ -708,39 +708,48 @@ class ActiveField extends Component $validators[] = $js; } } - if (!empty($validators)) { - $options['validate'] = new JsExpression("function (attribute, value, messages, deferred) {" . implode('', $validators) . '}'); - } } - $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; - if ($enableAjaxValidation) { - $options['enableAjaxValidation'] = 1; - } - - if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { - $inputID = Html::getInputId($this->model, $this->attribute); - $options['id'] = $inputID; - $options['name'] = $this->attribute; - foreach (['validateOnChange', 'validateOnBlur', 'validateOnType', 'validationDelay'] 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->selectors['error'])) { - $options['error'] = $this->selectors['error']; - } elseif (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'; - } - - $options['encodeError'] = !isset($this->errorOptions['encode']) || $this->errorOptions['encode'] !== false; - - return $options; - } else { + if (!$enableAjaxValidation && (!$enableClientValidation || empty($validators))) { return []; } + + $options = []; + + $inputID = Html::getInputId($this->model, $this->attribute); + $options['id'] = $inputID; + $options['name'] = $this->attribute; + + $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; + $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; + if (isset($this->selectors['error'])) { + $options['error'] = $this->selectors['error']; + } elseif (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'; + } + + $options['encodeError'] = !isset($this->errorOptions['encode']) || !$this->errorOptions['encode']; + if ($enableAjaxValidation) { + $options['enableAjaxValidation'] = true; + } + foreach (['validateOnChange', 'validateOnBlur', 'validateOnType', 'validationDelay'] as $name) { + $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; + } + + if (!empty($validators)) { + $options['validate'] = new JsExpression("function (attribute, value, messages, deferred) {" . implode('', $validators) . '}'); + } + + // only get the options that are different from the default ones (set in yii.activeForm.js) + return array_diff_assoc($options, [ + 'validateOnChange' => true, + 'validateOnBlur' => true, + 'validateOnType' => false, + 'validationDelay' => 200, + 'encodeError' => true, + 'error' => '.help-block', + ]); } } diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index f3ae54da13..e9526655be 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -288,7 +288,17 @@ class ActiveForm extends Widget } } - return $options; + // only get the options that are different from the default ones (set in yii.activeForm.js) + return array_diff_assoc($options, [ + 'encodeErrorSummary' => true, + 'errorSummary' => '.error-summary', + 'validateOnSubmit' => true, + 'errorCssClass' => 'has-error', + 'successCssClass' => 'has-success', + 'validatingCssClass' => 'validating', + 'ajaxParam' => 'ajax', + 'ajaxDataType' => 'json', + ]); } /**