Angular Directive with dynamically generated input fields not able to display validation - angularjs

After 3 days of scouring stackoverflow and other sites, I have found myself back at square one.
My task: I need to validate dynamically generated form fields.
The HTML:
<form name="myForm">
<form-field content="field" model="output[field.uniqueId]" ng-repeat="field in formFields"></form-field>
</form>
The controller:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.formFields = [
{
"fieldName": "Your Name",
"uniqueId": "your_name_0",
"fieldType": "text",
"isMandatory": true
},
{
"fieldName": "Description",
"uniqueId": "description_1",
"fieldType": "textarea",
"isMandatory": true,
}
];
$scope.output={};
}
The directive:
myApp.directive("formField",function($compile){
var templates = {
textTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span><span ng-show="form.content.fieldName.$invalid">Please check this field.</span><input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}"/> </div><br>',
textareaTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <span ng-show="content.isMandatory" class="sub_reqText">*</span> <span ng-show="form.content.fieldName.$invalid">Please check this field.</span> <textarea ng-model="model" name="{{content.uniqueId}}" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea> </div>'
};
var getTemplate = function(content, attrs){
var template = {};
template = templates[content.fieldType+"Template"];
if(typeof template != 'undefined' && template != null) {
return template;
}
else {
return '';
}
};
var linker = function(scope, element, attrs){
element.html(getTemplate(scope.content, attrs)).show();
$compile(element.contents())(scope);
}
return {
restrict:"E",
replace:true,
link:linker,
scope:{
content:'=',
model:'=?'
}
};
});
There is clearly some scope issue because I cannot access the form fields outside of the directive and I cannot access the form name inside the directive. I also know $scope.myForm.name property cannot be an angular binding expression but I am not sure how to rewrite it so that it works.
This is the jsfiddle: http://jsfiddle.net/scheedalla/57tt04ch/
Any guidance will be very useful, thank you!

While debugging the problem I found that, the name attribute is not properly compiled for form. It was showing {{content.uniqueId}} in name but actually it rendered properly on UI.
Eg.
For below html.
<input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control"
ng-required="content.isMandatory" id="{{content.uniqueId}}"/>
name rendered as name="your_name_0" but in form collection it was showing {{content.uniqueId}} with the interpolation directive.
Seems like name is not interpoluted properly.
Then found issue with AngularJS, "You can't set name attribute dynamically for form validation."
Note: Above mentioned issue has been fixed in Angular 1.3.(name
attributes interpolates properly)
& If you wanted to work them inside ng-repeat, then you should always use nested ng-form. Members inside ng-repeat will have their own form, and using that inner form you can handle your validation. Link For Reference
CODE CHANGE
var templates = {
textTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label> '+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span>'+
'<span ng-show="form.input.$invalid">'+
'Please check this field.'+
'</span>'+
'<input type="text" ng-model="model1" name="input" class="form-control" ng-required="content.isMandatory" id="{{content.uniqueId}}" /> '+
'</div>'+
'</ng-form>'+
'<br>',
textareaTemplate: '<ng-form name="form">'+
'<div class="form-group">'+
'<label for="{{content.uniqueId}}">{{content.fieldName}}</label>'+
'<span ng-show="content.isMandatory" class="sub_reqText">*</span> '+
'<span ng-show="form.textarea.$invalid">Please check this field.</span>'+
'<textarea ng-model="model" name="textarea" id="{{content.uniqueId}}" class="form-control" ng-required="content.isMandatory"></textarea>'+
'</div>'+
'</ng-form>'
};
Only i changed the template html, basically added <ng-form></ng-form> for templates and handled the validation on basis it in inner form.
Here is your Working Fiddle
Hope this have cleared your understanding. Thanks.

Related

In Angular, unable to bind to a radio button in a directive on page load

CURRENT SITUATION: I have form fields that are dynamically created via a directive. One of the types includes radio buttons.
GOAL: On page load, I need to bind the values of these elements to a predefined scope. This works correctly with other field types but not with the radio buttons despite the $parent.model matching the option exactly.As you can see in the JSFiddle, "your_name_0" and "description_1" bind correctly but not the radio buttons.
CONSTRAINTS: Due to code restrictions, the directive's ng-model and ng-value cannot change. I know it is possible to bind the model of a radio button to an object. See this for proof: http://plnkr.co/edit/OnHXect37Phij9VAB0ET?p=preview
The Controller:
function MyCtrl($scope) {
//defines the field types
$scope.formFields = [
{
"fieldName": "Your Name",
"uniqueId": "your_name_0",
"fieldType": "text"
},
{
"fieldName": "Description",
"uniqueId": "description_1",
"fieldType": "textarea"
},
{
"fieldName": "Favorite Color",
"uniqueId": "favorite_color_2",
"fieldType": "radio"
"fieldTypeOptionsArray":[
{
"formFieldId":"favorite_color_2",
"optionId":"favorite_color_2_0",
"optionValue":"yellow"
},
{
"formFieldId":"favorite_color_2",
"optionId":"favorite_color_2_1",
"optionValue":"blue"
}
]
}
];
//values to bind to the model
$scope.output={
"your_name_0":"Bob",
"description_1":"I am a developer",
"favorite_color_2":
{
"optionValue":"yellow",
"formFieldId":"favorite_color_2",
"optionId":"favorite_color_2_0"
}
};
};
The Directive:
myApp.directive("formField",function($compile){
var templates = {
textTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label><input type="text" ng-model="model" name="{{content.uniqueId}}" class="form-control" id="{{content.uniqueId}}"/> </div><br>',
textareaTemplate:'<div class="form-group"><label for="{{content.uniqueId}}" >{{content.fieldName}}</label> <textarea ng-model="model" name="{{content.uniqueId}}" id="{{content.uniqueId}}" class="form-control"></textarea> </div>',
radioTemplate : '<div class="form-group"><label>{{content.fieldName}}</label><div class=""><div class="radio" ng-repeat="option in content.fieldTypeOptionsArray"><label><input type="radio" name="{{content.uniqueId}}" ng-value="option" id="{{option.optionId}}" ng-model="$parent.model">{{option.optionValue}}</label></div></div></div>',
};
var getTemplate = function(content, attrs){
var template = {};
template = templates[content.fieldType+"Template"];
if(typeof template != 'undefined' && template != null) {
return template;
}
else {
return '';
}
};
var linker = function(scope, element, attrs){
element.html(getTemplate(scope.content, attrs)).show();
$compile(element.contents())(scope);
}
return {
restrict:"E",
replace:true,
link:linker,
scope:{
content:'=',
model:'=?'
}
};
});
Here is a JSFiddle:
http://jsfiddle.net/scheedalla/d2jzmmd7/
I have been stuck for a while and I cannot understand the root of the problem. It has something to do with it being a nested object or because it is within a directive. Any help will be useful and please let me know if you need further clarification! Thanks!!
Seems to work when I change the ng-value and ng-model to be the option's value as opposed to the entire option object.
Before and after:
<input type="radio" name="{{content.uniqueId}}" ng-value="option" id="{{option.optionId}}" ng-model="$parent.model" >
<input type="radio" name="{{content.uniqueId}}" ng-value="option.optionValue" id="{{option.optionId}}" ng-model="model.optionValue"
http://jsfiddle.net/txbtpLgd/1/
I found the solution. I added this to my controller. If I mapped the $scope.output[favorite_color_2] to the equivalent value in $scope.formFields it worked. The reason behind this is that the item in the ng-repeat can only be mapped to something within its parent scope. In this case, $scope.formFields.
angular.forEach($scope.output, function(value,key){
for(var f=0;f<$scope.formFields.length;f++){
if(key == $scope.formFields[f].uniqueId){
if($scope.formFields[f].fieldType == 'radio'){
console.log($scope.formFields[f].fieldTypeOptionsArray.length);
for (var z=0;z<$scope.formFields[f].fieldTypeOptionsArray.length;z++){
if($scope.formFields[f].fieldTypeOptionsArray[z].optionValue == value.optionValue){
$scope.output[key]=$scope.formFields[f].fieldTypeOptionsArray[z];
}
}
}
}
}
});
Updated JSFiddle: http://jsfiddle.net/scheedalla/d2jzmmd7/17/

Angular - validation messages for a form input directive with isolated scope

I think I am slowly going insane. What seems like a simple problem is giving me a headache :(
I have a form with a custom input element directive using isolate scope.
I simply want to able to display a error message based on the validity of the input elements "required" attribute but I seem to be going round in circles. I am not quite understanding the binding in this scenario.
Please take a look at my fiddle here:
http://jsfiddle.net/brogueady/zwbbLggL/
I would expect the error message "Invalid" to appear to the right of the input field because it is empty.
The HTML is
<div ng-app="UIComponents">
<form ng-submit="formSubmit()" name="vrmForm" >
<at-input name="registration" label="Registration" form="vrmForm" model="vrmLookup.registration" minlength="3" required>
</at-input>
</form>
</div>
The JS is
uiComponents.directive('atInput', function () {
return {
// use an inline template for increased
template: '<div>{{label}}</div><input name="{{name}}" required type="text" ng-model="model"/> <span class="error" ng-show="form.{{name}}.$error.required">Invalid</span>',
// restrict directive matching to elements
restrict: 'E',
scope: {
name: '#',
form: '=',
model: '=',
label: '#'
},
compile: function(element, attr) {
var input = element.find('input');
if (!_.isUndefined(attr.required)) {
input.attr("required", "true");
}
}
};
});
Thank you.
Your $scope.form.name property cannot be an angular binding expression. Return a template function instead from your directive and build the template string:
template: function($element, $attr) {
return '<div>{{label}}</div><input name="' + $attr.name + '" required type="text" ng-model="model"/> <span class="error" ng-show="form.' + $attr.name + '.$error.required">Invalid</span>';
},
Demo

How to add arbitrary attributes to an angular directive for data validation

I am attempting to create an angular directive that will be a custom tag for input fields in our application. Essentially what it will do is create the label, input field and the various bootstrap classes so there is a consistent look to them.
Along with that I would like it if I could add the various data validators that are appropriate for the particular input (such as required and custom validators) as attributes of the custom tag and then have those added to the input field and thus perform validation on that.
I have figured out a way that appears to put the attributes on the input field and the custom validator is getting called and properly evaluating the data, but the form never seems to think that the data is invalid. I think I am having a scope problem where the input being invalid is being set on the directive's scope rather than the parent scope but I'm not 100% sure about that and even if it is the problem I don't know how to fix it.
Here's a sample of what I'd like one of the tags to look like
<textinput ng-model="TestValue" name="TestValue" text="Label Text" config="GetConfigurationForm()" ngx-ip-address required></textinput>
which I want to generate something like
<div class="row">
<div class="form-group" ng-class="{ 'has-error': IsInvalid() }">
<label for="{{name}}" class="control-label">{{text}}</label>
<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}" ngx-ip-address required>
</div>
</div>
Note that the ngx-ip-address and required have been moved to the input field attributes.
My controller looks like the following (sorry it's so long)
var app = angular.module('test', []);
app.directive('ngxIpAddress', function()
{
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attributes, ngModel)
{
ngModel.$validators.ngxIpAddress = function(modelValue, viewValue)
{
// Value being blank is OK
if (ngModel.$isEmpty(modelValue))
return true;
// If the string starts with a character then
// this is not valid
if (isNaN(parseInt(viewValue[0])))
return false;
var blocks = viewValue.split(".");
if(blocks.length === 4)
{
return blocks.every(function(block)
{
return parseInt(block, 10) >= 0 && parseInt(block, 10) <= 255;
});
}
return false;
};
}
};
});
app.directive('textinput', function ()
{
return {
restrict: 'E',
scope: {
//# reads the attribute value, = provides two-way binding, & works with functions
ngModel: '=',
name: '#',
text: '#',
config: '&'
},
controller: function($scope) {
$scope.IsInvalid = function()
{
var getConfigurationFunction = $scope.config();
if (!getConfigurationFunction || !getConfigurationFunction[$scope.name])
return false;
return getConfigurationFunction[$scope.name].$invalid;
};
},
link: function(scope, element, attributes) {
var inputElement = element.find("input");
for (var attribute in attributes.$attr)
{
if (attribute !== "ngModel"
&& attribute !== "name"
&& attribute !== "text"
&& attribute !== "config")
{
inputElement.attr(attribute, attributes[attribute]);
}
}
},
template: '<div class="row">' +
'<div class="form-group" ng-class="{ \'has-error\': IsInvalid() }">' +
'<label for="{{name}}" class="control-label">{{text}}</label>' +
'<input id="{{name}}" type="text" class="form-control" ng-model="ngModel" name="{{name}}">' +
'</div>' +
'</div>'
};
});
app.controller(
"TestController",
[
"$scope",
function TestController(_scope)
{
_scope.TestValue = "TestTest";
_scope.GetConfigurationForm = function()
{
return _scope.ConfigurationForm;
};
}
]
);
If I put the attributes in the actual template then everything works as expected and the control turns red if the data isn't an ip address. When I add the attributes by moving them that doesn't work.
Here is a plunkr showing what I've got so far: http://plnkr.co/edit/EXkz4jmRif1KY0MdIpiR
Here is a plunkr showing what I'd like the end result to look like where I've added the tags to the template rather than the tag: http://plnkr.co/edit/mUGPcl1EzlHUiMrwshCr
To make this even more fun, in the future I will actually need to pass in a value to the data validation directives from the outside scope as well, but I'd like to get this working first.
Here you may find the correct answer.
The reasons of this issue are:
the attr will convert the attribute from ngxIpAddress to ngxipaddress, namely from the uppercase to lowercase, you can find this issue from this link. To solve it, just pass ngx-ip-address as parameter for function attr.
$compile(inputElement)(scope); need to be added into directive, when one directive is used in another directive. Here is one link.

validation of a form added with a directive

I would like to access the content of the validation variables provided from AngularJS for the forms.
I need to add forms using a directive like in the code, but if I do that I can't access the $dirty,$error,$invalid variables anymore and I need to access them.
JS:
myApp.directive('test', function() {
return {
scope : {
nameform: "=",
nameinput: "=",
type: "="
},
template: '<form name=nameform></form>',
restrict: 'E',
compile: function (elem,attr){
var form = elem.find("form");
if(attr.type == "text")
form.html('<input type="text" name="nameinput" ng-model="data.text" placeholder="type here..." required />');
}
};
});
HTML:
<test nameform="valform" nameinput="valinput" type="text"/>
{{valform.valinput.$invalid}}
I think that you can't. Because you are using isolated scope for building your directive, so you don't have acces to the information. You can try to build you directive using share scope, and I think that you would be able to access this information.
Change your directive to be ng-form instead of form (because ng-form is nestable). Then wrap your directive inside another form element and give that new form element a name. The outer form element will bind to your outer scope and you can access the properties that way.
Directive template:
"<ng-form name="valform"></ng-form>"
HTML:
<body ng-view>
<div>Valid: {{outerform.$valid}}</div>
<div>Errors: {{outerform.$error}}</div>
<form id="outerform" name="outerform">
<test type="text"/>
</form>
</body>
Side note, form names don't play nice with dynamic names. The plunkr below uses static names so I could help you with your current problem. I found a solution to the dynamic name problem on a different SO post...I think this one...
Dynamic validation and name in a form with AngularJS
Here's a plnkr with your working form...
http://plnkr.co/edit/RFrRXp2kWkP1Mefwl3Kn?p=preview
When you build your HTML for your input controls, make sure you append the name attribute correctly based on 'nameinput' passed in as an attribute:
myApp.directive('test', function() {
return {
scope : {
nameform: "=",
nameinput: "=",
type: "="
},
template: '<form name=nameform></form>',
restrict: 'E',
compile: function (elem,attr){
var form = elem.find("form");
if(attr.type == "text")
form.html('<input type="text" name="' + attr.nameinput +'" ng-model="data.text" placeholder="type here..." required />');
}
};
});

Transcluding Attributes in an AngularJS Directive

I was creating a select replacement directive to make it easy to style up selects according to the design without having to always right a bunch of markup (i.e. the directive does it for you!).
I didn't realize that attributes don't transclude to where you put ng-transclude and just go to the root element.
I have an example here: http://plnkr.co/edit/OLLntqMzbGCJS7g7h1j4?p=preview
You can see that it looks great... but there's one major flaw. The id and name attributes aren't being transferred. Which, ya know, without name, it doesn't post to the server (this form ties into an existing system, so AJAXing the model isn't an option).
For example, this is what I start with:
<select class="my-select irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
...this is what I want it to look like:
<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">
<span class="faux-value">{{viewVal}}</span>
<span class="icon-arrow-down"></span>
<select ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude class="my-select irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
...but this is what actually happens:
<div class="faux-select my-select irrelevant-class" ng-class="{ placeholder: default == viewVal, focus: obj.focus }" name="reason" id="reason" data-anything="banana">
<span class="faux-value">{{viewVal}}</span>
<span class="icon-arrow-down"></span>
<select ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude>
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
Specifically, the issue is that there's no name attribute on the select, so it doesn't actually send the data to the server.
Obviously, I can use a pre-compile phase to transfer the name and id attributes (that's what I am doing for now), but it would be nice if it would just automatically transfer all of the attributes so they can add any classes, arbitrary data, (ng-)required, (ng-)disabled attributes, etc, etc.
I tried getting transclude: 'element' to work, but then I couldn't the other attributes from the template onto it.
Note, I saw the post here: How can I transclude into an attribute?, but it looks like they just manually transfer the data, and I am aiming to get it to auto-transfer all the attributes.
You could use the compile function to access the element's attributes and build the template.
app.directive('mySelect', [function () {
return {
transclude: true,
scope: true,
restrict: 'C',
compile: function (element, attrs) {
var template = '<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">' +
'<span class="faux-value">{{viewVal}}</span>' +
'<span class="icon-arrow-down entypo-down-open-mini"></span>' +
'<select id="' + attrs.id + '" name="' + attrs.name + '" ng-model="val" ng-focus="obj.focus = true" ng-blur="obj.focus = false" ng-transclude>';
'</select>' +
'</div>';
element.replaceWith(template);
//return the postLink function
return function postLink(scope, elem, attrs) {
var $select = elem.find('select');
scope.default = scope.viewVal = elem.find('option')[0].innerHTML;
scope.$watch('val', function(val) {
if(val === '') scope.viewVal = scope.default;
else scope.viewVal = val;
});
if(!scope.val) scope.val = $select.find('option[selected]').val() || '';
}
}
};
}]);
The compile function is returning the postLink function, there are other ways to do this, you'll find more info here.
Here is a plunker
ng-transclude transcludes the content of an element on which the directive was placed. I would have assigned the attribute to its parent div and transcluded the entire select box in the template:
First Approach:
http://plnkr.co/edit/fEaJXh?p=preview
<div class="form-control my-select">
<select class="irrelevant-class" name="reason" id="reason" data-anything="banana">
<option value="">Reason for Contact...</option>
<option>Banana</option>
<option>Pizza</option>
<option>The good stuff</option>
<option>This is an example of a really, really, really, really, really, really long option item</option>
</select>
</div>
And remove replace option from the definition:
app.directive('mySelect', [function () {
return {
template:
'<div class="faux-select" ng-class="{ placeholder: default == viewVal, focus: obj.focus }">' +
'<span class="faux-value">{{viewVal}}</span>' +
'<span class="icon-arrow-down entypo-down-open-mini"></span>' +
'<div ng-transclude></div>' +
'</div>',
transclude: true,
//replace: true,
scope: true,
restrict: 'C',
link: function (scope, elem, attrs) {
var $select = elem.find('select');
scope.default = scope.viewVal = elem.find('option')[0].innerHTML;
scope.$watch('val', function(val) {
if(val === '') scope.viewVal = scope.default;
else scope.viewVal = val;
});
if(!scope.val) scope.val = $select.find('option[selected]').val() || '';
}
};
}]);
Second Approach:
In your demo, just include following line at the end of the link method:
$select.attr({'id': elem.attr('id'), 'name': elem.attr('name')});

Resources