How to find the count of items in an ngFor after the pipes have been applied
Asked Answered
T

9

29

I have an ngFor creating rows in a table that is both filtered and paged.

<tr *ngFor="#d of data.results | filter:filterText | pagination:resultsPerPage:currentPage">

There is another element on the page that displays the number of records displayed. These elements are initially bound to the data.Results' length.

How do I get the length of the data that is displayed after the filter pipe has been applied so that I can display it correctly. None of the provided local variables in ngFor seem to account for this.

Trawl answered 17/3, 2016 at 23:45 Comment(1)
So I had an answer written here but I don't think it will work the way I think it was going to. So I will just put it in a comment and hopefully someone smarter than me can help you! I was thinking you could do something like this here jilles.me/ng-filter-in-angular2-pipes and then watch for an onChange event in your constructor, and then filter data.results and do a count on it whenever filterText changes. You'd essentially filter twice, and that seems really hacky.Spillage
J
10

You can get the count of the items by transforming the array within a pipe.

The idea is that the pipe would transform the array into another array where each element has an item property, and a parent property representing the filtered (or original) array:

@Pipe({ name: 'withParent', pure: false })
export class WithParentPipe implements PipeTransform {
    transform(value: Array<any>, args: any[] = null): any {

        return value.map(t=> {
            return {
                item: t,
                parent: value
            }
        });
    }
} 

Here is how it would be used:

 <tr *ngFor="#d of data.results | 
        filter:filterText |
        pagination:resultsPerPage:currentPage | 
        withParent">
        Count:  {{d.parent.length }}
        Item:  {{ d.item.name}}
 </tr>
Junko answered 18/3, 2016 at 6:30 Comment(5)
I am seeing the data in the interpolated Count string, great! How do I get this data back in my component's .ts file to use with other controls? I need to get it outside the scope of the *ngFor.Trawl
I tried using a solution like this <span>{{setLocalVariable(d.parent.length)}}</span> from here to get the .parent.length elevated out of the ngFor's scope. This approach worked but the setLocalVariable function was called endlessly within the loop even when rendering should have been completed. There didn't appear to be any change detection occuring so I wasn't sure why the ngFor appeared to be executing forever.Trawl
@Trawl I don't think that's possible with this approach. I hope angular adds some way to define local template variables based on filtering (similar to index, but custom). And also a way to specify that it be available outside of iteration scope. This would be a very useful feature to take care of some common use cases with ngFor.Junko
@pixelbits Hope someone has created a feature request issue on angular github. This is a very much needed feature.Madura
this solution won't help if you need to get it inside the component.ts file.Enlarge
L
16

One way is to use template variables with @ViewChildren()

<tr #myVar *ngFor="let d of data.results | filter:filterText | pagination:resultsPerPage:currentPage">
@ViewChildren('myVar') createdItems;

ngAfterViewInit() {
  console.log(this.createdItems.toArray().length);
}
Lesbian answered 18/3, 2016 at 5:16 Comment(3)
This is very close to what I need, thank you. Unfortunately since the paging is lazy-loading the data I always get the .length of the first page only.Trawl
With this approach you can only get elements that are actually created in the DOM. @pixelbits approach might be a better fit then.Postnatal
also had to use ngAfterViewChecked() { this.cdRef.detectChanges(); }Ionone
P
15

I found the simplest solution to be the following:

  1. In your component: add a field that will hold the current count
  filterMetadata = { count: 0 };
  1. In your template: add the filterMetadata as a parameter to the filter pipe
  <tr *ngFor="#d of data.results | filter:filterText:filterMetadata | pagination:resultsPerPage:currentPage">
  1. interpolate filterMetadata.count into the element that displays the number of records displayed.
  <span> {{filterMetadata.count}} records displayed</span>
  1. In the filter pipe, update the filterMetadata.count field when done with filtering
  transform(items, ..., filterMetadata) {
    // filtering
    let filteredItems = filter(items);

    filterMetadata.count = filteredItems.length;
    return filteredItems;
  }

This solution still uses pure pipes, so there are no performance degradations. If you have multiple pipes that do filtering, the filterMetadata should be added as a parameter to all of them because angular stops calling the pipe sequence as soon as the a pipe returns an empty array, so you can't just add it to the first or last filtering pipe.

Phlox answered 28/2, 2018 at 21:36 Comment(1)
This is the only way I got it working with Angular 8. Very nice and clean solution.Miner
J
10

You can get the count of the items by transforming the array within a pipe.

The idea is that the pipe would transform the array into another array where each element has an item property, and a parent property representing the filtered (or original) array:

@Pipe({ name: 'withParent', pure: false })
export class WithParentPipe implements PipeTransform {
    transform(value: Array<any>, args: any[] = null): any {

        return value.map(t=> {
            return {
                item: t,
                parent: value
            }
        });
    }
} 

Here is how it would be used:

 <tr *ngFor="#d of data.results | 
        filter:filterText |
        pagination:resultsPerPage:currentPage | 
        withParent">
        Count:  {{d.parent.length }}
        Item:  {{ d.item.name}}
 </tr>
Junko answered 18/3, 2016 at 6:30 Comment(5)
I am seeing the data in the interpolated Count string, great! How do I get this data back in my component's .ts file to use with other controls? I need to get it outside the scope of the *ngFor.Trawl
I tried using a solution like this <span>{{setLocalVariable(d.parent.length)}}</span> from here to get the .parent.length elevated out of the ngFor's scope. This approach worked but the setLocalVariable function was called endlessly within the loop even when rendering should have been completed. There didn't appear to be any change detection occuring so I wasn't sure why the ngFor appeared to be executing forever.Trawl
@Trawl I don't think that's possible with this approach. I hope angular adds some way to define local template variables based on filtering (similar to index, but custom). And also a way to specify that it be available outside of iteration scope. This would be a very useful feature to take care of some common use cases with ngFor.Junko
@pixelbits Hope someone has created a feature request issue on angular github. This is a very much needed feature.Madura
this solution won't help if you need to get it inside the component.ts file.Enlarge
P
10

That is not exactly the purpose of the original question, but I was also looking for a way to display the count of items once that all pipes have been applied. By combining the index and last values provided by ngFor, I found this other solution :

<div *ngFor="#item of (items | filter); #i = index; #last = last">
...
  <div id="counter_id" *ngIf="last">{{index + 1}} results</div>
</div>
Prandial answered 15/5, 2016 at 21:42 Comment(1)
in angular 4 the following worked for me: index as i; last as lastEnlarge
B
6

I came across the same problem, although @bixelbits 's answer was approved, but I didn't find it ideal, specially for large data.

Instead of returning the original array in each element, I believe it's better just avoid Pipes for this problem, at least with the current Angular 2 implementation (rc4).

A better solution would be using normal component's function to filter the data, something likes bellow:

// mycomponent.component.ts  
filter() {
  let arr = this.data.filter(
      // just an example
      item => item.toLowerCase().includes(
        // term is a local variable I get it's from <input> 
        this.term.toLowerCase()
      )
    );
  this.filteredLength = arr.length;
  return arr;
}

Then, in the template:

<ul>
  <li *ngFor="let el of filter()"> 
    {{ el | json }}
  </li>
</ul>
// mycomponent.component.html
<p > There are {{ filteredLength }} element(s) in this page</p>

Unless you really want to use Pipes, I would recommend you to avoid them in situations like the above example.

Beauchamp answered 29/7, 2016 at 17:35 Comment(0)
O
4

So I found a workaround for this.

I created a pipe which takes an object reference and updates a property with the count currently passing through the pipe.

@Pipe({
    name: 'count'
})

export class CountPipe implements PipeTransform {
    transform(value, args) {
        if (!args) {
            return value;
        }else if (typeof args === "object"){

            //Update specified object key with count being passed through
            args.object[args.key] = value.length;

            return value;

        }else{
            return value;
        }
    }
}

Then in your view link up a pagination component like so.

pagination-controls(#controls="", [items]="items.length", (onChange)="o") 

tbody
    tr(*ngFor=`let item of items
        | filter_pipe: { .... }
        | count: { object: controls , key: 'items' }
        | pagination_pipe: { ... } `)

Once that count property is extracted from the pipe either to the current component or a child component you can do anything with it.

Ostiary answered 7/6, 2016 at 17:5 Comment(0)
C
2

In my case i needed to run through the filtered elements and run some analysis.

My Solutions is to simply pass in a function and return the array on the last pipe. You could also just create a pipe especially for this but i have added it to my last pipe in the filters:

HTML

<divclass="card" *ngFor="let job of jobs | employee:jobFilter.selectedEmployee | managerStatus:jobFilter.selectedStatus | dateOrder:jobFilter"> 

Component

this.jobFilter = {
  jobStatuses: {},  // status labels
  ordering: 'asc',
  selectedEmployee: {},
  selectedStatus: {}, // results status
  fn: this.parseFilteredArr
};

parseFilteredArr(arr) {
  console.log(arr);
}

Pipe

import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
  name: 'dateOrder'
})
export class DateOrderPipe implements PipeTransform {
 transform(value: any, args?: any): any {
   const arr = Array.isArray(value)
    ? value.reverse()
    : value;
  args.fn(arr);
  return arr;
}
}

As you can see i have called the function in the pipe

args.fn(arr);

and now can process it in the controller.

Convince answered 14/4, 2017 at 15:9 Comment(0)
C
0

Assume your ngFor looks something like:

<div #cardHolder>
    <app-card *ngFor="let card of cards|pipeA:paramX|pipeB:paramY"></app-card>
</div>

Then in your component you may use something like:

 get displayedCards() : number {
    let ch = this.cardHolder.nativeElement;

    // In case cardHolder has not been rendered yet...
    if(!ch)
      return 0;

    return ch.children.length;

  }

Which you may display in your view by simple interpolation

{{displayedCards}}

Advantages include not needing to modify the pipes to return additional data.

Chaffin answered 14/2, 2020 at 12:7 Comment(0)
R
0
  1. What worked for me is:

    • Don't use pipes, few months later you will not be able to tell what they mean nor figure out the weird syntax.

    • Frameworks, here Angular, are ok, but to a certain point, keep the template simple ngFor binding to an array of your data. Going beyond that means you will get tangled in particular framework peculiar syntax and (changing) mechanisms. (this explain why we have this post/question which should not exist in the first place)

    • HTML template is meant for layout keep it as such. All logic, data filtering, etc... should be kept in the code behind in straightforward classes.

  2. Simply make a filter method in your component or service and call it to filter your data.

  3. Expose a .Count prop on your component/service to display your actual filtered data dynamic count (ie. typically .length).
Redevelop answered 8/5, 2020 at 12:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.