77

I want to encapsulate my form fields in a directive so I can simply do this:

<div ng-form='myForm'>
  <my-input name='Email' type='email' label='Email Address' placeholder="Enter email" ng-model='model.email' required='false'></my-input>

</div>

How do I access the myForm in my directive so I can do validation checks, e.g. myForm.Email.$valid?

Chris Krycho
  • 3,125
  • 1
  • 23
  • 35
Emad
  • 4,110
  • 5
  • 30
  • 35

5 Answers5

155

To access the FormController in a directive:

require: '^form',

Then it will be available as the 4th argument to your link function:

link: function(scope, element, attrs, formCtrl) {
    console.log(formCtrl);
}

fiddle

You may only need access to the NgModelController though:

require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
     console.log(ngModelCtrl);
}

fiddle

If you need access to both:

require: ['^form','ngModel'],
link: function(scope, element, attrs, ctrls) {
    console.log(ctrls);
}

fiddle

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 3
    If you don't mind answering, why does this work with `'^form'` but not with `'^ngForm'`? I was trying to get that to work the first time, but it came up with a "no controller" error. I like your answer much better than mine. – OverZealous Jul 12 '13 at 18:24
  • 9
    @OverZealous, apparently the directive name that Angular uses when a `form` or `ng-form` directive is found in the HTML is `form` rather than `ngForm`. It took me a few tries to figure out that the name is `form`. I think this is the [Angular source code](https://github.com/angular/angular.js/blob/f1b94b4b599ab701bc75b55bbbbb73c5ef329a93/src/ng/directive/form.js#L304) where we see that `form` is used. – Mark Rajcok Jul 12 '13 at 19:08
  • this works but this does NOT work any ideas? – Emad Jul 12 '13 at 19:12
  • OK, I see that now. There's actually two directives, both `form` and `ngForm`. Thanks! – OverZealous Jul 12 '13 at 19:25
  • 2
    @eibrahim, Angular does not support dynamic form element names. – Mark Rajcok Jul 12 '13 at 19:26
  • 1
    @MarkRajcok Accessing the parent form inside the directive is only half of the job. You then want to use $invalid, $error and friends. ~~And this does not work, the only solution is to create a ng-form inside the directive~~ /!\ I was wrong, see my answer below: http://stackoverflow.com/questions/17618318/pass-form-to-directive/22291446#22291446 – tanguy_k Mar 10 '14 at 03:13
  • @MarkRajcok how would you pass the formCtrl reference to a directives controller? Or can you even do that? – mtpultz Oct 21 '14 at 15:46
  • 3
    @mtpultz, put it on the scope in your link function: `scope.formCtrl = formCtrl;`, then you can access it in your controller using `$scope`: `controller: function($scope) { ... }`. Note however, that your directive controller will run first, so the reference won't be there when the controller function first executes. – Mark Rajcok Oct 21 '14 at 16:20
  • @MarkRajcok would this work with component ? I tried `require: {form: '^form'}` though it did not seem to be working – Bren Jun 25 '16 at 13:13
  • Actually ignore my comment, documentation says "required controllers will not be available during the instantiation of the controller, but they are guaranteed to be available just before the $onInit method is executed!". And it is there when I need it. So all is good – Bren Jun 25 '16 at 13:19
  • Any idea how I can pass the form through my UpgradeComponent, trying to use an old directive in new Angular? – Squirrelkiller Feb 16 '22 at 10:48
32

Here a complete example (styled using Bootstrap 3.1)

It contains a form with several inputs (name, email, age, and country). Name, email and age are directives. Country is a "regular" input.

For each input is displayed an help message when the user does not enter a correct value.

The form contains a save button which is disabled if the form contains at least one error.

<!-- index.html -->
<body ng-controller="AppCtrl">
  <script>
    var app = angular.module('app', []);

    app.controller('AppCtrl', function($scope) {
      $scope.person = {};
    });
  </script>
  <script src="inputName.js"></script>
  <script src="InputNameCtrl.js"></script>
  <!-- ... -->

  <form name="myForm" class="form-horizontal" novalidate>
    <div class="form-group">
      <input-name ng-model='person.name' required></input-name>
    </div>

    <!-- ... -->

    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-4">
        <button class="btn btn-primary" ng-disabled="myForm.$invalid">
          <span class="glyphicon glyphicon-cloud-upload"></span> Save
        </button>
      </div>
    </div>
  </form>

  Person: <pre>{{person | json}}</pre>
  Form $error: <pre>{{myForm.$error | json}}</pre>
  <p>Is the form valid?: {{myForm.$valid}}</p>
  <p>Is name valid?: {{myForm.name.$valid}}</p>
</body>

// inputName.js
app.directive('inputName', function() {
  return {
    restrict: 'E',
    templateUrl: 'input-name.html',
    replace: false,
    controller: 'InputNameCtrl',
    require: ['^form', 'ngModel'],

    // See Isolating the Scope of a Directive http://docs.angularjs.org/guide/directive#isolating-the-scope-of-a-directive
    scope: {},

    link: function(scope, element, attrs, ctrls) {
      scope.form = ctrls[0];
      var ngModel = ctrls[1];

      if (attrs.required !== undefined) {
        // If attribute required exists
        // ng-required takes a boolean
        scope.required = true;
      }

      scope.$watch('name', function() {
        ngModel.$setViewValue(scope.name);
      });
    }
  };
});

// inputNameCtrl
app.controller('InputNameCtrl', ['$scope', function($scope) {
}]);

AngularJS form with directives

Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
tanguy_k
  • 11,307
  • 6
  • 54
  • 58
  • 2
    This is a great example. I've changed your Plunk to make your directive more generic in that it can now support any type of input, e.g. text or password or email etc. See it here: http://plnkr.co/edit/13rqpfrTiTwDMpCPmT7X?p=preview – Ruslans Uralovs Jan 15 '15 at 11:35
  • this doesnt seem to work when you have the same directive multiple times on the same view - this is because the "name" is hardcoded in the directive – Marty May 14 '15 at 14:27
  • I ended up having to have nested forms because I need to have the same directive multiple times in a view, see: http://stackoverflow.com/questions/14378401/dynamic-validation-and-name-in-a-form-with-angularjs – Marty May 14 '15 at 14:33
  • This works very nicely, thanks for posting. Although now I'm dealing with the inverse problem: when setting the object value from outside the directive scope, it does not update the UI and “freezes” the data, never changing again. Example here: http://plnkr.co/edit/HLKKY1ZH0Kla93P2SmGj?p=preview Any clue on how to solve this? – Rodrigo Brancher Jun 09 '15 at 16:53
  • Sorry I forgot to mention the (small) Plunk change: I've added a link, _on the right of submit button_, that changes the person name programatically. – Rodrigo Brancher Jun 09 '15 at 17:03
  • FYI AngularJS 1.3 features [ngMessage](https://docs.angularjs.org/api/ngMessages/directive/ngMessages) so the code exposed here could be modernized – tanguy_k Jun 26 '15 at 10:50
17

Edit 2: I'll leave my answer, as it might be helpful for other reasons, but the other answer from Mark Rajcok is what I originally wanted to do, but failed to get to work. Apparently the parent controller here would be form, not ngForm.


You can pass it in using an attribute on your directive, although that will get rather verbose.

Example

Here's a working, simplified jsFiddle.

Code

HTML:

<div ng-form="myForm">
    <my-input form="myForm"></my-input>
</div>

Essential parts of the directive:

app.directive('myInput', function() {
    return {
        scope: {
            form: '='
        },
        link: function(scope, element, attrs) {
            console.log(scope.form);
        }
    };
});

What's happening

We've asked Angular to bind the scope value named in the form attribute to our isolated scope, by using an '='.

Doing it this way decouples the actual form from the input directive.

Note: I tried using require: "^ngForm", but the ngForm directive does not define a controller, and cannot be used in that manner (which is too bad).


All that being said, I think this is a very verbose and messy way to handle this. You might be better off adding a new directive to the form element, and use require to access that item. I'll see if I can put something together.

Edit: Using a parent directive

OK, here's the best I could figure out using a parent directive, I'll explain more in a second:

Working jsFiddle using parent directive

HTML:

<div ng-app="myApp">
    <div ng-form="theForm">
        <my-form form="theForm">
            <my-input></my-input>
        </my-form>
    </div>
</div>

JS (partial):

app.directive('myForm', function() {
    return {
        restrict: 'E',
        scope: {
            form: '='
        },
        controller: ['$scope', function($scope) {
            this.getForm = function() {
                return $scope.form;
            }
        }]
    }
});

app.directive('myInput', function() {
    return {
        require: '^myForm',
        link: function(scope, element, attrs, myForm) {
            console.log(myForm.getForm());
        }
    };
});

This stores the form in the parent directive scope (myForm), and allows child directives to access it by requiring the parent form (require: '^myForm'), and accessing the directive's controller in the linking function (myForm.getForm()).

Benefits:

  • You only need to identify the form in one place
  • You can use your parent controller to house common code

Negatives:

  • You need an extra node
  • You need to put the form name in twice

What I'd prefer

I was trying to get it to work using an attribute on the form element. If this worked, you'd only have to add the directive to the same element as ngForm.

However, I was getting some weird behavior with the scope, where the myFormName variable would be visible within $scope, but would be undefined when I tried to access it. That one has me confused.

OverZealous
  • 39,252
  • 15
  • 98
  • 100
  • thanks for your answer... the problem is in my directive I can get scope.form but I cannot get to scope.form.Email – Emad Jul 12 '13 at 17:14
  • this is what i am doing http://jsfiddle.net/SwEM6/1/ (note that the name of the input is "calculated" from the directive scope property {{name}} but if I change that to a hard coded value e.g. "Email" then it works... any idea? – Emad Jul 12 '13 at 17:18
  • You are looking for a way to make dynamic form names, i.e. `name="{{name1}}"`, and I have yet to see an answer to this. – rGil Jul 12 '13 at 17:28
  • 1
    Spoke too soon. [This solution](http://stackoverflow.com/a/15846954/2013981) may work for you, but is a bit involved. @Overzealous you may want to add this to your answer. – rGil Jul 12 '13 at 17:30
  • I would look at Mark Rajcok's answer below. I apparently didn't target the correct directive. – OverZealous Jul 12 '13 at 18:25
8

Starting with AngularJS 1.5.0, there is much cleaner solution for this (as opposed to using the link function directly). If you want to access a form's FormController in your subcomponent's directive controller, you can simply slap the require attribute on the directive, like so:

return {
  restrict : 'EA',
  require : {
    form : '^'
  },
  controller : MyDirectiveController,
  controllerAs : 'vm',
  bindToController : true,
  ...
};

Next, you'll be able to access it in your template or directive controller like you would any other scope variable, e.g.:

function MyDirectiveController() {
  var vm = this;
  console.log('Is the form valid? - %s', vm.form.$valid);
}

Note that for this to work, you also need to have the bindToController: true attribute set on your directive. See the documentation for $compile and this question for more information.

Relevant parts from the documentation:

require

Require another directive and inject its controller as the fourth argument to the linking function. The require property can be a string, an array or an object:

If the require property is an object and bindToController is truthy, then the required controllers are bound to the controller using the keys of the require property. If the name of the required controller is the same as the local name (the key), the name can be omitted. For example, {parentDir: '^parentDir'} is equivalent to {parentDir: '^'}.

Community
  • 1
  • 1
Priidu Neemre
  • 2,813
  • 2
  • 39
  • 40
  • This looked almost right, but isn't quite working for me. If I just have the required property, it will be passed into a link function, but not bound to the controller(bindToController is true). Is it because of not isolating scope? – Darren Clark Dec 12 '16 at 03:26
  • Work fine and is very elegant. For me it doesn't work with {parentDir: '^'} but only with {parentDir: '^parentDir'}. – Jowy Aug 17 '17 at 12:27
  • This was a great starting point for me. However, I ran into issues because my directive has an **isolated scope**, in this case I did not use *bindToController* and used the *require* controllers in the 4th argument of the *link* block. – Gabe Gates Jun 02 '20 at 18:21
2

Made your 'What I'd prefer' fiddle work! For some reason you could see the "$scope.ngForm" string in a console.log, but logging it directly didn't work, resulting in undefined. However, you can get it if you pass attributes to the controller function.

app.directive('myForm', function() {
return {
    restrict: 'A',
    controller: ['$scope','$element','$attrs', function($scope,$element,$attrs) {
        this.getForm = function() {
            return $scope[$attrs['ngForm']];
        }
    }]
}
});

http://jsfiddle.net/vZ6MD/20/