在具有嵌入式表单的自定义指令上使用 ngModel,并进行有效验证?
Use ngModel on a custom directive with an embedded form, with working validation?
我有一组经常重复使用的表单输入,它们在我的整个应用程序中重复使用,因此我试图将它们封装在自定义指令中。我想在我的指令上设置一个 ngModel
并将其拆分为可在主指令中的几个不同输入(其中一些本身就是指令)中进行编辑。
同时,我需要将表单验证结果向上传递到父表单,以便我可以显示适当的消息和样式。
实现这个最简单最惯用的方法是什么?
这些(简化的)模板应该给你一个例子,说明我要做什么...
OuterTemplate.html
<form name="outerForm">
<my-directive
ng-model="ctrl.myComplexModel"
name="myDirectiveInstance"
custom-required="ctrl.EnableValidateOne"
toggle-another-validation="ctrl.EnableValidateTwo">
</my-directive>
<div ng-messages="outerForm.myDirectiveInstance.$error">
<ng-message when="customRequired">This is required.</ng-message>
<ng-message when="anotherValidation">This is required.</ng-message>
<ng-message when="innerValidationOne">Something wrong with field 1.</ng-message>
<ng-message when="innerValidationTwo">Something wrong with field 2.</ng-message>
<ng-message when="innerValidationThree">Something wrong with field 3.</ng-message>
<!-- etc... -->
</div>
</form>
myDirectiveTemplate.html
<div ng-form="myDirectiveForm">
<div ng-class="{'has-error': myDirectiveForm.fieldOne.$invalid}">
<ui-select
ng-model="model.fieldOne"
name="fieldOne"
required>
</ui-select>
</div>
<div ng-class="{'has-error': myDirectiveForm.fieldTwo.$invalid}">
<input
type="number"
ng-model="model.fieldTwo"
name="fieldTwo"
ng-pattern="directiveCtrl.someRegEx"
ng-required="directiveCtrl.fieldTwoIsRequired">
</div>
<!-- etc... -->
</div>
目前,myDirectiveForm
和 myDirectiveInstance
都将自己发布为 outerForm
FormController
的财产。我希望将这个指令变成一个黑盒子,所以 myDirectiveForm
直接附加到 outerForm
的事实让我很困扰,似乎表明我做错了什么。
这是我的指令定义现在的样子。
myDirective.js
app.directive('myDirective', function() {
return {
restrict: 'E',
template: 'myDirectiveTemplate.html',
controller: 'MyDirectiveCtrl',
scope: {
model: '=ngModel',
customRequired: '=?',
toggleAnotherValidation: '=?'
},
require: 'ngModel',
link: function(scope, iElem, iAttrs, ngModelController) {
// Black-box the internal validators
// Custom validator to avoid conflicts with ngRequired
ngModelController.$validators.customRequired = function(modelValue, viewValue) {
if(!scope.customRequired)
return true;
// On first digest the field isn't registered on the form controller yet
if(angular.isUndefined(scope.myDirectiveForm.fieldOne))
return true;
return !scope.myDirectiveForm.fieldOne.$error.required;
};
ngModelController.$validators.anotherValidation = function(modelValue, viewValue) {
if(!scope.anotherValidation)
return true;
return scope.passesBusinessRule();
};
ngModelController.$validators.innerValidationOne = function(modelValue, viewValue) {
if(!scope.anotherValidation)
return true;
if(angular.isUndefined(scope.myDirectiveForm.fieldTwo))
return true;
return !scope.myDirectiveForm.fieldTwo.$error.pattern;
};
/* etc... */
// Deep-watching model so that validations will trigger on updates of properties
scope.$watch('model', function() {
ngModelController.$validate();
}, true);
}
};
});
这就是我对指令的理解。在这种情况下,ng-model 和 myDirective 都是指令。我不清楚你是否正在做
1) ng-model 上的包装器或
2) 自定义指令
因为如果你想做自定义指令,你可以只传入数据,例如。
{ 范围:{ 数据:'=' }
而且如果你想做一个包装器,你可能不应该传入与 ngModel 相关的其他属性,这意味着你仍然可以传入数据
ctrl.myComplexModel
,顺便说一句。模型对象不能赋值给ng-model,因为ng-model不保存对象,它只保存数据。
注意:实际上我发现了这个 post、AngularJS - Create a directive that uses ng-model
显然,您可以传入模型,
https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
无论如何,这对我来说太复杂了 :) 如果你想做一个包装纸,我觉得这个模式
- 传入数据
- "has a" 对象
但显然你可能在做 "is a" 对象。
我想出了一个不错的解决方案。简而言之,我已经从自定义指令中删除了 NgModelController
实现,并且我完全依赖自定义指令中 form
指令的内部 FormController
。据我所知,NgModelController
并不是为了 包装 自定义指令中的表单而设计的。然而,嵌套表单在 Angular 中得到了很好的支持,所以这是可行的方法。
我没有意识到的是,从 Angular 1.3 开始,您可以动态地为表单分配名称。虽然我无法阻止 "black box" 泄漏并将其自身附加到父表单控制器,但我至少可以控制它用于在父范围内发布自身的名称,这是可以接受的并且与API 由 ngModel
.
提供
更新了以下示例。
OuterTemplate.html
<form name="outerForm">
<my-directive
model="ctrl.myComplexModel"
name="myDirectiveInstance"
custom-required="ctrl.EnableValidateOne"
toggle-another-validation="ctrl.EnableValidateTwo">
</my-directive>
<div>
<span ng-if="outerForm.myDirectiveInstance.fieldOne.$error.required">Internal field 1 is required.</span>
<span ng-if="outerForm.myDirectiveInstance.fieldTwo.$error.required">Internal field 2 is required.</span>
<span ng-if="outerForm.myDirectiveInstance.fieldTwo.$error.pattern">Internal field 2 format error.</span>
<!-- etc... -->
<ng-messages for="outerForm.myDirectiveInstance.$error">
<ng-message when="required">At least one required field is missing.</ng-message>
<ng-message when="custom">
Some directive-wide error set by validate-custom on outerForm.myDirectiveInstance.internalField
</ng-message>
<!-- etc... -->
</ng-messages>
</div>
</form>
在外部模板中,我删除了 ng-model
指令以支持自定义属性。 name
属性 仍可用于确定发布内部表单的名称。
或者,可以保留 ng-model
,并且可以使用属性 form-name
(对下面的隔离范围绑定进行适当更改)来发布自定义指令的 FormController
到父级 FormController
,但这可能有点误导,因为 ng-model
指令除了隔离作用域绑定外没有用于任何其他用途。
无论如何,对于此用例,ng-model
不应与 name
属性 结合使用。否则,可能会出现冲突,因为 NgModelController
和 FormController
试图以相同的 属性 名称(outerForm.myDirectiveInstance
).
由于验证错误冒泡到父 form
指令,ngMessages
可以与此自定义指令一起使用,如图所示。对于更精细的错误处理,也可以访问指令的内部字段。
myDirectiveTemplate.html
<div ng-form="{{ formName }}">
<div ng-class="{'has-error': isInvalid('fieldOne')}">
<ui-select
ng-model="model.fieldOne"
name="fieldOne"
required>
</ui-select>
</div>
<div ng-class="{'has-error': isInvalid('fieldTwo')}">
<input
type="number"
ng-model="model.fieldTwo"
name="fieldTwo"
ng-pattern="directiveCtrl.someRegEx"
ng-required="directiveCtrl.fieldTwoIsRequired">
</div>
<!-- etc... -->
<input
type="hidden"
ng-model="someCalculatedValue"
name="internalField"
validate-custom>
</div>
指令的内部模板基本保持不变。最大的不同是 ngForm
的名称现在是动态设置的。
为了用 ngClass 处理这个问题,angular 表达式不起作用,所以我更新了我的示例,改为在 $scope
上使用一个函数。
最后,对于指令范围的业务规则,我使用了带有 ngModel
指令和 name
集的隐藏输入。我附加了一个用于验证的自定义迷你指令到这个字段。此字段上的验证错误将冒出以供父指令使用。
myDirective.js
app.directive('myDirective', function() {
return {
restrict: 'E',
template: 'myDirectiveTemplate.html',
controller: 'MyDirectiveCtrl',
scope: {
model: '=',
customRequired: '=?',
toggleAnotherValidation: '=?',
formName: '@name'
},
};
});
现在几乎所有逻辑都已从指令定义中删除。
我有一组经常重复使用的表单输入,它们在我的整个应用程序中重复使用,因此我试图将它们封装在自定义指令中。我想在我的指令上设置一个 ngModel
并将其拆分为可在主指令中的几个不同输入(其中一些本身就是指令)中进行编辑。
同时,我需要将表单验证结果向上传递到父表单,以便我可以显示适当的消息和样式。
实现这个最简单最惯用的方法是什么?
这些(简化的)模板应该给你一个例子,说明我要做什么...
OuterTemplate.html
<form name="outerForm">
<my-directive
ng-model="ctrl.myComplexModel"
name="myDirectiveInstance"
custom-required="ctrl.EnableValidateOne"
toggle-another-validation="ctrl.EnableValidateTwo">
</my-directive>
<div ng-messages="outerForm.myDirectiveInstance.$error">
<ng-message when="customRequired">This is required.</ng-message>
<ng-message when="anotherValidation">This is required.</ng-message>
<ng-message when="innerValidationOne">Something wrong with field 1.</ng-message>
<ng-message when="innerValidationTwo">Something wrong with field 2.</ng-message>
<ng-message when="innerValidationThree">Something wrong with field 3.</ng-message>
<!-- etc... -->
</div>
</form>
myDirectiveTemplate.html
<div ng-form="myDirectiveForm">
<div ng-class="{'has-error': myDirectiveForm.fieldOne.$invalid}">
<ui-select
ng-model="model.fieldOne"
name="fieldOne"
required>
</ui-select>
</div>
<div ng-class="{'has-error': myDirectiveForm.fieldTwo.$invalid}">
<input
type="number"
ng-model="model.fieldTwo"
name="fieldTwo"
ng-pattern="directiveCtrl.someRegEx"
ng-required="directiveCtrl.fieldTwoIsRequired">
</div>
<!-- etc... -->
</div>
目前,myDirectiveForm
和 myDirectiveInstance
都将自己发布为 outerForm
FormController
的财产。我希望将这个指令变成一个黑盒子,所以 myDirectiveForm
直接附加到 outerForm
的事实让我很困扰,似乎表明我做错了什么。
这是我的指令定义现在的样子。
myDirective.js
app.directive('myDirective', function() {
return {
restrict: 'E',
template: 'myDirectiveTemplate.html',
controller: 'MyDirectiveCtrl',
scope: {
model: '=ngModel',
customRequired: '=?',
toggleAnotherValidation: '=?'
},
require: 'ngModel',
link: function(scope, iElem, iAttrs, ngModelController) {
// Black-box the internal validators
// Custom validator to avoid conflicts with ngRequired
ngModelController.$validators.customRequired = function(modelValue, viewValue) {
if(!scope.customRequired)
return true;
// On first digest the field isn't registered on the form controller yet
if(angular.isUndefined(scope.myDirectiveForm.fieldOne))
return true;
return !scope.myDirectiveForm.fieldOne.$error.required;
};
ngModelController.$validators.anotherValidation = function(modelValue, viewValue) {
if(!scope.anotherValidation)
return true;
return scope.passesBusinessRule();
};
ngModelController.$validators.innerValidationOne = function(modelValue, viewValue) {
if(!scope.anotherValidation)
return true;
if(angular.isUndefined(scope.myDirectiveForm.fieldTwo))
return true;
return !scope.myDirectiveForm.fieldTwo.$error.pattern;
};
/* etc... */
// Deep-watching model so that validations will trigger on updates of properties
scope.$watch('model', function() {
ngModelController.$validate();
}, true);
}
};
});
这就是我对指令的理解。在这种情况下,ng-model 和 myDirective 都是指令。我不清楚你是否正在做
1) ng-model 上的包装器或 2) 自定义指令
因为如果你想做自定义指令,你可以只传入数据,例如。
{ 范围:{ 数据:'=' }
而且如果你想做一个包装器,你可能不应该传入与 ngModel 相关的其他属性,这意味着你仍然可以传入数据
ctrl.myComplexModel
,顺便说一句。模型对象不能赋值给ng-model,因为ng-model不保存对象,它只保存数据。
注意:实际上我发现了这个 post、AngularJS - Create a directive that uses ng-model
显然,您可以传入模型, https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
无论如何,这对我来说太复杂了 :) 如果你想做一个包装纸,我觉得这个模式
- 传入数据
- "has a" 对象
但显然你可能在做 "is a" 对象。
我想出了一个不错的解决方案。简而言之,我已经从自定义指令中删除了 NgModelController
实现,并且我完全依赖自定义指令中 form
指令的内部 FormController
。据我所知,NgModelController
并不是为了 包装 自定义指令中的表单而设计的。然而,嵌套表单在 Angular 中得到了很好的支持,所以这是可行的方法。
我没有意识到的是,从 Angular 1.3 开始,您可以动态地为表单分配名称。虽然我无法阻止 "black box" 泄漏并将其自身附加到父表单控制器,但我至少可以控制它用于在父范围内发布自身的名称,这是可以接受的并且与API 由 ngModel
.
更新了以下示例。
OuterTemplate.html
<form name="outerForm">
<my-directive
model="ctrl.myComplexModel"
name="myDirectiveInstance"
custom-required="ctrl.EnableValidateOne"
toggle-another-validation="ctrl.EnableValidateTwo">
</my-directive>
<div>
<span ng-if="outerForm.myDirectiveInstance.fieldOne.$error.required">Internal field 1 is required.</span>
<span ng-if="outerForm.myDirectiveInstance.fieldTwo.$error.required">Internal field 2 is required.</span>
<span ng-if="outerForm.myDirectiveInstance.fieldTwo.$error.pattern">Internal field 2 format error.</span>
<!-- etc... -->
<ng-messages for="outerForm.myDirectiveInstance.$error">
<ng-message when="required">At least one required field is missing.</ng-message>
<ng-message when="custom">
Some directive-wide error set by validate-custom on outerForm.myDirectiveInstance.internalField
</ng-message>
<!-- etc... -->
</ng-messages>
</div>
</form>
在外部模板中,我删除了 ng-model
指令以支持自定义属性。 name
属性 仍可用于确定发布内部表单的名称。
或者,可以保留 ng-model
,并且可以使用属性 form-name
(对下面的隔离范围绑定进行适当更改)来发布自定义指令的 FormController
到父级 FormController
,但这可能有点误导,因为 ng-model
指令除了隔离作用域绑定外没有用于任何其他用途。
无论如何,对于此用例,ng-model
不应与 name
属性 结合使用。否则,可能会出现冲突,因为 NgModelController
和 FormController
试图以相同的 属性 名称(outerForm.myDirectiveInstance
).
由于验证错误冒泡到父 form
指令,ngMessages
可以与此自定义指令一起使用,如图所示。对于更精细的错误处理,也可以访问指令的内部字段。
myDirectiveTemplate.html
<div ng-form="{{ formName }}">
<div ng-class="{'has-error': isInvalid('fieldOne')}">
<ui-select
ng-model="model.fieldOne"
name="fieldOne"
required>
</ui-select>
</div>
<div ng-class="{'has-error': isInvalid('fieldTwo')}">
<input
type="number"
ng-model="model.fieldTwo"
name="fieldTwo"
ng-pattern="directiveCtrl.someRegEx"
ng-required="directiveCtrl.fieldTwoIsRequired">
</div>
<!-- etc... -->
<input
type="hidden"
ng-model="someCalculatedValue"
name="internalField"
validate-custom>
</div>
指令的内部模板基本保持不变。最大的不同是 ngForm
的名称现在是动态设置的。
为了用 ngClass 处理这个问题,angular 表达式不起作用,所以我更新了我的示例,改为在 $scope
上使用一个函数。
最后,对于指令范围的业务规则,我使用了带有 ngModel
指令和 name
集的隐藏输入。我附加了一个用于验证的自定义迷你指令到这个字段。此字段上的验证错误将冒出以供父指令使用。
myDirective.js
app.directive('myDirective', function() {
return {
restrict: 'E',
template: 'myDirectiveTemplate.html',
controller: 'MyDirectiveCtrl',
scope: {
model: '=',
customRequired: '=?',
toggleAnotherValidation: '=?',
formName: '@name'
},
};
});
现在几乎所有逻辑都已从指令定义中删除。