Angular material-autocomplete for searches with multiple words? - html

In my Angular project, I'm using material-autocomplete to search through some of my items. So far, it works as expected.
HTML
<form class="example-form">
<mat-form-field class="example-full-width">
<input type="text" placeholder="Pick one" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
TS
myControl = new FormControl();
options: string[] = ['One', 'Two', 'Three'];
filteredOptions: Observable<string[]>;
ngOnInit() {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map(value => this._filter(value))
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
if(filterValue == ''){
return []
}
return this.options.filter(option => option.toLowerCase().indexOf(filterValue) === 0);
}
This function works, but only for the first word in the search. For example, if you want to search for "Banana Apple", that will autocomplete if you type "B", but it won't autocomplete if you type "Apple." Is there a way to apply this search so that you can search for either word in a multi-word item?

You should change the last line of your _filter method:
return this.options.filter(option => option.toLowerCase().includes(filterValue));
This allows to you to filter finding in all of the value of your option.

Related

How to set the default/prefilled value of a input element in mat-form-field on load?

So I want to implement an html element such that the default data(the variable 'preselectedValue') should be already be entered/prefilled into the input field when the component loads and the user should be able to directly edit the search string.
autocomplete-display-example.html
<mat-form-field appearance="fill">
<mat-label>Assignee</mat-label>
<input type="text" matInput [formControl]="myControl" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{option.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
autocomplete-display-example.ts
export class AutocompleteDisplayExample implements OnInit {
myControl = new FormControl<string | User>('');
options: User[] = [{name: 'Mary'}, {name: 'Shelley'}, {name: 'Igor'}];
filteredOptions: Observable<User[]>;
preselectedValue = "John";
ngOnInit() {
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map(value => {
const name = typeof value === 'string' ? value : value?.name;
return name ? this._filter(name as string) : this.options.slice();
}),
);
}
displayFn(user: User): string {
return user && user.name ? user.name : '';
}
private _filter(name: string): User[] {
const filterValue = name.toLowerCase();
return this.options.filter(option => option.name.toLowerCase().includes(filterValue));
}
}
Any help would be appreciated.
The value of your mat-option is of type User. So you have to pass it a preselected value of that type. The code in your question does not show the definition of the User class but you could try something like this:
preselectedValue = {name: 'John} as User;
ngOnInit() {
this.myControl.value = preselectedValue;
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
map(value => {
const name = typeof value === 'string' ? value : value?.name;
return name ? this._filter(name as string) : this.options.slice();
}),
);
}

Clear mat-select selection Angular material

I have implemented mat-multi-select option. I have a search functionality and I am able to select multiple options at once. Post selection when I click generate button, I want to clear all the selections. I am able to clear the values from search bar but values are still selected if I open the multi select dropdown. How can I clear those values.
HTML Code
<mat-select [formControl]="cMultiCtrl" [multiple]="true" required (selectionChange)="selectionChange($event)">
<mat-option>
<ngx-mat-select-search placeholderLabel="Search" noEntriesFoundLabel="No Matching Value Found" [formControl]="cMultiFilterCtrl"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let val of filteredCMulti | async" [value]="val.value">
{{val.name}}
</mat-option>
</mat-select>
// the button on click of which I want to clear everything post API call
<button mat-raised-button (click)="generate()" class="download-button">Generate
</button>
TS Code
public filteredCMulti: ReplaySubject<any[]> = new ReplaySubject<any[]>(1);
public cMultiCtrl: FormControl = new FormControl();
ngOnInit() {
this.filteredCMulti.next(this.cvalues.slice());
this.cMultiFilterCtrl.valueChanges
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.filterBanksMulti();
});
}
ngAfterViewInit() {
this.setInitialValue();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
private setInitialValue() {
this.filteredCMulti
.pipe(take(1), takeUntil(this._onDestroy))
.subscribe(() => {
this.singleSelect.compareWith = (a, b) => a.id === b.id;
});
}
selectionChange(event){
this.cvalue = event.value;
}
private filterBanksMulti() {
if (!this.cvalues) {
return;
}
let search = this.cMultiFilterCtrl.value;
if (!search) {
this.filteredCMulti.next(this.cvalues.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the banks
this.filteredCMulti.next(
this.cvalues.filter(bank => bank.name.toLowerCase().indexOf(search) > -1)
);
}
generate(){
let msg = '';
// some logic here
else{
this.commonService.showSnackBar("Value generated")
this.filteredCMulti = new ReplaySubject; // this clears the search bar but not values, they are still selected
this.table();
}}
})
}
Provide an element ref to your <mat-select>
<mat-select #matRef [formControl]="cMultiCtrl" [multiple]="true" required (selectionChange)="selectionChange($event)">
<mat-option>
<ngx-mat-select-search placeholderLabel="Search" noEntriesFoundLabel="No Matching Value Found" [formControl]="cMultiFilterCtrl"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let val of filteredCMulti | async" [value]="val.value">
{{val.name}}
</mat-option>
</mat-select>
And update your ts file like:
#ViewChild('matRef') matRef: MatSelect;
clear() {
this.matRef.options.forEach((data: MatOption) => data.deselect());
}
You can optimize clear() by conditionally calling deselect()

Angular filter and binding (ngModelChange)

I'm working on the hotel project. I have a reservation screen. Here I ask the hotel name, region name, check-in and check-out dates, the number of adults and children. When the necessary information is entered, when I press the search button, I filter according to the values ​​there. I connected the date filter with html using the (NgModelChange) method, but I couldn't connect the html with the hotel and region methods correctly. I made a filter as an experiment, but I got the wrong result. I don't know where I'm making a mistake. How can I fix this?
.html
<form [formGroup]="form">
<mat-form-field>
<label>Which Hotel?</label>
<input type="text" matInput [matAutocomplete]="name" formControlName="name" (change)="changeHotel()" />
<mat-autocomplete #name="matAutocomplete">
<mat-option *ngFor="let hotel of filteredHotels | async" [value]="hotel">
{{hotel}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field>
<label>Which Region?</label>
<input type="text" matInput [matAutocomplete]="region" formControlName="region" (change)="changeRegion()" />
<mat-autocomplete #region="matAutocomplete">
<mat-option *ngFor="let region of filteredRegions | async" [value]="region">
{{region}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field>
<label>Entry Date?</label>
<input matInput [matDatepicker]="mdpStartDate" formControlName="startDate" (ngModelChange)="changeDate()">
<mat-datepicker-toggle matSuffix [for]="mdpStartDate"></mat-datepicker-toggle>
<mat-datepicker #mdpStartDate></mat-datepicker>
</mat-form-field>
<mat-form-field>
<label>Pay Date?</label>
<input matInput [matDatepicker]="mdpEndDate" formControlName="endDate" (ngModelChange)="changeDate()">
<mat-datepicker-toggle matSuffix [for]="mdpEndDate"></mat-datepicker-toggle>
<mat-datepicker #mdpEndDate></mat-datepicker>
</mat-form-field>
<br />
<mat-form-field>
<label>Adult Number?</label>
<input type="number" min="1" matInput formControlName="adult" (ngModelChange)="filterAge()">
</mat-form-field>
<mat-form-field>
<label>Chd Number?</label>
<input type="number" min="0" max="3" value="0" matInput formControlName="chd" (ngModelChange)="filterAge()">
</mat-form-field>
<button type="button" class="btn btn-success" (click)="search()">Search</button>
.ts
form = new FormGroup({
name: new FormControl(''),
region: new FormControl(''),
adult: new FormControl(1),
chd: new FormControl(0),
startDate: new FormControl(),
endDate: new FormControl(),
});
filtered: any[];
showList = false;
name = '';
regions: string[];
hotels: Hotel[] = [];
filteredHotels: Observable<string[]>;
filteredRegions: Observable<string[]>;
ngOnInit() {
this.regions = this.hotels.reduce((arr: string[], current) => {
if (!arr.includes(current.regionName)) {
arr.push(current.regionName);
}
return arr;
}, []);
this.filteredHotels = this.form.get("name").valueChanges
.pipe(
startWith(''),
map(v => {
return this.filterHotels(v)
})
);
this.filteredRegions = this.form.get("region").valueChanges
.pipe(
startWith(''),
map(value => this.filterRegions(value).map(h => h.regionName))
);
this.form.get("adult").valueChanges.
subscribe(v => {
this.adult = new Array(v).map(e => new Object)
});
this.form.get("chd").valueChanges
.subscribe(v => {
this.children = new Array(v).map(e => new Object)
});
this.hotelservice.getHotels().subscribe(
data => {
this.hotels = data;
this.dsHotels.data = data;
this.dsHotels.paginator = this.paginator;
},
err => {
console.error("Hata oluştu: ", err);
}
);
}
private filterHotels(value: string): string[] {
const name = value.toLowerCase();
return this.hotels
.map(x => x.hotelName)
.filter(option => option.toLowerCase().includes(name));
}
private filterRegions(value: string): Hotel[] {
const region = value.toLowerCase();
return this.hotels
.filter(hotel => hotel.regionName.toLowerCase().includes(region));
}
private isEqual(date1: Date, date2: Date) {
return date1.getDate() == date2.getDate() && date1.getMonth() == date2.getMonth() && date1.getFullYear() == date2.getFullYear();
}
private isDataEqualToDate(value1: any, date2: Date) {
if (value1 == null) return true;
return this.isEqual(new Date(value1), date2);
}
private getFilteredDate(): Hotel[] {
return this.hotels.filter(
x => this.isDataEqualToDate(this.form.get("startDate").value, new Date(x.checkInDate))
&& this.isDataEqualToDate(this.form.get("endDate").value, new Date(x.payDate))
);
}
changeHotel() {
this.dsHotels.data = this.filterHotels("value");
}
changeRegion() {
this.dsHotels.data = this.filterRegions("value");
}
changeDate() {
this.dsHotels.data = this.getFilteredDate();
}
filterAge() {
//this.dsHotels.data = this.hotels.filter(x => x.numberOfAd == this.form.get("adult").value && x.numberOfChd == this.form.get("chd").value);
this.dsHotels.data = this.hotels.filter(x => x.numberOfAd == x.numberOfChd);
}
search() {
this.showList = true;
this.filterHotels("");
this.filterRegions("");
this.changeDate();
}
As you created your form with reactive forms you can switch from event binding in template
< ... (change)="changeHotel()"
< ... (ngModelChange)="changeDate()">
to do it inside class.
ngOnInit() {
// ... initial logic
this.form.get('name').valueChanges.subscribe(this.changeHotel.bind(this))
this.form.get('region').valueChanges.subscribe(this.changeRegion.bind(this))
this.form.get('startDate').valueChanges.subscribe(this.changeDate.bind(this))
this.form.get('endDate').valueChanges.subscribe(this.changeDate.bind(this))
this.form.get('adult').valueChanges.subscribe(this.filterAge.bind(this))
this.form.get('chd').valueChanges.subscribe(this.filterAge.bind(this))
}
Reactive form instances like FormGroup and FormControl have a
valueChanges method that returns an observable that emits the latest
values. You can therefore subscribe to valueChanges to update instance
variables or perform operations.
source

How to fix a ExpressionChangedAfterItHasBeenCheckedError

I have an application with a mat-autocomplete element in it with cities as options. The cities come from a database. There is a button to add an input element. This is done by having a div element with a *ngFor property that loops over a property in the component.ts file. When the add-button is clicked an element is added to the property and so an input is added. But when i type in a city name in the first input and then try to add an input element i get the ExpressionChangedAfterItHasBeenCheckedError.
I have found that a lot of people get this error but i've tried all the given solutions but non of them of them seem to work. Like :
startWith(''),
delay(0),
map(value => this._filter(value))
);
ngAfterViewInit(){
this.cd.detectChanges();
}
var request = new CityCreationRequestModel();
request.name = "";
Promise.resolve(null).then(() => this.reportGroupCreationRequest.cityReportGroups.push(request));
and trying with timeOuts.
this is my html file :
<div class="container">
<mat-card>
<mat-card-header>
<mat-card-title>Voeg een reportgroep toe</mat-card-title>
<mat-card-subtitle>Geef emailadressen op die verbonden worden met de rapporten van bepaalde steden</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form (ngSubmit)="onSubmit()" #addReportGroup="ngForm" *ngIf="!isLoading">
<div *ngFor="let city of reportGroupCreationRequest.cityReportGroups" class="form-row">
<mat-form-field>
<input placeholder="Stad" required [(ngModel)]="city.name" type="text" matInput [formControl]="myControl" [matAutocomplete]="auto" name="cityInput">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
<mat-option *ngFor="let cityName of filteredOptions | async" [value]="cityName">
{{ cityName }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<button mat-raised-button (click)="addCity()">Voeg een stad toe</button>
<div class="form-row">
<button mat-raised-button type="submit" [disabled]="!addReportGroup.valid" class="submitButton">Opslagen</button>
</div>
</form>
<mat-spinner [style.display]="isLoading ? 'block' : 'none'"></mat-spinner>
</mat-card-content>
</mat-card>
and this is my .ts file :
the filter if for the mat-autocomplete element
export class AddReportGroupComponent implements OnInit {
myControl = new FormControl();
filteredOptions: Observable<string[]>;
public reportGroupCreationRequest: ReportGroupCreationRequestModel = new ReportGroupCreationRequestModel();
public cities: Array<CityResponseModel>;
public cityNames: Array<string>;
private isLoading: boolean;
constructor(private reportGroupService: ReportGroupService,
private cd: ChangeDetectorRef) {
}
ngOnInit() {
this.isLoading = true;
this.reportGroupService.getCities().subscribe(result => {
this.cities = result;
this.cities.sort((a,b) => a.name.localeCompare(b.name));
this.cityNames = this.cities.map(c=>c.name);
}).add(()=> {
this.isLoading = false;
this.filteredOptions = this.myControl.valueChanges.pipe(
startWith(''),
delay(0),
map(value => this._filter(value))
);
});
this.reportGroupCreationRequest.cityReportGroups.push(new CityCreationRequestModel());
this.reportGroupCreationRequest.emailReportGroups.push(new EmailCreationRequestModel());
}
//doesn't seem to do anything
ngAfterViewInit(){
this.cd.detectChanges();
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return this.cityNames.filter(cn => cn.toLowerCase().indexOf(filterValue) === 0);
}
addCity(){
var request = new CityCreationRequestModel();
request.name = "";
Promise.resolve(null).then(() => this.reportGroupCreationRequest.cityReportGroups.push(request));
}
and these are my models that I use :
export class ReportGroupCreationRequestModel {
public cityReportGroups: Array<CityCreationRequestModel> = new Array<CityCreationRequestModel>();
public emailReportGroups: Array<EmailCreationRequestModel> = new Array<EmailCreationRequestModel>();
}
export class CityCreationRequestModel {
public name: string;
}
Thanks in advance
I found a solution for the problem I had :
I used both [(ngModel)] and [FormsControl] and apparantly the [(ngModel)] was part of the problem so I only used [FormsControl] and the error is gone.
addCity(){
this.reportGroupCreationRequest.cityReportGroups[this.reportGroupCreationRequest.cityReportGroups.length - 1].name = this.myControl.value;
Promise.resolve(null).then(() => this.reportGroupCreationRequest.cityReportGroups.push(new CityCreationRequestModel()));
}
<div *ngFor="let city of reportGroupCreationRequest.cityReportGroups" class="form-row">
<mat-form-field>
<input placeholder="Stad" required type="text" matInput [formControl]="myControl" [matAutocomplete]="auto" name="cityInput">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
<mat-option *ngFor="let cityName of filteredOptions | async" [value]="cityName">
{{ cityName }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>

Filter autocomplete Material. Some input form work like one input

can you help me please. I have some problem with Autocomplete Material.
I don't know what is the problem, I follow this tutorial: https://material.angular.io/components/autocomplete/overview
In component.ts
import { Component } from '#angular/core';
import { FormControl } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { startWith } from 'rxjs/operators/startWith';
import { map } from 'rxjs/operators/map';
#Component({ selector: 'autocomplete-filter-example',
templateUrl: 'autocomplete-filter-example.html',
styleUrls: ['autocomplete-filter-example.css'] })
export class AutocompleteFilterExample {
myControl: FormControl = new FormControl();
myControl1: FormControl = new FormControl();
myControl2: FormControl = new FormControl();
options = [
'One',
'Two',
'Three' ];
options1 = [
'test',
'test2',
'test1' ];
options2 = [
'test3',
'test5',
'test10' ];
filteredOptions: Observable<string[]>;
filteredOptions1: Observable<string[]>;
filteredOptions2: Observable<string[]>;
ngOnInit() {
this.filteredOptions = this.myControl.valueChanges
.pipe(
startWith(''),
map(val => this.filter(val))
);
this.filteredOptions1 = this.myControl1.valueChanges
.pipe(
startWith(''),
map(val => this.filter1(val))
);
this.filteredOptions1 = this.myControl1.valueChanges
.pipe(
startWith(''),
map(val => this.filter1(val))
);
this.filteredOptions2 = this.myControl2.valueChanges
.pipe(
startWith(''),
map(val => this.filter2(val))
); }
filter(val: string): string[] {
return this.options.filter(option =>
option.toLowerCase().indexOf(val.toLowerCase()) === 0); }
filter1(value: string): string[] {
return this.options1.filter(option1 =>
option1.toLowerCase().indexOf(value.toLowerCase()) === 0); }
filter2(value: string): string[] {
return this.options1.filter(option1 =>
option1.toLowerCase().indexOf(value.toLowerCase()) === 0); }
}
Component.html
<form class="example-form">
<mat-form-field class="example-full-width">
<input type="text" placeholder="Pick one1" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-full-width">
<input type="text" placeholder="Pick one2" aria-label="Number" matInput [formControl]="myControl1" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option1 of filteredOptions1 | async" [value]="option1">
{{ option1 }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-full-width">
<input type="text" placeholder="Pick one3" aria-label="Number" matInput [formControl]="myControl2" [matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option2 of filteredOptions2 | async" [value]="option2">
{{ option2 }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</form>
My question is: Why input work like one and display only one array? In this link you can see my problem.
https://stackblitz.com/edit/angular-yw23zr-u25rqk?file=app%2Fautocomplete-filter-example.html
Please if have some idea, I want to help me. Thank you
You need to have different template reference for your different autocomplete.
your need <mat-autocomplete #auto="matAutocomplete"> / <mat-autocomplete #auto1="matAutocomplete"> and <mat-autocomplete #auto2="matAutocomplete">
and [matAutocomplete]="auto" [matAutocomplete]="auto1" [matAutocomplete]="auto2"
In your example, you use the reference #auto multiple times, so it keeps the last autocomplete as reference ( that why you see the same list three times).
Plus you have some typo in your example :
1/ your have this block twice
this.filteredOptions1 = this.myControl1.valueChanges
.pipe(
startWith(''),
map(val => this.filter1(val))
);
2/ your filter2 value is based upon this.value1 for filtering instead of this.option2
Here is the forked stackblitz