Pass form to directive

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?

63157 次浏览

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.

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

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/

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

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