Components: virtual-scroll inside mat-select new values not visible & big space

Created on 12 Sep 2018  路  13Comments  路  Source: angular/components

Bug, feature request, or proposal:

Bug

What is the expected behavior?

cdk-virtual-scroll-viewport inside a mat-select or mat-autocomplete should align with the overlay panel.

<mat-form-field>
    <mat-select placeholder="State">
    <cdk-virtual-scroll-viewport [itemSize]="10">
          <mat-option *cdkVirtualFor="let state of states" [value]="state">
             {{state}}
          </mat-option>
    </cdk-virtual-scroll-viewport>
   </mat-select>
</mat-form-field>

What is the current behavior?

When scrolled cdkVirtualFor does not seem to add more elements and moreover shows a big space from last element of the initial set of items to the end.

What are the steps to reproduce?

https://stackblitz.com/edit/angular-h4xptu-dgjd87?file=app/select-reset-example.html

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

  • Occasionally I run into scenarios where the items in the list are too many and makes the page unresponsive.

  • Seem-less integration of cdk-virtual-scroll-viewport with other components.

  • mat-select essentially can handle any number of elements when this works.

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

Angular CLI: 6.1.5
Node: 10.0.0
OS: win32 x64
Angular: 6.1.4
... animations, common, compiler, core, forms, http
... platform-browser, platform-browser-dynamic, router

Package                            Version
------------------------------------------------------------
@angular-devkit/architect          0.7.5
@angular-devkit/build-angular      0.7.5
@angular-devkit/build-ng-packagr   0.7.5
@angular-devkit/build-optimizer    0.7.5
@angular-devkit/build-webpack      0.7.5
@angular-devkit/core               0.7.5
@angular-devkit/schematics         0.7.5
@angular/cdk                       6.4.7
@angular/cdk-experimental          6.4.7
@angular/cli                       6.1.5
@angular/compiler-cli              6.1.7
@angular/flex-layout               6.0.0-beta.17
@angular/language-service          6.1.7
@angular/material                  6.4.7
@ngtools/webpack                   6.1.5
@schematics/angular                0.7.5
@schematics/update                 0.7.5
rxjs                               6.2.2
typescript                         2.9.2
webpack                            4.9.2

Is there anything else we should know?

Great work

P3 cdscrolling materiaform-field materiaselect

Most helpful comment

Also cdk-virtual-scroll-viewport inside a mat-menu doesn't work properly. Sometimes, blank space appears with mouse wheel scrollling.
StackBlitz

All 13 comments

Workaround:-

https://stackblitz.com/edit/angular-h4xptu-zyj4yh?file=styles.css

Added class virtual-scroll to mat-select

<mat-select placeholder="State" class="virtual-scroll">

and add global styles as below:-

In styles.css add

.mat-select-panel.virtual-scroll {
    max-height: 100% !important;
    overflow: inherit !important;
}
.mat-select-panel .cdk-virtual-scroll-viewport {
   max-height: 240px !important;
}

.mat-select-panel .cdk-virtual-scroll-content-wrapper {
    position: inherit !important;  
    top: inherit !important;  
    left: 0; 
}

I think the real solution would be that somehow mat-select-panel ideally starts behaving like cdk-virtual-scroll-viewport which is what this workaround crudely tries to do in totality by negating scrolling properties of mat-select-panel and having cdk-virtual-scroll-viewport take charge.

Not quite the workaround above: The above workaround has problems when the overlay is closed and reopened and scrolled in between.

When scrolled to a few elements down and the select panel is reopened next time we see a blank space above.

This is solved by setRenderedRange to the initial range which gets set initially on the scrolledIndexChange event.
Then further when the mat-select emits openedChange on close we reset the rendered range to the initialRange that we preserved upfront.

https://stackblitz.com/edit/angular-h4xptu-kuu5xg?file=app%2Fselect-reset-example.ts

  scrolledIndexChange($event) {
    if (!this.initialRange) {
      this.initialRange = this.cdkVirtualScrollViewport.getRenderedRange();
    }

  }
  openedChange(opened) {
    if (!opened) {
      this.cdkVirtualScrollViewport.setRenderedContentOffset(0);
      this.cdkVirtualScrollViewport.setRenderedRange(this.initialRange)
    } 
  }

Another issue with mat-select (for anyone working on this):-

If the currently selected value is not in the mat-options the SelectionModel fails because the world according it is limited to the options query which may not have this options if it is out of range.

Workaround:

Supplement additional mat-option in addition to mat-option that has cdkVirtualScroll.

<mat-option *ngIf="selectCtrl?.value" [value]="selectCtrl?.value"></mat-option>

Related to https://github.com/angular/material2/issues/10122

@yogeshgadge Thanks for posting these findings, good stuff to know.

workarround for mat select using @yogeshgadge s approach with a form control

(css in stylus syntax)

<ng-container *ngIf="isMultiple">
        <mat-option class="selected-options-bottom" *ngFor="let option of control.value" [value]="option">{{option.caption}}</mat-option>
      </ng-container>
      <ng-container *ngIf="!isMultiple">
        <mat-option class="selected-options-bottom" *ngIf="control.value" [value]="control.value">{{control.value.caption}}</mat-option>
      </ng-container>

with css:

.selected-options-bottom
   visibility hidden
   position absolute

giving the mat-select a panelClass, e.g. custom-mat-select with

::ng-deep .mat-select-panel.custom-mat-select

  overflow hidden
  max-width 280px

I just gave the example in the issue desription a quick look - itemSize should be the height of individual items, not the length of the array? Setting a height on the element + [itemSize]=48 works (albeit, it looks a bit crap when there are less than 4 options).

Also cdk-virtual-scroll-viewport inside a mat-menu doesn't work properly. Sometimes, blank space appears with mouse wheel scrollling.
StackBlitz

@StefanoLucchi Same thing happen to me. And when you reopen the menu it's empty

@StefanoLucchi @justindiaw I had the same problem, but helped me increasing minBufferPx and maxBufferPx on cdk-virtual-scroll-viewport to eg. 300 and 600

A slight change to @yogeshgadge's solution and changing the buffers as suggested by @kubex320 solved all the white space issues.

    <mat-select
      (openedChange)="onOpenedChange($event)"
    >
      <cdk-virtual-scroll-viewport
        [itemSize]="optionSizePx"
        [style.height.px]="numOptionsToShow * optionSizePx"
        [minBufferPx]="300"
        [maxBufferPx]="600"
      >
        <mat-option
          *cdkVirtualFor="let item of listOptions | async; let index = index"
          [value]="item.value"
          (onSelectionChange)="onSelectionChange(index)"
        >
          {{item.viewValue}}
        </mat-option>
      </cdk-virtual-scroll-viewport>
    </mat-select>
  @Input()
  optionSizePx = 48;

  @Input()
  numOptionsToShow = 5;

  @ViewChild(CdkVirtualScrollViewport, {static: true})
  cdkVirtualScrollViewport: CdkVirtualScrollViewport;

  selectedIndex: number;

  onOpenedChange(isOpen) {
    if (isOpen && this.selectedIndex) {
      this.cdkVirtualScrollViewport.setRenderedContentOffset(0);
      this.cdkVirtualScrollViewport.scrollToIndex(this.selectedIndex);
    }
  }

  onSelectionChange(i: number) {
    this.selectedIndex = i;
  }

Not quite the workaround above: The above workaround has problems when the overlay is closed and reopened and scrolled in between.

When scrolled to a few elements down and the select panel is reopened next time we see a blank space above.

This is solved by setRenderedRange to the initial range which gets set initially on the scrolledIndexChange event.
Then further when the mat-select emits openedChange on close we reset the rendered range to the initialRange that we preserved upfront.

https://stackblitz.com/edit/angular-h4xptu-kuu5xg?file=app%2Fselect-reset-example.ts

  scrolledIndexChange($event) {
    if (!this.initialRange) {
      this.initialRange = this.cdkVirtualScrollViewport.getRenderedRange();
    }

  }
  openedChange(opened) {
    if (!opened) {
      this.cdkVirtualScrollViewport.setRenderedContentOffset(0);
      this.cdkVirtualScrollViewport.setRenderedRange(this.initialRange)
    } 
  }

Exactly what I was looking for. Thank you. I was wondering why the panel seems to show white div space when scroll in between and close and reopen again.

Thanks to those who shared workarounds - it's very helpful! Anyone got the keyboard navigation (up/down arrow keys to browse through the options) working with virtual scrolling? It doesn't scroll when I use keyboard and I couldn't figure out how to get this working. I'm thinking of implementing custom keyboard event handlers for up/down arrow keys and manually setting the virtual scroll position (or the rendered index), but not sure if it's the best way to do as mat-select already has the keyboard handlers implemented. Would appreciate any inputs on this :)

I had a issue with only seeing a white dropdown after I selected a option and reopened the select. This only happened when I selected an option where I needed to scroll to get to.

I solved it purely in the template and just want to leave this here for reference in case someone else stumbles across this:
It's basically just a combination of a template variable and calling scrollToIndex on it

<mat-form-field>
    <mat-select [formControl]="form" placeholder="State" class="virtual-scroll"
        (openedChange)="scrollViewport.scrollToIndex(states.indexOf(form.value))">
        <cdk-virtual-scroll-viewport itemSize="10" #scrollViewport>
            <mat-option *cdkVirtualFor="let state of states" [value]="state">
                {{state}}
            </mat-option>
        </cdk-virtual-scroll-viewport>
    </mat-select>
</mat-form-field>

https://stackblitz.com/edit/angular-h4xptu-mmtdbj?file=app%2Fselect-reset-example.html

Was this page helpful?
0 / 5 - 0 ratings