Alessandro Celeghin
Alessandro Celeghin

Reputation: 4209

Angular Material Autocomplete force selection

In my angular 5 application, I have some matAutocomplete, but I want to force the selection of one of the suggestions, so I am following this approach: stackblitz but for some reason in one case I have an issue:

Cannot read property 'panelClosingActions' of undefined at CustomerDetailComponent.countryClosingActions (customer-detail.component.ts:199) at CustomerDetailComponent.ngAfterViewInit

I have multiple matAutocomplete but only this one have problems. (info about this method is here github

html

<mat-form-field>
    <input matInput #nation placeholder="{{'customer.detail.labels.country'
      | translate }}" required [matAutocomplete]="tdAuto" name="country"  
      #count="ngModel" [(ngModel)]="selected.country"
      (ngModelChange)="searchCountry($event)">
        <mat-autocomplete #tdAuto="matAutocomplete" [displayWith]="displayFn">
          <mat-option (onSelectionChange)="setCountry(country)" *ngFor="let country of countries" [value]="country">
             <div class="row">
               <img src="assets/img/flags24/{{country.alpha2Code | lowercase}}.png" />
                  <span>{{country.name}} ({{country.alpha2Code}})</span>
             </div>
         </mat-option>
        </mat-autocomplete>
    </mat-form-field>

component

@ViewChild('nation', { read: MatAutocompleteTrigger }) trigger: MatAutocompleteTrigger;
  subscription: Subscription;


ngAfterViewInit() {
    this.countryClosingActions();
  }

  private countryClosingActions(): void {
    if (this.subscription && !this.subscription.closed) {
      this.subscription.unsubscribe();
    }

    this.subscription = this.trigger.panelClosingActions
      .subscribe(e => {
        console.log('closing')
        if (!e || !e.source) {
          this.selected.country = null;
          this.selfCountry = null;
        }
      },
      err => this.countryClosingActions(),
      () => this.countryClosingActions());
  }

Upvotes: 25

Views: 36681

Answers (9)

Since Angular Material 16.2.1, you can use the requireSelection directive on mat-autocomplete:

This in the template:

<form class="example-form">
  <mat-form-field class="example-full-width">
    <mat-label>Number</mat-label>
    <input #input
           type="text"
           placeholder="Pick one"
           matInput
           [formControl]="myControl"
           [matAutocomplete]="auto"
           (input)="filter()"
           (focus)="filter()">
    <mat-autocomplete requireSelection #auto="matAutocomplete">
      <mat-option *ngFor="let option of filteredOptions" [value]="option">
        {{option}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
</form>

And this in the class:

@ViewChild('input') input: ElementRef<HTMLInputElement>;
  myControl = new FormControl('');
  options: string[] = ['One', 'Two', 'Three', 'Four', 'Five'];
  filteredOptions: string[];

  constructor() {
    this.filteredOptions = this.options.slice();
  }

  filter(): void {
    const filterValue = this.input.nativeElement.value.toLowerCase();
    this.filteredOptions = this.options.filter(o => o.toLowerCase().includes(filterValue));
  }

Docs: Angular Material - Autocomplete - Require an option to be selected

Upvotes: 5

zWinGaH
zWinGaH

Reputation: 236

I didn't want to use setTimeout() because it seem very dirty, and you will always have a short, unwanted delay.

So i found a solution without setTimeout(), but utilizing (mousedown) on the mat-option. Important (mousedown) happens before (blur)

private isOptionSelected = false;

mousedown() {
  this.isOptionSelected = true;
}
onFocus() {
  this.isOptionSelected = false;
}
inputBlur() {
  if (!this.isOptionSelected) {
    // Now we only arrive here, when the input is beeing left without selecting a valid option
    // Do something like resetting the control to enforce a valid option
  }
}
<input type="text" placeholder="Betriebsnummer" matInput (focus)="onFocus()" (blur)="inputBlur()" [formControl]="dealerAutocompleteControl" [matAutocomplete]="auto" />
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete" (optionSelected)="optionSelected($event.option.value)">
  <mat-option *ngFor="let dealer of filteredDealerOptions()" [value]="dealer" (mousedown)="mousedown()">
    {{ dealer }}
  </mat-option>
</mat-autocomplete>

Upvotes: 0

jrgokavalsa
jrgokavalsa

Reputation: 1

I had the same problem and I solved it with a custom directive. Here is the code I used on matautocomplete https://v14.material.angular.io/components/autocomplete/examples#autocomplete-display:

@Directive({
  selector: '[requireSelection]',
})
export class RequireSelectionDirective {
  @Input('requireSelection') matAutoComplete!: MatAutocomplete;
  constructor(private ngControl: NgControl) {}

  @HostListener('blur')
  onBlur() {
    const value = this.ngControl.control?.value;
    const matchingOptions = this.matAutoComplete.options.find(
      (option) => JSON.stringify(option.value) == JSON.stringify(value)
    );
    if (!matchingOptions) {
      this.ngControl.control?.setValue(null);
      this.ngControl.control?.setErrors({ selectionRequired: true });
    }
  }
}
<form>
  <mat-form-field floatLabel="always">
    <mat-label>Assignee</mat-label>
    <input
      type="text"
      matInput
      [formControl]="myControl"
      [matAutocomplete]="auto"
      [requireSelection]="auto"
      [required]="true"
    />
    <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
      <mat-option
        *ngFor="let option of filteredOptions | async"
        [value]="option"
      >
        {{option.name}}
      </mat-option>
    </mat-autocomplete>
    <mat-error *ngIf="myControl.hasError('selectionRequired')">
      Please select a valid option
    </mat-error>
  </mat-form-field>
</form>

Upvotes: -1

Amit kumar
Amit kumar

Reputation: 6147

Using blur event and matAutocomplete output event (optionSelected) we can force user to select option.

<mat-form-field class="example-full-width">
  <input type="text" placeholder="Country*" matInput formControlName="country" [matAutocomplete]="countryAutoList" (blur)="checkCountry()">
  <mat-autocomplete autoActiveFirstOption #countryAutoList="matAutocomplete" (optionSelected)="countryClick($event)">
    <mat-option *ngFor="let item of countryList" [value]="item.Name">{{item.Name}}</mat-option>
  </mat-autocomplete>
</mat-form-field>

ts file functions

countryClick(event: any) {
  this.selectedCountry = event.option.value;
}

checkCountry() {
 setTimeout(()=> {
  if (!this.selectedCountry || this.selectedCountry !== this.signatureFormGroup.controls['country'].value) {
    this.signatureFormGroup.controls['country'].setValue(null);
    this.selectedCountry = '';
  }
 }, 1000);
}

depend on your requirement you can always delay the function which you call in blur or optionSelect event using setTimeout window function.

setTimeout(()=> { 
// function contents
}, 1000);

Upvotes: 22

Murat Yıldız
Murat Yıldız

Reputation: 12060

You can also try the following method:

Angular Material Autocomplete Component Force Selection: Complete Example

Here is the demo on Stackblitz

Upvotes: 0

peterbrown
peterbrown

Reputation: 573

maybe I'm late, but I fount this https://onthecode.co.uk/force-selection-angular-material-autocomplete/ Really simple solution based on validator:

export function RequireMatch(control: AbstractControl) {
    const selection: any = control.value;
    if (typeof selection === 'string') {
        return { incorrect: true };
    }
    return null;
}

Upvotes: 1

Mustafa Kunwa
Mustafa Kunwa

Reputation: 867

You can Create custom Validator , this will validate it perfectly tested code:

stateCtrl = new FormControl(null,[forbiddenNamesValidator(this.states)])


export function forbiddenNamesValidator(States: any[]): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const index = States.findIndex(State=> {
      return (new RegExp('\^' + State.name + '\$')).test(control.value);
    });
    return index < 0 ? { 'forbiddenNames': { value: control.value } } : null;
  };
}

Upvotes: 1

hbthanki
hbthanki

Reputation: 559

I found this helpful:

private subscribeToClosingActions(): void {
if (this.subscription && !this.subscription.closed) {
  this.subscription.unsubscribe();
}

this.subscription = this.autoCompleteTrigger.panelClosingActions
  .subscribe((e) => {
      if (!e || !e.source) {
        const selected = this.matAutocomplete.options
          .map(option => option.value)
          .find(option => option === this.formControl.value);

        if (selected == null) {
          this.formControl.setValue(null);
        }
      }
    },
    err => this.subscribeToClosingActions(),
    () => this.subscribeToClosingActions());
}

Upvotes: 4

Iancovici
Iancovici

Reputation: 5731

The example they give on the site uses Reactive Form approach, but in your case you seem to lean toward Template Driven Approach, but you're not using a form period.

So you could directly access the dom element like you're doing now.

Create a local reference to your input, maybe call it autoComplInput

Inside your component file, you'll want to import ElementRef and View Child

import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';

Inside your component, import ViewChild, and declare it an ElementRef type

@ViewChild('autoComplInput') autoComplInput: ElementRef;

Then at some point of initialization, just assign referenced element value

  ngOnInit() {
    this.autoComplInput.nativeElement.value = this.countries[0];
  }

Demo based on Angular Material 2's example, since you didn't provide all the information needed.

In your html

<mat-form-field>
  <input matInput
         placeholder="{{'country'}}"
         required
         [matAutocomplete]="tdAuto"
         name="country"
         #autoComplInput
         (ngModelChange)="searchCountry($event)">
  <mat-autocomplete #tdAuto="matAutocomplete" >
    <mat-option *ngFor="let country of countries" [value]="country">
      <div class="row">
        <span>{{country}} </span>
      </div>
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

In your component

import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';

@Component({
  selector: '<...>',
  templateUrl: '<...>',
  styleUrls: ['<...>'],
})
export class <...> implements OnInit  {

  @ViewChild('autoComplInput') autoComplInput: ElementRef;

  countries = [
    'Alabama',
    'Alaska',
    'Arizona',
    'Arkansas',
    'California',
    'Colorado',
    'Connecticut',
    'Delaware',
    'Florida',
    'Georgia',
    'Hawaii',
    'Idaho',
    'Illinois',
    'Indiana',
    'Iowa',
    'Kansas',
    'Kentucky',
    'Louisiana',
    'Maine',
    'Maryland',
    'Massachusetts',
    'Michigan',
    'Minnesota',
    'Mississippi',
    'Missouri',
    'Montana',
    'Nebraska',
    'Nevada',
    'New Hampshire',
    'New Jersey',
    'New Mexico',
    'New York',
    'North Carolina',
    'North Dakota',
    'Ohio',
    'Oklahoma',
    'Oregon',
    'Pennsylvania',
    'Rhode Island',
    'South Carolina',
    'South Dakota',
    'Tennessee',
    'Texas',
    'Utah',
    'Vermont',
    'Virginia',
    'Washington',
    'West Virginia',
    'Wisconsin',
    'Wyoming',
  ];
  constructor( ) {}

  ngOnInit() {
    this.autoComplInput.nativeElement.value = this.countries[0];
  }


}

Upvotes: 2

Related Questions