Angular - Nested $scope will produce nested $watchers? - javascript

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
}
}
}

Related

Cannot initialize an element within ng-include's template

I'm building an SPA that can be described by the following image
Explanation:
- An object items exists in the controller of the component2.
- A user has the ability to manage this object by using the 4 different actions (buttons on the component1)
- These actions (for the sake of simplicity) can refer to a different kind of plot. i.e When Action 1 is pressed, data inside items object can be plotted as a pie chart, when action 2 is chosen will be plotted as a histogram and so on.
- These plots are going to appear in within the various ng-include templates.
In general, and so far, the way I approached it, is to use a service (injected into both of the components). When action buttons are clicked, an event is broadcasted from the service and when this event reaches the component 2, its controller sets the corresponding variable (action-1-on, action-2-on, action-3-on, etc) into true and the contents of the ng-include appear.
The problem I'm facing lies exactly in the last step. Let's say for example that inside of one of these templates (e.g action1.html) I have a range element and I want to initialize it as a noUiSlider element. To do that, on the $onInit function of the component2 controller, I add the following code:
var slider = document.getElementById('test-slider');
noUiSlider.create(slider, {
start: [20],
step: 1,
orientation: 'horizontal',
range: {
'min': 0,
'max': 100
},
format: wNumb({
decimals: 0
})
});
What I'm getting back is the error:
TypeError: Cannot read property 'nodeName' of null
and from that, I assume that javascript cannot find the element with the id #test-slider
So, what I want to ask before solving this error, is if this approach (with service and the two components) is reasonable and "angularish" or there is a more straight and clear way to do this (maybe ui-route?). I prefer to work with the more angular way than try to solve this error. If the way I'm approaching is ok, how am I gonna make that element initialized from the $onInit function? Should I add some kind of delay?
Any ideas and opinions are welcome.

AngularJS - Pagination not rendering on $scope.Watch change (CodePen included)

Question Background:
I'm learning AngularJS. I have created a simple app that takes in 3 form inputs, and on submission of this form a paginated list should be rendered. I have a dependancy on UI-Bootstrap. This is a Plnkr showing the pagination example I'm attempting to incorporate into my app: http://plnkr.co/edit/81fPZxpnOQnIHQgp957q?p=preview
The Issue & Demo:
This is the link to my code so far on CodePen:
http://codepen.io/daveharris/pen/NNMQyy
I have a list of 1000 items populating on my SearchingService model that is injected and shared between the two controllers.
I cannot get the paginated ng-repeat to populate when the searchingService.searchList list on the SearchingService model is changed,
It is as if the $scope.Watch is not being triggered.
Any help getting to the bottom of this will be much appreciated.
Change line 31 from $watch to $watchCollection. Since your service is mutating the array (via .push), the reference stays the same, so the $watch won't trigger. $watchCollection will watch references to the array members rather than the reference to the array itself.
As bchemy pointed out, $watch should be changed to $watchCollection. But this change won't be enough to have your app working correctly with your current code, since the pagination looks broken.
You should also add another watcher to control the pagination as the one in the example that you want to replicate:
$scope.$watch('currentPage + numPerPage', function() {
var begin = (($scope.currentPage - 1) * $scope.numPerPage)
, end = begin + $scope.numPerPage;
$scope.filteredItems = $scope.searchingService.searchList.slice(begin, end);
});
Also $scope.items array was getting duplicate searchLists in it and would mess the pagination length.
I have created this Plunker where you can see your example fully working.

TreeView with Angular Directive

I am relatively newer with Angular so please excuse my mistakes, if any. I have to modify and use this treeview directive from NgModules. It's code looks promising but I have to modify it a little bit to add the functionality of add/ delete or modify items.
Plunker
I was previously using jQuery to create my treeview and have accomplished all the relevant tasks. But since I decided to shift to Angular, I have to do it the angular way.
So as far as I could understand this directive uses nested recursive directive to create the tree, which is an efficient way to do it. It works fine, but I would have some 3000-4000 items to show in my treeview. So when I do that with this code it slows down the screen and consumes much memory due to the many watch expressions (5-7 per item).
Plunker
I have tried one time binding in ng-repeat with :: notation. That has helped a lot but now I couldn't implement my add or update methods. Is there any way I could accomplish this?
<li data-ng-repeat="node in ::' + treeModel + '">
https://plnkr.co/edit/KwnvyslibWd1dmIXxBYU?p=preview
You already had the selected node in the scope, so simply push the new node into it's children array.
$scope.AddNode = function(NewNode)
{
NewNode = {"roleName" : NewNode , "roleId" : "role11", "children" : []};
$scope.mytree.currentNode.children.push(NewNode);
};
edit: As name suggest - it is one time binding, so adding/removing won't work here. I think it's only normal that browser get's a bit stuck when trying to display such amount of data once. Instead, you could add nodes by bits, say 20 a step. Or you could check out other libraries as well. This one gives you the option to display array of nodes collapsed at first.

AngularJS - Reset array on cancel has flickering effect when scope refreshes

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.

AngularJS for loop not waiting for modifications to $scope

I've been wracking my brain over this for a few days, so I figured it's time to finally ask somebody who actually knows what they're doing. ;)
I'm building an AngularJS app and getting an odd behavior when I'm iterating through a couple of my for loops. I'm trying to find the 'best' option to add to a list given a few possibilities, and so here's my general process for how I'd like to get it done:
The Idea:
1) add each possibility to the desination list, say $scope.list1
2) calculate the 'score' based on the state of the lists
3) remove the possibility from $scope.list1
4) repeat for all possibilities.
Here's the basic idea of what I'm running in my controller:
$scope.getSuggestions = function(){
//for each possibility to consider:
for(var i = 0; i < possibilities.length; i++){
//add the possibility to the scope as per usual
$scope.list1.push(possibilities[i]);
//calculate the score given the state of the scope
$scope.calculateScore();
//save the score in a new object on the scope
$scope.results.push({"option": possibilities[i],
"score": $scope.score
});
//remove the possibility to reset the scope back to its original state
$scope.list1.pop();
}
};
The Problem:
It seems as if my for loop is actually moving on to further iterations before the code has finished executing. My $scope.calculateScore() function may take a bit of time to run, and it seems like it's being ignored by the for loop. I'm not doing anything obviously asynchronous in that function; it's mostly other for loops based on the state of the $scope and some math to get a 'score'.
My question:
Why does Angular think it's okay to continue blazing through a for loop when the code inside hasn't finished executing? I see how it could make sense in certain cases, but in this one it's vital that everything happens in order, after the previous functions have finished. Do you all have any suggestions on how to make this work?
Note: You can see the code in its entirety by viewing the source at http://www.PlatTheLeague.com
The actual code for calculateScore() is in scripts/controllers/teamBuilderCtrl.js, where the function is called $scope.populateGamePredictions(). My initial tries at the suggestions are in the same file, called $scope.getChampSuggestions. To see the problem, drag an item from the list on the left to the box that says 'Their Top' and then click on the 'Champion Suggestions' tab. That tab should be populated with the results of $scope.results listed above in the pseudo-code, but it seems that the teamScore on my scope object isn't being set properly.
Cheers!
Maybe you have some logics that change the internal states of $scope from outside of AngularJS, maybe from some kinds of events from DOM, ajax callbacks, setTimeout ... In that case, $apply method can help. You can have more references of $apply from here: https://github.com/angular/angular.js/wiki/When-to-use-$scope.$apply()
As it turns out, The above idea works in the typical synchronous manner, as I've proved to myself in this plunker: http://plnkr.co/edit/9spVbf5JWx9Hnq53sHSV?p=preview
My issue arose from the fact that the data model being worked on by my calculateScore() function was not formatted exactly how I thought it would be (my own fault, thanks to the semi-complicated nature of the app...) which meant that the scores weren't being updated properly in the first place, though the code was running as expected.

Categories