How to iterate over inner object's properties in an AngularJS template? - angularjs

I'm in the process of learning AngularJS. I would like to print out a list of objects and iterate over one of the object's inner object's properties. This looked like a standard procedure of using nested loops, however, it doesn't appear to be so simple.
My Controller is setup below. Essentially, it is a list of random vehicles.
var vehicleApp = angular.module("vehicleApp", []);
vehicleApp.controller('VehicleController', function ($scope) {
$scope.vehicles = [{
id: 0,
name: "car",
parts: {
wheels: 4,
doors: 4
}
}, {
id: 1,
name: "plane",
parts: {
wings: 2,
doors: 2
}
}, {
id: 2,
name: "boat",
parts: {
doors: 1
}
}];
});
I'd like to output the vehicles as such:
car
- wheels (4)
- doors (2)
plane
- wings (2)
- doors (2)
boat
- doors (1)
My template that I used was setup as such:
<div ng-app="vehicleApp" ng-controller="VehicleController">
<p ng-repeat="vehicle in vehicles">
{{ vehicle.name }}
</p>
<ul>
<li ng-repeat="(attribute, value) in vehicle.parts">
{{attribute}} ({{value}})
</li>
</ul>
</div>
This produces a list of the vehicles, but not the sub lists of the parts inner object.
Interestingly, enough, when I use {{ vehicle.parts }} it returns a JSON string of the parts inner object. Does AngularJS treat it as a string and hence, it is unable to print out the properties of the parts object?

You didn't enclose the second ngRepeat in the first one:
<div ng-app="vehicleApp" ng-controller="VehicleController">
<p ng-repeat="vehicle in vehicles">
{{ vehicle.name }}
<ul>
<li ng-repeat="(attribute, value) in vehicle.parts">
{{attribute}} ({{value}})
</li>
</ul>
</p>
</div>

Related

Compare two lists in angularJs

I have two list that creat from two different json object:
<ul ng-repeat="a in user.data">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
and :
<ul ng-repeat="x in job.data">
<li>
<md-checkbox>
{{ x.user.name }}
</md-checkbox>
</li>
</ul>
I have to compare two lists and remove {{ a.name }} that the same as {{ job.user.name }} . Comparing json objects that have different structure is hard. How can I compare this two list and remove repeated items?
You can use the filter filter ;) using a function instead of a string in the expression argument. Remember this is the syntax of filter
{{ filter_expression | filter : expression : comparator}}
The expression can be a string, an object or a function. Something like this will do
$scope.filterUnemployed = function(value) {
var jobs = $scope.job.data;
for (var i = 0; i < jobs.length; i++) {
// Search every object in the job.data array for a match.
// If found return false to remove this object from the results
if (jobs[i].user.name === value.name) {
return false;
}
}
// Otherwise return true to include it
return true;
}
And then apply the filter to your ng-repeat directive like this
<ul ng-repeat="a in user.data | filter:filterUnemployed">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
Note that this will not modify your original collection but will result in a new copy beign displayed in your html since this is usually the desired effect.
Check the sample for a working demo
angular.module('app', [])
.controller('SampleCtrl', function($scope) {
$scope.user = {
data: [{
name: 'John'
}, {
name: 'Mary'
}, {
name: 'Peter'
}, {
name: 'Jack'
}, {
name: 'Richard'
}, {
name: 'Elizabeth'
}]
};
$scope.job = {
data: [{
jobTitle: 'CEO',
user: {
name: 'John'
}
}, {
jobTitle: 'CFO',
user: {
name: 'Mary'
}
}, {
jobTitle: 'Analist',
user: {
name: 'Jack'
}
}]
};
$scope.filterUnemployed = function(value) {
var jobs = $scope.job.data;
for (var i = 0; i < jobs.length; i++) {
if (jobs[i].user.name === value.name) {
return false;
}
}
return true;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="SampleCtrl">
<h1>All Users</h1>
<ul ng-repeat="a in user.data">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
<h1>Unemployed users</h1>
<ul ng-repeat="a in user.data | filter:filterUnemployed">
<li>
<md-checkbox>
{{ a.name }}
</md-checkbox>
</li>
</ul>
<h1>Employed</h1>
<ul ng-repeat="x in job.data">
<li>
<md-checkbox>
{{ x.user.name }}
</md-checkbox>
</li>
</ul>
</div>
You can create a new array made from filtering one of your arrays :
var newArray = job.data.filter(function(jobData) {
return user.data.some(function(userData) {
return userData.name === jobData.user.name;
});
});
Something like this should help :
for(i=0; i<user.data.length;i++){
for(j=0; j<job.data.length;j++){
if(user.data[i].name === job.data[j].name){
user.date.splice(i,1);
}
}
}
I would appreciate some feedback, at least to know that my code helped you
You can use vanilla javascript, jQuery or library undescore.
Please check related links:
How can I merge two complex JSON objects with only unique or different values only showing in resultant array
How to merge two object values by keys
How to merge two arrays of JSON objects - removing duplicates and preserving order in Javascript/jQuery?
Merging two json objects in Java script?

AngularJS filter nested ng-repeat based on repeated object properties

I have an array of restaurant objects and I want to list them by grouping their cities
My object is like;
restaurant = {
id: 'id',
name: 'name',
city: 'city'
}
This HTML Markup can give some info about what I want to do.
<div ng-repeat="restaurant in restaurant | filter: ???">
<div class="header">
<h1 ng-bind="restaurant.city"></h1>
<a>Select All</a>
</div>
<div class="clearfix" ng-repeat="???">
<input type="checkbox" id="restaurant.id" />
<label ng-bind="restaurant.name"></label>
</div>
</div>
Can I do it with one single array or do i need to create seperate city and restaurant arrays to do it?
If you want to group restaurants by city, you can use groupBy of angular.filter module.
Just add the JS file from here: http://www.cdnjs.com/libraries/angular-filter to your project and use following code.
var myApp = angular.module('myApp',['angular.filter']);
function MyCtrl($scope) {
$scope.restaurants = [
{id: 1, name: 'RestA', city: 'CityA'},
{id: 2, name: 'RestB', city: 'CityA'},
{id: 3, name: 'RestC', city: 'CityC'},
{id: 4, name: 'RestD', city: 'CityD'}
];
}
<div ng-controller="MyCtrl">
<ul ng-repeat="(key, value) in restaurants | groupBy: 'city'">
<b>{{ key }}</b>
<li ng-repeat="restaurant in value">
<i>restaurant: {{ restaurant.name }} </i>
</li>
</ul>
</div>
I've created JSFiddle for you with working example.

Why does ng-init="things = […]; currentThing = things[0]" with ng-click="currentThing=thing" not update a bound value?

See the minimal AngularJS app below.
My expectation was that clicking a li would change the name and number in the paragraph, but they don't change away from their original value. Any ideas why?
<div ng-app ng-init="things=[ { name: 'one', num: 1 }, { name: 'two', num: 2 } ]; currentThing = things[0]">
<p>name: {{currentThing.name}} num: {{currentThing.num}}</p>
<ul>
<li ng-click="currentThing=thing" ng-repeat="thing in things">{{thing.name}}</li>
</ul>
</div>
Editable, runnable JSFiddle: http://jsfiddle.net/JbfZ5/1/
This is due to the fact that when using ng-repeat each template instance gets its own scope and due to how prototypal inheritance works in JavaScript.
In short, when you click a li element it will create the currentThing property on the newly created child scope belonging to the template, instead of updating currentThing of the parent.
If you use this instead and click the second li you will notice that it updates its own scope:
<li ng-click="currentThing=thing" ng-repeat="thing in things">
<pre>{{ currentThing | json }}</pre>
</li>
Demo: http://jsfiddle.net/A3eCe/
The recommended way to get around this is to use an object, for example:
<div ng-app ng-init="viewModel = { things: [ { name: 'one', num: 1 }, { name: 'two', num: 2 } ] }; viewModel.currentThing = viewModel.things[0]">
<p>name: {{ viewModel.currentThing.name}} num: {{ viewModel.currentThing.num}}</p>
<ul>
<li ng-click="viewModel.currentThing = thing" ng-repeat="thing in viewModel.things">{{thing.name}}</li>
</ul>
</div>
Demo: http://jsfiddle.net/4suvy/
An excellent explanation on the issue can be found here.

How do I only show an element if nested ng-repeat is not empty?

I have a List of lists, created with a nested ng-repeat. Each outer ng-repeat contains a div with the label of its inner list (eg: "Group A"). I'm now trying to create a way to avoid showing this label if the inner list is empty due to filtering(Applied by an input searchtext)
Here is a plunker explaining my issue and my attempted solution : Plnkr
Having a 'heavy' function like isGroupEmpty seems extremely cumbersome - Is there any way to do this in a much simpler fashion? I was toying with the idea of moving the label inside the inner ng-repeat and having ng-show="$first" but it doesnt look great
I ended up with the following solution which worked perfectly. Plnkr
By setting a variable in the inner ng-repeat I was able to evaluate ng-show based on this variables length like so :
<input ng-model='searchText'/>
<span ng-show='filtered.length > 0'>
<ul>
<li ng-repeat='el in filtered = (model | filter:searchText)'>
<div>{{el.label}}</div>
</li>
</ul>
</span>
you could leverage ng-init, that way you'll call the filter only once:
<div ng-repeat='(key,group) in model'>
<div ng-init="filtered = (group | filter:filterFn)"></div>
<div ng-show="filtered.length !== 0">
<div>{{key}}</div>
<ul>
<li ng-repeat="el in filtered">
<div>{{el.label}}</div>
</li>
</ul>
</div>
</div>
usually it is not a good practice to use ng-init out of no where, but I guess it solves calling the filter twice.
Another way is to use the filter through javascript - you could inject $filter and retrieve 'filter' $filter('filter') in your controller, calling it with group as its first argument, the filterFn as its second, and store its result in your scope.
I used the following:
<ul>
<li ng-repeat="menuItem in menuItems"><span class="fa {{menuItem.icon}} fa-lg"></span>{{menuItem.itemName}}
<span ng-show='menuItem.subItems.length > 0'>
<ul>
<li ng-repeat="subItem in menuItem.subItems">{{subItem.itemName}}</li>
</ul>
</span>
</li>
checking if an array has a length of 0 is not an expensive operation. if you want to only show lists that have item, put a filter on the outer array that takes an array of arrays and returns only the arrays that have a length different than 0.
you can also hide the inner div if the array == false.
http://plnkr.co/edit/gist:3510140
http://plnkr.co/edit/Gr5uPnRDbRfUYq0ILhmG?p=preview
Your plunkr was pretty complicated and hard to weed through so I re-created what you wanted using a fiddle. The general idea behind my approach is to filter out the items from the array, not the sub array. And only do the filtered items when the text changes. So here's the markup:
<div ng-app="app">
<div ng-controller="ParentCtrl">
<input data-ng-model="filterText" data-ng-change="updateTypes()" />
<div data-ng-repeat="type in filteredTypes">
{{ type.name }}
<ul>
<li style="margin-left:20px;" data-ng-repeat="entry in type.entries">
- {{ entry.name }}
</li>
</ul>
</div>
</div>
</div>
And here's the code:
angular.module('app', [])
function ParentCtrl($scope){
$scope.filterText = "";
$scope.types = [
{ name: "type1", entries: [{ name: "name1"}, { name: "name2"}, { name: "name3"}]},
{ name: "type2", entries: [{ name: "name1"}, { name: "name3"}, { name: "name3"}]},
{ name: "type3", entries: [{ name: "name1"}, { name: "name2"}, { name: "name5"}]},
{ name: "type4", entries: [{ name: "name4"}, { name: "name2"}, { name: "name3"}]}
];
$scope.filteredTypes = [];
$scope.updateTypes = function(){
$scope.filteredTypes.length = 0;
for(var x = 0; x < $scope.types.length; x++){
if($scope.filterText === ""){
$scope.filteredTypes.push($scope.types[x]);
}
else{
var entries = [];
for(var y = 0; y < $scope.types[x].entries.length; y++){
if($scope.types[x].entries[y].name.indexOf($scope.filterText) !== -1){
entries.push($scope.types[x].entries[y]);
}
}
if(entries.length > 0){
$scope.filteredTypes.push({
name: $scope.types[x].name,
entries: entries
});
}
}
}
}
$scope.updateTypes();
}
Fiddle: http://jsfiddle.net/2hRws/
The reason I'm creating a new array and not using an actual filter to remove the results is that angular doesn't like creating dynamic arrays on the fly in filters. This is because it doesn't assign $$hashKey and things just don't line up correctly when dirty checking. I got the idea of how to do what you needed from this topic on the matter: https://groups.google.com/forum/#!topic/angular/IEIQok-YkpU
I have only slightly modified your list-widget.html, see it in action: plunkr
The idea is simple - use the same filter for ng-show:
<div ng-show="group | filter:searchText">{{ key }}</div>
The label will be visible only if there are some unfiltered items.
In my example I'm using searchText for filter because I'm not familiar with CoffeeScript.

Nested checkboxes in AngularJS resource with external options

I have this data structure, search:
{
id: '1',
name: 'Foo'
service_ids:
[
3,
8,
12
]
}
I then have another data structure, services, that matches the ids from service_ids above with the below:
[
{
id: 3,
name: 'Fighter'
},
{
id: 4,
name: 'Typhoon'
},
{
id: 8,
name: 'Kung'
},
{
id: 12,
name: 'Builder'
}
]
I want to display this in a form using AngularJS. The name is fine. I want to display all possible services as checkboxes and if the search has one of the services checked then it is ticked in the checkbox. Something like:
<li ng-repeat="search in searches">
<input ng-model="search.name">
<ul>
<li ng-repeat="service in services">
<input type="checkbox">
{{service.name}}
</li>
</ul>
</li>
I don't know how to link that checkbox to the service_ids and the services. Any help appreciated.
I am using $resource.
Update
Ignore my answer, misread you nesting request.
If anyone needs nesting guidance see below:
The best way to nest in angular that I have found is to create a template which is aware of child items like this:
<script type="text/ng-template" id="tree_item_renderer.html">
<span>{{tag.Name}}</span>
<ul ng-show="tag.Children.length > 0">
<li ng-repeat="tag in tag.Children" ng-include="'tree_item_renderer.html'" ></li>
</ul>
</script>
<ul class="tag-list">
<li ng-repeat="tag in tags" ng-include="'tree_item_renderer.html'" ></li>
</ul>
If I understand your question it should be as easy as:
<input type="checkbox" ng-model="search.service_ids[service.id]">
And you should also get bi-directional binding.
Updated
Just realized search.service_ids is an array not a map. So the solution above is not accurate.
You might want instead to use a filter:
<input type="checkbox" ng-checked="search.service_ids | contains:service.id">
Then
filter('contains', function() {
return function(haystack, needle) {
return haystack.indexOf(needle) >= 0;
}
});

Resources