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.
Related
I am trying to build some custom directives for inputs with validations provided by ngMessages directive. Still, I can't link multiple variables from $scope to dynamically determine the form name and the input name. Here's my code so far:
The directive:
app.directive('textBox', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
label: "#",
fieldName: "#",
bindTo: "="
},
require: "^form",
templateUrl: '/WebClient/Directives/TextBox/textBoxTemplate.html',
link: function (scope, element, attrs, ctrl) {
$scope.formName = ctrl.$name;
}
};
}]);
The template:
<div>
<label>{{label}}</label>
<input type="text" name="{{fieldName}}" ng-model="{{field}}" required />
<div ng-messages="{{formName}}.{{fieldName}}.$error">
<div ng-message="required">You left the field blank...</div>
<div ng-message="minlength">Your field is too short</div>
<div ng-message="maxlength">Your field is too long</div>
<div ng-message="email">Your field has an invalid email address</div>
</div>
</div>
The usage:
<text-box bind-to="myField" field-name="myField"></text-box>
The issues I encounter are related to the ng-messages attribute value. Doesn't seem to work when I use curly braces and it renders the text "formName.fieldName.$error" if I don't. The other issue is related to the ng-model, the same scenario applies.
Thank you!
You can pass the formController, ctrl in your link method to directive scope like scope.form = ctrl;.
Then you can add <div ng-messages="form[fieldName].$error"> to access the $error property.
Curly braces are only required for the name of the input field. For ng-model you can directly add the model with-out curlies because ng-model requires two-way binding to work.
Please have a look at the demo below or this jsfiddle.
angular.module('demoApp', ['ngMessages'])
.controller('mainController', MainController)
.directive('textBox', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
label: "#",
fieldName: "#",
bindTo: "="
},
require: "^form",
templateUrl: '/WebClient/Directives/TextBox/textBoxTemplate.html',
link: function (scope, element, attrs, ctrl) {
//$scope.formName = ctrl.$name;
scope.form = ctrl;
}
};
}]);
function MainController($scope) {
$scope.myField = 'test';
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-messages.js"></script>
<div ng-app="demoApp" ng-controller="mainController">
<form name="testForm">
<text-box bind-to="model" field-name="myField"></text-box>
</form>
{{model}}
<script type="text/ng-template" id="/WebClient/Directives/TextBox/textBoxTemplate.html">
<div>
<label>{{label}}</label>
<input type="text" name="{{fieldName}}" ng-model="bindTo" required />
<div ng-messages="form[fieldName].$error">
<div ng-message="required">You left the field blank...</div>
<div ng-message="minlength">Your field is too short</div>
<div ng-message="maxlength">Your field is too long</div>
<div ng-message="email">Your field has an invalid email address</div>
</div>
</div>
</script>
</div>
I have an input element on which I have ng-required ng-pattern="/[0-9]{5}/".
It also has a directive which adds an error message, and which has the following linking function and other related properties:
return {
scope: {},
require: ['^^form', 'ngModel'],
link: function(scope, element, attrs, ctrls) {
// Places error message element after input element.
var el = angular.element('<span>')
.append(element.clone().removeAttr('mp-validated-field')) // else recurses
.append('<span ng-show="showError" class="errorMsg">{{ error }}</span>');
el = el[0].innerHTML;
var tmp = $compile(el)(scope);
element.replaceWith(tmp);
element = tmp;
ctrls[0].$addControl(ctrls[1]);
},
controller: // etc
What should be happening here is that a span operating as an error message is placed after the input element automatically by the directive. As written, this happens, however I have to use $addControl to get the formController to re-register the input after that.
What happens when I run this with a blank input (so it should fail required) is that the formController's $error hash for this field contains {pattern: false}, which is to say it thinks there's only a pattern validation, no required validation, and that the pattern validation is passing despite that it's the regex /[0-9]{5}/ (zipcode).
What am I missing here?
Edit: I moved the ng-required and ng-pattern directives to be before my custom directive and now it recognizes both of them, however it just fails on ng-required arbitrarily every time, regardless of whether it is blank.
Try this solution jsfiddle.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
})
.directive('myError', function($compile) {
return {
restrict: "A",
require: ['ngModel'],
scope: {
ngModel: "="
},
link: function(scope, elem, attr) {
var span = angular.element('<div ng-show="showError" class="error">{{error}}</div>');
elem.after(span);
$compile(span)(scope);
scope.error = 'Zip code must be not 0000';
scope.$watch('ngModel', function(val) {
if (val == '00000')
scope.showError = true;
else
scope.showError = false;
});
}
}
});
.error {
color: red;
font-style: italic;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<form name="testForm">
<input type="text" ng-model="zip" name="zip" my-error ng-pattern="/[0-9]{5}/" required>
<div ng-show='testForm.zip.$error.required' class='error'>Is required</div>
<div ng-show='testForm.zip.$error.pattern' class='error'>Invalid pattern</div>
<br>
<input type="text" ng-model="zip2" name="zip2" ng-pattern="/[0-9]{5}/" required my-error>
<div ng-show='testForm.zip2.$error.required' class='error'>Is required</div>
<div ng-show='testForm.zip2.$error.pattern' class='error'>Invalid pattern</div>
</form>
</div>
</div>
But for complex custom checks i recommend use-form-error.
Live example on jsfiddle.
angular.module('ExampleApp', ['use', 'ngMessages'])
.controller('ExampleController', function($scope) {
});
.errors {
color: maroon
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdn.rawgit.com/Stepan-Kasyanenko/use-form-error/master/src/use-form-error.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular-messages.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<form name="myForm" use-form-error="formInvalid" use-error-expression="digit==9">
<div ng-messages="myForm.$error" style="color:maroon">
<div ng-message="formInvalid">You form is not valid</div>
</div>
<label>Your number should be even, not divisible by three and should not be 6,7,9:</label>
<input type="text" ng-model="digit" name="myDigit" ng-minlength="1" ng-maxlength="20" required use-form-error="isEven" use-error-expression="digit%2==0" />
<span use-form-error="isDividedThree" use-error-expression="digit%3==0" use-error-input="myForm.myDigit"></span>
<span use-form-error="formInvalid" use-error-expression="digit==7" use-error-input="myForm.myDigit"></span>
<span use-form-error="formInvalid" use-error-expression="digit==6"></span>
<pre>myForm.myDigit.$error = {{ myForm.myDigit.$error | json }}</pre>
<div ng-messages="myForm.myDigit.$error" ng-messages-multiple="true" style="color:maroon">
<div ng-message="required">You did not enter a digit</div>
<div ng-message="minlength">Your digit is too short</div>
<div ng-message="maxlength">Your digit is too long</div>
<div ng-message="isEven">Your digit is even</div>
<div ng-message="isDividedThree">Your digit is divided by three</div>
</div>
</form>
</div>
</div>
I have an form with an text input and a validation message that has to be shown if the input changes. (This example make no sence but it is reduced to the base problem)
What I want to generate in my angularJS directive is:
<p>Username:<br>
<input type="text" name="user" ng-model="user" required>
<span style="color:red" ng-show="myForm.user.$dirty"> Username is dirty.</span>
</p>
See the example here. (The static passwd code works, the generated user code not)
var app = angular.module('myApp', []);
app.controller('myCrtl', function($scope) {
$scope.user = 'MyName';
$scope.passwd = 'MyPw';
})
.directive("mytest", function($compile){
return{
scope: true,
link: function(scope, element){
var template = '<input type="text" name="user" ng-model="user" required>'+
'<span style="color:red" ng-show="myForm.user.$dirty">'+
'Username is dirty.</span>';
var tmpFn= $compile(template);
var content = tmpFn(scope);
element.append(content);
}
}
});
<!DOCTYPE html>
<html>
<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.js"></script>
<body>
<h2>Validation</h2>
<form ng-app="myApp" ng-controller="myCrtl"
name="myForm" novalidate>
<!-- the generated code does not work -->
<p mytest>User:<br>
</p>
<!-- the static code works-->
<p>Password:<br>
<input type="text" name="passwd" ng-model="passwd" required>
<span style="color:red" ng-show="myForm.passwd.$dirty">
Password is dirty.</span>
</p>
</form>
</body>
</html>
The problem seems, that myForm.user is created in the directive
Remove scope: true so your directive doesn't create new scope and set priority. If that don't work try with this directive
function dynamicName($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1000,
link: function (scope, element, attrs) {
var name = scope.$eval(attrs.dynamicName);
if (name) {
element.attr('name', name);
element.removeAttr('dynamic-name');
$compile(element)(scope);
}
}
};
}
use like this
<input dynamic-name="inputName">
I want to add 'ng-repeat="n in counter"' to the 'form' tag inside my directive. How do I do this?
I tried accessing the element via compile but tElement.find('form') does not work.
See : http://jsfiddle.net/fea40v2c/1/
I tried all these variations:
console.log(tElement.find('form')); // fails
console.log(tElement[0].querySelector('form')); // null
console.log(document.querySelector('form')); // fails
Do you really need the add button to be defined by the directive's user? Because if you don't you can do this.
<script id="repeatableForm.html" type="text/ng-template">
<input type="button" value="add" ng-click="add()">
<div ng-repeat="c in counter">
<div ng-transclude></div>
</div>
</script>
Update
After a little work I got something that allows the user to provide their own markup for the add button. It a bit more complicated and involves a nested directive. A few points that are good to know:
The repeatableForm directive has no isolated scope. It modifies the host scope by adding/overwriting the repeatableForm property. This means multiple such directives cannot execute in the same host scope.
The repeatableForm publishes its controller in its host scope as the repeatableForm property. This is better than publishing the controller's methods directly in the scope because it namespaces those methods and leaves the host scope cleaner.
The view
<repeatable-form>
<input type="button" value="add" ng-click="repeatableForm.add()"/>
<form action="">
First Name: <input name="fname" type="text" />
Last Name: <input name="lname" type="text" />
<input type="checkbox" name="food" value="Steak"/> Steak
<input type="checkbox" name="food" value="Egg"/> Egg
<input type="button" value="remove" ng-click="repeatableForm.remove($index)" />
</form>
</repeatable-form>
The directives
app.directive('repeatableForm', function () {
return {
templateUrl:'repeatableForm.html',
restrict: 'E',
transclude: true,
controller: function () {
var repeatableForm = this
repeatableForm.add = function () {
repeatableForm.forms.push(repeatableForm.forms.length + 1);
};
repeatableForm.remove = function (index) {
repeatableForm.forms.splice(index, 1);
};
repeatableForm.forms = [1, 2, 3];
},
controllerAs: 'repeatableForm',
};
});
app.directive('form', function () {
return {
templateUrl: 'repeatedForm.html',
restrict: 'E',
transclude: true,
};
})
The templates
<script id="repeatableForm.html" type="text/ng-template">
<div ng-transclude></div>
</script>
<script id="repeatedForm.html" type="text/ng-template">
<div ng-repeat="form in repeatableForm.forms"><div ng-transclude></div></div>
</script>
Check this demo.
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