How do I implement autocomplete in a <mat-select> component?
Asked Answered
C

3

21

I would like to combine the autocomplete feature with a multiselect mat-select chooser because the optionslist will be quite long.

I've already searched on stackoverflow for the answer and the closest to an answer was this implement a search filter for the <mat-select> component of angular material

However the examples are about table and not mat-select.

My question is, is it possible to add the autocomplete feature to mat-select. And if not could i make an autocomplete that includes checkboxes in front of each item in the list?

Thank you

EDIT: I've found out that primefaces for angular does have a multiselect list that allows you to search for listitems. It also includes a builtin select-all button! You can find it here https://www.primefaces.org/primeng/#/multiselect

You can install primefaces with npm install primeng --save

Cavill answered 21/5, 2018 at 17:7 Comment(4)
But you do know that angular material docs contains example of suggestion box right?Breast
you mean autocomplete box? Never heard of suggestionbox. I don't need the autocomplete/suggestion box, i need a selectbox with item search functionality. There is no example of thatCavill
but thats basicly the same .Breast
i was talking about Multiselect select box, its not the same. autocomplete doesn't come with checkboxes for each itemCavill
B
30

You can implement an autocomplete multi-select using MatAutocomplete and some tricks. I'm fairly sure you can't do it with MatSelect since that doesn't give you control over the input element. The technique is a little odd, but it works fine. The idea is to manage selections in your controller, and set the "value" of each mat-option to the same thing - your selected options. You also need to trap clicks to allow users to interact with the list (rather than have it close immediately when clicked), and of course provide checkboxes inside the mat-option items. A few other tricks are necessary as well - here is a quick and dirty example that shows what to do.

HTML:

<mat-form-field class="example-full-width">
    <input type="text" placeholder="Select Users" aria-label="Select Users" matInput [matAutocomplete]="auto" [formControl]="userControl">
    <mat-hint>Enter text to find users by name</mat-hint>
</mat-form-field>

<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
    <mat-option *ngFor="let user of filteredUsers | async" [value]="selectedUsers">
        <div (click)="optionClicked($event, user)">
            <mat-checkbox [checked]="user.selected" (change)="toggleSelection(user)" (click)="$event.stopPropagation()">
                {{ user.firstname }} {{ user.lastname }}
            </mat-checkbox>
        </div>
    </mat-option>
</mat-autocomplete>

<br><br>

<label>Selected Users:</label>
<mat-list dense>
    <mat-list-item *ngIf="selectedUsers?.length === 0">(None)</mat-list-item>
    <mat-list-item *ngFor="let user of selectedUsers">
        {{ user.firstname }} {{ user.lastname }}
    </mat-list-item>
</mat-list>

TS:

import { Component, OnInit, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

export class User {
  constructor(public firstname: string, public lastname: string, public selected?: boolean) {
    if (selected === undefined) selected = false;
  }
}

/**
 * @title Multi-select autocomplete
 */
@Component({
  selector: 'multiselect-autocomplete-example',
  templateUrl: 'multiselect-autocomplete-example.html',
  styleUrls: ['multiselect-autocomplete-example.css']
})
export class MultiselectAutocompleteExample implements OnInit {

  userControl = new FormControl();

  users = [
    new User('Misha', 'Arnold'),
    new User('Felix', 'Godines'),
    new User('Odessa', 'Thorton'),
    new User('Julianne', 'Gills'),
    new User('Virgil', 'Hommel'),
    new User('Justa', 'Betts'),
    new User('Keely', 'Millington'),
    new User('Blanca', 'Winzer'),
    new User('Alejandrina', 'Pallas'),
    new User('Rosy', 'Tippins'),
    new User('Winona', 'Kerrick'),
    new User('Reynaldo', 'Orchard'),
    new User('Shawn', 'Counce'),
    new User('Shemeka', 'Wittner'),
    new User('Sheila', 'Sak'),
    new User('Zola', 'Rodas'),
    new User('Dena', 'Heilman'),
    new User('Concepcion', 'Pickrell'),
    new User('Marylynn', 'Berthiaume'),
    new User('Howard', 'Lipton'),
    new User('Maxine', 'Amon'),
    new User('Iliana', 'Steck'),
    new User('Laverna', 'Cessna'),
    new User('Brittany', 'Rosch'),
    new User('Esteban', 'Ohlinger'),
    new User('Myron', 'Cotner'),
    new User('Geri', 'Donner'),
    new User('Minna', 'Ryckman'),
    new User('Yi', 'Grieco'),
    new User('Lloyd', 'Sneed'),
    new User('Marquis', 'Willmon'),
    new User('Lupita', 'Mattern'),
    new User('Fernande', 'Shirk'),
    new User('Eloise', 'Mccaffrey'),
    new User('Abram', 'Hatter'),
    new User('Karisa', 'Milera'),
    new User('Bailey', 'Eno'),
    new User('Juliane', 'Sinclair'),
    new User('Giselle', 'Labuda'),
    new User('Chelsie', 'Hy'),
    new User('Catina', 'Wohlers'),
    new User('Edris', 'Liberto'),
    new User('Harry', 'Dossett'),
    new User('Yasmin', 'Bohl'),
    new User('Cheyenne', 'Ostlund'),
    new User('Joannie', 'Greenley'),
    new User('Sherril', 'Colin'),
    new User('Mariann', 'Frasca'),
    new User('Sena', 'Henningsen'),
    new User('Cami', 'Ringo')
  ];

  selectedUsers: User[] = new Array<User>();

  filteredUsers: Observable<User[]>;
  lastFilter: string = '';

  ngOnInit() {
    this.filteredUsers = this.userControl.valueChanges.pipe(
      startWith<string | User[]>(''),
      map(value => typeof value === 'string' ? value : this.lastFilter),
      map(filter => this.filter(filter))
    );
  }

  filter(filter: string): User[] {
    this.lastFilter = filter;
    if (filter) {
      return this.users.filter(option => {
        return option.firstname.toLowerCase().indexOf(filter.toLowerCase()) >= 0
          || option.lastname.toLowerCase().indexOf(filter.toLowerCase()) >= 0;
      })
    } else {
      return this.users.slice();
    }
  }

  displayFn(value: User[] | string): string | undefined {
    let displayValue: string;
    if (Array.isArray(value)) {
      value.forEach((user, index) => {
        if (index === 0) {
          displayValue = user.firstname + ' ' + user.lastname;
        } else {
          displayValue += ', ' + user.firstname + ' ' + user.lastname;
        }
      });
    } else {
      displayValue = value;
    }
    return displayValue;
  }

  optionClicked(event: Event, user: User) {
    event.stopPropagation();
    this.toggleSelection(user);
  }

  toggleSelection(user: User) {
    user.selected = !user.selected;
    if (user.selected) {
      this.selectedUsers.push(user);
    } else {
      const i = this.selectedUsers.findIndex(value => value.firstname === user.firstname && value.lastname === user.lastname);
      this.selectedUsers.splice(i, 1);
    }

    this.userControl.setValue(this.selectedUsers);
  }

}
Bashful answered 28/5, 2018 at 15:19 Comment(0)
D
10

One way is to add a manual filter inside mat-select so it will have auto-complete functionality. Find the example code of below given solution here

add a input controller for filter text

    public filterControl = new FormControl();

add the filter text field inside mat-select

     <mat-select
        [id]="fieldId"
        [formControl]="custonDropdown"
        (selectionChange)="onSelectChange($event.value)"
        (closed)="onPanelClose()"
        multiple
        panelClass="custom-mat-select">
        <input matInput 
             [formControl]="filterControl" 
             type="text" 
             name="filter-options" 
             id="filter-options" 
             placeholder="Search">
        <mat-option *ngFor="let option of filteredOptions | async"
             [value]="option.value" 
             [ngClass]="{'hide': !option.show}">
            {{option.name | translate}}
        </mat-option>
    </mat-select>

now add a event listener for filter text field value change

     this.filteredOptions = this.filterControl.valueChanges.pipe(
            startWith(''),
            map((value: string) => {
                this.optionItems
                    .forEach(option => {
                        option.show = option.name.toLocaleLowerCase().includes(value.toLowerCase());
                    });
                return this.optionItems;
            })
        );

also to make the whole list to be view in next dropdown open clear the filter text field on panel close, as mentioned in the template code (closed)="onPanelClose()"

 onPanelClose() {
    this.filterControl.setValue('');
}

Demo

Doridoria answered 30/3, 2020 at 12:34 Comment(4)
nice, we can also just make a keyup listener instead of formcontrol too.Kreplach
but how to fix dropdown width issue?Unselfish
@IgorZinchenko can you be a little more specific.Doridoria
@JEWELJACOB sry i done that issue ) that a bug with material!) than you!Unselfish
S
1

Yes you can use PrimeNG multiselect control. Only issue with that is if you are worried about themes.

There is another option using mat-select-autocomplet

How to install:

npm install select-autocomplete --save

npm: https://www.npmjs.com/package/mat-select-autocomplete

Demo: https://stackblitz.com/edit/mat-select-autocomplete

Secondhand answered 21/10, 2019 at 5:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.