I have a simple directive:
myDirective.html:
<div class="my-directive" ng-class="{'on':status,'off':!status}" ng-click="onClick()">
click me
</div>
myDirective.js:
app.directive('myDirective', function () {
return {
restrict: 'E',
templateUrl: 'myDirective.html',
controller: ['$scope', function ($scope) {
$scope.onClick = function () {
$scope.status = !$scope.status;
}
}]
}
});
I have a ng-form and I'm using this directive as a field of the form:
<ng-form name="frm">
<div class="form-field">
<input type="text" required ng-model="v1" />
</div>
<div class="form-field">
<my-directive status="v2" />
</div>
<input type="button" ng-disabled="!frm.$valid" value="save" />
</ng-form>
Live example in: JsFiddle.
When I set some text in the input frm.$valid turn to true automatically.
I want to apply to my-directive the same effect. Lets say...
Consider my-directive as valid only when status is true.
And if my-directive is invalid the form should be invalid too.
Can I set this effect in my-directive? And how can I do it?
I am trying to validate form while using directive. The directive creates form inputs by compiling and replacing the element.
If I use simple template version of directive it works fine. If the input created by directive is invalid form is also invalid. It even works when created in a loop as well : http://codepen.io/kuasha/pen/gbreKP
<div ng-app="app" ng-controller="MainCtrl">
<form name="myform" ng-submit="sendForm()" novalidate>
<div class="form-group">
<myinput />
</div>
<div class="form-group">
<button ng-show="myform.$valid" type="submit">Submit!</button>
</div>
</form>
</div>
The app and controller:
var app = angular.module('app', []);
app.controller('MainCtrl', function ($scope) {
$scope.sendForm = function () {
if ($scope.myform.$valid) {
alert('form valid');
}
};
});
Here is the simple working version of the directive:
app.directive('myinput', function () {
return {
restrict: 'E',
template: '<label>Test</label><input ng-model="value" type="text" required ng-minlength="5" />'
};
});
But if I change the directive to use a link function and compile then, even when the input inside the directive is invalid, forms $valid is still true: http://codepen.io/kuasha/pen/qEZoKv?editors=101
app.directive('myinput', function ($compile) {
return {
restrict: 'E',
link: function (scope, element, attributes) {
var template = '<div><input type="text" required ng-minlength=5 /></div>'
var newElement = angular.element(template);
$compile(newElement)(scope);
element.replaceWith(newElement);
}
};
});
Edit:
Directive could create its own validator-
app.directive('myinput', function ($compile) {
return {
require:'^form',
restrict: 'E',
scope: {
},
controller: ['$scope', function($scope){
}],
link: function (scope, element, attributes, ctrl) {
var template = '<label>Test</label><input name="myctrl" ng-model="value" type="text" required ng-minlength="5" />'
var newElement = angular.element(template);
$compile(newElement)(scope);
element.replaceWith(newElement);
scope.$watch("value", function(){
ctrl.$setValidity('myctrl', scope.value && scope.value.length > 0);
console.log("Validity set");
});
}
};
});
But its probably not right approach for this particular problem.
You should switch the replaceWith and $compile (DEMO):
element.replaceWith(newElement);
$compile(newElement)(scope);
I think it is because you can't really compile an element that is not yet in the DOM. But I'm not sure.
LIVE DEMO
I use Angular 1.2.18 (have to support IE8), and I'm trying to create something similar to ngMessages that exists in Angular 1.3:
HTML:
<form name="form" novalidate>
<div>
<label for="phone">Phone:</label>
<input id="phone" name="phone" ng-model="phone" type="text"
required ng-minlength="5">
<div form-errors-for="form.phone">
<div form-error="required">Required</div>
<div form-error="minlength">Too short</div>
</div>
</div>
</form>
JS:
angular.module("Validation", [])
.directive("formErrorsFor", function() {
return {
scope: {
model: '=formErrorsFor'
},
controller: function($scope) {
this.model = $scope.model;
}
};
})
.directive("formError", function() {
return {
require: '^formErrorsFor',
link: function(scope, element, attrs, ctrl) {
console.log(ctrl.model.$error); // Always {}. Why??
}
};
});
Unfortunately, accessing form.phone.$error from the formError directive, always results in an empty object. Why it doesn't have the required and the minlength properties?
PLAYGROUND HERE
I tried you jsbin. The issue here is you are trying to access errors too early.
Also the scope on the two directives are different.
I changed you jsbin and it seems to work. I added a watch
scope.$watch(function(){
return ctrl.model.$error;
},function(n,o){
console.log(n);
});
for error changes as it s not defined on scope. See this http://jsbin.com/duxewigi/3/edit
I have an AngularJS directive that has an input element in its template. Now I can add this directive inside a <form> tag and bind the ng-model without any issues.
The issue that I am running into is that the input that is being dynamically added into the <form> is not showing up in the formContoller which makes proper validation impossible.
Is there a way to have a directive that is being included with a <form> that has its own scope be able to dynamically add inputs that will be included in the formController that the directive is included in?
UPDATE
So as I was trying to build out the simpler example, I did figure out the root cause of the issue and that is the fact I am using $compile to generate the template used. I have created a simplified version of the issue occurring on plunker:
http://plnkr.co/edit/XsyZKW?p=preview
AngularJS Code
angular.module('directive', []).directive('containerDir', function() {
return {
scope: {
test: '#'
},
compile: function(element, attributes) {
return function(scope, element, attributes) {
scope.model = {
dataObject: {}
}
};
}
};
});
angular.module('directive').directive('inputDir', function($compile) {
return {
template: '<span></span>',
scope: {
model: '=dataModel'
},
compile: function(element, attributes) {
return {
pre: function(scope, element, attributes) {
var template = '<input type="text" name="username" ng-model="model.username" required />';
element.html($compile(template)(scope));
},
post: function(scope, element, attributes) {
}
};
}
};
});
template
<!doctype html>
<html ng-app='directive'>
<head>
<script data-require="jquery" data-semver="2.0.1" src="http://code.jquery.com/jquery-2.0.1.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div container-dir>
<form name="testForm" nag-resettable-form style="padding-left: 20px;">
<div>
<label for="first-name">First Name</label>
<input id="first-name"
type="text"
name="firstName"
ng-model="model.dataObject.firstName"
required
/>
<div>value: {{model.dataObject.firstName}}</div>
<div>{{testForm.firstName.$error | json}}</div>
</div>
<div>
<label for="username">Username</label>
<span input-dir data-data-model="model.dataObject"></span>
<div>value: {{model.dataObject.username}}</div>
<div>{{testForm.username.$error | json}}</div>
</div>
<div>
{{testForm | json}}
</div>
<div style="padding-top: 1rem;">
<button class="primary" ng-click="model.submitForm()">Submit</button>
<button ng-click="resetForm(formReveal, {}, model.resetForm)">Reset</button>
</div>
</form>
</div>
</body>
</html>
Here is the same example without using $compile to show it does work without it:
http://plnkr.co/edit/i8Lkt1?p=preview
Now I would still like to know if what I am trying to do it possible with adding input elements dynamically with directives that use $compile to generate the HTML for the directive.
While I might be able to rewrite the directive in question to not use $compile, I would still like to know if this is possible for future reference.
I have this form : http://jsfiddle.net/dfJeN/
As you can see the name value for the input is statically set :
name="username"
, the form validation works fine (add something and remove all text from the input, a text must appears).
Then I try to dynamically set the name value : http://jsfiddle.net/jNWB8/
name="{input.name}"
Then I apply this to my validation
login.{{input.name}}.$error.required
(this pattern will be used in an ng-repeat) but my form validation is broken. It is correctly interpreted in my browser (if I inspect the element I saw login.username.$error.required).
Any Idea ?
EDIT: After logging the scope in the console it appears that the
{{input.name}}
expression is not interpolate. My form as an {{input.name}} attribute but no username.
UPDATE: Since 1.3.0-rc.3 name="{{input.name}}" works as expected. Please see #1404
You can't do what you're trying to do that way.
Assuming what you're trying to do is you need to dynamically add elements to a form, with something like an ng-repeat, you need to use nested ng-form to allow validation of those individual items:
<form name="outerForm">
<div ng-repeat="item in items">
<ng-form name="innerForm">
<input type="text" name="foo" ng-model="item.foo" />
<span ng-show="innerForm.foo.$error.required">required</span>
</ng-form>
</div>
<input type="submit" ng-disabled="outerForm.$invalid" />
</form>
Sadly, it's just not a well-documented feature of Angular.
Using nested ngForm allows you to access the specific InputController from within the HTML template. However, if you wish to access it from another controller it does not help.
e.g.
<script>
function OuterController($scope) {
$scope.inputName = 'dynamicName';
$scope.doStuff = function() {
console.log($scope.formName.dynamicName); // undefined
console.log($scope.formName.staticName); // InputController
}
}
</script>
<div controller='OuterController'>
<form name='myForm'>
<input name='{{ inputName }}' />
<input name='staticName' />
</form>
<a ng-click='doStuff()'>Click</a>
</div>
I use this directive to help solve the problem:
angular.module('test').directive('dynamicName', function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
priority: 100000,
link: function(scope, elem) {
var name = $parse(elem.attr('dynamic-name'))(scope);
// $interpolate() will support things like 'skill'+skill.id where parse will not
elem.removeAttr('dynamic-name');
elem.attr('name', name);
$compile(elem)(scope);
}
};
});
Now you use dynamic names wherever is needed just the 'dynamic-name' attribute instead of the 'name' attribute.
e.g.
<script>
function OuterController($scope) {
$scope.inputName = 'dynamicName';
$scope.doStuff = function() {
console.log($scope.formName.dynamicName); // InputController
console.log($scope.formName.staticName); // InputController
}
}
</script>
<div controller='OuterController'>
<form name='myForm'>
<input dynamic-name='inputName' />
<input name='staticName' />
</form>
<a ng-click='doStuff()'>Click</a>
</div>
The problem should be fixed in AngularJS 1.3, according to this discussion on Github.
Meanwhile, here's a temporary solution created by #caitp and #Thinkscape:
// Workaround for bug #1404
// https://github.com/angular/angular.js/issues/1404
// Source: http://plnkr.co/edit/hSMzWC?p=preview
app.config(['$provide', function($provide) {
$provide.decorator('ngModelDirective', function($delegate) {
var ngModel = $delegate[0], controller = ngModel.controller;
ngModel.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) {
var $interpolate = $injector.get('$interpolate');
attrs.$set('name', $interpolate(attrs.name || '')(scope));
$injector.invoke(controller, this, {
'$scope': scope,
'$element': element,
'$attrs': attrs
});
}];
return $delegate;
});
$provide.decorator('formDirective', function($delegate) {
var form = $delegate[0], controller = form.controller;
form.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) {
var $interpolate = $injector.get('$interpolate');
attrs.$set('name', $interpolate(attrs.name || attrs.ngForm || '')(scope));
$injector.invoke(controller, this, {
'$scope': scope,
'$element': element,
'$attrs': attrs
});
}];
return $delegate;
});
}]);
Demo on JSFiddle.
Nice one by #EnISeeK.... but i got it to be more elegant and less obtrusive to other directives:
.directive("dynamicName",[function(){
return {
restrict:"A",
require: ['ngModel', '^form'],
link:function(scope,element,attrs,ctrls){
ctrls[0].$name = scope.$eval(attrs.dynamicName) || attrs.dynamicName;
ctrls[1].$addControl(ctrls[0]);
}
};
}])
Just a little improvement over EnlSeek solution
angular.module('test').directive('dynamicName', ["$parse", function($parse) {
return {
restrict: 'A',
priority: 10000,
controller : ["$scope", "$element", "$attrs",
function($scope, $element, $attrs){
var name = $parse($attrs.dynamicName)($scope);
delete($attrs['dynamicName']);
$element.removeAttr('data-dynamic-name');
$element.removeAttr('dynamic-name');
$attrs.$set("name", name);
}]
};
}]);
Here is a plunker trial. Here is detailed explantion
I expand the #caitp and #Thinkscape solution a bit, to allow dynamically created nested ng-forms, like this:
<div ng-controller="ctrl">
<ng-form name="form">
<input type="text" ng-model="static" name="static"/>
<div ng-repeat="df in dynamicForms">
<ng-form name="form{{df.id}}">
<input type="text" ng-model="df.sub" name="sub"/>
<div>Dirty: <span ng-bind="form{{df.id}}.$dirty"></span></div>
</ng-form>
</div>
<div><button ng-click="consoleLog()">Console Log</button></div>
<div>Dirty: <span ng-bind="form.$dirty"></span></div>
</ng-form>
</div>
Here is my demo on JSFiddle.
I used Ben Lesh's solution and it works well for me. But one problem I faced was that when I added an inner form using ng-form, all of the form states e.g. form.$valid, form.$error etc became undefined if I was using the ng-submit directive.
So if I had this for example:
<form novalidate ng-submit="saveRecord()" name="outerForm">
<!--parts of the outer form-->
<ng-form name="inner-form">
<input name="someInput">
</ng-form>
<button type="submit">Submit</button>
</form>
And in the my controller:
$scope.saveRecord = function() {
outerForm.$valid // this is undefined
}
So I had to go back to using a regular click event for submitting the form in which case it's necessary to pass the form object:
<form novalidate name="outerForm"> <!--remove the ng-submit directive-->
<!--parts of the outer form-->
<ng-form name="inner-form">
<input name="someInput">
</ng-form>
<button type="submit" ng-click="saveRecord(outerForm)">Submit</button>
</form>
And the revised controller method:
$scope.saveRecord = function(outerForm) {
outerForm.$valid // this works
}
I'm not quite sure why this is but hopefully it helps someone.
This issue has been fixed in Angular 1.3+
This is the correct syntax for what you are trying to do:
login[input.name].$invalid
if we set dynamic name for a input like the below
<input name="{{dynamicInputName}}" />
then we have use set validation for dynamic name like the below code.
<div ng-messages="login.dynamicInputName.$error">
<div ng-message="required">
</div>
</div>