Commit ae13b059 by Qiang Xue

Fixes #3410: yii.activeForm.js now supports adding/removing fields dynamically

parent 08d4d899
...@@ -133,6 +133,7 @@ Yii Framework 2 Change Log ...@@ -133,6 +133,7 @@ Yii Framework 2 Change Log
- Enh #3380: Allow `value` in `defaultValueValidator` to be a closure (Alex-Code) - Enh #3380: Allow `value` in `defaultValueValidator` to be a closure (Alex-Code)
- Enh #3384: Added callback-style transactions (leandrogehlen, Ragazzo, samdark) - Enh #3384: Added callback-style transactions (leandrogehlen, Ragazzo, samdark)
- Enh #3399, #3241: Added support for MS SQL Server older than 2012 (fourteenmeister, 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 #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 #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) - Enh #3518: `yii\helpers\Html::encode()` now replaces invalid code sequences with "�" (DaSourcerer)
......
...@@ -22,19 +22,24 @@ ...@@ -22,19 +22,24 @@
} }
}; };
// NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well
var defaults = { var defaults = {
// whether to encode the error summary // whether to encode the error summary
encodeErrorSummary: true, encodeErrorSummary: true,
// the jQuery selector for the error summary // the jQuery selector for the error summary
errorSummary: undefined, errorSummary: '.error-summary',
// whether to perform validation before submitting the form. // whether to perform validation before submitting the form.
validateOnSubmit: true, validateOnSubmit: true,
// the container CSS class representing the corresponding attribute has validation error // 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 // 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 // the container CSS class representing the corresponding attribute is being validated
validatingCssClass: 'validating', 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 // the URL for performing AJAX-based validation. If not set, it will use the the form's action
validationUrl: undefined, validationUrl: undefined,
// a callback that is called before submitting the form. The signature of the callback should be: // a callback that is called before submitting the form. The signature of the callback should be:
...@@ -57,13 +62,10 @@ ...@@ -57,13 +62,10 @@
ajaxBeforeSend: undefined, ajaxBeforeSend: undefined,
// a function to be called when the request finishes on AJAX-based validation. The signature of the callback should be: // a function to be called when the request finishes on AJAX-based validation. The signature of the callback should be:
// function ($form, jqXHR, textStatus) // function ($form, jqXHR, textStatus)
ajaxComplete: undefined, 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'
}; };
// NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well
var attributeDefaults = { var attributeDefaults = {
// a unique ID identifying an attribute (e.g. "loginform-username") in a form // a unique ID identifying an attribute (e.g. "loginform-username") in a form
id: undefined, id: undefined,
...@@ -71,16 +73,16 @@ ...@@ -71,16 +73,16 @@
name: undefined, name: undefined,
// the jQuery selector of the container of the input field // the jQuery selector of the container of the input field
container: undefined, container: undefined,
// the jQuery selector of the input field // the jQuery selector of the input field under the context of the container
input: undefined, input: undefined,
// the jQuery selector of the error tag // the jQuery selector of the error tag under the context of the container
error: undefined, error: '.help-block',
// whether to encode the error // whether to encode the error
encodeError: true, encodeError: true,
// whether to perform validation when a change is detected on the input // 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 // whether to perform validation when the input loses focus
validateOnBlur: false, validateOnBlur: true,
// whether to perform validation when the user is typing. // whether to perform validation when the user is typing.
validateOnType: false, validateOnType: false,
// number of milliseconds that the validation should be delayed when a user is typing in the input field. // number of milliseconds that the validation should be delayed when a user is typing in the input field.
...@@ -107,9 +109,12 @@ ...@@ -107,9 +109,12 @@
if (settings.validationUrl === undefined) { if (settings.validationUrl === undefined) {
settings.validationUrl = $form.prop('action'); settings.validationUrl = $form.prop('action');
} }
$.each(attributes, function (i) { $.each(attributes, function (i) {
attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this); attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this);
watchAttribute($form, attributes[i]);
}); });
$form.data('yiiActiveForm', { $form.data('yiiActiveForm', {
settings: settings, settings: settings,
attributes: attributes, attributes: attributes,
...@@ -117,8 +122,6 @@ ...@@ -117,8 +122,6 @@
validated: false validated: false
}); });
watchAttributes($form, attributes);
/** /**
* Clean up error status when the form is reset. * 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. * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE.
...@@ -134,6 +137,47 @@ ...@@ -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 () { destroy: function () {
return this.each(function () { return this.each(function () {
$(this).unbind('.yiiActiveForm'); $(this).unbind('.yiiActiveForm');
...@@ -256,6 +300,33 @@ ...@@ -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 validateAttribute = function ($form, attribute, forceValidate) {
var data = $form.data('yiiActiveForm'); var data = $form.data('yiiActiveForm');
......
...@@ -693,9 +693,9 @@ class ActiveField extends Component ...@@ -693,9 +693,9 @@ class ActiveField extends Component
return []; return [];
} }
$options = [];
$enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation;
$enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation;
if ($enableClientValidation) { if ($enableClientValidation) {
$validators = []; $validators = [];
foreach ($this->model->getActiveValidators($attribute) as $validator) { foreach ($this->model->getActiveValidators($attribute) as $validator) {
...@@ -708,39 +708,48 @@ class ActiveField extends Component ...@@ -708,39 +708,48 @@ class ActiveField extends Component
$validators[] = $js; $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 && (!$enableClientValidation || empty($validators))) {
if ($enableAjaxValidation) { return [];
$options['enableAjaxValidation'] = 1;
} }
if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { $options = [];
$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"; $inputID = Html::getInputId($this->model, $this->attribute);
$options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; $options['id'] = $inputID;
if (isset($this->selectors['error'])) { $options['name'] = $this->attribute;
$options['error'] = $this->selectors['error'];
} elseif (isset($this->errorOptions['class'])) { $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID";
$options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID";
} else { if (isset($this->selectors['error'])) {
$options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; $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; $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;
}
return $options; if (!empty($validators)) {
} else { $options['validate'] = new JsExpression("function (attribute, value, messages, deferred) {" . implode('', $validators) . '}');
return [];
} }
// 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',
]);
} }
} }
...@@ -288,7 +288,17 @@ class ActiveForm extends Widget ...@@ -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',
]);
} }
/** /**
......
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