I'm very new to Angular (not Javascript), so I apologize if I don't use the correct terms/procedure.
I have a model like so:
BuyingGroups: {
availableBuyingGroups: [
obj1: {
ID: XX,
Title: XX
},
....
],
affiliatedBuyingGroups: [
obj1: {
ID: XX,
Title: XX
},
....
]
}
There are (2) panes (think of them as list boxes). A user can add an object from the left pane to the right, and vice versa. Basically I'm just moving objects from one group to the other.
On load, I take those (2) lists, and use angular.copy to copy those to another static list, so as not to affect the original (I read that angular uses the context if you don't use angular.copy).
On reset (cancel button), I reset the original lists back to the copies (to keep the original state). The problem is on the cancel button event, the list seems to "duplicate" itself for about .3 seconds, which flickers on the pane, then reverts back to it's original state (on page load).
I've attempted to clear out the array, set a timeout, etc, but nothing seems to have an effect. Is there (or should there be) a more efficient, better way of doing this? Perhaps I do not fully understand how angular binds to the $scope on changes?
self = $scope (fyi)
self.companyBuyingGroups = response.data.BuyingGroups;
self.staticCompanyBuyingGroupsModel = angular.copy(self.companyBuyingGroups);
self.staticCompanyAffiliationsModel = angular.copy(self.companyAffiliations);
HTML
<!-- affiliated instance -->
<li ng-repeat="group in companyBuyingGroups.affiliatedBuyingGroups">
.....
</li>
Reset button function
self.resetBuyingGroupsForm = function () {
self.companyBuyingGroups.affiliatedBuyingGroups = [];
self.companyBuyingGroups.availableBuyingGroups = [];
//setTimeout(function () {
self.companyBuyingGroups.affiliatedBuyingGroups = angular.copy(self.staticCompanyBuyingGroupsModel.affiliatedBuyingGroups);
self.companyBuyingGroups.availableBuyingGroups = angular.copy(self.staticCompanyBuyingGroupsModel.availableBuyingGroups);
//self.companyBuyingGroups = angular.copy(self.staticCompanyBuyingGroupsModel);
//}, 50)
}
EDIT
I have tried to clear the arrays prior to reset, along w/ not clearing them. Also my arrays are super small (less than 50 objects).
To explain more of the "flickering", on the cancel function, the right pane seems to add the copy (instead of using the original). Meaning, if the original list had 3 items, and I added an item from the left pane (making it 4), then the cancel function was called, the right pane shows 7 items for about .3s, then reverts back to 3 items (which was the original).
You should append track by group.ID to your ng-repeat expression, this will allow AngularJS to reuse the DOM when the list resets, which will fix the issue.
When you reset your list, Angular first needs to remove every item from the DOM (since they are not in the array anymore), then add them back. Using track by group.ID will allow ngRepeat to track them by ID instead of reference, and it now knows which item to remove and which to keep.
#1
It probably blinks because you first empty the arrays.
Try commenting out the first two assignments to =[] in you Reset function, and keep only two angular.copy assignments.
#2
If you have huge lists that might take time to be rerendered, consider comparing the arrays model in controller to the initial one - to find and keep those list items that are already rendered, and add/remove only the non-corresponding ones.
Related
How it is
I have an array of objects called vm.queued_messages (vm is set to this in my controller), and vm.queued_messages is used in ng-repeat to display a list of div's.
When I make an API call which changes the underlying model in the database, I have the API call return a fresh list of queued messages, and in my controller I set the variable vm.queued_messages to that new value, that fresh list of queued messages.
vm.queued_messages = data; // data is the full list of new message objects
The problem
This "full replacement" of vm.queued_messages worked exactly as I wanted, at first. But what I didn't think about was the fact that even objects in that list which had no changes to any properties were leaving and new objects were taking their place. This made no different to the display because the new objects had identical keys and values, they were technically different objects, and thus the div's were secretly leaving and entering every time. THIS MEANS THERE ARE MANY UNWANTED .ng-enter's AND .ng-leave's OCCURRING, which came to my attention when I tried to apply an animation to these div's when they entered or left. I would expect a single div to do the .ng-leave animation on some click, but suddenly a bunch of them did!
My solution attempt
I made a function softRefreshObjectList which updates the keys and values (as well as any entirely new objects, or now absent objects) of an existing list to match those of a new list, WITHOUT REPLACING THE OBJECTS, AS TO MAINTAIN THEIR IDENTITY. I matched objects between the new list and old list by their _id field.
softRefreshObjectList: function(oldObjs, newObjs) {
var resultingObjList = [];
var oldObjsIdMap = {};
_.each(oldObjs, function(obj) {
oldObjsIdMap[obj._id] = obj;
});
_.each(newObjs, function(newObj) {
var correspondingOldObj = oldObjsIdMap[newObj._id];
if (correspondingOldObj) {
// clear out the old obj and put in the keys/values from the new obj
for (var key in correspondingOldObj) delete correspondingOldObj[key];
for (var key in newObj) correspondingOldObj[key] = newObj[key];
resultingObjList.push(correspondingOldObj);
} else {
resultingObjList.push(newObj);
};
});
return resultingObjList;
}
which works for certain things, but with other ng-repeat lists I get odd behavior, I believe because of the delete's and values of the objects being references to other controller variables. Before continuing down this rabbit hole, I want to make this post in case I'm thinking about this wrong, or there's something I'm missing.
My question
Is there a more appropriate way to handle this case, which would either make it easier to handle, or bypass my issue altogether?
Perhaps a way to signal to Angular that these objects are identified by their _id instead of their reference, so that it doesn't make them leave and enter as long as the _id doesn't change.
Or perhaps a better softRefreshObjectList function which iterates through the objects differently, if there's something fishy about how I'm doing it.
Thanks to Petr's comment, I now know about track by for ng-repeat. It's where you can specify a field in your elements that "identifies" that element, so that angular can know when that element really is leaving or entering. In my case, that field was _id, and adding track by message._id to my ng-repeat (ng-repeat="message in ctrl.queued_messages track by message._id") solved my issue perfectly.
Docs here. Search for track by.
http://plnkr.co/edit/HgjGS9LSZ0VhyBMkwy6L?p=preview
vs.unshift = function() {
vs.feedList.unshift(unshifted_item);
};
vs.push = function() {
vs.feedList.push(push_item);
};
In our real app with APIs, when I use unshift to add a new alert into the alerts feed (via websockets) the item takes forever to get added to the top of the array because of the digest cycle. It seems to iterate over every time in the array by the amount of items in the array (ie: 10x10), before it finally adds the item to the top of the array.
However if we use push the new alert is immediately added because Angular does not have to check all the other items in the array first.
This problem is hard to replicate in the plnkr example I created above, because the unshift instantly adds the item to the top of the array. However, how can one use push and then move the item that was pushed to the top of the array without causing a digest cycle?
As #Redu suggested, the splice method works without causing the Angular digest cycle to hang.
feedList.splice(0, 0, item)
I'm working with Angular and part of my page utilizes ng-repeat to display some bug tracker tickets. As part of the site, I also want to provide the ability to search tickets. I'm able to get that part working as I want, and if I'm just appending new tickets they show up fine.
However I would like to be able to, if a user searches, delete all of the currently visible ticket divs and replace them with the search results.
My initial thinking, since I have the ng-repeat set as item in tickets track by item.id, was to just set $scope.tickets equal to the new data. However, this didn't cause Angular to update the DOM.
So, I tried setting $scope.tickets equal to an empty array and then setting it equal to the new data:
$scope.$apply(function() {
$scope.tickets = [];
$scope.tickets = data;
});
Still no update to the DOM, even though console.log($scope.tickets) shows the correct objects.
I'm aware of the method of
$scope.$apply(function() {
array.splice(index, 1);
});
to remove individual elements, but I'm not sure how I would apply that removing all of the elements.
I'll try and get a Plunkr or JSBin added to the Q soon.
What would be the proper way for me to make Angular replace all of the current elements with the new elements created from the data?
try setting array.length = 0
this deletes all elements, while not removing the reference to the array, which actually seems to be the problem in your case.
but another way would be to have a additional data bag.
for example have $scope.data.tickets then you can reasign tickets as usual. important thing is, you have to reference your items using item in data.tickets
Did you test $watch?
$scope.$watch('tickets', function() {
// update data HERE
});
I'm encountering performance issues, I think due to lots of watchers in the page (more than 4000!!). The scenario is a (small, about 5) list of items in ng-repeat once, each one contains another ng-repeat for every day of week (so 7), and in each day container there are 1 or 2 input field. Each day's element has its own scope and controller and some watch at parent's properties, in order to update parent state at child changes. So a bit complex scenario...imagine an agenda view where each day as some input fields or buttons which update same property in the main scope, like "10 days selected/filled/clicked".
I started with about 5000 watchers, now reduced to about 4000 removing some filters and switching to translate-once directive insted of translate (angular-translate).
So the main question is:
How to further reduce the number of watchers?
Is every child scope inheriting the parent watchers, resulting in 7x for each watcher? If I remove child's controllers, leaving the job to the the parent (passing in the function the child item), will I decrease the number of watchers? Could this be a solution? Any help is appreciate.
In our experience that number of watchers cause no speed problems. The performance problems we have encountered in the last 8 months of development on a single big application were caused by slow third part's components.
For example, we have a page with two drag and drop trees with 14.600 watchers (because of high number of items in both trees). We experienced performance problems because of the component used, angular-ui-tree, and we reduced them opening the page with most of the tree collapsed.
We cannot change that component because it is the only one which features drag and drop between trees, but in another page where we had drag & drop between simple lists we have tried those two components: angular-dragdrop and angular-drag-and-drop-lists. The first had a lot of performance problems (with about 500 items) while the second run really really fast. In his documentation on github, section "Why another drag & drop library?" you can read why it is so fast and why the other is so slow.
So, I can speculate that third part's components bring you the real performance problems, and not the watchers.
In any case, we often write our watchers with a check like the one below to not run the code unless needed.
$scope.$watch('variableToWatch', function(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
... watcher code ...
}
Another way to reduce watchers from html is using one-time-binding.
Example:
<div ng-if="::vm.user.loggedIn"></div>
Related to performance... - One pattern i came up with is to use a private object and assign the prototype of a function for easy access. then in any function ,controllers, directives...ect you can access the prototype of other function,controllers,directives easily. instead of using watchers you can use this pattern like a event loop. instead of angular running 300+ watchers every digest cycles. using this pattern only what triggers the function call matters.
An example of this pattern
var private = {} //accesable to entire app
var app = angular.module('some-app',[])
.controller('someCtrl',['$scope',someCtrl])
.directive('someDirective',someDirective);
function someCtrl($scope){
private.someCtrl = someCtrl.prototype
someCtrl.prototype.update = function(someDate){
//do something....
//access to someCtrl arguments
$scope.something = someDate
}
}
function someDirective(){
var someCtrlProto = private.someCtrl;
return{
link:function(scope ,elm ,attr){
elm[0].addEventListener('click',fucntion(){
someCtrlProto.update(someData)
});
//or
elm[0].addEventListener('click',someCtrlProto.update) //to trigger someCtrl.update from here
}
}
}
I don't think this is strictly infinite scrolling but it was the best i could think of that compares to what i am seeing.
Anyway, we are using ng-grid to show data in a table. We have roughly 170 items (rows) to display. when we use ng-grid it creates a repeater. When i inspect this repeater from the browser its limited to 35 items and, as you scroll down the list, you start to lose the top rows from the dom and new rows are added at the bottom etc (hence why i don't think its strictly infinite scrolling as that usually just adds more rows)
Just so I'm clear there is always 35 'ng-repeat=row in rendered rows' elements in the dom no matter how far you have scrolled down.
This is great until it comes to testing. I need to get the text for every item in the list, but using element.all(by.binding('item.name')) or by.repeater or by.css doesn't help as there is only ever 35 items present on the page.
Now to my question, how can i make it so that i can grab all 170 items as an object that i can then iterate through to grab the text of and store as an array?
on other pages where we have less than 35 items iv just used the binding to create an object, then using async.js to go over each row and get text (see below for an example, it is modified extract i know it probably wouldn't work as it is, its just to give you reference)
//column data contains only 35 rows, i need all 170.
var columnData = element.all(by.binding('row.entity.name'))
, colDataTextArr = []
//prevOrderArray gets created elsewhere
, prevOrderArray = ['item1', 'item2'... 'item 169', 'item 170'];
function(columnData, colDataTextArr, prevOrderArray){
columnData.then(function(colData){
//using async to go over each row
async.eachSeries(colData, function(colDataRow, nRow){
//get the text for the current async row
colDataRow.getText().then(function(colDataText){
//add each rows text to an array
colDataTextArr.push(colDataText);
nRow()
});
}, function(err){
if(err){
console.log('Failed to process a row')
}else{
//perform the expect
return expect(colDataTextArr).toEqual(prevOrderArray);
}
});
});
}
As an aside, I am aware that iterating through 170 rows and storing the text in an array isn't very efficient so if there is a better way of doing that as well I'm open to suggestions.
I am fairly new to JavaScript and web testing so if im not making sense because I'm using wrong terminology or whatever let me know and i'll try and explain more clearly.
I think it is an overkill to test all the rows in the grid. I guess it would be sufficient to test that you get values for the first few rows and then, if you absolutely need to test all the elements, use an evaluate().
http://angular.github.io/protractor/#/api?view=ElementFinder.prototype.evaluate
Unfortunately there is no code snippet in the api page, but it would look something like this:
// Grab an element where you want to evaluate an angular expression
element(by.css('grid-selector')).evaluate('rows').then(function(rows){
// This will give you the value of the rows scope variable bound to the element.
expect(rows[169].name).toEqual('some name');
});
Let me know if it works.