angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.32k stars 6.72k forks source link

[Paginator] Allow 'All' option #8150

Open chris93007 opened 6 years ago

chris93007 commented 6 years ago

Feature request

What is the expected behavior?

Ability to provide "ALL" as page size option.[5,10,50, All]

What is the current behavior?

Page size options are fixed numbers.[5,10,50]

What is the use-case or motivation for changing an existing behavior?

Most paginators offer this function, therefore user expects it.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

All

vadost commented 6 years ago

@andrewseguin When are you planning to implement this functionality and are there any solutions?

andrewseguin commented 6 years ago

Its on our roadmap but we haven't yet planned it for implementation.

rramsey commented 6 years ago

This works, but isn't great: <mat-paginator [pageSizeOptions]="getPageSizeOptions()"></mat-paginator>

then in code:

getPageSizeOptions(): number[] {
  return [5, 10, 20, myTableDataSourceArray.length];
}

Labels for the entries would be nice, something like

[pageSizeOptions]="[5, 10, 20, 57]"  
[pageSizeLabels]="[5, 10, 20, ALL]"
jeff-a-andersen commented 5 years ago

I've implemented a solution locally, but I've never actually contributed to Material2 before. I've read through the CONTRIBUTING.md file and it says to search for existing issues. Doing so resulted in finding this issue so I would assume I should not create a new issue for my contribution. What are the appropriate next steps? Should I simply attach my contribution here for review? Thanks

jotatoledo commented 5 years ago

@jeff-a-andersen

  1. Fork this repo
  2. Clone your fork into your local machine
  3. Checkout a new branch in your clone
  4. Do the required changes in the new branch
  5. Commit and push to your fork
  6. Submit a pull request

Further details in the contributing guide

lnbxyz commented 5 years ago

Is this still planned?

jeff-a-andersen commented 5 years ago

I'm still planning to make the change, just keep getting pulled away for my daily work..

lebnic commented 5 years ago

+1

dchirgwin commented 5 years ago

+1 I need this feature as well. Using an ugly workaround for now.

MihhailSokolov commented 4 years ago

Has this been implemented yet?

jeff-a-andersen commented 4 years ago

I did make the changes to my copy of the material2 repository about a year ago. I didn't have time to jump through the hoops to get it released at the time. Since then, we've moved to using PrimeNG so my changes sat dormant because it was less of a priority for me. If you're interested in taking this up, you can see what I changed in the following commits:

https://github.com/jeff-a-andersen/material2/commit/5b185510f28c98d97b39f254c95ad5125b3f8c26 https://github.com/jeff-a-andersen/material2/commit/4b7f10fb102745eb2fd9fe587a25922d213dd1ba

If I can ever find time to jump back into it, I will.

dilyandimitrov commented 4 years ago

+1 I need this feature as well. Using an ugly workaround for now.

What kind of workaround are you using?

dchirgwin commented 4 years ago

+1 I need this feature as well. Using an ugly workaround for now.

What kind of workaround are you using? @dilyandimitrov I'm using a very large number to mean "all" e.g. [5, 10, 20, 1000] Obviously this is not clean, nice or safe. Have you seen the proposed solution by @rramsey above? To be honest, his workaround looks better than mine.

Lavdaft commented 4 years ago

Hey, guys... no update here yet?

kevinbischof commented 4 years ago

I used a solution from stackoverflow: https://stackoverflow.com/questions/47588162/how-to-add-all-option-for-angular-material-paginator It's similar to @rramsey 's solution, but the styling part was missing.

To make the solution work I needed to add encapsulation: ViewEncapsulation.None inside of @Component decorator. The only problem I got is that through ViewEncapsulation.None the "All" Option appears in every component. Yes, that's what ViewEncapsulation.None is made for. Does anybody have another solution to make he "All" thing work in a specific component without using ViewEncapsulation.None?

Lavdaft commented 4 years ago

Hey, @KevCanon . I did it, but not in a pretty good way, it worked tho. So, I changed directly the DOM by manipulating a high number (because I needed it to be translated according to the culture) and I used the pageEvent:


    this.textALL = this.translate.instant('All');
     addOptionAllToPaginator(): void {
        const element: NodeListOf<Element> = this.window.document.querySelectorAll('[role="option"]');

        if (element !== null && element !== undefined) {
            for (let i = 0; i < element.length; i++) {
                const optText = element[i].getElementsByClassName('mat-option-text');

                if (optText !== null && optText !== undefined && optText.length === 1) {
                    const text: HTMLSpanElement = optText[0] as HTMLSpanElement;
                    const textWithoutSpace = text.innerText.replace(/\s/g, '');
                    if (Number(textWithoutSpace) && textWithoutSpace === '9999') {
                        text.innerHTML = this.textALL;
                    }
                }
            }
        }
    }

    pageEventChanged(pageEvent: PageEvent): void {
        if (this.previousPageSize !== pageEvent.pageSize) {
            const ariaLabelTranslated: string = this.paginator._intl.itemsPerPageLabel;
            const element: NodeListOf<Element> = this.window.document.querySelectorAll(`[aria-label="${ariaLabelTranslated}"]`);

            if (element !== null && element !== undefined) {
                for (let i = 0; i < element.length; i++) {
                    const optText: HTMLCollectionOf<Element> = element[i].getElementsByClassName('mat-select-value-text');

                    if (optText !== null && optText !== undefined && optText.length === 1) {
                        const text: HTMLSpanElement = optText[0] as HTMLSpanElement;

                        if (Number(text.innerText) && pageEvent.pageSize === 9999) {
                            text.innerHTML = this.textALL;
                        } else {
                            text.innerHTML = pageEvent.pageSize.toString();
                        }
                    }
                }
            }

            this.previousPageSize = pageEvent.pageSize;
        }
    }

    getPageSizeOptions(): number[] {
        const pageSizeOptions: number[] = [5, 10, 25];

        if (this.templates !== null && this.templates.length > 0) {
            pageSizeOptions.push(9999);
            this.addOptionAllToPaginator();
        }

        return pageSizeOptions;
    }```
vadost commented 4 years ago

@andrewseguin I've been waiting for this implementation for 2 years, when the update?))

dbgod commented 4 years ago

I submitted #20367 based on @jeff-a-andersen's solution... fingers crossed!

dbgod commented 4 years ago

I submitted a more focus solution: #20397.

fasfsfgs commented 3 years ago

@Lavdaft I'm using a similar approach but it broke somehow after upgrading angular 8 to 11. We are now facing an issue when the default value is already "All". Did you manage to make your solution work in this scenario?

When the default is already "All", we show 9999 in the text selected.

And if we force the selected text to "All", it never gets back to a number even after further selections.

dbgod commented 3 years ago

My solution does work as expected even when you start with a length that is equal to your page size (with label "All"). For example: in my typescript I set the following properties:

  length = 100;
  pageSize = 100;
  pageIndex = 0;
  pageSizeOptions: {[key: string]: string} = {5: '5', 10: '10', 25: '25,' 100: 'All'};

And in my HTML:

  <mat-paginator #paginator
                 (page)="handlePageEvent($event)"
                 [length]="length"
                 [pageSize]="pageSize"
                 [pageSizeOptions]="showPageSizeOptions ? pageSizeOptions : {}"
                 [pageIndex]="pageIndex">
  </mat-paginator>

In this scenario, the page size will be defaulted to "All", and I can successfully change it to 5, 10 or 25.

I developed the solution on Angular 10.

Katia-Ch commented 3 years ago

My solution does work as expected even when you start with a length that is equal to your page size (with label "All"). For example: in my typescript I set the following properties:

  length = 100;
  pageSize = 100;
  pageIndex = 0;
  pageSizeOptions: {[key: string]: string} = {5: '5', 10: '10', 25: '25,' 100: 'All'};

And in my HTML:

  <mat-paginator #paginator
                 (page)="handlePageEvent($event)"
                 [length]="length"
                 [pageSize]="pageSize"
                 [pageSizeOptions]="showPageSizeOptions ? pageSizeOptions : {}"
                 [pageIndex]="pageIndex">
  </mat-paginator>

In this scenario, the page size will be defaulted to "All", and I can successfully change it to 5, 10 or 25.

I developed the solution on Angular 10.

What is "showPageSizeOptions"? Can you show its code?

dbgod commented 3 years ago

showPageSizeOptions is simply a boolean property found in dev-app contained in the the Angular Material components source code.

You can download the source code and run dev-app to see demos of the components with the ability to adjust various options on the fly. In the case of mat-paginator, the `showPageSizeOptions in the paginator-demo.ts file supports a toggle in the paginator demo page that allows you to show/hide the dropdown list of page size options.

Katia-Ch commented 3 years ago

dbgod, thank you, I viewed this document. I need to do label for page size option, that shows all elements. I can do it with variable that shows number on front-end, but I need to show label "All".

Your decision - "pageSizeOptions: {[key: string]: string} = {5: '5', 10: '10', 25: '25', 100: 'All'};" looks good and this is that what I looked for. But it doesn't work with my code. I work with Angular 11, not sure that it's matter in this case.

Here is my code, maybe you can see and tell what is wrong:

HTML:

<mat-paginator
            (page)="pageEvent = $event; onPaginateChange($event)"
            [length]="numberPatientTests"
            [pageSize]="defaultTestsPerPage"
            [disabled]="disabled"
            [showFirstLastButtons]="showFirstLastButtons"
            [pageSizeOptions]="showPageSizeOptions ? pageSizeOptions : {}"
            [hidePageSize]="hidePageSize"
            [pageIndex]="pageIndex">
</mat-paginator>

TS:

numberPatientTests = this.tests.length;
  defaultTestsPerPage: any = 5;
  filteredTests: any[] = [];
  pageSizeOptions: {[key: string]: string} = {5: '5', 10: '10', 25: '25', 100: '100', 1000: 'All'};
  pageIndex = 0;
  pageEvent: PageEvent;
  hidePageSize = false;
  showPageSizeOptions = true;
  showFirstLastButtons = true;
  disabled = false;

onPaginateChange(data: PageEvent) {
    this.filteredTests = this.tests.slice(data.pageIndex*data.pageSize, data.pageIndex*data.pageSize + data.pageSize);
  }
michaelfaith commented 2 years ago

Is there any movement on this, by chance? I've had some teams asking for this recently, and the possible workarounds don't really work very well. Doesn't seem to be a straightforward way to accomplish this use case, as it stands today.

carlitomurta commented 2 years ago

I made a not most beautiful solution, but it works perfectly. My API accept 999 number as 'All', so I just change the styles to make it shows the 'All' text on the component.

styles.css

mat-option[ng-reflect-value='999']:before {
  content: 'All';
  float: left;
  text-transform: none;
  top: 0px;
  position: relative;
}
mat-option[ng-reflect-value='999'] .mat-option-text {
  display: none;
  position: relative;
}
mat-select[ng-reflect-value='999'] .mat-select-value-text span {
  display: none;
}
mat-select[ng-reflect-value='999'] .mat-select-value-text:after {
  content: 'All';
}

pageSize

pageSize = [5,10,25,50,100,999]
Lavdaft commented 2 years ago

The problem with all those solutions is that the "ALL" is hardcoded. We need a solution that accepts translation/culture change...

der-andy commented 2 years ago

I made a not most beautiful solution, but it works perfectly. My API accept 999 number as 'All', so I just change the styles to make it shows the 'All' text on the component.

styles.css

mat-option[ng-reflect-value='999']:before {
  content: 'All';
  float: left;
  text-transform: none;
  top: 0px;
  position: relative;
}
mat-option[ng-reflect-value='999'] .mat-option-text {
  display: none;
  position: relative;
}
mat-select[ng-reflect-value='999'] .mat-select-value-text span {
  display: none;
}
mat-select[ng-reflect-value='999'] .mat-select-value-text:after {
  content: 'All';
}

pageSize

pageSize = [5,10,25,50,100,999]

Unfortunately, ng-reflect-value isn't available in a production environment. I'm still searching for a way to select the "all" option in CSS without using ng-reflect-value and without using :last-child which applies changes to all select components.

A translatable version would be great, but for now, I'd even be happy with a CSS hack like this that works in production.

malciin commented 2 years ago

I think I managed a temporarily solution that should work also for translatable "All" label. It works by wrapping some of private methods and direct DOM manipulation.

const allValue = 999999;
const allValueLabel = 'All'; // or inject i18n service and get translation of "All" in directive constructor

@Directive({
  selector: '[appAddAllToPaginator]',
})
export class AddAllToPaginator {
  public constructor(private readonly host: MatPaginator, private readonly elRef: ElementRef) {
    // @ts-ignore
    const proxiedUpdateDisplayedPageSizeOptions = host._updateDisplayedPageSizeOptions.bind(host);
    // @ts-ignore
    host._updateDisplayedPageSizeOptions = () => {
      proxiedUpdateDisplayedPageSizeOptions();
      // @ts-ignore
      const displayedPageSizeOptions = host._displayedPageSizeOptions;

      if (!displayedPageSizeOptions) return;

      const newDisplayedPageSizeOptions = [
        ...displayedPageSizeOptions.filter((x) => x !== allValue),
        allValueLabel,
      ];

      // @ts-ignore
      host._displayedPageSizeOptions = newDisplayedPageSizeOptions;
      // @ts-ignore
      host._changeDetectorRef.markForCheck();
    };

    const proxiedChangePageSize = host._changePageSize.bind(host);
    host._changePageSize = (v) => {
      // converting to number if value is string. We need to do so because whole paging logic depend that pageSize is a number.
      // @ts-ignore
      if (v === allValueLabel) v = allValue;

      proxiedChangePageSize(v);
      elRef.nativeElement.querySelector('.mat-select-value').innerText = v === allValue ? allValueLabel : v;
    };
  }
}

Usage:

<!-- we dont need to modify pageSizeOptions. 'All' option is appended automatically -->
<mat-paginator
  [length]="10000"
  [pageSize]="10"
  [pageSizeOptions]="[5, 10, 25, 100]"
  aria-label="Select page"
  appAddAllToPaginator>
</mat-paginator>

Working demo: https://stackblitz.com/edit/angular-wy4dmj?file=src%2Fapp%2Fadd-all-to-paginator.directive.ts

It should be also easily convertible to also use some lookup ({ [id: number]: string }) to translate each label entry in case if you need to translate other labels also.

Note that that solution can easily break up if someone changes private methods names, but that should be easily fixable. Tested on angular material 14.1.1 & 12.0

sudhagarmsd5 commented 2 years ago

I think I managed a temporarily solution that should work also for translatable "All" label. It works by wrapping some of private methods and direct DOM manipulation.

const allValue = 999999;
const allValueLabel = 'All'; // or inject i18n service and get translation of "All" in directive constructor

@Directive({
  selector: '[appAddAllToPaginator]',
})
export class AddAllToPaginator {
  public constructor(private readonly host: MatPaginator, private readonly elRef: ElementRef) {
    // @ts-ignore
    const proxiedUpdateDisplayedPageSizeOptions = host._updateDisplayedPageSizeOptions.bind(host);
    // @ts-ignore
    host._updateDisplayedPageSizeOptions = () => {
      proxiedUpdateDisplayedPageSizeOptions();
      // @ts-ignore
      const displayedPageSizeOptions = host._displayedPageSizeOptions;

      if (!displayedPageSizeOptions) return;

      const newDisplayedPageSizeOptions = [
        ...displayedPageSizeOptions.filter((x) => x !== allValue),
        allValueLabel,
      ];

      // @ts-ignore
      host._displayedPageSizeOptions = newDisplayedPageSizeOptions;
      // @ts-ignore
      host._changeDetectorRef.markForCheck();
    };

    const proxiedChangePageSize = host._changePageSize.bind(host);
    host._changePageSize = (v) => {
      // converting to number if value is string. We need to do so because whole paging logic depend that pageSize is a number.
      // @ts-ignore
      if (v === allValueLabel) v = allValue;

      proxiedChangePageSize(v);
      elRef.nativeElement.querySelector('.mat-select-value').innerText = v === allValue ? allValueLabel : v;
    };
  }
}

Usage:

<!-- we dont need to modify pageSizeOptions. 'All' option is appended automatically -->
<mat-paginator
  [length]="10000"
  [pageSize]="10"
  [pageSizeOptions]="[5, 10, 25, 100]"
  aria-label="Select page"
  appAddAllToPaginator>
</mat-paginator>

Working demo: https://stackblitz.com/edit/angular-wy4dmj?file=src%2Fapp%2Fadd-all-to-paginator.directive.ts

It should be also easily convertible to also use some lookup ({ [id: number]: string }) to translate each label entry in case if you need to translate other labels also.

Note that that solution can easily break up if someone changes private methods names, but that should be easily fixable. Tested on angular material 14.1.1 & 12.0

Works Like charm @malciin Thankyou for your effort and now i understood the real power of angular built-in features like directives, pipes etc,

in my scenario image i have a input field to search items in the table and the input field is default state is disabled when the user clicks the length of the datasource in the paginator then only search option will work <input [disabled]="paginator.pageSize == dataSource.data.length? false : true" matInput (keyup)="applyFilter($event)"

input

      />

i have used your custom directive code and used the Input decorator on it @Input() allValue:number; and in the paginator used these. <mat-paginator [pageSizeOptions]="[5, 10, 20]" [pageSize]="5" [allValue]="dataSource.data.length" appAddAllToPaginator

michaelfaith commented 2 years ago

With what you've solutioned here, is there any learnings that might feed into a possible contribution to the library to address it for everyone? e.g. a prop on the paginator to enable it natively, without the need for a separate directive

Kiro1232 commented 1 year ago

The code implementation seems to be functioning well for my requirements. When dealing with multiple paginators (2 or 3), I can effectively incorporate an 'All' option within the pagination selection. Importantly, this customization doesn't adversely affect the mat-option elements.

After thorough testing, it's evident that this solution addresses the intended functionality while preserving the integrity of the mat-option elements. //In the ts file ,

   import { Component, OnInit, ViewChild,ElementRef } from '@angular/core';
  import { MatPaginator } from '@angular/material/paginator';

    @ViewChild('Table1paginator') Table1paginator: MatPaginator;
    @ViewChild('Table2paginator') Table2paginator: MatPaginator;

    @ViewChild('Table1PaginatorElement',{ read: ElementRef })
   Table1PaginatorHtmlElement: ElementRef;

    @ViewChild('Table2PaginatorElement',{ read: ElementRef })
    Table2PaginatorHtmlElement: ElementRef;

In the .html file,

//For the 1st paginator

     <mat-paginator (page)="Table1PaginatorHtmlElement.nativeElement.querySelector
                          ('div.mat-select-value > span > span').innerHTML = 
                          Table1paginator.pageSize == 2147483647? 'All': Table1paginator.pageSize" 
               #Table1PaginatorElement 
               #Table1paginator
               [length]="dataSourceOfapprovedRevenueData.data.length" 
               [pageSizeOptions]="[5, 10, 25, 50, 100, 2147483647]" 
               aria-label="Select page of users"
               showFirstLastButtons
               [pageSize]="5">
      </mat-paginator>

// For the 2nd Paginator

   <mat-paginator (page)="Table2PaginatorHtmlElement.nativeElement.querySelector
                          ('div.mat-select-value > span > span').innerHTML = 
                          Table2paginator.pageSize == 2147483647? 'All': Table2paginator.pageSize"
               #Table2PaginatorElement 
               #Table2paginator
               [length]="dataSource.data.length" 
               [pageSizeOptions]="[5, 10, 25, 50, 100, 2147483647]" 
               aria-label="Select page of users"
               showFirstLastButtons
               [pageSize]="5">
   </mat-paginator>

In the css File ,

  ::ng-deep .mat-select-panel[aria-label="Items per page:"] mat-option:last-child::before { 
    content: 'All';
    position: relative;
    display: block;
  }

  ::ng-deep .mat-select-panel[aria-label="Items per page:"] mat-option:last-child .mat-option-text {
    display: none;
  }
kapil26021994 commented 2 months ago

hi @malciin I have used ur code and its working well but in 1 case i got below issue:

Suppose in pageSizeOptions option will be [10,50,100,All] but if user select 10 from itemPerPage and total Item length also 10 in such case from pageSizeOptions 10 will be remove from array can you let me how to fix this issue ?