I have a commonly reused set of form inputs that are reused throughout my application, so I am trying to encapsulate them in a custom directive. I want to set an ngModel
on my directive and have that split up to be editable in several different inputs (some of them are directives themselves) within the main directive.
At the same time, I need the form validation results to be passed up the chain to a parent form so that I can display appropriate messages and styles.
What is the simplest and most idiomatic way to implement this?
These (simplified) templates should give you an example of what I'm going for...
<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>
<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>
At the moment, both myDirectiveForm
and myDirectiveInstance
are publishing themselves as properties of the outerForm
FormController
. I hoping to make this directive a black box, so the fact that myDirectiveForm
is attaching directly to outerForm
bothers me and seems to indicate that I'm doing something wrong.
Here's what my directive definition looks like right now.
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);
}
};
});
I've worked out a decent solution. In brief, I've removed the NgModelController
implementation from my custom directive, and I'm relying entirely on the internal FormController
from the form
directive inside my custom directive. As far as I can tell, NgModelController
just wasn't designed to wrap a form in a custom directive. However, nested forms are supported quite nicely in Angular, so this is the way to go.
Something I hadn't realized was that you can dynamically assign a name to a form as of Angular 1.3. While I can't prevent the "black box" from leaking up and attaching itself to a parent form controller, I can at least control the name it uses to publish itself in the parent scope, which is acceptable and is very similar to the API provided by ngModel
.
Updated examples below.
<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>
In the outer template, I removed the ng-model
directive in favor of a custom attribute. The name
property may still be used to determine what name the internal form is published under.
Alternativately, the ng-model
could be kept around, and an attribute form-name
(with an appropriate alteration to the isolate scope binding below) could be used to publish the custom directive's FormController
to the parent FormController
, but this could be somewhat misleading since the ng-model
directive isn't being used for anything except an isolate scope binding.
Either way, ng-model
should not be used in conjunction with the name
property for this use case. Otherwise, there may be conflicts as the NgModelController
and the FormController
attempt to publish themselves to the parent FormController
(outerForm
) under the same property name (outerForm.myDirectiveInstance
).
Since validation errors bubble up to parent form
directives, ngMessages
may be used with this custom directive as shown. For more granular error handling, the internal fields of the directives can be accessed as well.
<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>
The internal template of the directive stays mostly the same. The big difference is that the name for ngForm
is now dynamically set.
To handle that with ngClass, angular expressions wouldn't work, so I updated my example to use a function on the $scope
instead.
Last, for directive-wide business rules, I used a hidden input with an ngModel
directive and a name
set. I attached a custom mini-directive for validation to just this field. Validation errors on this field will bubble up to be used by the parent directive.
app.directive('myDirective', function() {
return {
restrict: 'E',
template: 'myDirectiveTemplate.html',
controller: 'MyDirectiveCtrl',
scope: {
model: '=',
customRequired: '=?',
toggleAnotherValidation: '=?',
formName: '@name'
},
};
});
Pretty much all the logic has been removed from the directive definition now.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments