Angular: How do I compile once then destroy all watches? - angularjs

Due to performance issue, I would like to be able to simply compile a template piece once, then completely remove all watches etc from it, simply use the final template for display purpose only.
I tried with $compile but as soon as I use $destroy on scope then everything including the parsed content is reversed back to default.
Note: this is regarding Angularjs 1.5, not Angular 2
-----Edit 1------
#Stepan Kasyanenko suggested I can use one-way binding. I'm actually using it but with some issues:
I have thousands of form rows I need to display and angularjs cannot handle this amount of watches, so I decided to cheat by printing a display version of these rows only. Whenever the user clicks on a row to edit then I swap it out with a real editable model.
For these display only rows I'm using one-way binding.
It would be great if I can skip the one-way binding as well since it still creates some performance issue though much less than ngModel, that's why I asked the question.
With one-way binding, it seems like for some reason, even with the same code on the different sites behavior is flaky. Sometimes the live model is updated with 1 long text when the user types something, but the display version only get the 1st text (probably because of the way one-way binding should works. The only solution I can think of is to re-compile the display row at this time?

You can use one-way binding.
For example jsfiddle:
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
$scope.oneWay = "one";
$scope.twoWay = "two";
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController">
<div> One way
<input ng-model="oneWay"> {{::oneWay}}
</div>
<div> Two way
<input ng-model="twoWay"> {{twoWay}}
</div>
</div>
</div>
UPDATE
I did some comparisons for drawing large amounts of data.
Example on jsfiddle
AngularJs v1.4.8. With one-way binding. 100k records - 7 sec on script, 7 second on render.
jQuery v2.2.3. 100k records - 8 sec on script, 6 second on render. Results may be better. It is necessary to pass a separate examination.
Native JS. 100k records - 0.3 sec on script, 6 second on render.
As you can see the fastest way - the Native JS.
angular.module('ExampleApp', [])
.controller('ExampleController', function() {
var vm = this;
vm.countRow = 100000;
vm.arrayAngular = [];
vm.startGenerateAngular = function() {
vm.arrayAngular = [];
for (var i = 0; i < vm.countRow; i++) {
vm.arrayAngular.push(i);
}
}
});
function startGenerateJQuery() {
var count = $("#countRow").val() * 1;
var $content = $("#contentJQuery");
$content.html("");
for (var i = 0; i < count; i++) {
var divParent = $('<div>');
var divChild = $('<div>');
divChild.text(i);
divParent.append(divChild);
$content.append(divParent);
}
}
function startGenerateNative() {
var count = $("#countRow").val() * 1;
var content = document.querySelector("#contentNative");
content.innerHTML = "";
for (var i = 0; i < count; i++) {
var divParent = document.createElement('div');
var divChild = document.createElement('div');
divChild.innerText = i;
divParent.appendChild(divChild);
content.appendChild(divParent);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="ExampleApp">
<div ng-controller="ExampleController as vm">
<input id="countRow" ng-model="vm.countRow">
<div>Generate angular: 100k records - 7 sec on script, 7 second on render
<br>
<input type="button" ng-click="vm.startGenerateAngular()" value="Go">
</div>
<div>Generate jQuery: 100k records - 8 sec on script, 6 second on render
<br>
<input type="button" onclick="startGenerateJQuery()" value="Go">
</div>
<div>Generate Native: 100k records - 0.3 sec on script, 6 second on render
<br>
<input type="button" onclick="startGenerateNative()" value="Go">
</div>
<div ng-repeat="item in vm.arrayAngular">
<div>{{::item}}</div>
</div>
<div id="contentJQuery">
</div>
<div id="contentNative">
</div>
</div>
</div>

Related

Improve performance of ng-repeat with 1600 items. Causing page load delay and performance issues

I have a UI layout with perfect-scrollbar to render a list of items. There are 1600 items which I need to display (without limiting the number of items displayed with any pagination) within the scrollable section so that user can scroll all the items at once (this is a requirement for me and I have less control over this).
The angular template rendering this view is below:
<my-scrollable-section>
<div
ng-class="myCtrl.itemId == item.itemId ? 'item-active-background' : ''"
ng-click="myCtrl.itemClickHandler(item)"
ng-repeat="item in myCtrl.items | filter:myCtrl.search track by item.itemId">
<span>{{item.name}}</span>
<div ng-repeat="(key, value) in ::item.models">
<span>{{::value}}</span>
</div>
<div ng-repeat="(key, value) in ::item.frameworks">
<span>{{::value}}</span>
</div>
</div>
</my-scrollable-section>
The filter in this repeat is linked to a search bar just above this view to narrow down the items being displayed.
The problem now is:
The page does not load instantaneously and freezes for 5-8 seconds. The number of watchers is not the cause for this, as I tried one-time bindings to bring the watcher count below 1500.
Once the page has loaded, the scroll is very slow and does not seem user-friendly at all.
I tried suggesting a pagination to limit the number of items being rendered at a time, but as mentioned earlier, I have little control over the requirements and it's required that all items be present on the scrollable list.
Can these load and performance issues be fixed with angular? Please do not suggest infinite-scroll as even if we use an infinite scroll, in the end, once all items are on the page, the UI will again become slow.
// Try with, on scroll call function & update renderLimit value.
check example here - plunker demo
// set initial limit to say 30.
$scope.renderLimit = 30;
// bind this function with directive.
$scope.updateLimit = function(value){
if(value == 'bottom'){
$scope.contValue += 1;
$scope.renderLimit += 30;
}
};
// directive will be
// custome directive for scrollHandler
app.directive('scrollHandler', function(){
return{
scope: {
scrollHandler: '&',
dataChange:'='
},
link:function(scope,element){
scope.$watch(function(){return scope.dataChange;},function(){
if(element[0].scrollTop > (element[0].scrollHeight - element[0].clientHeight - 50))
element[0].scrollTop=(element[0].scrollHeight - element[0].clientHeight - 50);
});
element.bind('scroll',function(){
var scrPosition = element[0].scrollTop;
if(scrPosition === 0)
scrPosition = "top";
else if(scrPosition === (element[0].scrollHeight - element[0].clientHeight))
scrPosition = "bottom";
scope.$apply(function() {
scope.scrollHandler()(scrPosition);
});
});
},
restrict:"A"
};
});
HTML::
<div scroll-handler="myCtrl.updateLimit" data-change="contValue">
<div
ng-class="myCtrl.itemId == item.itemId ? 'item-active-background' : ''"
ng-click="myCtrl.itemClickHandler(item)"
ng-repeat="item in myCtrl.items| limitTo:renderLimit | filter:myCtrl.search track by item.itemId">
// item contents...
</div>
</div>
Have you looked into vs-repeat?
I've been using this api to handle large number of items to be repeated. And i haven't encountered any problems.
Just a simple:
<div vs-repeat>
<div ng-repeat="item in someArray">
<!-- content -->
</div>
</div>
would solve your problem.

AngularJS ng-repeat update does not apply when object keys stay the same?

I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.

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>

angularjs - resetting li after ngrepeat

I want to flow different data through a user clickable ul but I can't reset the state of the li's which have the isactive style set. Stripping down to bare minimum to demonstrate the input box takes two numbers separated by '-', the first is the number of clickable boxes, the second is the number of unclickable boxes at the beginning.
Note when new input is sent the li's that are currently active remain active. I want to reset the li's to inactive. [ note: trying to do this without jQuery to learn "The Angular Way". I have a pure jQuery version of this ]. angular.copy has not worked (though that could be ignorance)
I'm starting to think this might have to go but I'm keeping the graphic representation exclusively in the .html:
html
<div ng-controller="BoxScreen">
<input type="text" ng-model="inbox" />
<button ng-click="getBox()" /></button>
<div>
<br />
<h2>{{dys}}, {{dst}}</h2>
<div>
<ul class="smallbox">
<li data-ng-repeat="s in skip"> </li>
<li data-ng-repeat="d in ar" ng-class="{'button': !isActive, 'button active': isActive}" ng-init="isActive = false" ng-click="isActive = !isActive; clickMe(d)">{{d}}</li>
</ul>
</div>
</div>
</div>
javascript
angular.module('myApp', [])
.controller('BoxScreen', ['$scope', function($scope) {
$scope.getBox = function() {
indat = $scope.inbox.split('-');
$scope.dys = indat[0];
$scope.dst = indat[1];
$scope.ar = [];
$scope.skip = [];
for(var s=0; s < $scope.dst; s++) {
$scope.skip.push(s);
}
for(var d=1; d <= $scope.dys; d++) {
$scope.ar.push(d);
}
}
$scope.clickMe = function(did) {
//
}
}]);
I believe your problem is related to ng-repeat creating new child scopes for the child elements it attaches to the DOM. When you expand the list with new elements, ng-repeat doesn't actually destroy the old elements (as long as they're unchanged, as is true in your case), but reuse them. See more here.
The way you have designed your structures on the scope seems very messy to me. A better approach is to create all the data beforehand, and not introduce all the logic in the HTML.
Example:
<li data-ng-repeat="d in ar.items" ng-class="{'button': !d.isActive, 'button active': d.isActive}" ng-click="ar.toggle(d)">{{d.text}}</li>
where ar here is an object:
$scope.ar = {
items: [
{
text: '1',
isActive: false
},
more items...
],
toggle: function(d) {
d.isActive = !d.isActive;
}
}
This way you have access to the data in other places as well, and not some hidden away variables set on the child scope.

Angular.js ng-repeat: opening/closing elements after x iterations

I have a simple ng-repeat going on:
<div class="row-fluid">
<div class="span4" ng-repeat="piece in clothes | filter:query">
<img ng-src="{{piece.mainImage[0]}}" class="thumbImg" />
{{piece.name}}
</div>
</div>
After 3 repeats, I'd like to stop the repeat, add in a closing <div> and open a new .row-fluid (to start a new line), then re-start the loop where I left off, inserting the tags each 3rd time.
The docs for Angular are really hard to traverse, making it difficult to work out how to do this.
You could create a filter for array partitioning.
(If you can use some library, you may be able to get something shorter & more efficient.)
app.filter('partition', function() {
var part = function(arr, size) {
if ( 0 === arr.length ) return [];
return [ arr.slice( 0, size ) ].concat( part( arr.slice( size ), size) );
};
return part;
});
You can use it like:
<div ng-repeat="rows in list | partition:3">
<div class="row" ng-repeat="item in rows">
<div class="span4">{{ item }}</div>
</div>
</div>
ngRepeat will keep re-evaluating the filter infinitely because it gets different values every time it's ran.
This terrible code seems to work in the general case. If you have identical arrays things could get hairy. We need to be able to compare arrays for equality, but that's a difficult problem. I took the easy/flimsy way out and just used stringify(). Could not get angular.equals() to work for me; I think it probably just works on shallow objects.
app.filter('partition', function($cacheFactory) {
var arrayCache = $cacheFactory('partition')
return function(arr, size) {
var parts = [], cachedParts,
jsonArr = JSON.stringify(arr);
for (var i=0; i < arr.length; i += size) {
parts.push(arr.slice(i, i + size));
}
cachedParts = arrayCache.get(jsonArr);
if (JSON.stringify(cachedParts) === JSON.stringify(parts)) {
return cachedParts;
}
arrayCache.put(jsonArr, parts);
return parts;
};
});

Resources