Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to auto-expand/collapse Angular Material Autocomplete with Chiplist on click

I have an application with a pre-populated list of items in an autocomplete which are displayed as "Chips" once selected. Users are able to select, add and remove. The problem is that many users expect the autocomplete list to expand upon a mouse-click on the input field... But that only happens if the input field is gaining the focus (and not if the focus is already set AND the list is collapsed).

The problem is that half the users are looking to "mouse-click" on the input control and expect the autocomplete to expand (or toggle expand/collapse). The problem is that once selecting an item, the "chip" is created and the cursor is left on the autocomplete's "input" control with its list collapsed. When users intuitively "mouse-click" on the "input" control again to select a another item, the autocomplete does not expand... They need to "click" (loose the focus) somewhere else and then click again on the "input" field to see the list.

This is confusing for some end users not using the keyword (not using the autocomplete search functionality), they expect a mouse-click on the input control to expand for a second time the way it happened the first time (they don't intuitively realize the first time expanded because the "input" field gained the focus).

Users using the keyboard are OK since likely they have an idea of what's in the list and it auto-expands (or collapse) while typing.

https://stackblitz.com/angular/eknbvbpdqyo?file=app%2Fchips-autocomplete-example.ts

<mat-form-field class="example-chip-list">
  <mat-chip-list #chipList aria-label="Fruit selection">
    <mat-chip
      *ngFor="let fruit of fruits"
      [selectable]="selectable"
      [removable]="removable"
      (removed)="remove(fruit)">
      {{fruit}}
      <mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
    </mat-chip>
    <input
      placeholder="New fruit..."
      #fruitInput
      [formControl]="fruitCtrl"
      [matAutocomplete]="auto"
      [matChipInputFor]="chipList"
      [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
      [matChipInputAddOnBlur]="addOnBlur"
      (matChipInputTokenEnd)="add($event)">
  </mat-chip-list>
  <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
    <mat-option *ngFor="let fruit of filteredFruits | async" [value]="fruit">
      {{fruit}}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

import {COMMA, ENTER} from '@angular/cdk/keycodes';
import {Component, ElementRef, ViewChild} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatAutocompleteSelectedEvent, MatAutocomplete} from '@angular/material/autocomplete';
import {MatChipInputEvent} from '@angular/material/chips';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';

/**
 * @title Chips Autocomplete
 */
@Component({
  selector: 'chips-autocomplete-example',
  templateUrl: 'chips-autocomplete-example.html',
  styleUrls: ['chips-autocomplete-example.css'],
})
export class ChipsAutocompleteExample {
  visible = true;
  selectable = true;
  removable = true;
  addOnBlur = true;
  separatorKeysCodes: number[] = [ENTER, COMMA];
  fruitCtrl = new FormControl();
  filteredFruits: Observable<string[]>;
  fruits: string[] = ['Lemon'];
  allFruits: string[] = ['Apple', 'Lemon', 'Lime', 'Orange', 'Strawberry'];

  @ViewChild('fruitInput', {static: false}) fruitInput: ElementRef<HTMLInputElement>;
  @ViewChild('auto', {static: false}) matAutocomplete: MatAutocomplete;

  constructor() {
    this.filteredFruits = this.fruitCtrl.valueChanges.pipe(
        startWith(null),
        map((fruit: string | null) => fruit ? this._filter(fruit) : this.allFruits.slice()));
  }

  add(event: MatChipInputEvent): void {
    // Add fruit only when MatAutocomplete is not open
    // To make sure this does not conflict with OptionSelected Event
    if (!this.matAutocomplete.isOpen) {
      const input = event.input;
      const value = event.value;

      // Add our fruit
      if ((value || '').trim()) {
        this.fruits.push(value.trim());
      }

      // Reset the input value
      if (input) {
        input.value = '';
      }

      this.fruitCtrl.setValue(null);
    }
  }

  remove(fruit: string): void {
    const index = this.fruits.indexOf(fruit);

    if (index >= 0) {
      this.fruits.splice(index, 1);
    }
  }

  selected(event: MatAutocompleteSelectedEvent): void {
    this.fruits.push(event.option.viewValue);
    this.fruitInput.nativeElement.value = '';
    this.fruitCtrl.setValue(null);
  }

  private _filter(value: string): string[] {
    const filterValue = value.toLowerCase();

    return this.allFruits.filter(fruit => fruit.toLowerCase().indexOf(filterValue) === 0);
  }
}

What would be the best way to "toggle" (Expand/Collapse) or always expand the autocomplete list regardless if the control has already the focus or not?

(I tried setting the focus on the last chip that was selected but that would affect users using the keyboard so it is not really a solution).

like image 538
Alex Avatar asked Oct 29 '25 16:10

Alex


1 Answers

One solution is to manually refocus on click, which will cause the dropdown to open back up. It feels weird, but simple enough to implement. Just add a function like:

focusFruitInput(){
  this.fruitInput.nativeElement.blur();
  this.fruitInput.nativeElement.focus();
}

You can then bind this focus function to a click event on the input:

<input placeholder="New fruit..."
       #fruitInput
       (click)="focusFruitInput()"
       //...
>

Now, whether you already have focus on the input or not, when you click on the input while active it will open the autocomplete dropdown.

like image 99
CGutz Avatar answered Oct 31 '25 07:10

CGutz