components icon indicating copy to clipboard operation
components copied to clipboard

[Paginator] Allow 'All' option

Open chris93007 opened this issue 6 years ago • 28 comments

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

chris93007 avatar Oct 31 '17 11:10 chris93007

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

vadost avatar May 03 '18 12:05 vadost

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

andrewseguin avatar May 09 '18 22:05 andrewseguin

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]"

rramsey avatar Aug 21 '18 19:08 rramsey

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

jeff-a-andersen avatar Oct 17 '18 18:10 jeff-a-andersen

@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

jotatoledo avatar Oct 18 '18 09:10 jotatoledo

Is this still planned?

lnbxyz avatar Dec 07 '18 11:12 lnbxyz

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

jeff-a-andersen avatar Dec 07 '18 17:12 jeff-a-andersen

+1

lebnic avatar Apr 17 '19 01:04 lebnic

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

dchirgwin avatar May 09 '19 09:05 dchirgwin

Has this been implemented yet?

MihhailSokolov avatar Dec 12 '19 16:12 MihhailSokolov

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.

jeff-a-andersen avatar Dec 12 '19 16:12 jeff-a-andersen

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

What kind of workaround are you using?

dilyandimitrov avatar Jan 04 '20 12:01 dilyandimitrov

+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.

dchirgwin avatar Jan 04 '20 16:01 dchirgwin

Hey, guys... no update here yet?

Lavdaft avatar Apr 15 '20 16:04 Lavdaft

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?

kevinbischof avatar May 12 '20 09:05 kevinbischof

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;
    }```



Lavdaft avatar Jun 04 '20 14:06 Lavdaft

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

vadost avatar Jul 21 '20 09:07 vadost

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

dbgod avatar Aug 19 '20 23:08 dbgod

I submitted a more focus solution: #20397.

dbgod avatar Aug 23 '20 22:08 dbgod

@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.

fasfsfgs avatar Mar 02 '21 19:03 fasfsfgs

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.

dbgod avatar Mar 03 '21 01:03 dbgod

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?

Katia-Ch avatar Mar 11 '21 21:03 Katia-Ch

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.

dbgod avatar Mar 12 '21 00:03 dbgod

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);
  }

Katia-Ch avatar Mar 12 '21 09:03 Katia-Ch

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.

michaelfaith avatar May 20 '22 12:05 michaelfaith

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]

carlitomurta avatar Jul 06 '22 23:07 carlitomurta

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

Lavdaft avatar Jul 07 '22 14:07 Lavdaft

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.

der-andy avatar Jul 26 '22 13:07 der-andy

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

malciin avatar Aug 05 '22 11:08 malciin

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 >

sudhagarmsd5 avatar Aug 20 '22 06:08 sudhagarmsd5