1
votes

I am trying to create a field where the behavior is similar to select but the input is editable. The requirement is 'The user should be able to select from options or type in a valid value'. The options are not filtered but instead should focus on matching value in the options. I am trying to create one using mat autocomplete but I am not able to fix the scroll part. How can I scroll the selected option into view when typing in or focusing on the field again after selection?

HTML

<div>
  <mat-form-field [appearance]="'outline'">
    <mat-label>Select color</mat-label>
    <input type="text" matInput [(ngModel)]="color" [matAutocomplete]="colorOptions">
    <i class="icon-caret-down select-arrow" matSuffix></i>
    <mat-hint>Select or type a color</mat-hint>
  </mat-form-field>
  <mat-autocomplete #colorOptions="matAutocomplete">
    <mat-option *ngFor="let option of colors; let i=index" [value]="option"
      [ngClass]="{'active-option': option == color}">
      {{option}}
    </mat-option>
  </mat-autocomplete>
</div>

TS

public colors = ['Red', 'Green', 'Blue', 'Yellow', 'Orange', 'White', 'Black', 'Purple', 'Grey', 'Brown'];
public color = '';

SCSS

.active-option {
  background-color: #f5f5f5 !important;
  font-weight: bold !important;
}

https://stackblitz.com/edit/angular-ivy-vetnpq

2

2 Answers

2
votes

Ok here we go....

Let start with a little StackBlitz demo.

Note that you should also focus on the selected element when someone uses the keydown from the input, which I didn't implement as it is a bit beyond the scope of the question. Overall I would recommend using the default behaviour, but since you asked..

The css

In the html is a div you can recognise by its role='listbox'. This div contains the mat-option elements. When the mat-option elements don't fit in the div the div will add a scrollbar with overflow: auto. So we just have to set the scrollTop value on the div to scroll.

How to get the element

Get the div through a property of the autocomplete object called panel. In order to do that get the autocomplete object and reference it using @ViewChild().

Calculate the value to set on scrollTop

To calculate the value, get the height of the mat-option. The default is 48, so you could just set that. You should be able to get the height from the AUTOCOMPLETE_OPTION_HEIGHT.

Note: I wasn't able to get visible results from modifying this value. Maybe I did something wrong. Or there could be something going on why modifying this constant has no real effects. So I just set it to the default value of 48.

To get the correct scrollTop value, calculate it by using the index of the matched element.

Add the logic with a method call

Call this logic with a method that detects a change in value on the input: (input)='changed_input().

Below my code

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {AUTOCOMPLETE_OPTION_HEIGHT} from '@angular/material/autocomplete';

@NgModule({
  imports:      [ 
    BrowserModule, 
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatAutocompleteModule
  ],
  declarations: [ AppComponent, HelloComponent ],
  bootstrap:    [ AppComponent ],
  providers: [
    {provide: AUTOCOMPLETE_OPTION_HEIGHT, useValue: 48 }
  ]
})
export class AppModule { }

app.component.html

<div [formGroup]="testForm">
  <mat-form-field [appearance]="'outline'">
    <mat-label>Select color</mat-label>
    <input type="text" matInput [(ngModel)]="color"
    (input)='changed_input()'formControlName="color" [matAutocomplete]="colorOptions">
    <i class="icon-caret-down select-arrow" matSuffix></i>
    <mat-hint>Select or type a color</mat-hint>
  </mat-form-field>
  <mat-autocomplete 
  #matAutocomplete #colorOptions="matAutocomplete">
    <mat-option *ngFor="let option of colors; let i=index" [value]="option">
      {{option}}
    </mat-option>
  </mat-autocomplete>
</div>

app.component.ts

import { Component, VERSION, ViewChild, Inject } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import {AUTOCOMPLETE_OPTION_HEIGHT} from '@angular/material/autocomplete';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  @ViewChild('matAutocomplete') matAutocomplete;
  color = '';
  colors = ['Red', 'Green', 'Blue', 'Yellow', 'Orange', 'White', 'Black', 'Purple', 'Grey', 'Brown'];
  testForm: FormGroup;

  constructor(
    @Inject(AUTOCOMPLETE_OPTION_HEIGHT) public option_height: number
  ) {}
  ngOnInit(){
    this.testForm = new FormGroup({
      color: new FormControl('')
    })
  }
  public changed_input(): void {
    const color_index = this.colors.findIndex( color_option => {
      return color_option.toLowerCase() === this.color.toLowerCase();
    });
    if(color_index === -1 ) return;
    this.matAutocomplete.panel.nativeElement.scrollTop = this.option_height*color_index;
  }
}

A final note

This is all fun and giggles to play with, but seriously just use the default behaviour and save your future self some misery.

0
votes

If I understood what you want is to add the user typed option into the options of the mat-autocomplete. If I've got your point than try to take a look in the following StackBlitz edit of your code edited-stackblitz.

I've added a button and when user press the button, I'm adding the option to the list in the autocomplete. Also you can intercept when the user press "enter" and trigger the action that I've added to the button.

Here the edited code:
HTML:

   <div [formGroup]="testForm">
     <mat-form-field [appearance]="'outline'">
       <mat-label>Select color</mat-label>
       <input type="text" matInput formControlName="color" 
             [matAutocomplete]="colorOptions" [(ngModel)]="currentOption">
       <i class="icon-caret-down select-arrow" matSuffix></i>
       <mat-hint>Select or type a color</mat-hint>
     </mat-form-field>
     <mat-autocomplete #colorOptions="matAutocomplete">
       <mat-option *ngFor="let option of colors; let i=index" [value]="option"
         [ngClass]="{'active-option': option == testForm.controls.color.value}">
         {{option}}
       </mat-option>
     </mat-autocomplete>
     <button (click)="addOption()">+</button>
     {{currentOption}}
    </div>

TS:

  name = 'Angular ' + VERSION.major;
  color = '';
  colors = ['Red', 'Green', 'Blue', 'Yellow', 'Orange', 'White', 'Black', 'Purple', 'Grey', 'Brown'];
  testForm: FormGroup;

  ngOnInit(){
    this.testForm = new FormGroup({
      color: new FormControl('')
    })
  }

  public currentOption: any;

  public addOption(): void {
    this.colors.push(this.currentOption);
  }

Let me know if I understood right or not.