Datepicker directive causes infdig (infinite digest loop) error - angularjs

This AngularJS 1.2.21 directive is used as an universal solution to be used in browsers supporting type="date" and not supporting it.
directive('datePicker', function(){
var el = document.createElement('input');
el.setAttribute('type','date');
var typeDateSupport = (el.type === 'date');
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
if (typeDateSupport) {
elm.attr('type', 'date');
elm.attr('placeholder', null);
/* Type date takes care of the right format. The display value is localized, the real value is always YYYY-MM-DD,
so that we do not need to add any parsers and formatters */
} else {
elm.attr('type', 'text');
elm.attr('readonly', 'readonly');
elm.datepicker({
dateFormat: 'yy-mm-dd', // TODO: internationalize this
onSelect: function(date) {
scope.$apply(function() {
ctrl.$setViewValue(date);
});
}
});
if (attrs.hasOwnProperty('max')) {
elm.datepicker('option', 'maxDate', $.datepicker.parseDate('yy-mm-dd', attrs.max));
}
ctrl.$formatters.unshift(function(value) {
if (angular.isUndefined(value)) {
return undefined;
}
return moment.utc(value).tz(Config.timeZone).format('YYYY-MM-DD');
});
ctrl.$parsers.unshift(function(value) {
if (angular.isUndefined(value)) {
return undefined;
}
var tmp = moment.tz(value, Config.timeZone);
ctrl.$setValidity('date', tmp.isValid());
return tmp.isValid()? tmp : undefined;
return value;
});
}
}
};
})
The code causes this error: Error: [$rootScope:infdig] in IE 11 (not supporting type="date"). I have actually no idea, why....
Could somebody help?

Remove the call to scope.$apply. It's usually related to such errors.

Related

Use an angular directive to activate or deactivate a call to a service

I'm checking whether or not a username already exists when I create a new user. I also want to be able to edit a user, and I'm actually using the same form for this. When the form is in edit-mode, you are not allowed to change the username and the input field will be disabled.
I have a directive that checks with the database if the username is unique, and validates or invalidates the input. This directive should not fire when the form is in edit-mode, because the username is not allowed to be edited, and the form should validate.
<input name="alias"
class="form-control"
type="text"
ng-model="user.alias"
required
ng-model-options="{ debounce: { default : 500, blur: 0 }}"
validate-alias="{{formMode == 'new' ? true : false}}"
/>
And the directive:
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
require: 'ngModel',
validateAlias: '=',
link: function(scope, element, attrs, ngModel) {
attrs.$observe('validateAlias', function(){
console.log("validateAlias: " + attrs.validateAlias + ", which is a " + typeof attrs.validateAlias);
//this returns "validateAlias: false, which is a string"
if (attrs.validateAlias === 'true') console.log("should work"); else console.log("n/a");
//this returns "should work"
if (attrs.validateAlias === 'true') {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})
So what happens is that in the edit-mode the first if-statement returns what I expect (false, aka do not validate, and the attribute in the DOM matches this, displaying validate-alias='false'), yet the username is still checked with the database.
Everything works fine when I'm in new-mode, but that's probably just coincidence, not because the code works as it should.
I am doing something wrong, but I can't figure out where...!
Edit:
Right, I've changed things around a little bit. I now have the following directive:
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
scope: {
validateAlias: '#'
},
link: function(scope, element, attrs) {
scope.$watch(attrs.validateAlias, function(v){
console.log("validateAlias: " + v + ", which is a " + typeof v);
if (v === true) console.log("the check is active"); else console.log("the check is NOT active");
if (v === true) {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})
Now the $watch does not seem to work... what am I missing?
Final edit:
The DOM had a set variable "false" for the attribute I made... the watch worked fine, but the DOM was generated once, so there was nothing to watch.
Luckily for me, every time the attribute changed, I could reload the DOM as well (virtually switching screens), and so the scope reads the attribute again, and the variable is passed, making the directive do exactly what I want. The code I posted after the first edit is what I used.
I'm not sure if that's the Angular way, but it works for now. When I find a better solution, I'll update the code.
Try this:-
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
validateAlias: '=',
}
link: function(scope, element, attrs, ngModel) {
scope.$watch('validateAlias', function(watchData){
console.log("validateAlias: " + watchData + ", which is a " + typeof watchData);
if (watchData === 'true') console.log("should work"); else console.log("n/a");
//this returns "should work"
if (watchData.value === 'true') {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})

How do I attach validators on an input field in angular that accesses another model in the form

I have a range input (between [lowerValue] and [upperValue]) on a form and I want to make a reusable directive called 'validateGreaterThan' that can be attached to any form and use the ngModel $validators functionality so I can attach multiple ones onto an input.
You can check a simple demo on jsbin here:
http://jsbin.com/vidaqusaco/1/
I've set up a directive called nonNegativeInteger and that works correctly, however, the validateGreaterThan directive I have isn't working. How can I get it to reference the lowerValue?
I appreciate any help with this.
Here is the basic idea:-
Define 2 directives and let each directive refer to the other field buy passing its name. When the validator runs on the current field you can retrieve the model value of another field and now you have values from both fields and you can ensure the relation between the 2 fields.
As per my code below I have 2 fields minimumAmount and maximumAmount where the minimumAmount cannot be greater than the maximum amount and vice-versa.
<input name="minimumAmount" type="number" class="form-control"
ng-model="entity.minimumAmount"
less-than-other-field="maximumAmount" required/>
<input name="maximumAmount" type="number"
ng-model="entity.maximumAmount"
greater-than-other-field="minimumAmount"
class="form-control"/>
Here we have 2 directives lessThanOtherField and greaterThanOtherField and they both refer to other field as we pass the other field name. greater-than-other-field="minimumAmount" we are passing the other field.
.directive('lessThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'lessThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue < +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])
.directive('greaterThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'greaterThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue > +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])

How to force date input bind to model as JavaScript date Date()

I'm trying to get input from text boxes to bind to scope variables as actual JavaScript Date objects, not strings. The inputs are generated dynamically so I'm unable to cast/convert before the values are sent to the server.
So far, I have created a directive that uses moment.js to parse the value on the scope, and convert it to a Date() object. The problem seems to be that angular converts the value back to a string immediately after. I guess it rechecks the html input and overwrites the Date() object set in the directive.
Here is a working Plunkr demonstrating the issue
(function () {
'use strict';
angular.module('app', ['ng'])
.controller('myController', ['$scope', function() {
$scope.testObj = null;
}])
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
scope.$watch(
function() {
console.log('watching model', ngModel.$modelValue);
return ngModel.$modelValue;
},
function (val) {
console.log('recieved model', val);
if (val && typeof val == 'string') {
console.log('attempting parse date', val);
if(moment(val, parseFormat).isValid())
{
console.log('string is valid date');
ngModel.$modelValue = moment(val, parseFormat).toDate();
console.log('completed value assignment', ngModel.$modelValue);
console.log('model is of type ' + typeof ngModel.$modelValue);
console.log('model is date', (ngModel.$modelValue instanceof Date));
}
else
{
console.log('string is not a valid date');
}
}
}
);
}
};
})
} ());
You can see the behaviour by opening the console in a browser while running the plunkr. The line 'completed value assignment' shows that at least momentarily, ngModel.$modelValue (from $scope.testObj) is a Date() object.
The final line in the output below shows the watch firing again, and the model value is once again a string as it appears in the html input.
How can I have the value persist as a Date object (once a valid date can be parsed).
You have to use the $parsers and $formatters pipelines, described in the docs of ngModelController. The code would be:
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
function parse(value) {
var m = moment(value, parseFormat);
if( m && m.isValid() ) {
ngModel.$setValidity('dateBinding', true);
return m.toDate();
}
else {
ngModel.$setValidity('dateBinding', false);
return; // undefined
}
}
function format(value) {
if( value && value instanceof Date ) {
return moment(d).format(parseFormat);
}
else {
return '';
}
}
ngModel.$formatters.push(format);
ngModel.$parsers.unshift(parse);
}
};
});
See (and play with) the forked plunk: http://plnkr.co/edit/VboH2iq6HRlaDhX3g1AY?p=preview

AngularJS custom validation, dates are either before or after other date

I have these custom validation directives which work as intended.
Problems is I want to trigger validation of the input when the other input that is used to compare with is changed.. Is this solvable in the watch block?
Lets say i have
<input afterOtherDate="dateB" ng-model="dateA" value="2014"/>
<input beforeOtherDate="dateA" ng-model="dateB" value="2013"/>
If i then set dateA to be after dateB, dateA will become invalid, but dateB wont know.
Same the other way around, if i have
<input afterOtherDate="dateB" ng-model="dateA" value="2013"/>
<input beforeOtherDate="dateA" ng-model="dateB" value="2014"/>
Both inputs need to be re-validated when the other one changes. So both become valid.
validationHelpers.directive('afterOtherDate', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var doValidation = function () {
ctrl.$parsers.unshift(function (viewValue) {
if (viewValue <= compareTo) {
ctrl.$setValidity('afterOtherDate', false);
return viewValue;
} else {
ctrl.$setValidity('afterOtherDate', true);
return viewValue;
}
});
};
var scopeHierarchy = attrs["afterOtherDate"].split('.');
var compareTo = scope;
for (var k = 0; k < scopeHierarchy.length; k++) {
compareTo = compareTo[scopeHierarchy[k]];
}
scope.$watch(attrs["afterOtherDate"], function (val) {
});
doValidation();
}
};
});
validationHelpers.directive('beforeOtherDate', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var doValidation = function () {
ctrl.$parsers.unshift(function (viewValue) {
if (viewValue <= compareTo) {
ctrl.$setValidity('beforeOtherDate', true);
return viewValue;
} else {
ctrl.$setValidity('beforeOtherDate', false);
return viewValue;
}
});
};
var scopeHierarchy = attrs["beforeOtherDate"].split('.');
var compareTo = scope;
for (var k = 0; k < scopeHierarchy.length; k++) {
compareTo = compareTo[scopeHierarchy[k]];
}
scope.$watch(attrs["beforeOtherDate"], function (val) {
});
doValidation();
}
};
});
Would be cool to solve it inside the custom directives!
BR
twd
What I do in my directive, that I get the controller of ngModel of the field which I'm trying to compare to:
var compareModel = scope.$eval([formCtrl.$name, attr.dateAfter].join("."));
I'm passing the model name in the attribute. Then you can access its parsers and push a method that would fire a validation in your model.
compareModel.$parsers.push(function (value) {
I force the compareModel to revalidate by using $setViewValue:
compareModel.$parsers.push(function (value) {
checkBefore(value);
model.$setViewValue(model.$viewValue);
return value;
});

How to autocapitalize the first character in an input field in AngularJS?

How to autocapitalize the first character in an input field inside an AngularJS form element?
I saw the jQuery solution already, but believe this has to be done differently in AngularJS by using a directive.
Yes, you need to define a directive and define your own parser function:
myApp.directive('capitalizeFirst', function($parse) {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
if (inputValue === undefined) { inputValue = ''; }
var capitalized = inputValue.charAt(0).toUpperCase() +
inputValue.substring(1);
if(capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
modelCtrl.$parsers.push(capitalize);
capitalize($parse(attrs.ngModel)(scope)); // capitalize initial value
}
};
});
HTML:
<input type="text" ng-model="obj.name" capitalize-first>
Fiddle
Please remember that not everything needs an Angular solution. You see this a lot with the jQuery crowd; they like to use expensive jQuery functions to do things that are simpler or easier to do with pure javascript.
So while you might very well need a capitalize function and the above answers provide that, it's going to be a lot more efficient to just use the css rule "text-transform: capitalize"
<tr ng-repeat="(key, value) in item">
<td style="text-transform: capitalize">{{key}}</td>
<td>{{item}}</td>
</tr>
You can create a custom filter 'capitalize' and apply it to any string you want:
<div ng-controller="MyCtrl">
{{aString | capitalize}} !
</div>
JavaScript code for filter:
var app = angular.module('myApp',[]);
myApp.filter('capitalize', function() {
return function(input, scope) {
return input.substring(0,1).toUpperCase()+input.substring(1);
}
});
Use the CSS :first-letter pseudo class.
You need to put everything lowercase and after apply the uppercase only to the first letter
p{
text-transform: lowercase;
}
p:first-letter{
text-transform: uppercase;
}
Here's an example: http://jsfiddle.net/AlexCode/xu24h/
Modified his code to capitalize every first character of word. If you give 'john doe', output is 'John Doe'
myApp.directive('capitalizeFirst', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
var capitalized = inputValue.split(' ').reduce(function(prevValue, word){
return prevValue + word.substring(0, 1).toUpperCase() + word.substring(1) + ' ';
}, '');
if(capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
modelCtrl.$parsers.push(capitalize);
capitalize(scope[attrs.ngModel]); // capitalize initial value
}
};
});
I would prefer a filter and directive. This should work with cursor movement:
app.filter('capitalizeFirst', function () {
return function (input, scope) {
var text = input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
return text;
}
});
app.directive('capitalizeFirst', ['$filter', function ($filter) {
return {
require: 'ngModel',
link: function (scope, element, attrs, controller) {
controller.$parsers.push(function (value) {
var transformedInput = $filter('capitalizeFirst')(value);
if (transformedInput !== value) {
var el = element[0];
el.setSelectionRange(el.selectionStart, el.selectionEnd);
controller.$setViewValue(transformedInput);
controller.$render();
}
return transformedInput;
});
}
};
}]);
Here is a fiddle
To fix the cursor problem (from where Mark Rajcok's solution),
you can store element[0].selectionStart at the beginning of your method,
and then ensure to reset element[0].selectionStart and element[0].selectionEnd to the stored value before the return.
This should capture your selection range in angular
Generate directive:
ng g directive capitalizeFirst
Update file capitalize-first.directive.ts:
import {Directive, ElementRef, HostListener} from '#angular/core';
#Directive({
selector: '[appCapitalizeFirst]'
})
export class CapitalizeFirstDirective {
constructor(private ref: ElementRef) {
}
#HostListener('input', ['$event'])
onInput(event: any): void {
if (event.target.value.length === 1) {
const inputValue = event.target.value;
this.ref.nativeElement.value = inputValue.charAt(0).toUpperCase() + inputValue.substring(1);
}
}
}
Usage:
<input appCapitalizeFirst>
This code woks with Angular 11+
Comment to Mark Rajcok solution: when using $setViewValue, you trigger the parsers and validators again. If you add a console.log statement at the beginning of your capitalize function, you'll see it printed twice.
I propose the following directive solution (where ngModel is optional):
.directive('capitalize', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
var capitalize = function (inputValue) {
return (inputValue || '').toUpperCase();
}
if(ngModel) {
ngModel.$formatters.push(capitalize);
ngModel._$setViewValue = ngModel.$setViewValue;
ngModel.$setViewValue = function(val){
ngModel._$setViewValue(capitalize(val));
ngModel.$render();
};
}else {
element.val(capitalize(element.val()));
element.on("keypress keyup", function(){
scope.$evalAsync(function(){
element.val(capitalize(element.val()));
});
});
}
}
};
});
Here's a codepen for a filter that capitalizes the first letter:
http://codepen.io/WinterJoey/pen/sfFaK
angular.module('CustomFilter', []).
filter('capitalize', function() {
return function(input, all) {
return (!!input) ? input.replace(/([^\W_]+[^\s-]*) */g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}) : '';
}
});
Further to the CSS-only answers, you could always use Twitter Bootstrap:
<td class="text-capitalize">
Building off Mark Rajcok's solution; It's important to consider that the directive evaluate only when the input field in engaged, otherwise you'll get error messages firing off until the input field has a 1st character.
Easy fix with a few conditionals:
A jsfiddle to go with that: https://jsfiddle.net/Ely_Liberov/Lze14z4g/2/
.directive('capitalizeFirst', function(uppercaseFilter, $parse) {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
if (inputValue != null) {
var capitalized = inputValue.charAt(0).toUpperCase() +
inputValue.substring(1);
if (capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
};
var model = $parse(attrs.ngModel);
modelCtrl.$parsers.push(capitalize);
capitalize(model(scope));
}
};
});
The problem with css-ony answers is that the angular model is not updated with the view. This is because css only applies styling after rendering.
The following directive updates the model AND remembers the cursors location
app.module.directive('myCapitalize', [ function () {
'use strict';
return {
require: 'ngModel',
restrict: "A",
link: function (scope, elem, attrs, modelCtrl) {
/* Watch the model value using a function */
scope.$watch(function () {
return modelCtrl.$modelValue;
}, function (value) {
/**
* Skip capitalize when:
* - the value is not defined.
* - the value is already capitalized.
*/
if (!isDefined(value) || isUpperCase(value)) {
return;
}
/* Save selection position */
var start = elem[0].selectionStart;
var end = elem[0].selectionEnd;
/* uppercase the value */
value = value.toUpperCase();
/* set the new value in the modelControl */
modelCtrl.$setViewValue(value);
/* update the view */
modelCtrl.$render();
/* Reset the position of the cursor */
elem[0].setSelectionRange(start, end);
});
/**
* Check if the string is defined, not null (in case of java object usage) and has a length.
* #param str {string} The string to check
* #return {boolean} <code>true</code> when the string is defined
*/
function isDefined(str) {
return angular.isDefined(str) && str !== null && str.length > 0;
}
/**
* Check if a string is upper case
* #param str {string} The string to check
* #return {boolean} <code>true</code> when the string is upper case
*/
function isUpperCase(str) {
return str === str.toUpperCase();
}
}
};
}]);
You can use the provided uppercase filter.
http://docs.angularjs.org/api/ng.filter:uppercase
You could use pure css:
input {
text-transform: capitalize;
}

Resources