Determine where this AngularJs Error $rootScope:infdig is coming from - angularjs

Ok, I'm having a hard time trying to figure out what's causing this error. I think it may be due to a watch that I have in my routeOrderValidator directive but I can't see where I'm modifying the watch value. :(
Here is my Plunker and the error can be reproduced by entering a "3" into the Routing Order field for the John Smith reviewer.
Here is my directive code:
(function () {
'use strict';
angular
.module('app')
.directive('routeOrderValidator', routeOrderValidator);
routeOrderValidator.$inject = ['$filter'];
function routeOrderValidator($filter) {
var directive = {
require: 'ngModel',
link: link,
restrict: 'A'
};
return directive;
function link(scope, element, attrs, ngModel) {
scope.$watch(attrs.routeOrderValidator, function () {
ngModel.$validate();
}, true);
ngModel.$validators.routeOrder = function (modelValue, viewValue) {
return validate(scope, element, attrs, ngModel, modelValue, viewValue);
};
}
function validate(scope, element, attrs, ngModel, modelValue, viewValue) {
var isValid = true;
var reviewers = scope.$eval(attrs.routeOrderValidator);
if (!reviewers) {
return isValid;
}
var sorted = $filter('orderBy')(reviewers, 'route');
var i, len, count = 0;
for (i = 0, len = sorted.length; i < len; i++) {
if (sorted[i].office == 'Branch') {
count++;
}
}
for (i = 0, len = sorted.length; i < len; i++) {
if (count > 0) {
if (sorted[i].office == 'Branch') {
count--;
} else {
isValid = false;
break;
}
}
}
return isValid;
}
}
})();
Here is my controller code:
(function() {
'use strict';
angular
.module('app')
.controller('controller1', controller1);
controller1.$inject = ['$location'];
function controller1($location) {
var data = {
'title': 'More Snacks Please',
'description': 'Add beef jerky to the breakroom snacks.',
'reviewers': [{
'name': 'John Smith',
'office': 'Branch',
'route': '1'
}, {
'name': 'Amy Jones',
'office': 'Corporate',
'route': '2'
}, {
'name': 'Foo Bar',
'office': 'Corporate',
'route': '3'
}]
};
var vm = this;
vm.data = data;
}
})();
Here is my html:
<form name="form1">
<label>
Title:
<input type="text" name="title" ng-model="vm.data.title" />
</label>
<label>
Description:
<textarea type="text" name="description" ng-model="vm.data.description"></textarea>
</label>
<h3>Reviewers</h3>
<ul ng-repeat="reviewer in vm.data.reviewers">
<li>
<label>
Name:
<input type="text" name="name_{{$index}}" ng-model="reviewer.name" />
</label>
<label>
Office:
<select name="office_{{$index}}" ng-model="reviewer.office">
<option>Branch</option>
<option>Corporate</option>
</select>
</label>
<label>
Routing Order:
<input type="text"
name="route_{{$index}}"
ng-model="reviewer.route"
route-order-validator="vm.data.reviewers" />
</label>
<p ng-show="form1.route_{{$index}}.$error.routeOrder" class="error">Branch employees must be first in the routing order!</p>
</li>
</ul>
</form>

The issue that you have is that $validator will remove the incorrect object/attribute if it is invalid.
Example :
When Routing Order for John Smith is 3 then the route attribute will be removed as it is invalid.
This causing $watch to be triggered as the object is changed already and causing infinite loop.
One of the possible solution is to avoid ngModel.validate() until all other validation is completed (i.e validation of the individual routing order)
From :
scope.$watch(attrs.routeOrderValidator, function () {
ngModel.$validate();
}, true);
To:
scope.$watch(attrs.routeOrderValidator, function () {
if(ngModel.$valid )
{
ngModel.$validate();}
}, true);

Related

Predefine error message with angular validation custom directive

I am doing angular validation as follows:
<form name="form" ng-submit="vm.create(vm.job)" validation="vm.errors">
<input name="vm.job.position" type="text" ng-model="vm.job.position" validator />
When the form is submitted the directive gets the name of the property, e.g. position, from the ng-model. It then checks if vm.errors has a message for that property. If yes then adds a span with the error message after the input.
However, I would also like to use the same directive in another way:
<form name="form" ng-submit="vm.create(vm.job)" validation="vm.errors">
<input name="vm.job.position" type="text" ng-model="vm.job.position" />
<span class="error" validator="position"></span>
In this case I removed the validator from the input and added the span already allowing me to control where the error will be displayed. In this case I am using validator="position" to define to which model property the error message is associated.
I am not sure how should I add this functionality to my current code ... Any help is appreciated.
The following is all the code I have on my directives:
(function () {
"use strict";
angular.module("app").directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
var vm = this;
$scope.$watch(function () {
return $scope.validation;
}, function () {
vm.errors = $scope.validation;
})
}
}
angular.module("app").directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function () {
return controller.errors;
}, function () {
if (controller.errors) {
var result = controller.errors.filter(function (error) {
if (error.flag == null)
return false;
var position = attributes.name.lastIndexOf(".");
if (position > -1)
return attributes.name.slice(position + 1).toLowerCase() === error.flag.toLowerCase();
else
return attributes.name.toLowerCase() === error.flag.toLowerCase();
});
if (result.length > 0) {
var error = element.siblings("span.error").first();
if (error.length == 0)
element.parent().append("<span class='error'>" + result[0].info + "</span>");
else
error.text(result[0].info);
} else {
element.siblings("span.error").first().remove();
}
}
}, true);
}
}
})();

Same AngularJS custom directive on same page

I have a custom search directive and need to use multiple instances of it on the same page. The page makes use of bootstrap tabs and there will be an instance of this search component in each tab.
The issue is that the search directive in the second tab is overriding the callback of the search directive in the first tab. Here is a snippet of my search directive:
class SearchDirective {
constructor($timeout) {
this.require = '^ngModel';
this.restrict= "AE";
this.$timeout = $timeout;
this.scope = {
ngModel: '=',
searchTime: '=',
searchCallback: '&'
};
}
compile(tElem, tAttrs) {
return this.link.bind(this);
}
link(scope, element, attrs) {
this.scope = scope;
var timer = null;
scope.$watch('ngModel', (value, preValue) => {
if (value === preValue) return;
if (timer) {
this.$timeout.cancel(timer);
}
timer = this.$timeout(() => {
timer = null;
if (value.length === 0) {
this.scope.searchCallback();
}
}, this.scope.searchTime)
});
}
}
And here is a snippet of the HTML for the search component on the first tab:
<input search search-callback="searchWindowsController.searchPcs()" search-time="600" data-ng-model="searchWindowsController.searchQuery" type="text" class="searchBox" placeholder="Search Windows...">
And this is what i have in the second tab:
<input search search-callback="searchMacController.searchPcs()" search-time="600" data-ng-model="searchMacController.searchQuery" type="text" class="searchBox" placeholder="Search Macs...">
For some reason when you search using the Windows search, it is calling the Mac callback. Can someone point me to what I am doing wrong? I am new to custom directives.
The error due to this within the $timeout function.
See live example on jsfiddle.
'use strict'
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope, $log) {
$scope.callback1 = function(){
console.log('callback1');
};
$scope.callback2 = function(){
console.log('callback2');
};
})
.directive('search',function($timeout){
return new SearchDirective($timeout);
});
class SearchDirective {
constructor(timeout) {
this.require = '^ngModel';
this.restrict = "AE";
this.$timeout = timeout;
this.scope = {
ngModel: '=',
searchTime: '=',
searchCallback: '&'
};
}
compile(tElem, tAttrs) {
return this.link.bind(this);
}
link(scope, element, attrs) {
this.scope = scope;
var timer = null;
scope.$watch('ngModel', (value, preValue) => {
if (value === preValue) return;
if (timer) {
this.$timeout.cancel(timer);
}
timer = this.$timeout(() => {
timer = null;
if (value.length === 0) {
scope.searchCallback();
}
}, scope.searchTime)
});
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<input search search-callback="callback1()" search-time="600" data-ng-model="searchQuery1" type="text" class="searchBox" placeholder="Search Mac...">
<input search search-callback="callback2()" search-time="600" data-ng-model="searchQuery2" type="text" class="searchBox" placeholder="Search Windows...">
</div>
</div>

custom currency filter to the input field in angularjs

How to formatting the value in to indian currency type using angularjs.
for example, 454565 to 4,54,565.00
I have input field like this:
<input type="text" ng-model="item.cost />
function FormatMyNumber(yourNumber) {
// Limit to two decimal places
yourNumber = parseFloat(yourNumber).toFixed(2);
//Seperates the components of the number
var n = yourNumber.toString().split(".");
//Comma-fies the first part
n[0] = n[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
//Combines the two sections
return n.join(".");
}
FormatMyNumber(454565); //yields 454,565.00
You can create directive for it as follows
HTML
<div ng-app="myApp">
<div ng-controller="myCtrl">
{{amount}}
<input format-to-currency amount="amount">
</div>
</div>
JS
angular.module('myApp', [])
.controller('myCtrl', function($scope) {
$scope.ampunt = 2;
})
.directive('formatToCurrency', function($filter) {
return {
scope: {
amount: '='
},
link: function(scope, el, attrs) {
el.val($filter('currency')(scope.amount));
el.bind('focus', function() {
el.val(scope.amount);
});
el.bind('input', function() {
scope.amount = el.val();
scope.$apply();
});
el.bind('blur', function() {
el.val($filter('currency')(scope.amount));
});
}
}
});
Link http://jsfiddle.net/moL8ztrw/6/

AngularJS: How to refresh $viewValue even when $modelValue is unchanged

In the example below, the value in input 'ia' can get out of sync with the model value (e.g. if you type in 'aaa'). What I would like to achieve is when the input 'ia' loses focus, update its value to the formatted value of the current model value (e.g. when model value is null, update the view value to string 'N/A'). Anybody know I this might be achieved? Thanks.
The current behaviour is if you type in 'aaa' as profession, model value is updated to null bot 'aaa' stays in the input.
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<!DOCTYPE html>
<html ng-app="myapp">
<head>
<script>
var myapp = angular.module('myapp', []);
myapp.controller('mycontroller', function($scope) {
$scope.model = {
name: "ttt",
age: 24,
profession: null
};
});
var professionList = [{
id: 1,
name: "p1"
}, {
id: 2,
name: "p2"
}, {
id: 3,
name: "p3"
}, {
id: 4,
name: "p4"
}];
myapp.directive("prof", function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$formatters.push(function(value) {
for (var i = 0; i < professionList.length; i++) {
if (value == professionList[i].id)
return professionList[i].name;
}
return "N/A";
});
//format text from the user (view to model)
ngModel.$parsers.push(function(value) {
for (var i = 0; i < professionList.length; i++) {
if (value == professionList[i].name)
return professionList[i].id;
}
return null;
});
element[0].ngModel = ngModel;
}
};
});
</script>
</head>
<body ng-controller="mycontroller">
<form>
Name:
<input ng-model="model.name" />
<br />Age:
<input ng-model="model.age" />
<br />Profession:
<input prof="" ng-model="model.profession" id="ia" />
<input ng-model="model.profession" />
</form>
<hr />Name: {{model.name}}
<br />Age: {{model.age}}
<br />Profession: {{model.profession}}
</body>
</html>
UPDATE:
I found a solution based on the answer in this question
//add this to the link function:
ngModel.$render = function(){
element[0].value = ngModel.$viewValue;
};
element[0].onblur = function(){
var viewValue = ngModel.$modelValue;
for (var i in ngModel.$formatters) {
viewValue = ngModel.$formatters[i](viewValue);
}
ngModel.$viewValue = viewValue;
ngModel.$render();
};
While returning null you are not re-initializing ngModel and I dont think directive is needed here , there is something called ng-blur in angularjs,
Profession : <input ng-model="model.profession" ng-blur="checkProfession() "/>
Profession ID : <input ng-model="model.id" />
In your controller write a function as,
$scope.checkProfession=function(){
var flag=0;
for (var i = 0; i < professionList.length; i++) {
if ($scope.model.profession == professionList[i].name){
$scope.model.id=professionList[i].id;
flag=1;
}
if(!flag){
$scope.model.id="";
$scope.model.profession="";
}
}
}

Use ngMessages with Angular 1.2

Does anyone know if there is a fork of Angular 1.2 that supports ngMessages?
I'd love to use this but I have a requirement for IE8.
Thanks in advance for your help.
Here is my directive I use:
/**
* Ui-messages is similar implementation of ng-messages from angular 1.3
*
* #author Umed Khudoiberdiev <info#zar.tj>
*/
angular.module('uiMessages', []).directive('uiMessages', function () {
return {
restrict: 'EA',
link: function (scope, element, attrs) {
// hide all message elements
var messageElements = element[0].querySelectorAll('[ui-message]');
angular.forEach(messageElements, function(message) {
message.style.display = 'none';
});
// watch when messages object change - change display state of the elements
scope.$watchCollection(attrs.uiMessages, function(messages) {
var oneElementAlreadyShowed = false;
angular.forEach(messageElements, function(message) {
var uiMessage = angular.element(message).attr('ui-message');
if (!oneElementAlreadyShowed && messages[uiMessage] && messages[uiMessage] === true) {
message.style.display = 'block';
oneElementAlreadyShowed = true;
} else {
message.style.display = 'none';
}
});
});
}
};
});
I've used ui-messages instead of ng-messages to avoid conflicts.
<div ui-messages="form.name.$error">
<div ui-message="minlength">too short</div>
<div ui-message="required">this is required</div>
<div ui-message="pattern">pattern dismatch</div>
</div>
I don't know for sure if a fork exists but it would be easy enough to roll your own ng-message (or something that serves the same purpose). I think the following would do it:
Controller
app.controller("Test", function ($scope) {
$scope.messages = {
"key1": "Message1",
"key2": "Message2",
"key3": "Message3"};
$scope.getMessage = function (keyVariable) {
return $scope.messages[keyVariable.toLowerCase()];
};
$scope.keyVariable = 'key1';
});
HTML (example)
ENTER A KEY: <input type="text" ng-model="keyVariable" />
<h1 ng-bind="getMessage(keyVariable)" ng-show="getMessage(keyVariable) != ''"></h1>
See It Working (Plunker)
I've updated pleerock's answer to handle element directives having for and when attributes like ngMessages and ngMessage. You can find the same in this github repo
angular.module('uiMessages', []).directive('uiMessages', function() {
return {
restrict: 'EA',
link: function(scope, element, attrs) {
// hide all message elements
var messageElements = element.find('ui-message,[ui-message]').css('display', 'none');
// watch when messages object change - change display state of the elements
scope.$watchCollection(attrs.uiMessages || attrs['for'], function(messages) {
var oneElementAlreadyShowed = false;
angular.forEach(messageElements, function(messageElement) {
messageElement = angular.element(messageElement);
var message = messageElement.attr('ui-message') || messageElement.attr('when');
if (!oneElementAlreadyShowed && messages[message] && messages[message] === true) {
messageElement.css('display', 'block');
oneElementAlreadyShowed = true;
} else {
messageElement.css('display', 'none');
}
});
});
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js"></script>
<form name="userForm" ng-app="uiMessages" novalidate>
<input type="text" name="firstname" ng-model="user.firstname" required />
<ui-messages for="userForm.firstname.$error" ng-show="userForm.firstname.$dirty">
<ui-message when="required">This field is mandatory</ui-message>
</ui-messages>
<br />
<input type="text" name="lastname" ng-model="user.lastname" required />
<div ui-messages="userForm.lastname.$error" ng-show="userForm.lastname.$dirty">
<div ui-message="required">This field is mandatory</div>
</div>
</form>

Resources