How to "connect" 2 variables in Angular by some value - angularjs

I have 2 variables in controller:
$scope.first = [
{ id="11", nameF="aaaa1" },
{ id="12", nameF="bbbb1" }
]
$scope.second = [
{ id="21", nameS="aaaa2", idFirst="11" },
{ id="22", nameS="bbbb2", idFirst="12" },
{ id="23", nameS="cccc2", idFirst="12" }
]
In a template I have ngRepeat for variable second:
<div ng-repeat="item in second>
<div>{{item.nameS}}</div>
<div>{{item.idFirst}}</div>
</div>
For every item.idFirst I would like to write out matching nameF instead. What is the best practice to achieve that? I can't seem to figure out a simple way to do it, but suppose there has to be one. Thanx!

You can use custom filter if you don't want to create one single object holding the expected structure.
HTML :
<div ng-repeat="item in second">
<div>{{item.nameS}}</div>
<div>{{item.idFirst | getMatchName:first}}</div>
</div>
JS:
.filter('getMatchName', function() {
return function(strName, arrFirst) {
arrFirst.forEach(function(val, key) {
if (val.id == strName) {
strName = val.nameF;
}
})
return strName;
}
})
Here is working plunker

If I understand correctly,
$scope.getNameF = function(idFirst){
for(var i = 0; i < $scope.first.length; i++){
if($scope.first[i].id === idFirst){
return $scope.first[i].nameF;
}
}
return undefined;
}
<div ng-repeat="item in second">
<div>{{item.nameS}}</div>
<div>{{getNameF(item.idFirst)}}</div>
</div>
EDIT
Also you can prepare data before rendering:
for(var i = 0; i < $scope.second.length; i++){
$scope.second[i].nameF = getNameF($scope.second[i].idFirst);
}
<div ng-repeat="item in second">
<div>{{item.nameS}}</div>
<div>{{item.nameF}}</div>
</div>

You can think of your template logic a bit like code, calling values from other variables: yourValue[another.value].nameF
Change your code to this and it should work
<div ng-repeat="item in second>
<div>{{item.nameS}}</div>
<div>{{first[item.idFirst].nameF}}</div>
</div>

Related

Angularjs Filter by an Array of values

i've checqued this :
AngularJS filter based on array of strings?
But i've still got difficulties to know how to do :
My data model is this, they are footballers :
$scope.footballers = [
{'identifiant':1,'prenom':'Jean','nom':'Valjean','categorie':1,'ville':'Détroit','age':12,'date_embauche':'','salaire':25,'photo':'1.jpg','vitesse':55,'agilite':3,'deduction':25,'choisi':false},
{'identifiant':2,'prenom':'Aziz','nom':'Jojo','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':57,'agilite':31,'deduction':25,'choisi':false},
{'identifiant':3,'prenom':'Thierry','nom':'Goubert','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'photo':'2.jpg','vitesse':45,'agilite':3,'deduction':2,'choisi':false},
{'identifiant':4,'prenom':'Roland','nom':'Grondin','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':5,'agilite':34,'deduction':2,'choisi':false},
{'identifiant':5,'prenom':'Gogok','nom':'Rodolphe','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'photo':'3.jpg','vitesse':68,'agilite':75,'deduction':2,'choisi':false},
{'identifiant':6,'prenom':'Thierry','nom':'Chalamerto','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'vitesse':55,'agilite':57,'deduction':75,'choisi':false},
{'identifiant':7,'prenom':'Gawivk','nom':'Gonzogues','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':10,'agilite':44,'deduction':2,'choisi':false},
{'identifiant':8,'prenom':'Thomas','nom':'Choubal','categorie':1,'ville':'Paris','age':12,'date_embauche':'','salaire':28,'vitesse':5,'agilite':3,'deduction':2,'choisi':false}
];
Now, I would like to display only the footballer who has the identifiant 2,3 and 8 for example.
Let's say I 've got this array :
var iwanttofilter = [2,3,8];
How could i do to filter with angularJs, firstly, in my ng-repeat, and secondly directly into my controller ?
Thank you.
In pure angular way
var filteredList = $filter('filter')($scope.footballers, function (i) {
return (i.identifiant === 2 || i.identifiant === 3 || i.identifiant === 8);
});
you can create a custom filter like this
.filter('cust',function(){
var iwanttofilter = [2,3,8];
return function(item){
return item.filter(o=>iwanttofilter.find(k=> o.identifiant == k))
}
})
in here array will filter according the iwanttofilter array and return the result
Demo
angular.module("app",[])
.controller("ctrl",function($scope){
$scope.footballers = [
{'identifiant':1,'prenom':'Jean','nom':'Valjean','categorie':1,'ville':'Détroit','age':12,'date_embauche':'','salaire':25,'photo':'1.jpg','vitesse':55,'agilite':3,'deduction':25,'choisi':false},
{'identifiant':2,'prenom':'Aziz','nom':'Jojo','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':57,'agilite':31,'deduction':25,'choisi':false},
{'identifiant':3,'prenom':'Thierry','nom':'Goubert','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'photo':'2.jpg','vitesse':45,'agilite':3,'deduction':2,'choisi':false},
{'identifiant':4,'prenom':'Roland','nom':'Grondin','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':5,'agilite':34,'deduction':2,'choisi':false},
{'identifiant':5,'prenom':'Gogok','nom':'Rodolphe','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'photo':'3.jpg','vitesse':68,'agilite':75,'deduction':2,'choisi':false},
{'identifiant':6,'prenom':'Thierry','nom':'Chalamerto','categorie':1,'ville':'Paris','age':17,'date_embauche':'','salaire':28,'vitesse':55,'agilite':57,'deduction':75,'choisi':false},
{'identifiant':7,'prenom':'Gawivk','nom':'Gonzogues','categorie':2,'ville':'Paris','age':14,'date_embauche':'','salaire':25,'vitesse':10,'agilite':44,'deduction':2,'choisi':false},
{'identifiant':8,'prenom':'Thomas','nom':'Choubal','categorie':1,'ville':'Paris','age':12,'date_embauche':'','salaire':28,'vitesse':5,'agilite':3,'deduction':2,'choisi':false}
];
var iwanttofilter = [2,3,8];
$scope.cust = function(){
return function(item){
return iwanttofilter.find(k=> item.identifiant == k)
}
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div ng-repeat="item in footballers | filter:cust() track by $index ">{{item.identifiant}} </div>
</div>
You pass the iwanttofilter into the filter, and filter your list based on each item.
MyApp.filter("fewerFootballers", [
function() {
return function(footballers, iwanttofilter) {
return arrayIntersection(footballers, iwanttofilter);
function arrayIntersection(a, b) {
return a.filter(function(x) {
return b.indexOf(x.identifiant) != -1;
});
}
}
}]);
In your html you use the filter.
{{ $scope.footballers | fewerFootballers: $scope.iwanttofilter }}

$filter with OR [duplicate]

I want to use the filter in angular and want to filter for multiple values, if it has either one of the values then it should be displayed.
I have for example this structure:
An object movie which has the property genres and I want to filter for Action and Comedy.
I know I can do filter:({genres: 'Action'} || {genres: 'Comedy'}), but what to do if I want to filter it dynamically. E.g. filter: variableX
How do I set variableX in the $scope, when I have an array of the genres I have to filter?
I could construct it as a string and then do an eval() but I don't want to use eval()...
I would just create a custom filter. They are not that hard.
angular.module('myFilters', []).
filter('bygenre', function() {
return function(movies,genres) {
var out = [];
// Filter logic here, adding matches to the out var.
return out;
}
});
template:
<h1>Movies</h1>
<div ng-init="movies = [
{title:'Man on the Moon', genre:'action'},
{title:'Meet the Robinsons', genre:'family'},
{title:'Sphere', genre:'action'}
];" />
<input type="checkbox" ng-model="genrefilters.action" />Action
<br />
<input type="checkbox" ng-model="genrefilters.family" />Family
<br />{{genrefilters.action}}::{{genrefilters.family}}
<ul>
<li ng-repeat="movie in movies | bygenre:genrefilters">{{movie.title}}: {{movie.genre}}</li>
</ul>
Edit here is the link: Creating Angular Filters
UPDATE: Here is a fiddle that has an exact demo of my suggestion.
You can use a controller function to filter.
function MoviesCtrl($scope) {
$scope.movies = [{name:'Shrek', genre:'Comedy'},
{name:'Die Hard', genre:'Action'},
{name:'The Godfather', genre:'Drama'}];
$scope.selectedGenres = ['Action','Drama'];
$scope.filterByGenres = function(movie) {
return ($scope.selectedGenres.indexOf(movie.genre) !== -1);
};
}
HTML:
<div ng-controller="MoviesCtrl">
<ul>
<li ng-repeat="movie in movies | filter:filterByGenres">
{{ movie.name }} {{ movie.genre }}
</li>
</ul>
</div>
Creating a custom filter might be overkill here, you can just pass in a custom comparator, if you have the multiples values like:
$scope.selectedGenres = "Action, Drama";
$scope.containsComparator = function(expected, actual){
return actual.indexOf(expected) > -1;
};
then in the filter:
filter:{name:selectedGenres}:containsComparator
Here is the implementation of custom filter, which will filter the data using array of values.It will support multiple key object with both array and single value of keys. As mentioned inangularJS API AngularJS filter Doc supports multiple key filter with single value, but below custom filter will support same feature as angularJS and also supports array of values and combination of both array and single value of keys.Please find the code snippet below,
myApp.filter('filterMultiple',['$filter',function ($filter) {
return function (items, keyObj) {
var filterObj = {
data:items,
filteredData:[],
applyFilter : function(obj,key){
var fData = [];
if (this.filteredData.length == 0)
this.filteredData = this.data;
if (obj){
var fObj = {};
if (!angular.isArray(obj)){
fObj[key] = obj;
fData = fData.concat($filter('filter')(this.filteredData,fObj));
} else if (angular.isArray(obj)){
if (obj.length > 0){
for (var i=0;i<obj.length;i++){
if (angular.isDefined(obj[i])){
fObj[key] = obj[i];
fData = fData.concat($filter('filter')(this.filteredData,fObj));
}
}
}
}
if (fData.length > 0){
this.filteredData = fData;
}
}
}
};
if (keyObj){
angular.forEach(keyObj,function(obj,key){
filterObj.applyFilter(obj,key);
});
}
return filterObj.filteredData;
}
}]);
Usage:
arrayOfObjectswithKeys | filterMultiple:{key1:['value1','value2','value3',...etc],key2:'value4',key3:[value5,value6,...etc]}
Here is a fiddle example with implementation of above "filterMutiple" custom filter.
:::Fiddle Example:::
If you want to filter on Array of Objects then you can give
filter:({genres: 'Action', key :value }.
Individual property will be filtered by particular filter given for that property.
But if you wanted to something like filter by individual Property and filter globally for all properties then you can do something like this.
<tr ng-repeat="supp in $data | filter : filterObject | filter : search">
Where "filterObject" is an object for searching an individual property and "Search" will search in every property globally.
~Atul
I've spent some time on it and thanks to #chrismarx, I saw that angular's default filterFilter allows you to pass your own comparator. Here's the edited comparator for multiple values:
function hasCustomToString(obj) {
return angular.isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
}
var comparator = function (actual, expected) {
if (angular.isUndefined(actual)) {
// No substring matching against `undefined`
return false;
}
if ((actual === null) || (expected === null)) {
// No substring matching against `null`; only match against `null`
return actual === expected;
}
// I edited this to check if not array
if ((angular.isObject(expected) && !angular.isArray(expected)) || (angular.isObject(actual) && !hasCustomToString(actual))) {
// Should not compare primitives against objects, unless they have custom `toString` method
return false;
}
// This is where magic happens
actual = angular.lowercase('' + actual);
if (angular.isArray(expected)) {
var match = false;
expected.forEach(function (e) {
e = angular.lowercase('' + e);
if (actual.indexOf(e) !== -1) {
match = true;
}
});
return match;
} else {
expected = angular.lowercase('' + expected);
return actual.indexOf(expected) !== -1;
}
};
And if we want to make a custom filter for DRY:
angular.module('myApp')
.filter('filterWithOr', function ($filter) {
var comparator = function (actual, expected) {
if (angular.isUndefined(actual)) {
// No substring matching against `undefined`
return false;
}
if ((actual === null) || (expected === null)) {
// No substring matching against `null`; only match against `null`
return actual === expected;
}
if ((angular.isObject(expected) && !angular.isArray(expected)) || (angular.isObject(actual) && !hasCustomToString(actual))) {
// Should not compare primitives against objects, unless they have custom `toString` method
return false;
}
console.log('ACTUAL EXPECTED')
console.log(actual)
console.log(expected)
actual = angular.lowercase('' + actual);
if (angular.isArray(expected)) {
var match = false;
expected.forEach(function (e) {
console.log('forEach')
console.log(e)
e = angular.lowercase('' + e);
if (actual.indexOf(e) !== -1) {
match = true;
}
});
return match;
} else {
expected = angular.lowercase('' + expected);
return actual.indexOf(expected) !== -1;
}
};
return function (array, expression) {
return $filter('filter')(array, expression, comparator);
};
});
And then we can use it anywhere we want:
$scope.list=[
{name:'Jack Bauer'},
{name:'Chuck Norris'},
{name:'Superman'},
{name:'Batman'},
{name:'Spiderman'},
{name:'Hulk'}
];
<ul>
<li ng-repeat="item in list | filterWithOr:{name:['Jack','Chuck']}">
{{item.name}}
</li>
</ul>
Finally here's a plunkr.
Note: Expected array should only contain simple objects like String, Number etc.
you can use searchField filter of angular.filter
JS:
$scope.users = [
{ first_name: 'Sharon', last_name: 'Melendez' },
{ first_name: 'Edmundo', last_name: 'Hepler' },
{ first_name: 'Marsha', last_name: 'Letourneau' }
];
HTML:
<input ng-model="search" placeholder="search by full name"/>
<th ng-repeat="user in users | searchField: 'first_name': 'last_name' | filter: search">
{{ user.first_name }} {{ user.last_name }}
</th>
<!-- so now you can search by full name -->
You can also use ngIf if the situation permits:
<div ng-repeat="p in [
{ name: 'Justin' },
{ name: 'Jimi' },
{ name: 'Bob' }
]" ng-if="['Jimi', 'Bob'].indexOf(e.name) > -1">
{{ p.name }} is cool
</div>
The quickest solution that I've found is to use the filterBy filter from angular-filter, for example:
<input type="text" placeholder="Search by name or genre" ng-model="ctrl.search"/>
<ul>
<li ng-repeat="movie in ctrl.movies | filterBy: ['name', 'genre']: ctrl.search">
{{movie.name}} ({{movie.genre}}) - {{movie.rating}}
</li>
</ul>
The upside is that angular-filter is a fairly popular library (~2.6k stars on GitHub) which is still actively developed and maintained, so it should be fine to add it to your project as a dependency.
I believe this is what you're looking for:
<div>{{ (collection | fitler1:args) + (collection | filter2:args) }}</div>
Please try this
var m = angular.module('yourModuleName');
m.filter('advancefilter', ['$filter', function($filter){
return function(data, text){
var textArr = text.split(' ');
angular.forEach(textArr, function(test){
if(test){
data = $filter('filter')(data, test);
}
});
return data;
}
}]);
Lets assume you have two array, one for movie and one for genre
Just use the filter as: filter:{genres: genres.type}
Here genres being the array and type has value for genre
I wrote this for strings AND functionality (I know it's not the question but I searched for it and got here), maybe it can be expanded.
String.prototype.contains = function(str) {
return this.indexOf(str) != -1;
};
String.prototype.containsAll = function(strArray) {
for (var i = 0; i < strArray.length; i++) {
if (!this.contains(strArray[i])) {
return false;
}
}
return true;
}
app.filter('filterMultiple', function() {
return function(items, filterDict) {
return items.filter(function(item) {
for (filterKey in filterDict) {
if (filterDict[filterKey] instanceof Array) {
if (!item[filterKey].containsAll(filterDict[filterKey])) {
return false;
}
} else {
if (!item[filterKey].contains(filterDict[filterKey])) {
return false;
}
}
}
return true;
});
};
});
Usage:
<li ng-repeat="x in array | filterMultiple:{key1: value1, key2:[value21, value22]}">{{x.name}}</li>
Angular Or Filter Module
$filter('orFilter')([{..}, {..} ...], {arg1, arg2, ...}, false)
here is the link: https://github.com/webyonet/angular-or-filter
I had similar situation. Writing custom filter worked for me. Hope this helps!
JS:
App.filter('searchMovies', function() {
return function (items, letter) {
var resulsts = [];
var itemMatch = new RegExp(letter, 'i');
for (var i = 0; i < items.length; i++) {
var item = items[i];
if ( itemMatch.test(item.name) || itemMatch.test(item.genre)) {
results.push(item);
}
}
return results;
};
});
HTML:
<div ng-controller="MoviesCtrl">
<ul>
<li ng-repeat="movie in movies | searchMovies:filterByGenres">
{{ movie.name }} {{ movie.genre }}
</li>
</ul>
</div>
Here is my example how create filter and directive for table jsfiddle
directive get list (datas) and create table with filters
<div ng-app="autoDrops" ng-controller="HomeController">
<div class="row">
<div class="col-md-12">
<h1>{{title}}</h1>
<ng-Multiselect array-List="datas"></ng-Multiselect>
</div>
</div>
</div>
my pleasure if i help you
Too late to join the party but may be it can help someone:
We can do it in two step, first filter by first property and then concatenate by second filter:
$scope.filterd = $filter('filter')($scope.empList, { dept: "account" });
$scope.filterd = $scope.filterd.concat($filter('filter')($scope.empList, { dept: "sales" }));
See the working fiddle with multiple property filter
OPTION 1:
Using Angular providered filter comparator parameter
// declaring a comparator method
$scope.filterBy = function(actual, expected) {
return _.contains(expected, actual); // uses underscore library contains method
};
var employees = [{name: 'a'}, {name: 'b'}, {name: 'c'}, {name: 'd'}];
// filter employees with name matching with either 'a' or 'c'
var filteredEmployees = $filter('filter')(employees, {name: ['a','c']}, $scope.filterBy);
OPTION 2:
Using Angular providered filter negation
var employees = [{name: 'a'}, {name: 'b'}, {name: 'c'}, {name: 'd'}];
// filter employees with name matching with either 'a' or 'c'
var filteredEmployees = $filter('filter')($filter('filter')(employees, {name: '!d'}), {name: '!b'});
My solution
ng-repeat="movie in movies | filter: {'Action'} + filter: {'Comedy}"
the best answer is :
filter:({genres: 'Action', genres: 'Comedy'}

How to apply dynamic filters in AngularJS

I'm starting to learn AngularJS now and I have some issues with filters.
I need to apply two types of filters and I can't figure out how.
I have a device list JSON that looks like this:
[{
"ID": 1,
"Name": "Device 1",
"Price": 1998.92,
"Colors": [{
"ColorCode": "Red",
"ColorName": "#FF0000"
},
{
"ColorCode": "Green",
"ColorName": "#2EFE2E"
}],
"Type": {
"TypeID": 1,
"TypeName": "Mobile device"
},
"Company": {
"CompanyID": 1,
"CompanyName": "Alcatel"
}
}]
I display the list like this:
<div ng-repeat="device in devices | filter:companyFilters | filter:colorFilters">
<span>{{device.Company.CompanyID}}</span> // 1
<span>{{device.Company.CompanyName}}</span> // Google
<span>{{device.Name}}</span> // Nexus 6P
</div>
I have some filters that I applied but there are two filters that I can't understand how to apply.
Filter 1:
A checkbox list of companies that filters the items by the selected
companies.
Filter 2:
A color filter that when clicking on a color will filter the devices
that has that color
For the company filter I have this checkbox list:
<div ng-repeat="company in deviceCompanies">
<input type="checkbox" data-ng-model="companyFilters" id="{{company.CompanyID}}" data-ng-true-value='{{company.CompanyID}}' data-ng-false-value='' />
<label for="{{company.CompanyID}}">{{company.CompanyName}}</label>
</div>
And on the controller side I have this:
$scope.companyFilters = [];
For the color filter I have this:
<div>
<a ng-click="???">All</a>
<div ng-repeat="color in deviceColors" style="display:inline-block; margin-right:10px;">
<div style="width:20px;height:20px;background-color:{{color.ColorCode}}"></div>
<a ng-model="selColor" data-ng="color.ColorCode" ng-click="colorFilters">{{color.ColorName}}</a>
</div>
</div>
And on the controller:
$scope.colorFilters = function (device) {
if (!$scope.selColor)
return true;
for (var i = 0; i < device.Colors.length; i++) {
if (device.Colors[i].ColorCode == $scope.selColor)
return true;
}
return false;
};
But it doesn't work...
Can anyone please tell me how to apply these filters ?
Since you are using ng-repeat which creates its own scope, anything you do in ng-repeat will not get recognized on the controller scope. Using a tool like ng-inspector or batarang will illustrate this.
I recommend using controllerAs Syntax, in your controller add.
angular.module('myModule').controller('CustomFilterController', function() {
var vm = this;
this.devices = //your list of data
this.companyFilters = [];
this.colorFilters = //your function
}
On your view declare your controller like this:
<div ng-controller='CustomFilterController as custom'>
(Note the value after as can be whatever you want it to be)
Then reference anything on that controller as custom.ThingOnController
EX:
<div ng-repeat="device in custom.devices | filter:custom.companyFilters | filter:custom.colorFilters">
After trying some workarounds, this is what I came up with:
<div ng-repeat="company in deviceCompanies">
<!--the ng-click will call a function that updated an array of values-->
<input type="checkbox" id="{{company.CompanyID}}" ng-click="selectCompany(company.CompanyID)">
<label for="{{company.CompanyID}}">{{company.CompanyName}}</label>
</div>
And the controller part is so simple:
$scope.selectedCompanies = [];
//when the array is upted the filter function will also launch
$scope.selectCompany = function (companyId) {
var i = $.inArray(companyId, $scope.selectedCompanies);
if (i > -1) {
$scope.selectedCompanies.splice(i, 1);
} else {
$scope.selectedCompanies.push(companyId);
}
}
$scope.companyFilter = function (device) {
if ($scope.selectedCompanies.length > 0) {
if ($.inArray(device.Company.CompanyID, $scope.selectedCompanies) < 0)
return;
}
return device;
}
Same goes to the colors filter:
<div>
<a ng-click="selectColor()">All</a>
<div ng-repeat="color in deviceColors" style="display:inline-block; margin-right:10px;">
<div style="width:20px;height:20px;background-color:{{color.ColorCode}}"></div>
<a ng-click="selectColor(color.ColorCode)">{{color.ColorName}}</a>
</div>
</div>
And controller part:
$scope.selectedColor;
$scope.selectColor = function (colorCode) {
$scope.selectedColor = colorCode;
}
$scope.colorFilters = function (device) {
if (!$scope.selectedColor)
return true;
for (var i = 0; i < device.Colors.length; i++) {
if (device.Colors[i].ColorCode == $scope.selectedColor)
return true;
}
return false;
};
And finally, applying the filters:
<div ng-repeat="device in devices | filter:companyFilter | filter:colorFilters">
...
</div>
You can have a look here to see how it works
I think the color filter can be more elegant, but, for now, this does the trick.
If I'll come up with a better solution I'll post it here.

Filtering a nested ng-repeat: Hide parents that don't have children

I want to make some kind of project list from a JSON file. The data structure (year, month, project) looks like this:
[{
"name": "2013",
"months": [{
"name": "May 2013",
"projects": [{
"name": "2013-05-09 Project A"
}, {
"name": "2013-05-14 Project B"
}, { ... }]
}, { ... }]
}, { ... }]
I'm displaying all data using a nested ng-repeat and make it searchable by a filter bound to the query from an input box.
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.projects | filter:query | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
If I type "Project B" now, all the empty parent elements are still visible. How can I hide them? I tried some ng-show tricks, but the main problem seems so be, that I don't have access to any information about the parents filtered state.
Here is a fiddle to demonstrate my problem: http://jsfiddle.net/stekhn/y3ft0cwn/7/
You basically have to filter the months to only keep the ones having at least one filtered project, and you also have to filter the years to only keep those having at least one filtered month.
This can be easily achieved using the following code:
function MainCtrl($scope, $filter) {
$scope.query = '';
$scope.monthHasVisibleProject = function(month) {
return $filter('filter')(month.children, $scope.query).length > 0;
};
$scope.yearHasVisibleMonth = function(year) {
return $filter('filter')(year.children, $scope.monthHasVisibleProject).length > 0;
};
and in the view:
<div class="year" ng-repeat="year in data | filter:yearHasVisibleMonth | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:monthHasVisibleProject | orderBy:sortMonth:true">
This is quite inefficient though, since to know if a year is accepted, you filter all its months, and for each month, you filter all its projects. So, unless the performance is good enough for your amount of data, you should probably apply the same principle but by persisting the accepted/rejected state of each object (project, then month, then year) every time the query is modified.
I think that the best way to go is to implement a custom function in order to update a custom Array with the filtered data whenever the query changes. Like this:
$scope.query = '';
$scope.filteredData= angular.copy($scope.data);
$scope.updateFilteredData = function(newVal){
var filtered = angular.copy($scope.data);
filtered = filtered.map(function(year){
year.children=year.children.map(function(month){
month.children = $filter('filter')(month.children,newVal);
return month;
});
return year;
});
$scope.filteredData = filtered.filter(function(year){
year.children= year.children.filter(function(month){
return month.children.length>0;
});
return year.children.length>0;
});
}
And then your view will look like this:
<input type="search" ng-model="query" ng-change="updateFilteredData(query)"
placeholder="Search..." />
<div class="year" ng-repeat="year in filteredData | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Example
Why not a custom $filter for this?
Efficiency: the nature of the $diggest cycle would make it much less efficient. The only problem is that this solution won't be as easy to re-use as a custom $filter would. However, that custom $filter wouldn't be very reusable either, since its logic would be very dependent on this concrete data structure.
IE8 Support
If you need this to work on IE8 you will have to either use jQuery to replace the filter and map functions or to ensure that those functions are defined, like this:
(BTW: if you need IE8 support there is absolutely nothing wrong with using jQuery for these kind of things.)
filter:
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
'use strict';
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
map
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(" this is null or not defined");
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (thisArg) {
T = thisArg;
}
A = new Array(len);
k = 0;
while(k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[ k ];
mappedValue = callback.call(T, kValue, k, O);
A[ k ] = mappedValue;
}
k++;
}
return A;
};
}
Acknowledgement
I want to thank JB Nizet for his feedback.
For those who are interested: Yesterday I found another approach for solving this problem, which strikes me as rather inefficient. The functions gets called for every child again while typing the query. Not nearly as nice as Josep's solution.
function MainCtrl($scope) {
$scope.query = '';
$scope.searchString = function () {
return function (item) {
var string = JSON.stringify(item).toLowerCase();
var words = $scope.query.toLowerCase();
if (words) {
var filterBy = words.split(/\s+/);
if (!filterBy.length) {
return true;
}
} else {
return true;
}
return filterBy.every(function (word) {
var exists = string.indexOf(word);
if(exists !== -1){
return true;
}
});
};
};
};
And in the view:
<div class="year" ng-repeat="year in data | filter:searchString() | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:searchString() | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | filter:searchString() | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Here is the fiddle: http://jsfiddle.net/stekhn/stv55sxg/1/
Doesn't this work? Using a filtered variable and checking the length of it..
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true" ng-show="filtered.length != 0">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in filtered = (month.projects | filter:query) | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>

AngularJS - ng-repeat show one item at a time

looking for some ideas here. i have a meal plan object that contains an array of meals. only one meal can be set as primary at a time but i want the user to be able to cycle through the array of meals and mark a meal as primary. i am stuck trying to figure out if ngrepeat makes sense here or ngswitch or ngshow. any thoughts or samples would be highly appreciated!
I have tried multiple approaches with no luck.
thanks
You could cycle through the meals by index of the meal and have a button to choose the meal like this:
http://jsfiddle.net/c6RZK/
var app = angular.module('mealsApp',[]);
app.controller('MealsCtrl',function($scope) {
$scope.meals = [
{name:'Meatloaf'},
{name:'Tacos'},
{name:'Spaghetti'}
];
$scope.meal_index = 0;
$scope.meal = {};
$scope.next = function() {
if ($scope.meal_index >= $scope.meals.length -1) {
$scope.meal_index = 0;
}
else {
$scope.meal_index ++;
}
};
$scope.choose = function(meal) {
$scope.meal = meal;
}
});
HTML
<div ng-app="mealsApp" ng-controller="MealsCtrl">
<div ng-repeat="m in meals">
<div ng-if="meal_index == $index">
<strong>{{m.name}}</strong>
<button ng-click="choose(m)">Choose</button>
</div>
</div>
<hr>
<button ng-click="next()">Next</button>
<hr>Your Choice: {{meal.name}}
</div>
You could just attach a property to the plan, with a flag that says whether or not it's the primary plan.
Here's a sample implementation:
$scope.plans = [{name:"One"}, {name:"Two"}, {name:"Three"}];
$scope.selectPlan = function(plan) {
for(var i = 0, l = $scope.plans.length; i < l; i++) {
$scope.plans[i].primary = false;
if($scope.plans[i] === plan) {
$scope.plans[i].primary = true;
}
}
};
HTML:
<ul>
<li ng-click="selectPlan(plan)" ng-repeat="plan in plans" ng-class="{primary: plan.primary}"><a href>{{plan.name}}</a></li>
</ul>
If you'd rather not attach properties you could use something like a selected index property on your controller.

Resources