angularjs directive: $rootScope:infdig error - angularjs

I'm trying to build a pagination directive with angularjs 1.2.15:
This is my view:
<input type="text" ng-model="filter.user">
<input type="number" ng-model="filter.limit" ng-init="filter.limit=5">
<ul>
<li ng-repeat="user in (filteredUsers = (users | orderBy:order:reverse | filter:filter.user ) | limitTo: filter.limit)" ng-click="showDetails(user)">
{{user.id}} / {{user.firstname}} {{user.lastname}}
</li>
</ul>
<div pagination data='filteredUsers' limit='filter.limit'></div>
and here is my pagination directive:
app.directive('pagination', function(){
return {
restrict: 'A',
templateUrl: 'partials/pagination.html',
scope: {
data: '=data',
limit: '=limit'
}
}
})
Everything works perfectly fine without the pagination directive. However with my new directive as soon as I load the page I get a $rootScope:infdig error which I don't understand since the directive is not doing anything to manipulate data that could end up in an infinite loop.
What is the problem here and how can I solve it? Thanks!
Update:
Here are the controller and the resource.
Controller:
usersModule.controller('usersController',
function ($scope, Users) {
function init(){
$scope.users = Users.get();
}
init();
})
Resource (gets users as an array from a REST API):
app.factory('Users', function($resource) {
return $resource('http://myrestapi.tld/users', null,{
'get': { method:'GET', isArray: true, cache: true }
});
});
Update 2
Here is a demo: http://plnkr.co/edit/9GCE3Kzf21a7l10GFPmy?p=preview
Just type in a letter (e.g. "f") into the left input.

The problem is not within the directive, it's within the $watch the directive creates.
When you send filteredUsers to the directive, the directive creates the following line:
$scope.$watch("filteredUsers", function() {
// Directive Code...
});
Notice in the following example how we reproduce it without a directive:
http://plnkr.co/edit/uRj19PyXkvnLNyh5iY0j
The reason it happens is because you are changing filteredUsers every time a digest runs (since you put the assignment in the ng-repeat statement).
To fix this you might consider watching and filtering the array in the controller with the extra parameter 'true' for the $watch:
$scope.$watch("users | orderBy:order:reverse | filter:filter.user", function(newVal) {
$scope.filteredUsers = newVal;
}, true);
You can check the solution here:
http://plnkr.co/edit/stxqBtzLsGEXmsrv3Gp6
the $watch without the extra parameter (true) will do a simple comparison to the object, and since you create a new array in every digest loop, the object will always be different.
When you're passing the true parameter to the $watch function, it means it will actually do deep comparison to the object that returns before running the $watch again, so even if you have different instances of arrays that has the same data it will consider them equal.

A quick fix is to add a "manual" $watchCollection in the directive, instead of a 2-way binding.
app.directive('pagination', function($parse){
return {
restrict: 'A',
template: '',
scope: {
limit: '=limit'
},
link: function(scope, elem, attrs) {
var dataExpr = $parse(attrs.data);
var deregister = scope.$parent.$watchCollection(dataExpr, function(val) {
scope.data = val;
});
scope.$on('$destroy', deregister);
}
}
})
$watchCollection monitors the contents of the array, not the reference to it.
See it running here.
Generally speaking, i don't like expressions like that:
filteredUsers = (users | orderBy:order:reverse | filter:filter.user )
inside views. Views should only render $scope properties, not create new ones.

This error may remove to clear the browser history from setting. I got same issue and apply many solution to resolve this, But cannot resolve.
But when I remove browser history and cache this issue is resolve. May this help for you.

Related

Inserting a function into directive without isolate scope

I'm not sure if I'm going about this the right way. I am using the ui-select directive which does not seem to support the HTML required directive. So I built my own, ui-select-required. It seems I am unable to use isolate scope because ui-select already instantiates an isolate scope.
I want to make ui-select-required take in a function as an attribute. If the attribute is present, the it should validate with the return value of this function. If the attribute is not present then it should validate on presence of a value. This is all a part of a component.
product_details.js
angular
.module('ProductComponents')
.component('productDetails', {
bindings:{
product: '=product',
},
templateUrl: "/template/admin/products/details",
controllerAs: 'prodDetails',
controller: [
'v3Stitcher',
'AjaxLoaderSvc',
'ModelInformationSvc',
'$filter',
'$http',
'current_site',
function(
v3Stitcher,
AjaxLoaderSvc,
ModelInformationSvc,
$filter,
$http,
current_site
){
var prodDetails = this;
...
prodDetails.templateRequired = function(){
// Product types requiring a template
// 3 - customizable_downloadable
// 6 - static_variable_downloadable
var productTypes = [3, 6];
// Specification types requiring a template
var specificationTypes = ["print_on_demand"];
if(productTypes.indexOf(prodDetails.product.product_type) > -1){
return true;
}
if(specificationTypes.indexOf(prodDetails.specification.specification_type) > -1){
console.log('here'); // this gets called
return true;
}
return false;
};
.directive('uiSelectRequired',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope, elem, attrs, ctrl){
var form = angular.element(document).find('form');
var input = angular.element(elem.find('input')[0]);
var requiredFn = scope[attrs['requiredFn']];
if(requiredFn){
ctrl.$validators.uiSelectRequired = function(){
return requiredFn();
};
} else {
ctrl.$validators.uiSelectRequired = function(modelValue){
return !ctrl.$isEmpty(modelValue)
};
}
form.on('submit', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-valid').addClass('ng-invalid');
}
});
elem.on('change', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-invalid').addClass('ng-valid');
}
});
}
};
});
details.slim
label(ng-class="{'label label-danger': prodDetails.templateRequired()}")
| Template
ui-select(ng-model="prodDetails.product.template_id" name="template" ng-model-options="{ debounce: { default:500, blur: 0 } }" ui-select-required required-fn="prodDetails.templateRequired")
ui-select-match(placeholder="Search Templates...")
| {{$select.selected.name}}
ui-select-choices(position="down" repeat="template.id as template in prodDetails.templates" refresh="prodDetails.refreshTemplates($select.search)" minimum-input-length="1" refresh-delay="0")
| {{ template.name }}
br
| id: {{template.id}}
br
| created: {{template.created_at | date : 'yyyy-MM-dd'}}
The problem I'm having is that the variable requireFn is undefined. However, if in the HTML I send in the controller variable prodDetails alone then requireFn has the correct value of the controller variable.
I think your problem is that:
You are doing controllerAs: 'prodDetails' in your isolate scope and
You are looking to reference the function directly on the scope in your uiSelectRequired directive
I think if you switch this:
var requiredFn = scope[attrs['requiredFn']];
to:
var requiredFn = scope.$eval(attrs.requiredFn);
You should get what you are looking for. This is assuming that templateRequired property has been added to the productDetails component's controller instance.
To reiterate, your issue was that you were looking for the property directly on the isolate scope itself, where it has been added to the controller reference. By doing a scope.$eval, you will essentially be parsing the path prodDetails.templateRequired -- which will hopefully resolve to the function reference you were hoping to get in the first place.
Edit: So the second part of your question in the comments lead me to believe you never needed a function into a directive with isolate scope. I think what you are trying to do is make the template model required conditionally. Angular already gives you this functionality through required and ng-required directives. You state in your question these are not available on ui-select, but they are "helper" directives with ngModel. I believe this is a mostly working example of what you want to do where I switch to required/ng-required and eliminate the need for your custom directive.

Passing keys of object to directive

I have created a directive as a wrapper for md-autocomplete so that it's easier to re-use. In the parent controller, I have an object. I want to pass the keys of the object to my custom directive, but I'm having trouble. Simplified code, without md-autocomplete:
Here's the script
var app = angular.module('myApp',[])
.controller('parentController', function(){
var parent = this;
parent.things = {item1: {color: "blue"}, item2: {color: "red"}};
})
.directive('childDirective',function(){
return {
scope: {},
bindToController: {
items:'&'
},
controller: childController,
controllerAs: 'child',
template: '<pre>{{child.items | JSON}}<pre>' //should be [item1,item1]
}
function childController(){
//Just a dummy controller for now
}
})
HTML
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="Object.keys(parent.things)">
</my-directive>
</div>
TL;DR: How do I pass the keys of an object defined in the parent controller to a child directive? I need to pass just the keys, not the object itself, because my directive is designed to deal with an array of strings.
Try using a directive with local scope from user attribute (=)
app.directive('childDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {
items: '='
},
template: '<pre>{{items | JSON}}<pre>'
};
});
Using the directive, object in attribute "items" is passed "as is" , as a scope variable "items"
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="getKeys(parent.things)">
</my-directive>
</div>
Using Object.keys(obj) as source will cause an infinite loop digest (the function is always returning a new different object). You need a function to save the result to a local updatable object, like in this example:
https://jsfiddle.net/FranIg/3ut4h5qm/3/
$scope.getKeys=function(obj){
//initialize result
this.result?this.result.length=0:this.result=[];
//fill result
var result=this.result;
Object.keys(obj).forEach(function(item){
result.push(item);
})
return result;
}
I'm marking #Igor's answer as correct, because ultimately it led me to the right place. However, I wanted to provide my final solution, which is too big for a comment.
The search for the answer to this question led me to create a directive that is more flexible, and can take several different types of input.
The real key (and my actual answer to the original question) was to bind the items parameter to a proxy getter/setter object in the directive. The basic setup is:
app.directive('myDirective',function(){
return {
...
controller: localControl,
bindToController: {
items: '<' //note one-way binding
}
...
}
function localControl(){
var child = this;
child._items = [],
Object.defineProperties(child,{
items: {
get: function(){return child._items},
set: function(x){child._items = Object.keys(x)}
}
});
}
});
HTML
<my-directive items="parent.items">
<!-- where parent.items is {item1:{}, item2:{}...} -->
</my-directive>
Ultimately, I decided I wanted my directive to be able to accept a variety of formats, and came up with this plunk as a demonstration.
Please feel free to offer comments/suggestions on improving my code. Thanks!

angular array item not updating on scope change

Spent a few hours on this already, sifted through numerous stack posts and blogs but can't seem to get this to make my model update. More specifically, I am trying to update an array item (ng-repeat). In the simple case below, I iterate over venues list, and upon toggling a "like" button, I update the server appropriately, and reflect the change on the venues item on the $scope.
in my search.html I have a directive:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
and search controller I have:
app.controller('bleh', function(Service) {
...
$scope.venues = [{ id: 1, name: 'Venue1', like: false },{ id: 2, name: 'Venue2', like: false }];
...
});
Nothing unusual there. Then in my venues-list directive:
app.directive('venues-list', function() {
function venueListController($scope, Service) {
$scope.likeToggle = function(venue, $index) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venues[$index].like= !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueListController,
scope: {
venues: '='
}
}
});
then in my venue.html I have:
<div ng-repeat="venue in venues">
<p>{{venue.name}}</p>
<button ng-click="likeToggle(venue, $index)">Like</button>
</div>
I have tried many different options:
$scope.$apply() // after updating the $scope or updating scope within apply's callback function;
$scope.$digest()
$timeout(function() { // $scope.venues[$index] .... }, 0);
safe(s,f){(s.$$phase||s.$root.$$phase)?f():s.$apply(f);}
so
safe($scope, function() { $scope.venues[$index].like = !venue.like });
I haven't yet used the link within the directive, but my venues.html template is obviously a little more elaborate than presented here.
EDIT:
Just to keep the discussion relevant, perhaps I should have mentioned - the data is coming back from the server with no issues, I am handling errors and I am definitely hitting the portion of the code where the $scope is to be updated. NOTE: the above code is a small representation of the full app, but all the fundamental pieces are articulated above.
Search Controller
Venues Service
venue-list directive and venue.html template to accompany the directive
directive controller
EDIT #2
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
Just to keep it even simpler, the above doesn't work - so really, the bottom line is my items within an array are not reflecting the updates ...
EDIT #3
$scope.foo = function() {
$scope.venues[0].like = !$scope.venues[0].like;
}
My apologies - just to re-iterate what I was trying to say above - the above is changing the scope, however, the changes are not being reflected on the view.
Perhaps the issue is with your service and promise resolution.. Can you put a console.log there and see if the promise resolution is working fine? or Can you share that code bit. Also where are you checking for scope update? within directive or outside
OK after some refactoring I finally got it working.
The "fix" (if you want to call it that) to my specific problem was:
instead of passing an array of venues, I was iterating over the array on the parent controller, passing in a venue as an element attribute that would bind (two-way) on the isolated scope of the directive.
so, instead of:
<ion-content>
<venues-list venues="venues"></venues-list>
</ion-content>
I now have:
<ion-content>
<venues-list ng-repeat="venue in venues" venue="venue"></venues-list>
</ion-content>
and my directive now looks like:
app.directive('venues-list', function() {
function venueController($scope, Service) {
$scope.likeToggle = function(venue) {
Service.likeVenue(venue.id, !venue.like).then(function() {
$scope.venue.like = !venue.like;
});
}
}
return {
strict: 'E',
templateUrl: 'venue.html',
controller: venueController,
scope: {
venue: '='
}
}
});
This did the trick!

Using a bindOnce directive with $http

I want to use the following bindOnce directive:
.directive('bindOnce', function() {
return {
scope: true,
link: function( $scope, $element ) {
setTimeout(function() {
$scope.$destroy();
$element.removeClass('ng-binding ng-scope');
}, 0);
}
}
});
If I use this on a simple piece of html like so:
<span class="highlight" data-bind-once> "{{listing.searchTerm}}"</span>
What happens is that there is nothing but the "" being displayed!
I am loading my data using the $http service, I think the bind-once must get removed before I have loaded my data and then it obviously doesn't get bound.
I want to use this in many places in my app, is this a limitation or am I doing this incorrectly?
The newer versions of angular have the ability to bind once within them:
<span class="highlight"> "{{ ::listing.searchTerm }}"</span>
Link: https://docs.angularjs.org/guide/expression#one-time-binding

How can I tell what is calling one of my scope functions in angular?

I have a directive that has a template such as:
<div ng-show="isVisible()"> ... </div>
I've changed isVisible so it now simply returns true and does a console.log.
This directive is included a single time on my page, yet the log occurs 50+ times. I want to know what code is calling isVisible so many times. Is there any way to trace what is calling a function? The ACTUAL stack trace is completely useless, because angular appears to hide everything.
Is there some tool to figure out what non-angular code triggered a function call.
In some other cases I have data changing and I can't find where it's changing [it's not actually my code] and because the original code is in angular, I can't just use stack traces, or use watches or do my normal type of debugging.
here is the directive:
require('app').directive('kmFormLanguages', ["$q", function ($q)
{
return {
restrict: 'EAC',
template: '<span ng-show="isVisible()"><span km-select multiple ng-model="binding" binding-type="string" placeholder="Available Languages" items="locales|orderBy:\'name\'" minimum="1"></span></span>',
transclude: true,
scope: {
binding : '=ngModel',
},
controller: ["$scope", "IStorage", "editFormUtility", function ($scope, storage, editFormUtility) {
$scope.locales = [
{identifier:"ar", name:"Arabic" },
{identifier:"en", name:"English" },
{identifier:"es", name:"Spanish" },
{identifier:"fr", name:"French" },
{identifier:"ru", name:"Russian" },
{identifier:"zh", name:"Chinese" }
];
$scope.isVisible = function() {
console.log('binding: ', $scope.binding);
debugger;
return $scope.binding!==undefined && $scope.binding!==null;
}
}]
};
}])
and the html which uses it (no angular controls that are ancestors are shown):
<div ng-controller="editMeasure">
[...]
<div ng-show="!isLoading() && status!='hidden'">
[...]
<span km-form-languages ng-model="document.header.languages"></span>
It is hard to say only from text. If you can post sample code, more accurate answers can be given.
But from what you have described, it seems that your function is being called as part of angular js
$digest or $compile
cycle.

Resources