Can't bind inside ng-show that is in ng-repeat - angularjs

So I am working on a small login form using AngularJS and it seemed extremely natural to remove duplicated code by using an ng-repeat directive. Everything is very natural and works well except any kind of binding inside ng-show, this is where things gets unintuitive and break down. A fiddle of the work so far can be found here.
What I am wondering is why does everything break down for ng-show? If I dump the ng-repeat and duplicate the code everything works fine for all three ng-show instances assuming that I manually type the references to the elements and values.
Below is a copy of the fiddle html and javascript:
<div ng-app='loginApp' ng-controller='loginController'>
<form name='loginForm' novalidate>
<div class='form-group' ng-repeat='field in fields'>
<label>{{field.label}}</label>
<input type='{{field.inputType}}' class='form-control' name='{{field.inputName}}' ng-minlength='{{field.minlength}}' ng-maxlength='{{field.maxlength}}' ng-model='field.value' ng-focus='inputFocused'/>
<!-- The ng-show below doesn't work as expected -->
<div ng-show="canShowUserMsgs(inputFocused, loginForm.{{field.inputName}}.$dirty, loginForm.{{field.inputName}}.$invalid)">
<!-- The ng-show below doesn't work as expected -->
<p ng-show="loginForm.{{field.inputName}}.$error.minlength" class='help-block'>Must be more than {{field.minlength}} characters long.</p>
<!-- The ng-show below doesn't work as expected -->
<p ng-show="loginForm.{{field.inputName}}.$error.maxlength" class='help-block'>Must be less than {{field.maxlength}} characters long.</p>
</div>
</div>
</form>
</div>
var loginApp = angular.module('loginApp', []);
loginApp.controller('loginController', function($scope, $http) {
$scope.fields = [
{
label : "User Name",
inputType : "text",
inputName : "userName",
value : "",
minlength : 5,
maxlength : 15
},
{
label : "Password",
inputType : "password",
inputName : "password",
value : "",
minlength : 5,
maxlength : 15
}
];
$scope.canShowUserMsgs = function(inputFocused, inputDirty, inputInvalid) {
return (inputDirty && inputInvalid && !inputFocused); };
});
loginApp.directive('ngFocus', [function() {
return {
restrict: 'A',
link: function(scope, element, attrs, ctrl) {
var modelName = attrs['ngFocus'];
scope[modelName] = false;
element.bind('focus', function(evt) {
scope.$apply(function() {scope[modelName] = true;});
}).bind('blur', function(evt) {
scope.$apply(function() {scope[modelName] = false;});
});
}
}
}]);

you have to add ng-form inside the ng-repeat and need to do some modifications to how you use the ng-show. Check the working sample of your code.
working copy

This doesn't work: <p ng-show="loginForm.{{field.inputName}}.$error.minlength" because the interpretation of the curly brace expression only happens once, there's not two passes: one to generate the bind path without any curly braces, then a second to evaluate the bind path against the scope. It's not like that. Everything happens in a single pass. So, convert it to a controller function call:
<p ng-show="minLength(field)"

Related

Validating list of variables in ng-repeat

In this plunk I have an ng-repeat of input fields. Each field has to pass two validations: (1) the value cannot be empty, and (2) if num = 1 then the value needs to be a number. I'm using ng-form in each row to validate the values independently (I cannot have the ng-repeat inside a <form>.
The problem is that messages are not displayed correctly. To replicate, in the plunk add a 1 to the third field. It becomes b1 that is not a valid number, still the Value should be a number error message is not displayed. It is displayed after you change the value again, for example to b11. Where is the problem and how to fix it?
HTML
<div ng-repeat="v in vals">
<ng-form name="formval">
<input type="text" name="val" ng-model="v.val" style="float:left"
ng-change="seeError(formval,v.num,v.val)" required/>
<div ng-show="!formval.val.$valid" ng-messages="formval.val.$error" class="errorMsg">
<div ng-message="shouldBeNumber">Value should be a number</div>
<div ng-message="required">Value cannot be empty</div>
</div>
{{ 'error row #' + $index}} {{formval.val.$error}}
<br/><br/><br/>
</ng-form>
</div>
Javascript
var app = angular.module('app', []);
app.controller('ctl', function ($scope) {
$scope.vals = [
{val: 'a', num: 0},
{val: 2, num: 1 },
{val: 'b', num: 1}
];
$scope.seeError = function(form,num,value){
delete form.val.$error.shouldBeNumber;
if (value && value.trim()==="") // omit as form will show an error
return;
if (num===1) // should be a number
if (isNaN(parseFloat(value)))
form.val.$error.shouldBeNumber = true;
};
});
I've changed a little of your code.
Please check this fiddle.
form validation
$scope.seeError = function(form,num,value){
form.val.$error.required = false;
form.val.$error.shouldBeNumber = false;
if (value===undefined) // omit as form will show an error
form.val.$error.required = true;
if (num===1&&isNaN(Number(value))) // should be a number
form.val.$error.shouldBeNumber = true;
};
I am not certain that my fiddle is proper to your purpose,
but I hope this can help you. :)
You should fix some bugs at first:
1) add this to the head-tag
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.js"></script>
2) and in the JS
var app = angular.module('app', ['ngMessages']);
then...
in html
<input type="text" name="val" ng-model="v.val" style="float:left" required ng-model-sniffer/>
<div ng-messages="formval.val.$error" class="errorMsg">
<div ng-message="shouldBeNumber">Value should be a number</div>
<div ng-message="required">Value cannot be empty</div>
</div>
in JS
app.directive('ngModelSniffer', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, model)
{
model.$validators.shouldBeNumber = function(modelValue, viewValue)
{
return !isNaN(parseFloat(viewValue));
};
}
};
});
when the list is initially displayed ng-change is not fired because
The ngChange expression is only evaluated when a change in the input
value causes a new value to be committed to the model.
It will not be evaluated:
if the value returned from the $parsers transformation pipeline has not changed
if the input has continued to be invalid since the model will stay null
if the model is changed programmatically and not by a change to the input value
you can use a custom filter to achieve the same behaviour
html
<div ng-repeat="v in vals | filter:checkError">
</div>
js
$scope.checkError = function (item) {
if(item.val is not number)
displayError = true;
return true;
};

AngularJS operations with directive attributes

Small question. I can't find any solution
I have some directives that I need to insert into my tags in some cases.
My form object
function AppCtrl($scope) {
$scope.form ={
name:{
type: 'text',
validate: 'letters'
},
tel:{
type: 'number',
validate: 'digits'
}
}
}
My directive
<body ng-app ng-controller="AppCtrl">
<input type="{{v.type}}" placeholder="{{k}}" {{v.type=='number' ? 'string-to-number' : '' }} {{v.validate}} value="" ng-repeat="(k,v) in form">
</body>
I need to render 'validate' and 'string-to-number' atributes in inputs. But my code doesn't fire
Here is jsfiddle example
http://jsfiddle.net/FxM3B/517/
I updated the plunker with a possible solution
<div ng-repeat="(k,v) in form">
<input ng-if="v.type ==='number'" type="{{v.type}}" placeholder="{{k}}" validate="{{v.validate}}" value="" string-to-number>
<input ng-if="v.type !=='number'" type="{{v.type}}" placeholder="{{k}}" validate="{{v.validate}}" value="">
</div>
Check with ng-if the value and add markup for the two options.
You can't add an HTML attribute this way, you should take a look at ng-attr, but you might need to change your directive because it adds the attribute in all case, only the content is dynamic.
Otherwise this article explain several solutions : http://www.thinkingmedia.ca/2015/03/conditionally-add-a-html-element-attribute-value-in-angularjs/
Since the names of attributes you want to add to an element are the values of properties of your model, you can't use ng-attr directive.
You can author you own directive that handles such situations.
Here's jsfiddle example
.directive('boolAttrs', function(){
return function(scope, element, attrs){
var propExpr = attrs['boolAttrs'];
if (propExpr)
{
var data = angular.fromJson(propExpr);
if (angular.isArray(data) && data.length > 0) {
angular.forEach(data, function (value, index, array) {
var propValue = scope.$eval(value);
if (angular.isFunction(propValue))
propValue = propValue();
if (propValue)
element.attr(propValue, "");
});
}
}
};
})
You can apply this directive this way:
bool-attrs='["v.validate", "v.type == \"number\" ? \"string-to-number\" : \"\" "]'
A value for bool-attrs should be a valid json array string, that defines expressions to evaluete to determine the names of attributes to be inserted.

scope in directive breaks view scope with async validation

I am following a Year of Moo tutorial on async form validations and ngMessages (I'm using 1.3.0-beta.14 so I can't use the actual $async validator).
My validation is working, but the scope in the view is non-existent! On form submission, there is no username value and adding the appropriate {{username}} binding elsewhere in the view never returns a value. However, the console Log at the end of my directive does return the correct value, it just never transfers to the view?
Here is the directive, basically lifted from the article (the console.log before the final return does log the correct value from the view):
.directive('recordAvailabilityValidator', function($http) {
return {
require : 'ngModel',
link : function(scope, element, attrs, ngModel) {
var apiUrl = attrs.recordAvailabilityValidator;
function setAsLoading(bool) {
ngModel.$setValidity('recordLoading', !bool);
}
function setAsAvailable(bool) {
ngModel.$setValidity('recordAvailable', bool);
}
ngModel.$parsers.push(function(value) {
if(!value || value.length == 0) return;
setAsLoading(true);
setAsAvailable(false);
$http.get(apiUrl, { params: {attr : value }} )
.success(function(response) {
setAsLoading(false);
setAsAvailable(true);
})
.error(function() {
setAsLoading(false);
setAsAvailable(false);
});
console.log(value)
return value;
})
}
}
});
Here is the relevant part of the html template:
<p>
<label>Username</label>
<input type="text" ng-model="signup.username" class='form-control' name='username'
require minlength='3' record-availability-validator="/api/v1/validations/username"
ng-model-options="{ debounce : { 'default' : 500, blur : 0 } }">
</p>
<div ng-messages="signupForm.username.$error">
<div ng-message="required">You did not enter a username</div>
<div ng-message="minlength">Username must be at least 4 characters</div>
<div ng-message="recordLoading">Checking database...</div>
<div ng-message="recordAvailable">The username is already in use...</div>
</div>
{{signup.username}}
The debug {{signup.username}} never shows a value. If I change it to just {{signup}} the other values show fine. Also, if I add this directive to another input, like email the same strange behavior is there. I googled around and tried added scope: true to my directive, but nothing happened.

Binding the placeholder to the model causes ng-change to execute on load in IE

Using angularjs, if I bind the placeholder of an input to its model, the change event is fired when the document loads in IE. This does not appear to be correct and I'm not seeing this behavior in other browsers.
JS Fiddle
Html:
<div ng-app="angularjs-starter" data-ng-controller="MainCtrl">
<div data-ui-view="viewMain">
<input
placeholder="{{theValue}}"
data-ng-model="theValue"
data-ng-change="valueChanged(theValue)" />
</div>
Javascript:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
$scope.valueChanged = function(theValue) {
alert("Value Change Called On Load in IE.");
};
});
It's possible to use the built-in ng-attr-placeholder directive as well.
ng-attr-placeholder="{{theValue}}"
I know this is old but just in case anyone else runs in to this I created a small directive that goes around putting a dynamic value in the placeholder and instead have it assign when it changes:
.directive('dynamicPlaceholder',
function() {
return {
restrict: 'A',
link: function ($scope, element, attrs) {
attrs.$observe('dynamicPlaceholder', function(value) {
element.attr('placeholder', value);
});
}
};
});
Then to use this all you need to do is use dynamic-placeholder instead of placeholder:
<input ng-model='someValue' dynamic-placeholder='{{someDynamicPlaceholder}}' />
Not sure what is causing the problem in IE though

How to validate dynamic form fields in angular directive?

I would like to create form with fields created in directive. Data binding of data working correctly but validation doesn't work.
this is html:
<body ng-controller="MainCtrl">
<h1>form</h1>
<form name="form">
<div ng-repeat="conf in config">
<div field data="data" conf="conf"></div>
</div>
</form>
<pre>{{data|json}}</pre>
</body>
controller and field directive:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.data = {name: '', age: ''}
$scope.config = [
{field: 'name', required:true},
{field: 'age'}
];
});
app.directive('field', function ($compile) {
return {
scope: {
data: '=',
conf: '='
},
link: function linkFn(scope, element, attrs) {
// field container
var row = angular.element('<div></div>');
// label
row.append(scope.conf.field + ': ');
// field input
var field = angular.element('<input type="text" />');
field.attr('name', scope.conf.field);
field.attr('ng-model', 'data.' + scope.conf.field);
if (scope.conf.required) {
field.attr('required', 'required');
}
row.append(field);
// validation
if (scope.conf.required) {
var required = angular.element('<span>required</span>');
required.attr('ng-show',
'form.' + scope.conf.field + '.$error.required');
row.append(required);
}
$compile(row)(scope);
element.append(row);
}
}
});
problem is that validation for field name doesn't work and validation text required is never shown. May be form in ng-show is unknown in directive. But I don't know how to pass form into field directive. Can you help me how to fix it? Thanks.
here is live code: http://plnkr.co/edit/j0xc7iV1Sqid2VK6rMDF?p=preview
Todo:
before:
$compile(row)(scope);
element.append(row);
after:
element.append(row);
$compile(row)(scope);
p/s in 'planker' for facilities add css:
.ng-invalid {
border: 1px solid red;
}
You'll need to use ng-form directive and push the dynamic field directly into form object.
This thread can help you out:
https://github.com/angular/angular.js/issues/1404
Here is a plunker forked from yours to fix you're issue:
http://plnkr.co/edit/qoMOPRoSnyIdMiZnbnDF?p=preview
To summarize, I added a watch that will toggle the error message instead of using the ng-show directive. Things can get hairy when you attempt to dynamically add a directive within a directive link. For a simple use case as this, it is quicker to add your own watch.
You may also look at this directive which is preconfigured to handle many use cases for validation as well as allow you to create custom validations easily https://github.com/nelsonomuto/angular-ui-form-validation
var toggleRequiredErrorMessage = function (invalid) {
if(invalid === true) {
addRequiredErrorMessage();
} else {
removeRequiredErrorMessage();
}
};
scope.$watch( watchIfRequired, toggleRequiredErrorMessage );

Resources