components icon indicating copy to clipboard operation
components copied to clipboard

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

Open yogeshgadge opened this issue 7 years ago • 19 comments

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

yogeshgadge avatar Sep 11 '18 22:09 yogeshgadge

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.

yogeshgadge avatar Sep 12 '18 19:09 yogeshgadge

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

yogeshgadge avatar Sep 12 '18 20:09 yogeshgadge

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>

yogeshgadge avatar Sep 17 '18 22:09 yogeshgadge

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

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

mmalerba avatar Sep 17 '18 22:09 mmalerba

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

danderwald avatar Nov 27 '18 14:11 danderwald

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

yusijs avatar Nov 30 '18 06:11 yusijs

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

StefanoLucchi avatar Dec 10 '18 10:12 StefanoLucchi

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

justindiaw avatar Dec 12 '18 18:12 justindiaw

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

kubex320 avatar Dec 19 '18 13:12 kubex320

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

jsdevtom avatar May 28 '20 17:05 jsdevtom

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.

gitalvininfo avatar Aug 07 '20 04:08 gitalvininfo

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 :)

tsriram avatar Jan 08 '21 13:01 tsriram

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

kaihenzler avatar Feb 16 '21 08:02 kaihenzler

Another smaller issue I saw here was that mat-active is being inappropriately assigned to options in the virtual scrolling component. Using yogeshgadge's stackblitz (https://stackblitz.com/edit/angular-h4xptu-zyj4yh?file=styles.css) you can see that if you select the first option and then reopen the dropdown and start scrolling down, every 11th or so option will have the mat-active class set on it.

stpwebdev avatar Aug 10 '21 18:08 stpwebdev

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 :)

Did you ever figure out a way to do this? I'm currently trying to implement this, but it seems like set scrollToIndex or setRenderedRange stops the next option from being selected/highlighted/focused.

dickinr avatar Oct 19 '21 15:10 dickinr

@dickinr I found that all of the solutions above result in a hacky solution. I looked at the source code, but couldn't find a reasonable way of getting the virtual scrolling to work, without rewriting the component. I think it would be best to either create a custom component, or to use a different UI component. Maybe a filtered autocomplete would fit your purposes: https://material.angular.io/components/autocomplete/examples

joshuawwright avatar Oct 21 '21 17:10 joshuawwright

Hello,

We are using Angular 16 and still facing issue.

If we select an item wich is not in default rendered view range (0 to N), the viewport appears blank until a scroll event.

Is there some news ?

JoranLive avatar Sep 22 '23 13:09 JoranLive

If it helps. If you want to show the selected value you should try it like this:

<mat-form-field>
	<mat-select [formControl]="form" placeholder="State" class="virtual-scroll"
		(openedChange)="scrollViewport.scrollToIndex(states.indexOf(form.value))">
                <mat-select-trigger>
                     {{ form.value }}
                </mat-select-trigger>
                 <mat-option [value]="form.value" [hidden]="true"></mat-option>
		<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>

For me it helped

lvoiculescu-plenty avatar Apr 25 '25 17:04 lvoiculescu-plenty

<mat-select-trigger> doesn't work: https://github.com/angular/components/blob/ea08fb070e8160d433552a7ad8b66b1aab97796f/src/material/select/select.html#L8-L18 Here empty == true unless the selected option is included in the list, and for the virtual scroll that isn't the case.

HitkoDev avatar Nov 12 '25 10:11 HitkoDev