Unable to reduce the number of watchers in ng-repeat - angularjs

In a performance purpose, i wanted to remove the double data-binding (so, the associated watchers) from my ng-repeat.
It loads 30 items, and those data are static once loaded, so no need for double data binding.
The thing is that the watcher amount remains the same on that page, no matter how i do it.
Let say :
<div ng-repeat='stuff in stuffs'>
// nothing in here
</div>
Watchers amount is 211 (there is other binding on that page, not only ng-repeat)
<div ng-repeat='stuff in ::stuffs'>
// nothing in here
</div>
Watchers amount is still 211 ( it should be 210 if i understand it right), but wait :
<div ng-repeat='stuff in ::stuffs'>
{{stuff.id}}
</div>
Watchers amount is now 241 (well ok, 211 watchers + 30 stuffs * 1 watcher = 241 watchers)
<div ng-repeat='stuff in ::stuffs'>
{{::stuff.id}}
</div>
Watchers amount is still 241 !!! Is :: not supposed to remove associated watcher ??
<div ng-repeat='stuff in ::stuffs'>
{{stuff.id}} {{stuff.name}} {{stuff.desc}}
</div>
Still 241...
Those exemples has really been made in my app, so those numbers are real too.
The real ng-repeat is far more complex than the exemple one here, and i reach ~1500 watchers on my page. If i delete its content ( like in the exemple), i fall down to ~200 watchers. So how can i optimize it ? Why :: does't seem to work ?
Thank you to enlighten me...

It's hard to figure out what's the exact problem in your specific case, maybe it makes sense to provide an isolated example, so that other guys can help.
The result might depend on how you count the watchers. I took solution from here.
Here is a Plunker example working as expected (add or remove :: in ng-repeat):
HTML
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#1.5.6" data-semver="1.5.6" src="https://code.angularjs.org/1.5.6/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-controller="mainCtrl">
<div>{{name}}</div>
<ul>
<li ng-repeat="item in ::items">{{::item.id}} - {{::item.name}}</li>
</ul>
<button id="watchersCountBtn">Show watchers count</button>
<div id="watchersCountLog"></div>
</body>
</html>
JavaScript
angular
.module('app', [])
.controller('mainCtrl', function($scope) {
$scope.name = 'Hello World';
$scope.items = [
{ id: 1, name: 'product 1' },
{ id: 2, name: 'product 2' },
{ id: 3, name: 'product 3' },
{ id: 4, name: 'product 4' },
{ id: 5, name: 'product 5' },
{ id: 6, name: 'product 6' },
{ id: 7, name: 'product 7' },
{ id: 8, name: 'product 8' },
{ id: 9, name: 'product 9' },
{ id: 10, name: 'product 10' }
];
});
function getWatchers(root) {
root = angular.element(root || document.documentElement);
var watcherCount = 0;
function getElemWatchers(element) {
var isolateWatchers = getWatchersFromScope(element.data().$isolateScope);
var scopeWatchers = getWatchersFromScope(element.data().$scope);
var watchers = scopeWatchers.concat(isolateWatchers);
angular.forEach(element.children(), function (childElement) {
watchers = watchers.concat(getElemWatchers(angular.element(childElement)));
});
return watchers;
}
function getWatchersFromScope(scope) {
if (scope) {
return scope.$$watchers || [];
} else {
return [];
}
}
return getElemWatchers(root);
}
window.onload = function() {
var btn = document.getElementById('watchersCountBtn');
var log = document.getElementById('watchersCountLog');
window.addEventListener('click', function() {
log.innerText = getWatchers().length;
});
};
Hope this helps.

Related

Return array of objects from Handlebars Helper

I'm trying to write a helper that will return an array of objects that can then be looped through. Here's what I have now:
Handlebars.registerHelper('testHelper', () => {
return [
{ slug: 'Test', title: 'This is it!' },
{ slug: 'Test 2', title: 'This is the second it!' },
];
});
Using it like:
{{#entries this}}
{{title}}
{{/entries}}
And I'm receiving [object, Object] for each object in the array instead of the individual values. Please help :)
Thanks!
The way helpers in Handlebars work is a bit tricky. Instead of passing data from the helper to the main template body, you pass the portion of the template body related to the helper to the helper.
So, for example, when you do this:
{{#entries this}}
{{title}}
{{/entries}}
You are providing two things to the entries helper:
1) the current context (this)
2) some template logic to apply
Here's how the helper gets these items:
Handlebars.registerHelper('entries', (data, options) => {
// data is whatever was provided as a parameter from caller
// options is an object provided by handlebars that includes a function 'fn'
// that we can invoke to apply the template enclosed between
// #entries and /entries from the main template
:
:
});
So, to do what you want to do:
Handlebars.registerHelper('testHelper', (ignore, opt) => {
var data = [
{ slug: 'Test', title: 'This is it!' },
{ slug: 'Test 2', title: 'This is the second it!' },
];
var results = '';
data.forEach( (item) => {
results += opt.fn(item);
});
return results;
});
The opt.fn(item) applies this portion of template:
{{title}}
and the idea is to create a string (a portion of your html) that is then returned and placed into the string being formulated by your main template.
Here's a sample to show this working.
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.js"></script>
</head>
<body>
<script id="t" type="text/x-handlebars">
{{#testHelper this}}
{{title}}
{{/testHelper}}
</script>
<script>
Handlebars.registerHelper('testHelper', (ignore, opt) => {
var data = [
{ slug: 'Test', title: 'This is it!' },
{ slug: 'Test 2', title: 'This is the second it!' },
];
var results = '';
data.forEach((item) => {
results += opt.fn(item);
});
return results;
});
var t = Handlebars.compile($('#t').html());
$('body').append(t({}));
</script>
</body>
</html>
Let me also echo what others have been trying to tell you. It doesn't make a lot of sense to try to populate data within your templates. This should be passed as context for your templates to act on. Otherwise, you are mixing your business logic with your template logic (view) and this complicates things needlessly.
Here's a simple change you can make in the same snippet, passing the data to your templates:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.js"></script>
</head>
<body>
<script id="t" type="text/x-handlebars">
{{#testHelper this}}
{{title}}
{{/testHelper}}
</script>
<script>
Handlebars.registerHelper('testHelper', (ignore, opt) => {
var results = '';
data.forEach((item) => {
results += opt.fn(item);
});
return results;
});
var data = [
{ slug: 'Test', title: 'This is it!' },
{ slug: 'Test 2', title: 'This is the second it!' },
];
var t = Handlebars.compile($('#t').html());
$('body').append(t(data));
</script>
</body>
</html>
This way you can retrieve your data in your javascript and keep the templates for what they were intended - formulating html.

issue with $filter in angularjs not working with exact search

I am using angularjs $filter to filter data based upon Id field.
Its searching for partial of Id .
Eg. I have array of 3 objects and in all three objects there is Id field having values 4105,41,4159 respectively.
Now when I use filter to filter data based upon Id = 41 . Its returning 1st object which is having value 4105.
I am using filter as below
var filteredData = $filter('filter')($scope.gridUserData.data, { Id: userid });
It should return object with value 41 as that what I filter for?
Try adding strict comparison to your filter:
// Setting the last parameter as 'true' triggers strict comparison. In other
// words, filtered elements must have their 'Id' index match the input value
// precisely.
var filteredData =
$filter('filter')(
$scope.gridUserData.data,
{ Id: userid },
true); // <- Enable strict comparison
You shoud add which comparator u want exact or any.
$filter('filter')(array, expression, comparator, anyPropertyKey)
https://docs.angularjs.org/api/ng/filter/filter
Please look into the solution. Link to solution
<!DOCTYPE html>
<html>
<head>
<title>www.W3docs.com</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
</head>
<body ng-app="MyModule" ng-controller="MyController">
<input type='text' ng-model="filterId" name='filterGender' />
<ul>
<li ng-repeat="user in users | id:filterId" >{{user.name}}</li>
</ul>
<script>
angular.module('MyModule', [])
.controller('MyController', function($scope){
$scope.users = [
{name: 'Mike', gender: 'male', id: 23},
{name: 'Jenifer', gender: 'female', id: 32},
{name: 'Tom', gender: 'male', id: 14},
{name: 'Hayk', gender: 'male', id: 14},
{name: 'Eliana', gender: 'female', id: 28}
];
})
.filter('id', function(){
return function(users, id){
if(!id){
return users;
}
var arr = [];
angular.forEach(users, function(v){
if(v.id== id){
arr.push(v);
}
})
return arr;
}
})
</script>
</body>
</html>

Angularjs - Enumerate objects in template

I have two services, Location and Category.
Each location can be connected to one or more locations.. Location's category is store as array of categories id's, for example:
Locations: [{id: 1,
name: "NJ",
locations: [0, 2, 3]
},{
id: 2,
name: "NY",
location: [0, 2]
}]
Categories: [{
id: 0,
name: "Cities"
}, {
id: 2,
name: "Canyons"
}, {
id: 3,
name: "Work"
}]
Now, I want in my template to enumerate the categories of each location by it's name with a comma like:
NJ categories: Cities, Canyons, Work.
NY categories: Cities, Work.
My code inject the two services (Location and Category) into the controller and each of the services have "getAll()" function that return array of all it's objects..
What is the right way to do it?
Maybe I can put it on directive?
Thanks :)
How about this? It's a simple implementation (all in one HTML, as I don't know what you're application components look like) using a directive. It works, there's definitely room for improvement though.. But it should be enough to get the hang of it.
Please note that the services you mentioned are not implemented, I just expose the data on the controller and pass it to the directive. You could do the same with the data from your service or inject the services in your directive:
<html>
<head>
<script type="text/javascript" src="http://code.angularjs.org/1.4.6/angular.js"></script>
<script type="text/javascript">
var geoApp = angular.module("geography", []);
geoApp.controller("geographyController", function () {
this.locations = [
{
id: 1,
name: "NJ",
categories: [0, 2, 3]
}, {
id: 2,
name: "NY",
categories: [0, 2]
}
];
this.categories =
[
{
id: 0,
name: "Cities"
}, {
id: 2,
name: "Canyons"
}, {
id: 3,
name: "Work"
}
];
});
geoApp.directive("categoriesFor", function () {
var link = function (scope) {
var getCategoryName = function (id) {
for (var i = 0; i < scope.categories.length; i++) {
var category = scope.categories[i];
if (category.id === id) {
return category.name;
}
}
return "not available (" + id + ")";
};
scope.getCategoriesForLocation = function () {
var availableCategories = [];
angular.forEach(scope.location.categories, function (categoryId) {
availableCategories.push(getCategoryName(categoryId));
});
return availableCategories.join(scope.splitChar);
}
};
return {
restrict: "A",
scope: {
location: "=categoriesFor",
categories: "=",
splitChar: "="
},
template: "{{location.name}} categories: {{getCategoriesForLocation()}}",
link: link
};
});
</script>
</head>
<body data-ng-app="geography" data-ng-controller="geographyController as geo">
<ul>
<li data-ng-repeat="location in geo.locations">
<span data-categories-for="location"
data-categories="geo.categories"
data-split-char="', '"></span>
</li>
</ul>
</body>
</html>
One improvement might be in changing your data structure, i.e. making objects (instead of arrays) where the key is the id. That would make it easier to access them.
And, as always with Angular, there are several different approaches. It always depends and your needs (i.e. re-usability, etc.).
What is the right way to do it?
You could use ngRepeat directive + custom filter.
<span ng-repeat="city in cities">{{ categories | CategoriesInCity:city }}</span>
Filter :
myApp.filter('CategoriesInCity', function(){
return function(categories , city){
// build categoriesStringFound
return city+'categories'+categoriesStringFound;
}
})
I'd say the easiest way to achieve this is to build up a third array with the Strings that you want to display and then use an ng-repeat to display them on your page.
So in your controller, you'd have something like (note the map and filter functions will only work in modern browsers):
var locs = Locations.getAll()
var cats = Categories.getAll()
$scope.display = locs.map(function(loc) {
return loc.name + ' categories: ' + loc.locations.map(function(id) {
return cats.filter(function(cat) {
return id === cat.id;
})[0].name;
});
});
And on your page, you'd have:
<ul>
<li ng-repeat="line in display">{{line}}</li>
</ul>

Summing all items contained in the Controller

I'm just beginning with AngularJS. I'd need to upgrade this shopping cart example from AngularJS' book so that the total of all (items.price*item.quantity) is displayed at the bottom of the page. Which is the recommended way to achieve it ?
<HTML ng-app>
<head>
<script src="js/angular.min.js"></script>
<script>
function CartController($scope) {
$scope.items = [{
title: 'Paint pots',
quantity: 8,
price: 3.95
},
{
title: 'Pebbles',
quantity: 5,
price: 6.95
}];
$scope.remove = function(index) {
$scope.items.splice(index, 1);
}
}
</script>
</head>
<body ng-controller='CartController'>
<div ng-repeat='item in items'>
<span>{{item.title}}</span>
<input ng-model='item.quantity'>
<span>{{item.price}}</span>
<span>{{item.price * item.quantity}}</span>
<button ng-click="remove($index)">Remove</button>
</div>
</body>
</html>
Thanks!
Here is a plunker:
Create a function which iterates all items like so:
$scope.sum = function(){
return $scope.items.reduce(function(p,item){
return p + (item.quantity * item.price)
},0)
}
Markup:
<span>Sum : {{ sum() }}</span>
read more about reduce method
I think this would be a good candidate for a 'sum' filter. The bonus of writing a sum filter is that it's generic and you can use it anywhere in your app.
The simpliest implementation would take as input an array of objects and a string parameter that is the property on each object to sum.
angular.module('app')
.filter('sum', function () {
return function (input, propertyToSum) {
var sum = 0;
angular.forEach(input, function (value, key) {
sum = sum + value [propertyToSum];
}
return sum;
}
});
Then use it like this:
<span>Sum: {{ items | sum:'price' }}</span>
Not 100% on the syntax here. Build it in fiddler and let me know if it doesn't comes through.
There's a whole host of assumptions being made here that tests and whatnot should cover. But that's the basic idea.
You could also use a utility library like underscore in conjunction with this filter, which provides plenty of useful operations on collections.
Have a total property on the scope with a watch on the item collection:
$scope.total = 0;
$scope.$watch( 'items', updateTotal, true );
function updateTotal(){
$scope.total = 0;
angular.forEach( $scope.items, function(item){
$scope.total += (item.price * item.quantity);
});
}
And in the view:
<p>Total {{total}}</p>

AngularJS - Is there an easy way to set a variable on "sibling" scopes?

I have this problem where I am trying to make a click on a div hide all the other divs of the same "kind". Basically I'd have to, from a child scope set the variable on all other "sibling" scopes.
To illustrate this, I have created the following:
HTML
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-repeat="model in models" ng-controller="MyChildCtrl">
<a ng-click="toggleVisibility()">toggle {{ model.name }} {{ visibility }}</a>
<div ng-show="visibility">
{{ model.name }}
</div>
</div>
</div>
</div>​
JavaScript
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
console.debug('scope');
$scope.models = [
{ name: 'Felipe', age: 30 },
{ name: 'Fernanda', age: 28 },
{ name: 'Anderson', age: 18 }
];
}
function MyChildCtrl($scope) {
$scope.visibility = false;
$scope.toggleVisibility = function() {
$scope.visibility = !$scope.visibility;
}
}
JSFiddle: http://jsfiddle.net/fcoury/sxAxh/4/
I'd like that, every time I show one of the divs, that all other divs would close, except the clicked one.
Any ideas?
#kolrie while your approach works I would suggest a different solution which doesn't require any changes to the model. The basic idea is to keep a reference to a selected item and calculate viability by comparing a current item (inside ng-repeat) with a selected one.
Using this solution the toggle function would become:
$scope.toggleVisibility = function(model) {
$scope.selected = model;
};
and calculating visibility is as simple as:
$scope.isVisible = function(model) {
return $scope.selected === model;
};
Finally the relevant part of the markup is to be modified as follows:
<div ng-controller="MyCtrl">
<div ng-repeat="model in models">
<a ng-click="toggleVisibility(model)">toggle {{ model.name }} {{ isVisible(model) }}</a>
<div ng-show="isVisible(model)">
{{ model.name }}
</div>
</div>
</div>
Here is a complete jsFiddle: http://jsfiddle.net/XfsPp/
In this solution you can keep your model untouched (important if you want to persist it back easily) and have AngularJS do all the heavy-lifting.
OK, I have added a visible attribute to the model, and I managed to get this done:
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
console.debug('scope');
$scope.models = [
{ name: 'Felipe', age: 30, visible: false },
{ name: 'Fernanda', age: 28, visible: false },
{ name: 'Anderson', age: 18, visible: false }
];
}
function MyChildCtrl($scope) {
$scope.toggleVisibility = function() {
angular.forEach($scope.models, function(model) {
model.visible = false;
});
$scope.model.visible = true;
}
}
Live here: http://jsfiddle.net/fcoury/sxAxh/5/
Is this the most efficient way? Do you think it's a good practice if I inject this visible attribute into my model data after getting it via AJAX?

Resources