Trouble loading array safely in my angular html template - arrays

I have an array that is populated after a .subscribe to my API. Console shows it populated as expected. Accessing an element of the array results to an error thrown because of it being undefined
<div *ngIf="!invoices || invoices.length === 0">
No invoices
</div>
<div *ngIf="invoices || async ">
{{ invoices[0]?.invoice_id || async}}
</div>
If I remove the elvis operator my content will load fine however the console will throw errors InvoicesComponent.html:10 ERROR TypeError: Cannot read property 'invoice_id' of undefined until the array gets populated from the subscribe function.
The invoices array is initialised in my service
invoices: Array<Invoice> = [];
And I populate the array
getInvoices(){
var _invoices = this.invoices;
if(this.afAuth.user){
// users/uid/invoices/invoice_id/
var userRef = this.afs.doc(`users/${this.afAuth.auth.currentUser.uid}`)
userRef.collection('invoices').get().subscribe(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
_invoices.push({
'invoice_id': doc.id,
'customer_company': doc.data().customer_company,
'year_id':doc.data().year_id,
'date_created': doc.data().date_created,
'date_modified': doc.data().date_modified})
});
console.log(_invoices)
});
return _invoices
}
Based on the suggestion of trichetriche, an `Invoice class was created
import { QueryDocumentSnapshot } from "#angular/fire/firestore";
import { of } from 'rxjs'
export class Invoice {
invoice_id: string;
customer_company: string;
date_created: string;
date_modified: string;
year_id: string;
constructor(invoiceDoc: QueryDocumentSnapshot<any>){
this.invoice_id = invoiceDoc.id
this.customer_company = invoiceDoc.data().customer_company
this.date_created = invoiceDoc.data().date_created
this.date_modified = invoiceDoc.data().date_modified
this.year_id = invoiceDoc.data().year_id
}
toObservable(){
return of(this)
}
}

Original
<div *ngIf="!invoices || invoices.length === 0">
No invoices
</div>
<div *ngIf="invoices || async ">
{{ invoices[0]?.invoice_id || async}}
</div>
Edited
<ng-container *ngIf="invoices | async as invoicesSync; else noInvoices">
<p>{{ invoicesSync[0]?.invoice_id || 'No ID for invoice' }}</p>
</ng-container>
<ng-template #noInvoices>
<p>No invoices</p>
</ng-template>
1 - It's | async, not || async : | is a pipe, || is a fallback to a falsy statement.
2 - There should be a single async in your code, which create a template variable through as XXX.
3 - You don't need several conditions. Use a single one with a then statement.

i think you are using the Async pipe in wrong way .
you can passe Observable directly to template and the code will like this :
<div *ngIf="invoices|async as invoicesList; else noInvoices">
{{ invoicesList[0]?.invoice_id}}
</div>
<ng-template #noInvoices>
<div >
No invoices
</div>
</ng-template>

Right so after some research it seems that I was better off subscribing to an observable and dealing with the data as it arrives from my API with the async pipe.
So my final functions look kind of like this:
ngOnInit() {
this.observableInvoices = this.auth.getObservableInvoices().pipe(map(
(data) => data));
console.log(this.observableInvoices)
}
<li *ngFor="let invoice of observableInvoices | async; index as i">
getObservableInvoices(): Observable<any> {
this.observable_invoices = this.afs
.collection(`users/${this.afAuth.auth.currentUser.uid}/invoices`)
.valueChanges() as Observable<any[]>;
return this.observable_invoices;
}

Related

Can't make table in LWC

I want to make a table like this:
I got the values from the apex in js and it looks like this:
#track data2 = [];
#track data = [];
testClick() {
getExchangeRates({baseCurrency : this.defaultCurrency, dateFrom : this.dateFrom, dateTo : this.dateTo, value : this.defaultValue})
.then(resultJSON => {
let result = JSON.parse(resultJSON);
console.log(result);
console.log('////////');
console.log(result.rates);
let recordsByDates = result.rates;
for (var key in recordsByDates) {
console.log(key);
let record = {
date : key,
USD : recordsByDates[key].USD,
CAD : recordsByDates[key].CAD,
EUR : recordsByDates[key].EUR,
GBP : recordsByDates[key].GBP
}
this.data.push(record);
this.data2 = JSON.stringify(this.data);
}
console.log(this.data);
console.log(this.data2);
})
.catch(error => {
this.error = error;
console.log(error);
});
}
I tried to make a table, but something did not work out for me:
<template for:each={data2} for:item="dat">
<tr key={dat} onclick={testClick}>
<td data-label="data2">
{dat.key}
</td>
<td data-label="data2">
{dat.value}
</td>
</tr>
</template>
please tell me how to fix this
there are a couple of issues in the code:
not referring date value properly
should add a unique key, need in for:each
Here is the updated code
for (var key in recordsByDates) {
let record = {
key: key, // index of object can be used
date : recordsByDates[key].date,
USD : recordsByDates[key].USD,
CAD : recordsByDates[key].CAD,
EUR : recordsByDates[key].EUR,
GBP : recordsByDates[key].GBP
}
}
in HTML, you are referring wrong variable, it should data, in place of data2 (which is stringified).
no need to have table based tags for this type of structure, you can use div for better control.
in HTML file, it should be something like:
<template for:each={data} for:item="dat">
<div key={dat.key} onclick={testClick}>
<div class="date-wrap">
Date: {dat.date}
</div>
<div class="value-wrap">
USD: {dat.USD}
</div>
<div class="value-wrap">
CAD: {dat.CAD}
</div>
<div class="value-wrap">
EUR: {dat.EUR}
</div>
<div class="value-wrap">
GBP: {dat.GBP}
</div>
</div>
</template>
Moreover, you can create a nested array of objects inside the data variable which can help to make the structure more generic.

async pipe filter for ngFor Loop Angular 2

I have a JSON array that i iterate through to display data in an NGfor Loop, I then want to apply filters after loading to refine results. The data is loaded asynchronously. So far that works, but my pipe filter just returns cannot read property of undefined. What am i doing wrong? I have simplified the component, by not including the http Request to get data, as well as the return logic on the pipe statement.
// component
import { Observable } from 'rxjs/Rx';
currentData:any = httpRequest; // Simplified
// html
<div *ngFor="let item of currentData | async | datePipe; let i = index">
//display content
</div>
// pipe
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'datePipe'
})
export class DatePipe implements PipeTransform {
transform(time:any []): any[] {
console.log(time);
return time.filter(it => it["createDt"] != -1); // Simplified
}
}
*Updated fix *
// html
<div *ngIf="currentData?.length > 0">
<div *ngFor="let item of currentData | datePipe; let i = index">
//display content
</div>
</div>
//pipe
transform(time:any): any {
for(let key of time){
console.log(key.somevalue);
}
}
// html
<div *ngIf="currentData?.length > 0">
<div *ngFor="let item of currentData | datePipe; let i = index">
//display content
</div>
</div>
//pipe
transform(time:any): any {
for(let key of time){
console.log(key.somevalue);
}
}

$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'}

Angular asynchronous search filter-- Expected array but received: {0}

Trying to enable a search input for a friends array, however these friends are being grabbed async, so I get this error Expected array but received: {0}---- because the array is empty when the filter loads.... is there anyway around this?
<span class="friendHeaders">Online Friends</span>
<input type="text" width="10%" class="friendSearch" placeholder="Search friends" ng-model="searchText"/>
<div class="friendScroll" scroll-glue-top>
<ul class="friendList">
<li ng-if='friend.online' ng-repeat="friend in friends track by $index | orderBy:'name' | filter:searchText" ng-click='startChat(friend)'>
<div ng-class='(friend.username === activeFriend.username) ? "activeFriendPanel" : ""' class='panel panel-default friendPanel'>
<span ng-if="friend.service === 'Locket'" class="glyphicon glyphicon-lock" aria-hidden="true"></span>
<span ng-if="friend.service === 'Facebook'" aria-hidden="true"><img class='icon' src='../../facebook.png'/></span>
<span ng-if="friend.service !== 'Locket' && friend.service !== 'Facebook'" class='friendService'>{{friend.service}}</span>
<span class='friendName'>{{friend.name}}</span>
<span class='friendArrow'><span class="glyphicon glyphicon-chevron-right vertical-center" aria-hidden="true"></span></span>
<div class="unreadMessage" ng-if="friend.unreadMessage">
New message
</div>
</div>
</li>
</ul>
</div>
here is some of the relevant controller code
the friends array is an array of objects
var keyResponseTimeout = 15000;
angular.module('Locket.chat', ['luegg.directives', 'ngAnimate'])
.controller('chatController', function ($scope, authFactory, $stateParams, socket, encryptionFactory, $timeout) {
console.log('chat');
authFactory.signedin().then(function(resp){
if (resp.auth === 'OK') {
socket.connect();
var keyring = encryptionFactory.generateKeyPair();
var publicKey;
// send public key to friends on login
keyring.then(function (keypair) {
publicKey = keypair.pubkey;
socket.emit('sendPGP', keypair.pubkey);
});
$scope.currentUser = $stateParams.username || resp.username;
$scope.friends = [];
$scope.sentRequest = false;
function createFriendObj(username, online, name, service) {
return {
service: service || 'Locket',
username: username,
name: name || (username + ' daawwggg'),
unreadMessage: false,
online: online || false,
key: null,
messages: [],
unsentMessages: [], // added this in for revoke and show decrypted message for sender
unsentFBMessages: [], // Follows same convention. Will not work for messages from prev session
sentKey: false
};
}
// Listen for events from our extension
window.addEventListener('message', function(event) {
if (event.source != window)
return;
// Recieve a facebook friends list
if (event.data.type && (event.data.type === 'facebookFriendsList')) {
for (var i = 0; i < event.data.text.length; i++) {
var friend = event.data.text[i];
var friendObj = createFriendObj(friend.username, true, friend.name, "Facebook");
$scope.friends.push(friendObj);
}
// After receiving a facebook friends list, begin monitoring the facebook DOM
window.postMessage({ type: 'scanFacebookDOM', text: ''}, '*');
}
Figured it out, the track by index interferes with the filter. I took it out and works
I think you're suppose to keep it track by $index for performance reasons. I read that it should just be moved to the end of the function
friend in friends | orderBy:'name' | filter:searchText track by $index

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>

Resources