Pass form to directive

62,865

Solution 1

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

Solution 2

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

Solution 3

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.

Solution 4

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: '^'}.

Solution 5

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/

Share:
62,865

Related videos on Youtube

Hola
Author by

Hola

.Net Developer, Entrepreneur, passionate technologist and currently writing a book on ASP.NET &amp; TDD Check out my latest product developed on ASP.NET MVC at http://www.yonkly.com

Updated on August 23, 2022

Comments

  • Hola
    Hola almost 2 years

    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?

    • Hola
      Hola almost 11 years
      based on answers below angular doesn't support dynamic form element names.
    • Ruslans Uralovs
      Ruslans Uralovs over 9 years
      There is a workaround however. See it here Plunk, it is based on @tanguy_k answer
  • Hola
    Hola almost 11 years
    thanks for your answer... the problem is in my directive I can get scope.form but I cannot get to scope.form.Email
  • Hola
    Hola almost 11 years
    this is what i am doing 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?
  • rGil
    rGil almost 11 years
    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
    rGil almost 11 years
    Spoke too soon. This solution may work for you, but is a bit involved. @Overzealous you may want to add this to your answer.
  • OverZealous
    OverZealous almost 11 years
    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
    OverZealous almost 11 years
    I would look at Mark Rajcok's answer below. I apparently didn't target the correct directive.
  • Mark Rajcok
    Mark Rajcok almost 11 years
    @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 where we see that form is used.
  • Hola
    Hola almost 11 years
    this works <input type="{{type}}" name='Email' ng-model="model"> but this does NOT work <input type="{{type}}" name='{{name}}' ng-model="model"> any ideas?
  • OverZealous
    OverZealous almost 11 years
    OK, I see that now. There's actually two directives, both form and ngForm. Thanks!
  • Mark Rajcok
    Mark Rajcok almost 11 years
    @eibrahim, Angular does not support dynamic form element names.
  • tanguy_k
    tanguy_k over 10 years
    @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: stackoverflow.com/questions/17618318/pass-form-to-directive/‌​…
  • mtpultz
    mtpultz over 9 years
    @MarkRajcok how would you pass the formCtrl reference to a directives controller? Or can you even do that?
  • Mark Rajcok
    Mark Rajcok over 9 years
    @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.
  • Ruslans Uralovs
    Ruslans Uralovs over 9 years
    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: plnkr.co/edit/13rqpfrTiTwDMpCPmT7X?p=preview
  • Marty
    Marty about 9 years
    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
    Marty about 9 years
    I ended up having to have nested forms because I need to have the same directive multiple times in a view, see: stackoverflow.com/questions/14378401/…
  • Rodrigo Brancher
    Rodrigo Brancher about 9 years
    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: plnkr.co/edit/HLKKY1ZH0Kla93P2SmGj?p=preview Any clue on how to solve this?
  • Rodrigo Brancher
    Rodrigo Brancher about 9 years
    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.
  • tanguy_k
    tanguy_k about 9 years
    FYI AngularJS 1.3 features ngMessage so the code exposed here could be modernized
  • Bren
    Bren about 8 years
    @MarkRajcok would this work with component ? I tried require: {form: '^form'} though it did not seem to be working
  • Bren
    Bren about 8 years
    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
  • Darren Clark
    Darren Clark over 7 years
    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?
  • Jowy
    Jowy almost 7 years
    Work fine and is very elegant. For me it doesn't work with {parentDir: '^'} but only with {parentDir: '^parentDir'}.
  • Gabe Gates
    Gabe Gates about 4 years
    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.
  • Squirrelkiller
    Squirrelkiller over 2 years
    Any idea how I can pass the form through my UpgradeComponent, trying to use an old directive in new Angular?