Summary: I have a working as-you-type field dropdown search. When I choose from the dropdown, which happily sets the field to the object I've received in the search, but unhappily registers THAT change and sends the whole object off to be searched.
HTML:
<form [formGroup]="myForm" novalidate>
<mat-form-field>
<input matInput
placeholder="SKU / Item Number"
[matAutocomplete]="auto"
formControlName='itemName'>
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete" [displayWith]="parseDropDownSelection">
<mat-option
*ngFor="let row of searchQueryResult"
[value]="row"
(onSelectionChange)="onItemSelection($event)">
<span>{{ row.Name }}</span>
</mat-option>
</mat-autocomplete>
</form>
Setup:
import {FieldSearchServiceItem} from './../services/field-search.service';
constructor(
private dialog: MatDialog,
private formBuilder: FormBuilder,
private http: HttpClient,
private appServiceItem: FieldSearchServiceItem,
) {}
ngOnInit()
ngOnInit(){
this.myForm = this.formBuilder.group
({
itemName: '',
});
this.myForm
.get('itemName')
.valueChanges
.pipe(
debounceTime(200),
switchMap(value => this.appServiceItem.search({name: value}, 1))
)
.subscribe(serviceResult => this.searchQueryResult = serviceResult.qResult);
}
The service:
@Injectable()
export class FieldSearchServiceItem
{
constructor(private http: HttpClient) {}
search(filter: {name: string} = {name: ''}, page = 1): Observable<apiQueryResponseForItem>
{
var queryResponse;
return this.http.get<apiQueryResponseForItem>(`example.com/search/item/${filter.name}`)
.pipe(
tap((response: apiQueryResponseForItem) =>
{
response.qResult = response.qResult
.map(unit => new ItemUnit(unit.Id, unit.Name))
return response;
})
);
}
}
The class defs:
export class ItemUnit
{
constructor
(
public Id:number,
public Name:string,
) {}
}
export interface apiQueryResponseForItem
{
qSuccess: boolean;
qResult: ItemUnit[];
}
I've seen other answers where the solution has been to use the emitEvent:false when setting the value, like so:
this.myForm.get('itemName').patchValue('theDataThatCameBackFromSeach', {emitEvent:false})
That makes sense... but I get the feeling that solution doesn't match up with this observable/injectable/Material approach... mainly because I'm not using a call that does a .setValue() or .patchValue(), I'm guessing there's a binding somewhere in the Material stuff that's handling that.
The server ending up seeing calls like this:
http://example.com/search/item/a (when the letter a is typed)
http://example.com/search/item/[object%20Object] (after clicking the dropdown, JS tries to search 'object' after clumsily falling back to string representation )
My onItemSelection()
is currently uninvolved, it's working but does nothing other than dump to the console. The .qResult
that's coming back from my searchQueryResult
service contains {Id:number,Name:string}
. How can I have the field-setting action of the autocomplete still do its work of setting the field, but not creating a change event when it does that, while still honoring the onItemSelection()
so I can get my other processing done?
I've faced the same issue some time ago, and this is the final solution that I've been using for several inputs in my system.
The steps are:
class Component {
ngOnInit() {
/* SEPARATE THE EVENTS */
// If it's an object, it's been selected.
// I also don't allow selecting `null` but it's up to you.
const itemSelected$ = this.myForm.get('itemName').valueChanges.pipe(
filter(val => typeof val === 'object' && val !== null),
);
// If it's a string, it's been typed into the input
const itemTyped$ = this.myForm.get('itemName').valueChanges.pipe(
filter(val => typeof val === 'string'),
);
/* HANDLE ITEM SELECTED */
itemSelected$.subscribe(item => {
// If you want, you can also handle "fake" items here.
// I use this trick to show a placeholder like "Create New Item" in the dropdown
if (item.id === 0 && item.name === 'Create New Item') {
this.createNewItem().subscribe(
newItem => this.myForm.get('itemName').setValue(newItem),
);
return;
}
// I use this in a custom input component (with ControlValueAccessor)
// So this is where I notify the parent
this.onChange(item);
});
/* HANDLE ITEM TYPED */
const searchQueryResult$ = itemTyped$.pipe(
debounce(200),
tap(value => {/* you could handle starting a loading spinner or smth here */}),
switchMap(name => this.appServiceItem.search({name}, 1)),
);
// now you can either use searchQueryResult$ with async pipe:
// this.filteredResults$ = searchQueryResult$;
// or subscribe to it and update a field in your component class:
searchQueryResult$.subscribe(result => this.searchQueryResult = result);
// If you subscribe, don't forget to clean up subscriptions onDestroy
}
}
I've taken the liberty to add a few suggestions and tricks, but you get the general idea - create two separate mutually exclusive observables and handle them separately.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With