Use ngModel on a custom directive with an embedded form, with working validation?

Josh Rickert

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...

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>

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.

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);
    }
  };
});
Josh Rickert

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.

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>

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.

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>

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.

myDirective.js

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.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

Use ngModel on a custom directive with an embedded form, with working validation?

From Dev

Custom directive for form validation

From Dev

Custom Validation Directive not working

From Dev

Custom Validation Directive not working

From Dev

Custom form validation directive to compare two fields

From Dev

Binding ngModel to a custom directive

From Dev

django 1.8 custom validation of form not working

From Dev

Spring Boot - Custom validation annotation on form not working

From Dev

Angular 5 form custom validation is not working properly

From Dev

ngModel.$parsers not working in directive

From Dev

ngModel.$parsers not working in directive

From Dev

AngularJS - Inherit ngModel in custom directive

From Dev

Dynamic ngModel and ngBind with a custom directive

From Dev

Form field validation inside Angular directive not working properly

From Dev

Using ng-repeat in directive causing form validation not working properly

From Dev

AngularJS custom form validation directive doesn't work in my modal

From Dev

Custom validation - "blank" directive

From Dev

form validation not working when use ajax id

From Dev

Angular directives - How to use JQuery to add ngModel and ngBind to custom directive elements?

From Dev

custom AngularJS directive not working

From Dev

custom AngularJS directive not working

From Dev

How to retrieve the ngmodel value in the custom directive

From Dev

Angularjs: form validation and input directive

From Dev

AngularJS - Form Validation in a templateUrl of a directive

From Dev

Conditionally disable validation for embedded form

From Dev

Conditionally disable validation for embedded form

From Dev

Angular validation directive not working properly

From Dev

Validation using custom directive with angularjs

From Dev

Validation using custom directive with angularjs

Related Related

  1. 1

    Use ngModel on a custom directive with an embedded form, with working validation?

  2. 2

    Custom directive for form validation

  3. 3

    Custom Validation Directive not working

  4. 4

    Custom Validation Directive not working

  5. 5

    Custom form validation directive to compare two fields

  6. 6

    Binding ngModel to a custom directive

  7. 7

    django 1.8 custom validation of form not working

  8. 8

    Spring Boot - Custom validation annotation on form not working

  9. 9

    Angular 5 form custom validation is not working properly

  10. 10

    ngModel.$parsers not working in directive

  11. 11

    ngModel.$parsers not working in directive

  12. 12

    AngularJS - Inherit ngModel in custom directive

  13. 13

    Dynamic ngModel and ngBind with a custom directive

  14. 14

    Form field validation inside Angular directive not working properly

  15. 15

    Using ng-repeat in directive causing form validation not working properly

  16. 16

    AngularJS custom form validation directive doesn't work in my modal

  17. 17

    Custom validation - "blank" directive

  18. 18

    form validation not working when use ajax id

  19. 19

    Angular directives - How to use JQuery to add ngModel and ngBind to custom directive elements?

  20. 20

    custom AngularJS directive not working

  21. 21

    custom AngularJS directive not working

  22. 22

    How to retrieve the ngmodel value in the custom directive

  23. 23

    Angularjs: form validation and input directive

  24. 24

    AngularJS - Form Validation in a templateUrl of a directive

  25. 25

    Conditionally disable validation for embedded form

  26. 26

    Conditionally disable validation for embedded form

  27. 27

    Angular validation directive not working properly

  28. 28

    Validation using custom directive with angularjs

  29. 29

    Validation using custom directive with angularjs

HotTag

Archive