Angular: How to populate a dynamic array for select using ngOptions? - arrays

Ideally, I want to populate the array via computed property. For example,
Object.defineProperty(MyObj.prototype, 'linkedList', {
get: function () {
var list = [];
this.dataList.forEach(function (cs) {
if (cs !== this) {
list.push(cs.name);
}
});
return list;
}
});
However, if I used this computed property as select options like
<select data-ng-model="name" data-ng-options="g for g in myObj.linkedList"></select>
I will get this error:
Error: 10 $digest() iterations reached. Aborting!
In fact, I still get this error even if I return a fixed list like:
Object.defineProperty(MyObj.prototype, 'linkedList', {
get: function () {
return ['test1', 'test2'];
}
});
However, it is okay if I return a static array:
MyObj._linkedList = ['test1', 'test2'];
Object.defineProperty(MyObj.prototype, 'linkedList', {
get: function () {
return MyObj._linkedList;
}
});
I also tried to use a filter in the ngOptions and the same error is thrown. Why and How?
UPDATE:
As it turns out, my original question above doesn't truly reflect the problem, because the problem happens only if I use a directive. To simplify the question, I removed the directive part.
Anyway, in my directive, I have
scope: {
name: '=',
isEditing: '=',
isHidden: '=',
options: '='
}
<div>
<div data-ng-hide="!isHidden" style="background-color: lightgrey"> </div>
<div data-ng-show="!isEditing && !isHidden">{{name}}</div>
<select class="select-input" data-ng-show="isEditing && !isHidden" data-ng-model="name" data-ng-options="g for g in {{options}}"><option value=""></option></select>
</div>
Here is how to use it:
<td><ms-table-select-cell name="myObj.link" is-editing="myObj.isEditing" options="myObj.linkedList" /></td>
This is how the error occurs. If I directly use the select tag with ngOptions, it's not a problem.

I think the problem is that your 'get' property is called at each loop.
You can try to assign the variable value once and then loop on it.
<select data-ng-model="name" data-ng-options="g for g in myArray = myObj.linkedList"></select>

In your first case every time angular calls myObj.linkedList it gets a new object, so angular repeatedly begin new digest cycle and does it untill the number of iterations exceeds 10 - than it throw an error "10 $digest() iterations reached".
To avoid this you must construct your code so that every call to the myObj.linkedList returns the same object.
Try to move the var list = []; expression out of the Object.defineProperty call.
I prepared 2 fiddles:
fiddle 1 - with the Error
fiddle 2 - without the Error and with tiny modification.
Compare them.
Hope it will help you

Related

angular filter on object works but causes Infinite $digest Loop

I gave an object as followed
{
key1: [{...}, {...} ....],
key2: [{...}, {...} ....],
.........so on .....
}
I have an ng-repeat ng-repeat="(key, values) in data" and then inside that ng-repeat="val in values"
I want to set up an filter based on some property of objects stored in the array. I have set up below filter
.filter('objFilter', function () {
return function (input, search,field) {
if (!input || !search || !field)
return input;
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
result[key].push(value[i]);
}
}
}
});
console.log(result);
return result;
};
The filter seems to work fine when I use ng-repeat="(date, values) in data| objFilter:search:'url'" but for some reason it is called too many times and causes Infinite $digest Loop.
Any solutions??
Edit:
I have created below plunker to show the issue. The filter works but look in the console for the errors. http://plnkr.co/edit/BXyi75kXT5gkK4E3F5PI
Your filter causes an infinite $digest loop because it always returns a new object instance. With the same parameters it returns a new object instance (it doesn't matter if the data inside the object is the same as before).
Something causes a second digest phase. I'm guessing it's the nested ng-repeat. Angular calls filters on every digest phase and because you filter returns a new value it causes the framework to reevaluate the whole outer ng-repeat block which causes the same on the inner ng-repeat block.
Option 1 - modify the filter
One fix you can do is to "stabilize" the filter. If it's called 2 times in a row with the same value it should return the same result.
Replace your filter with the following code:
app.filter('objFilter', function () {
var lastSearch = null;
var lastField = null;
var lastResult = null;
return function (input, search, field) {
if (!input || !search || !field) {
return input;
}
if (search == lastSearch && field == lastField) {
return lastResult;
}
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
//if(result[key]){
result[key].push(value[i]);
//}else{
// result[key] = [value[i]];
//}
}
}
}
});
// Cache params and last result
lastSearch = search;
lastField = field;
lastResult = result;
return result;
};
});
This code will work but it's bad and prone to errors. It might also cause memory leaks by keeping the last provided arguments in memory.
Option 2 - move the filter on model change
Better approach will be to remember the updated filtered data on model change. Keep you JavaScript as is. Change only the html:
<body ng-controller="MainCtrl">
<div ng-if="data">
Search:
<input type="text"
ng-model="search"
ng-init="filteredData = (data | objFilter:search:'url')"
ng-change="filteredData = (data | objFilter:search:'url')">
<div ng-repeat="(date, values) in filteredData">
<div style="margin-top:30px;">{{date}}</div>
<hr/>
<div ng-repeat="val in values" class="item">
<div class="h-url">{{val.url}}</div>
</div>
</div>
</div>
</body>
First we add a wrapper ng-if with a requirement that data must have a value. This will ensure that our ng-init will have "data" in order to set the initial filteredData value.
We also change the outer ng-repeat to use filteredData instead of data. Then we update filtered data on the model change with the ng-change directive.
ng-init will fire once after data value is set
ng-change will be executed only when the user changes the input value
Now, no matter how many consecutive $digest phases you'll have, the filter won't fire again. It's attached on initialization (ng-init) and on user interaction (ng-change).
Notes
Filters fire on every digest phase. As a general rule try avoiding attaching complex filters directly on ng-repeat.
Every user interaction with a field that has ng-model causes a $digest phase
Every call of $timeout causes a $digest phase (by default).
Every time you load something with $http a digest phase will begin.
All those will cause the ng-repeat with attached filter to reevaluate, thus resulting in child scopes creation/destruction and DOM elements manipulations which is heavy. It might not lead to infinite $digest loop but will kill your app performance.

Simple Angular Date filter throws a TypeError exception

First, I have to point out that the filter works, even though I get the exception. The exception is:
TypeError: Cannot read property 'substr' of undefined
I register the filter to the module, and as far as I know, I can use that filter in any controller. The ngRepeat is in one controller, and the h3 is in another. The ngRepeat usage works, without any exceptions, but the h3 one throws the one above, even though it shows the date in the correct format.
The filter
var Filter = function() {
return function(jsonDate) {
return new Date(parseInt(jsonDate.substr(6)));
};
};
AppModule.filter("jsonToAngularDate", Filter);
Usage
{{ order.Date | jsonToAngularDate | date:'dd.MM.yyyy' }}
The exact same usage is used in both places.
UPDATE
Turns out I get the error because I load the date asynchronously. How do I avoid getting the error? Should I grab the data in the init function?
Change the filter to account for nulls.
var Filter = function() {
return function(jsonDate) {
return jsonDate ? new Date(parseInt(jsonDate.substr(6))) : '';
};
};
AppModule.filter("jsonToAngularDate", Filter);

AngularJS Filter throws infdig error when it creates new array

i am about to bang my head to walls. i thought i had an understanding of how angular works (filters too). but i just cant find the problem about my filter. it causes infdig. and i even dont change source array in filter.
(function () {
angular.module('project.filters').filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var resultArray = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
resultArray.push(data.slice(i, i + chunk));
}
return resultArray;
};
});
})();
i have lists where i need to split data to x columns. it is complicated to solve with limitTo.
(limitTo: $index*x | limitTo: $last ? -z : -x)
it causes a dirty template file. so i decided to create a filter which splits an array to groups.
[1,2,3,4,5,6,7,8] -> [[1,2,3],[4,5,6],[7,8]]
so i can easily use it in my template.
Can u help me about what causes infdig in this filter?
Edit: the error message itself looks strange with some numbers in that don't appear anywhere in the code, which can be seen at http://plnkr.co/edit/pV1gkp0o5KeimwPlEMlF
10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":23,"oldVal":20}],[{"msg":"fn: regularInterceptedExpression","newVal":26,"oldVal":23}],[{"msg":"fn: regularInterceptedExpression","newVal":29,"oldVal":26}],[{"msg":"fn: regularInterceptedExpression","newVal":32,"oldVal":29}],[{"msg":"fn: regularInterceptedExpression","newVal":35,"oldVal":32}]]
HTML Template
<div class="row" ng-repeat="chunk in docProfile.SysMedicalInterests | splitListFilter: 3">
<div class="col-md-4" ng-repeat="medInterest in chunk">
<label style="font-weight:normal;">
<input type="checkbox" value="{{medInterest.ID}}" ng-click="docProfile.saveInterest(medInterest.ID)" ng-checked="docProfile.isMedChecked(medInterest.ID)"> {{medInterest.Name}}
</label>
</div>
</div>
Controller Code
var me = this;
me['SysMedicalInterests'] = null;
var loadMedicalInterests = function(){
var postData = { 'Data': me['data']['subData'] };
return docService.loadMedicalInterests(postData).then(function(resp) {
me['SysMedicalInterests'] = resp['data'];
}, function(){});
};
loadMedicalInterests();
so array starts with a null reference and loads data from server. which changes array causes a second filter run. but it doesnt stop after that
Edit: here is plunkr http://plnkr.co/edit/OmHQ62VgiCXeVzKa5qjz?p=preview
Edit: related answer on so https://stackoverflow.com/a/21653981/1666060 but this still doesn't explain angular built in filters.
here is angularjs limitTo filter source code
https://github.com/angular/angular.js/blob/master/src/ng/filter/limitTo.js#L3
About what exactly causes it, I suspect is something to do with the fact that every time you run the filter a new array reference is created and returned. However, Angular's built-in filter filter does the same thing, so I'm not sure what is going wrong. It could be something to do with the fact that it's an array of arrays that is being returned.
The best I have come up with is a workaround/hack, to cache the array reference manually as an added property, which I've called $$splitListFilter on the array, and only change it if it fails a test on angular.equals with the correct results calculated in the filter:
app.filter('splitListFilter', function () {
return function (data, chunk) {
if(!data || data.length === 0){
return data;
}
var results = [];
for (var i = 0, j = data.length; i < j; i += chunk) {
results.push(data.slice(i, i + chunk));
}
if (!data.$$splitListFilter || !angular.equals(data.$$splitListFilter, results)) {
data.$$splitListFilter = results;
}
return data.$$splitListFilter;
};
});
You can see this working at http://plnkr.co/edit/vvVJcyDxsp8uoFOinX3V
The answer uses Angular 1.3.15
The JS fiddle works fine: http://jsfiddle.net/3tzapfhh/1/
Maybe you use the filter wrongly.
<body ng-app='app'>
<div ng-controller='ctrl'>
{{arr | splitListFilter:3}}
</div>
</body>

Firebase $save doesn't work inside an event handler function

I must be missing something really basic. I have an input box where the list name is entered. The name is then saved to the Firebase.
When using $watch, it works just fine. However, if done through ng-keyup event, it returns the following error
TypeError: undefined is not a function.
What am I missing?
HTML:
<input id="which_list" ng-keyup="enterThis($event)" ng-model="which_list.name" >{{which_list.name}}</span>
Controller:
$scope.which_list = sync.$asObject();
$scope.$watch('which_list.name', function() {
gDataService.which_list.name= $scope.which_list.name;
$scope.which_list.$save() // THIS WORKS
// $scope.which_list => d {$$conf: Object, $id: "id", $priority: null, name: "to1_list", $save: function…}
.then(function(){
console.log($scope.which_list.name);
});
});
$scope.enterThis = function(event){
if (event.keyCode === 13) {
gDataService.which_list.name= $scope.which_list.name;
$scope.which_list.$save(); // THIS DOESN't WORK
// $scope.which_list = Object {name:"list_name"}
}
};
EDIT: In the comment, I included the value of $scope.which_list shown at the breakpoint.
Currently as you are changing in scope which_list converting to plain old JavaScript objects (POJO), I believe you need to unable 3 way binding between scope variable and $asObject().
Code
var which_list = sync.$asObject();
// set up 3-way data-binding
which_list.$bindTo($scope, "which_list");
Update
Also as you are using $scope.which_list object which contains name and other property,So do initialize it on starting of your controller like
$scope.which_list = {}
Hope this could help you, Thanks.

Clicking an element inside repeater when another element equals specific input with protractor and jasmine

My HTML structure is this:
<li ng-repeat="m in members">
<div class="col-name">{{m.name}}</div>
<div class="col-trash">
<div class="trash-button"></div>
</div>
</li>
What I want to be able to do is using protractor, click on the trash when m.name equals a specific value.
I've tried things like:
element.all(by.repeater('m in members')).count().then(function (number) {
for (var i = 0 ; i < number ; i++) {
var result = element.all(by.repeater('m in members').row(i));
result.get(0).element(by.binding('m.name')).getAttribute('value').then(function (name) {
if (name == 'John') {
result.get(0).element(by.className('trash-button')).click();
};
});
};
});
This seems like it should work, however, it seems like my function does not even run this.
I've also looked into promises and filters though have not been successful with those either. Keep getting errors.
var array = element.all(by.repeater('m in members'));
array.filter(function (guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (selected) {
selected.element(by.className('trash-button')).click()
}
All I would like to do is click on the corresponding trash when looking for a specific member list!
Any help would be much appreciated.
EDIT: suggested I use xpath once I find the correct element, which is fine, the problem is I cannot get the filter function's promise to let me use element(by.xpath...)
If I try:
var array = element.all(by.repeater('m in members'));
array.filter(function (guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (selected) {
selected.element(by.xpath('following-sibling::div/div')).click()
}
I get the error:
Failed: Object has no method 'element'
Figured it out. Following the Protractor filter guideline in the API reference and using alecxe's xpath recommendation, this code will click on any element you find after filtering it.
element.all(by.className('col-name')).filter(function (member, index) {
return member.getText().then(function (text) {
return member === 'John';
});
}).then( function (elements) {
elements[0].element(by.xpath('following-sibling::div/div/')).click();
});
You can change the xpath to select different stuff incase your HTML does not look like mine.
It looks like you can avoid searching by repeater, and use by.binding directly. Once you found the value you are interested in, get the following sibling and click it:
element.all(by.binding('m.name')).then(function(elements) {
elements.filter(function(guy) {
guy.getText().then(function (text) {
return text == 'John';
})
}).then(function (m) {
m.element(by.xpath('following-sibling::div/div')).click();
});
});
Or, a pure xpath approach could be:
element(by.xpath('//li/div[#class="col-name" and .="John"]/following-sibling::div/div')).click();

Resources