2

I'm using angular-material Autocomplete (version 7) for my application. I'm using <cdk-virtual-scroll-viewport> inside . Besides numerious problems I've resolved, there is one I don't understand:

the dropdown menu doesn't display when I add max-height css, and if I add height, it will display but in fixed height.

Here is part of my code: html:

<mat-form-field class="single-select">
  <input matInput #singleInput class="single-input layout flex" type="text" [formControl]="selectControl" [matAutocomplete]="auto" (blur)="onBlur()" (focus)="onFocus()">

  <mat-autocomplete class="single-autocomplete" #auto="matAutocomplete" [displayWith]="displayFn">
    <cdk-virtual-scroll-viewport itemSize="45" minBufferPx="360" maxBufferPx="360" class="virtual-scroll">
      <mat-option *cdkVirtualFor="let option of filteredOptions" [class.selected]="option === oldValue" [value]="option" (click)="onSelect(option)">{{option.label}}</mat-option>
    </cdk-virtual-scroll-viewport>
  </mat-autocomplete>
</mat-form-field>

ts:

export class SingleSelectComponent implements OnInit {
  @Input() value;
  @Input('options')
  set options(value) {
    if (value) {
      this.filteredOptions = value.slice();
      this.data = value;
      this.initValue();
    }
  }
  @Output() formValue = new EventEmitter<any>();

  @ViewChild('singleInput') singleInput;

  selectControl = new FormControl('');
  filteredOptions;
  oldValue: any;
  data: any;
  destroy: Subject<boolean> = new Subject<boolean>();

  constructor(private appService: AppService,
              private translationService: TranslationService) { }

  ngOnInit() { this.selectControl.valueChanges.pipe(takeUntil(this.destroy)).subscribe((value)=>{
      let valStr = typeof value === 'string' ? value : value.label;

      this.filteredOptions = valStr ? this.filter(valStr) : this.data.slice();
      this.value = this.selectControl.value;
      if(typeof value !== 'string' || value === '') this.formValue.emit(value);
    });
  }

  ngOnDestroy() {
    this.destroy.next(true);
    this.destroy.unsubscribe();
  }

  private filter(name: string) {
    return this.data.filter(option => this.normalizeInput(option.label).indexOf(this.normalizeInput(name)) >= 0);
  }

  private normalizeInput(value: string) {
    return value.normalize('NFD').replace(/[\u0300-\u036f]/g, "").toLowerCase();
  }

  initValue() {
    let value = this.data.find(option => option.code === this.value.code);

    this.selectControl.setValue(value ? value : '');
    this.filteredOptions = this.filter(value ? value.label : '');
    this.oldValue = value;
  }

  displayFn(option) {
    return option ? option['label'] : '';
  }

  onSelect(option) {
    this.oldValue = option;
    this.singleInput.nativeElement.blur();
  }

  onBlur() {
    if(this.selectControl.value) {
      let found = this.data.find(option => this.selectControl.value.code === option.code);

      if(!found) {
        setTimeout(()=> {
          this.selectControl.setValue(this.oldValue);
          this.filter(this.oldValue.label);
        }, 200);
      } else {
        this.filter(this.oldValue.label);
      }
    } else {
      this.oldValue = null;
      this.filteredOptions = this.data;
    }
  }

  onFocus() {
    let virtualScrollEl = document.body.getElementsByClassName('virtual-scroll')[0];
    if(virtualScrollEl) {
      virtualScrollEl.scrollTo(0, 0);
    }
  }
}

css:

.single-input {
    height: 100%;
    min-width: 20%;
    max-width: 100%;
}

.virtual-scroll {
    height: 350px;
    overflow-x: hidden;
}

mat-option {
    height: 45px;
    font-size: 13px;
}

::ng-deep .mat-autocomplete-panel.single-autocomplete {
    max-height: 350px;
}

.virtual-scroll ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
    width: 100%;
}

In the virtual-scroll class I have to add a fixed height. When items' height is smaller than 350px, it will create a blank space.

This is the fiddle: https://stackblitz.com/edit/angular-fs4voi. Since I'm totally new to Angular and I have made some modifications for autocomplete, it may cause this problem too.

Thanks very much!

G. Tranter
  • 13,240
  • 1
  • 32
  • 51
Hailan
  • 33
  • 1
  • 6

2 Answers2

4

You can use the class option on mat-autocomplete to specify style for the dropdown panel. Because the panel is in the overlay, the class needs to be in your global style. And for max-height, because mat-autocomplete also defines it, you need !important to override. Then you don't need to implement your own virtual scroll at all.

Component:

<mat-autocomplete class="single-autocomplete" #auto="matAutocomplete" [displayWith]="displayFn">
    <mat-option *ngFor="let option of filteredOptions" [class.selected]="option === oldValue" [value]="option" (click)="onSelect(option)">{{option.label}}</mat-option>
</mat-autocomplete>

Global style.css

.single-autocomplete {
    max-height: 350px !important;
}

https://stackblitz.com/edit/angular-jngfv7?embed=1&file=src/styles.css

G. Tranter
  • 13,240
  • 1
  • 32
  • 51
  • Thank you very much for the help ! – Hailan Apr 09 '19 at 08:44
  • Im sorry I untick the best answer because I really want virtual-scroll with autocomplete, not mat-option. Any idea for this? Thanks very much – Hailan Apr 09 '19 at 10:06
  • Very tricky - not sure it can be done. CdkVirtualScroll seems like it is meant to have a single fixed height at all times. You can try binding on `[style.height]` to a function that does some sort of calculation, but you would need to know how many "items" you have in the list, which is of course dynamic, so the two principles kind of work against each other. – G. Tranter Apr 09 '19 at 21:15
  • Yes, I agree with. Making auto height of virtual-scroll seems not a right concept. I'll use md-options for small number of elements and virtual-scroll for large number. Thanks again for the help. – Hailan Apr 15 '19 at 07:54
  • There is a slightly better way to do this without having to use `!important`: nest it under it's parent style, so your selector gets a higher priority, which overwrites the default without using `!important`. – Michael Trouw Sep 09 '20 at 21:54
0

You can use [style.height] and calculate height in function:

HTML:

<mat-autocomplete #autoPolicy="matAutocomplete" [panelWidth]="'450px'" >
              <cdk-virtual-scroll-viewport itemSize="20" [style.height]="caclVSHeight()">
                <mat-option *cdkVirtualFor="let policy of filteredPolicies | async" [value]="policy.name">
                  <span style="font-size: smaller;">{{ policy.name }}</span>
                </mat-option>
              </cdk-virtual-scroll-viewport>

            </mat-autocomplete>

TS:

virtScrollLength: number;
filteredPolicies: Observable<any[]>;

ngOnInit() {
  this.filteredPolicies = this.fcPolicy.valueChanges
    .pipe(
      startWith(''),
      map(value => this._filterPolicy(value))
    );
}

  private _filterPolicy(value: string): any[] {
    if (value) {
      const filterValue = value.toLowerCase();
      const arr = this.policies.filter(pol => pol.name.toLowerCase().includes(filterValue))
      this.virtScrollLength = arr.length;
      return arr;
    } else {
      return this.policies;
    }
  }

caclVSHeight() {
    if (this.virtScrollLength > 10) {
      return '240px';
    } else {
      return '120px';
    }
  }