Angularjs - Enumerate objects in template - arrays

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>

Related

Unable to reduce the number of watchers in ng-repeat

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.

Angular Grouping Directive starting point

I'am trying to create a grouping and filtering mechanism with several predefined filters. I have a collection of undefined rules and some predefined grouping actions, for example "relativeDate" (today, tomorrow, yesterday, this week, ...), "boolean" or . The set of actions should be expandable.
I've managed to get this working in a controller. But I want to outsource this into a directive to get this working with other object collections. The Problem is: I need to specify the template of the list dynamically.
Imagine the following collections:
$scope.memosReceived = [
{ id: 1, from: 'Henry Ford', title: 'Want your Model T?', received: '2015-05-04T12:30:00', read: true },
{ id: 2, from: 'Oliver Newton', title: 'Look at this!', received: '2015-06-15T08:00:00', read: true }
...
];
$scope.memosSent = [
{ id: 1, to: 'Henry Ford', title: 'That is an old car', sent: '2015-05-04T12:35:21', read: true },
{ id: 2, to: 'Oliver Newton', title: 'Stop Spam', sent: '2015-06-15T08:01:47', read: false }
...
];
Now the markup should be like the following:
<div ng-controller="controller">
<h2>Received</h2>
<grouped-list ng-model="memosReceived" item-var="received" grouping-options="groupingReceived">
<h2>{{ received.title }} <sub>by {{ received.from }}</h2>
</grouped-list>
<h2>Sent</h2>
<grouped-list ng-model="memosSent" item-var="sent" grouping-options="groupingSent">
<h2>{{ sent.title }} <sub>to {{ sent.to }}</h2>
</grouped-list>
</div>
Options could be like:
$scope.groupingReceived = [
{ modelKey: 'received', action: 'relativeDate', options: { [.. passed to grouping action, like value->name mapping ..] },
{ modelKey: 'read', action: 'boolean', options: { [...] } }];
$scope.groupingSent = [
{ modelKey: 'sent', action: 'relativeDate', options: { [.. passed to grouping action, like value->name mapping ..] },
{ modelKey: 'read', action: 'boolean', options: { [...] } }];
The rendered HTML should look like this "PseudoHtml":
<div ng-controller="controller">
<h2>Received</h2>
<div class="grouped-list">
<div class="filter-section">
<button ng-click="openFilters = !openFilters>Open Filters</button>
<div class="filter-options" ng-hide="!openFilters">
<h4>Group by</h4>
[selectbox given group actions] [selectbox sort ascending or descending]
<h4>Filter by</h4>
[build filters by similar to group actions given filter actions]
</div>
</div>
<div class="group">
<div class="group-header">
<h3>Yesterday</h3>
</div>
<ul class="group-list">
<li ng-repeat="received in ngModel | customFilters">
<!-- something like transclusion starts here -->
<h2>{{ received.title }} <sub>by {{ received.from }}</h2>
<!-- something like transclusion ends here -->
</li>
</ul>
</div>
<div class="group">
<div class="group-header">
<h3>Last Week</h3>
</div>
<ul class="group-list">
<li ng-repeat="received in ngModel | customFilters">
<!-- something like transclusion starts here -->
<h2>{{ received.title }} <sub>by {{ received.from }}</h2>
<!-- something like transclusion ends here -->
</li>
</ul>
</div>
</div>
<h2>Sent</h2>
<div class="grouped-list">
[... same like above ...]
</div>
</div>
I am really struggeling how to achieve this behavior, where to store the several parts of the logic (e.g. the grouping actions, the custom filters) and how to transclude this correctly.
Maybe someone can give me a good starting point for that.
You could create a custom filter and call it from the controller of your directive.
Inside of this filter you can decide which filter action should be triggered by passing parameters to the filter.
I would call it from the controller instead of the template because there you can easier chain your filters.
Please have a look at the demo below or in this jsfiddle.
During adding my code to SO I've detected a bug (not displaying the item) in my code with a newer AngularJS version. Not sure what it is but with 1.2.1 it's working.
I'll check this later. Seems like a scoping issue.
angular.module('demoApp', [])
.filter('aw-group', function($filter) {
var filterMethods = {
relativeDate: function(input, action) {
console.log('relative date called', input);
return input; // do the translation to relative date here
},
filterByNumber: function(input, action, options) {
// if you need mor parameters
return $filter('filter')(input, options.number);
},
otherFilter: {
}
};
return function(input, action, options) {
return filterMethods[action](input, action, options);
};
})
.directive('groupedList', function () {
return {
restrict: 'E',
scope: {
model: '=',
itemVar: '=',
filter: '='
},
transclude: true,
template: '<ul><li ng-repeat="item in filteredModel" ng-transclude>'+
'</li></ul>',
controller: function($scope, $filter) {
//console.log($scope.filter);
$scope.filteredModel = $filter('aw-group')($scope.model, 'filterByNumber', { number: 2 }); // passing action from $scope.filter.action as second parameter, third is an options object
}
};
})
.controller('mainController', function () {
this.data = [{
title: 'Test1',
from: 'tester1'
}, {
title: 'Test2',
from: 'tester1'
}, {
title: 'Test3',
from: 'tester1'
}, ];
this.groupingReceived = [{
modelKey: 'received',
action: 'relativeDate',
options: {},
modelKey: 'read',
action: 'boolean',
options: {}
}];
this.memosReceived = [{
id: 1,
from: 'Henry Ford',
title: 'Want your Model T?',
received: '2015-05-04T12:30:00',
read: true
}, {
id: 2,
from: 'Oliver Newton',
title: 'Look at this!',
received: '2015-06-15T08:00:00',
read: true
}];
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
<div ng-app="demoApp" ng-controller="mainController as ctrl">
<grouped-list model="ctrl.data" item-var="received" filter="ctrl.groupingReceived">
<h2>{{item.title}}<sub>{{item.from}}</sub></h2>
</grouped-list>
</div>

AngularJS how to compare similar objects in ng-repeat

I currently want to make a system that can compare products and their prices and list them.
I have found a way to filter duplicate products in AngularJS using the unique filter of AngularUI. I have two JSON files each filled with products of two different providers.
But I don't want it to remove the duplicate I want to get back a list of the similar products so someone can compare the prices and choose his favorite provider of the product.
If I run a ng-repeat with the unique filter it simply removes a duplicate. Is there a compare filter?
In order to do what you want, you'd need to write a custom filter that does the comparison and determines whether two objects are similar by your definition. Something like this. Here is a plunk:
var app = angular.module('app', [])
.filter('similar', function() {
return function(input, comparedObject) {
console.log(comparedObject);
var newArray = [];
for (i = 0; i < input.length; i++) {
if (input[i].itemType === comparedObject.itemType) {
newArray.push(input[i]);
}
}
return newArray;
}
});
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.baseObject = {
name: 'fred',
itemType: 1
};
$scope.castList = [
{
name: 'wilma',
itemType: 2
},
{
name: 'bam-bam',
itemType: 1
},
{
name: 'barney',
itemType: 1
},
{
name: 'dino',
itemType: 3
}
];
});
Then you would need something like this in the view's HTML.
<body ng-app="app">
<div ng-controller="SomeCtrl">
<div ng-repeat="person in castList | similar : baseObject">
<p>{{person.name}}</p>
</div>
</div>
</body>
Notice that the "baseObject" referred to in the filter in the view is defined in the scope from the controller.
This custom filter compares based upon the object's "itemType" property, which in my example is just a number. You'd have to re-write the filter to compare based on what you want. So what you get here is a display of only those objects that have an itemType property of 1, which is the same as the baseObject's.

angularjs - how to get in the controller the index of an item in a ng-repeat filter based on the value of one of its properties?

I use a ng-repeat in my html file to display filtered items:
<li ng-repeat="item in (filteredItems = (items | filter:query))">
{{ item.name }}
</a>
In the controller, I'd like to get the index of an item based on one of his property.
Precision: I'd like to get the index in the filtered list and not in the whole list.
Here for example, it will be the index of the item witch name is some_item_7.
var app = angular.module('myApp', []);
app.controller('MyCtrl', ['$scope',
function MyCtrl($scope) {
$scope.query = 'some';
$scope.items =
[
{ name: 'some_item_1' },
{ name: 'another_item_2' },
{ name: 'some_item_3' },
{ name: 'another_item_4' },
{ name: 'some_item_5' },
{ name: 'another_item_6' },
{ name: 'some_item_7' },
{ name: 'another_item_8' },
{ name: 'some_item_9' }
];
$scope.itemNext = function (item) {
console.log(item.name);
};
$scope.getIndexFromName = function (name) {
console.log("trying to get the index of the item with name = " + name);
}
$scope.getIndexFromName('some_item_7');
}
]);
http://plnkr.co/edit/C8gL9qV1MyonTwDENO9L?p=preview
Any idea ?
Your ng-repeat expression creates the filteredList array on your scope.
<li ng-repeat="item in (filteredItems = (items | filter:query))">
You can loop through it like any array, checking for the item matching the name parameter.
$scope.filteredItems
Here is a demo: http://plnkr.co/69nnbaZaulgX0odG7g7Y
See this related post: AngularJS - how to get an ngRepeat filtered result reference
Update
Your comments indicate that you don't want to wait for ng-repeat to create the array of filtered items. You can use the $filter service to easily initialize the same array before the page loads. Use:
$scope.filteredItems = $filter('filter')($scope.items, {name: $scope.query}, false)
Doing so does not interfere with ng-repeat saving its filter results to the same filteredItems array during DOM creation.
Here is an updated (and interactive) demo: http://plnkr.co/NSvBz1yWvmeFgXITutZF

Get Selected Object Of Kendo Drop Down List

I am using the Kendo Drop Down List. More specifically, I'm using Kendo Angular directives. Currently, I have the following in my markup:
<input id='myDropDownList' kendo-drop-down-list ng-model="selectedSport" k-data-source="sports" k-data-text-field="'name'" />
<button ng-click='send()'>Submit</button>
My controller has the following:
$scope.selectedSport = null;
$scope.sports: [
{ id: 1, name: 'Basketball' },
{ id: 2, name: 'Football' },
{ id: 3, name: 'Tennis' }
];
$scope.send = function () {
alert($scope.selectedSport);
};
When I run this code, I get the selectedSport ID. However, I want the entire object. Every other StackOverflow post I've found uses retrieves the ID. For the life of me, I can't figure out how to get the object. Does anyone know how to get the selected JSON object instead of just the id?
Thanks!
This answer is probably outdated for current versions of the Kendo Angular bindings.
As mentioned in hally9k's answer, there is now an attribute k-ng-model that can handle this, so you would use
k-ng-model="selectedSport"
in place of
ng-model="selectedSport"
Previous answer below; this may or may not still be relevant in case you're using an older version of Kendo UI:
I don't think you can configure the kendo widget to store the dataItem directly - underneath it all is still a <select> with a primitive value. So you'll probably have to get the dataItem from the widget's data source, e.g. like this:
HTML:
<div ng-controller="MyController">
<select id='myDropDownList' kendo-drop-down-list ng-model="selectedSport" k-data-source="sports" k-data-value-field="'id'" k-data-text-field="'name'"></select>
<button ng-click='send()'>Submit</button>
</div>
JS:
function MyController($scope) {
$scope.selectedSport = null;
$scope.sports = new kendo.data.DataSource({
data: [{
id: 1,
name: 'Basketball'
}, {
id: 2,
name: 'Football'
}, {
id: 3,
name: 'Tennis'
}]
});
$scope.send = function () {
var dataItem = $scope.sports.get($scope.selectedSport);
console.log(dataItem);
};
}
You could, however, create your own directive for kendoDropDownList which uses a k-data-item attribute (for example) and use it like this:
HTML:
<select id='date' k-ddl k-data-source="sports" k-data-text-field="name" k-data-item="dataItem">
JS:
var app = angular.module('Sample', ['kendo.directives']).directive("kDdl", function () {
return {
link: function (scope, element, attrs) {
$(element).kendoDropDownList({
dataTextField: attrs.kDataTextField,
dataValueField: "id",
dataSource: scope[attrs.kDataSource],
change: function () {
var that = this;
var item = that.dataItem();
scope.$apply(function () {
scope[attrs.kDataItem] = item.toJSON();
});
}
});
}
};
});
function MyController($scope) {
$scope.sports = [{
id: 1,
name: 'Basketball'
}, {
id: 2,
name: 'Football'
}, {
id: 3,
name: 'Tennis'
}];
$scope.dataItem = $scope.sports[0];
$scope.send = function () {
console.log($scope.dataItem);
};
}
That way, you could keep the controller free from Kendo UI DataSource-specific code and instead only work with JS data types. (see JSBin)
Using k-ng-model will bind the dataItem instead of just the text value:
<input id='myDropDownList' kendo-drop-down-list k-ng-model="selectedSport" k-data-source="sports" k-data-text-field="'name'" />
I know this is an old question, but you could use the select event of the dropdownlist to get the underlying json object:
select: function (e) {
var item = this.dataItem(e.item.index());
...
}
You would then store the json object (item variable above) from the select event so that you can get to it from your submit method. There is probably a way to get the selected json object without having to use the select event as well.
The proper way to get the text value is to use the 'k-select' event of Kendos dropdownlist:
<select kendo-drop-down-list k-select="vm.test" k-data-text-field="'groupName'" k-data-value-field="'id'" k-data-source="vm.groupList" ng-model="vm.groupId"></select>
Then in your angular controller expose the 'test' function (example assumes you are using 'controller as vm'):
function DocumentTypeController() {
var vm = this;
vm.test = test;
vm.groupId = null;
function test(dropdown) {
alert('text is:' + dropdown.item.text());
}
}
I hope that helps.

Resources