Reputation: 17
I'm using angular material input chips with autocomplete, I have a bit of a complicated setup where if the user attempts to select an option that doesn't exist yet, a dialog pops up and they can create a new option and a chip is created for it.
The problem is when a user enters part of an option name e.g. 'we' , while searching for 'western', moves the mouse down and clicks on 'western', the dialog still pops up from inside matChipInputTokenEnd even though I believe optionSelected should be triggered.
when i use the arrow keys to select 'western' and hit enter instead of using the mouse, it works as expected. a 'western' chip is created.
how do i make a mouse click on an option from the dropdown, act the same as using the arrows to highlight an option and pressing enter?
edit: the issue is gone when the dialog is removed from inside the add()
edit 2: I've decided to abandon this feature for now, I've spent a solid week trying everything, its really a shame because im so close, if there was some kind of way to detect if matChipInputTokenEnd was triggered via a click versus an enter i could solve this, If anyone figures out a way to open a dialog from within the add() Or if there is a more appropriate way to do this please let me know. The only alternative i can think of is using a basic add() like from the exampeles and adding dialogData to an array and then onSubmit for the parent form poping up a dialog to fill in the rest of the item details, but i dont like the user experience of that so im not sure its worth it.
template
<mat-form-field class="full-width">
<mat-chip-list #modifierChipList>
<mat-chip
*ngFor="let modifier of selectedModifiers"
[selectable]="selectable"
[removable]="removable"
(removed)="remove(modifier,'selectedModifiers','modifierList')">
{{modifier}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input
(click)="focusOut()"
placeholder="Modifiers"
#modifierInput
[formControl]="modifierCtrl"
[matAutocomplete]="autoModifier"
[matChipInputFor]="modifierChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event, 'selectedModifiers', 'modifierList', 'modifierCtrl', 'modifier', filteredModifiers)">
</mat-chip-list>
<mat-autocomplete #autoModifier="matAutocomplete" (optionSelected)="selected($event, 'selectedModifiers', 'modifierInput', 'modifierCtrl', 'modifier')">
<mat-option *ngFor="let modifier of filteredModifiers | async" [value]="modifier">
{{modifier}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
ts:
add(event: MatChipInputEvent, selectedItems, allItemsList, ctrl, type, filteredList): void {
const options = {};
const input = event.input;
const value = event.value;
if(!this.autoModifier.isOpen || this[allItemsList].indexOf(value) < 0 ){
// Add our new item to both the DB and local selected items
if ((value || '').trim()) {
if(!this[allItemsList].includes(value.trim())){
switch(type){
case 'category':
break;
case 'option':
options['Type'] = this.typeList;
options['Categories'] = this.categoryList;
break;
case 'component':
options['Options'] = this.optionList;
options['Categories'] = this.categoryList;
break;
case 'modifier':
options['Additions'] = this.optionList;
options['Removals'] = this.optionList;
options['Options'] = this.optionList;
options['Categories'] = this.categoryList;
options['Type'] = this.typeList;
break;
case 'tax':
break;
default:
break;
}
let dialogQuestions = this.service.getQuestions(type, options);
//open a modal to create that item
const dialogRef = this.dialog.open(DialogComponent, {
width: '70%',
data: {
dialog: this,
type: type,
details:{
name:value,
questions: dialogQuestions
}
},
});
dialogRef.afterClosed().subscribe(result => {
this[allItemsList].push(result.name);
this[selectedItems].push(result.name);
this.autocomplete.closePanel();//this doesnt work for some reason
//based on the result add it to the allItemsList and the selectedItems
//also create a chip for it
});
}
}
// Reset the input value
if (input) {
input.value = '';
}
this[ctrl].setValue(null);
}
}
selected(event: MatAutocompleteSelectedEvent, selectedItems, inputType, ctrl): void {
this[selectedItems].push(event.option.viewValue);
this[inputType].nativeElement.value = '';
this[ctrl].setValue(null);
}
the ENTIRE ts file
import { Component, OnInit, ElementRef, ViewChild, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators, NgForm, FormControl } from '@angular/forms';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { ApiService } from '../../api.service';
import { forkJoin, Observable } from 'rxjs';
import { map, startWith, filter } from 'rxjs/operators';
import { MatChipsModule, MatAutocompleteSelectedEvent, MatChipInputEvent, MatAutocomplete, MatAutocompleteTrigger } from '@angular/material'
import {COMMA, ENTER} from '@angular/cdk/keycodes';
import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material';
import { DialogComponent } from '../../shared/dialog/dialog.component';
import { DialogQuestionService } from '../../shared/dialog/dialog-question.service';
import { a } from '@angular/core/src/render3';
@Component({
selector: 'app-item-create',
templateUrl: './item-create.component.html',
styleUrls: ['./item-create.component.css'],
providers:[DialogQuestionService]
})
export class ItemCreateComponent implements OnInit {
visible = true;
selectable = true;
removable = true;
addOnBlur = true;
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
itemCreateForm: FormGroup;
modifierCtrl = new FormControl();
optionCtrl = new FormControl();
filteredModifiers: Observable<string[]>;
filteredOptions: Observable<string[]>;
categoryList: any[] = [];
optionList: any[] = []; //string[] = ['White Bread', 'Wheat Bread', 'Extra Mayo', 'Mustard', 'Grilled Onions', 'Toasted'];
modifierList: string[] = []; //string[] = ['Add Bacon', 'Add Avocado', 'Double Meat', 'Double Cheese'];
taxList: any; //string[] = ['Defaut Tax', 'Dine-in Tax', 'Hot Food Tax'];
componentList: any; //string[] = ['White Bread', 'Wheat Bread', 'Mayo', 'Turkey', 'Ham', 'Lettuce', 'Tomato', 'Onions'];
typeList: any;
//item = {};
selectedModifiers: string[] = [];
selectedOptions: any[] = [];
pageType = 'Create';
id = this.route.snapshot.params['id'];
formData:any;
//dialogQuestions:any;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
@ViewChild('modifierInput') modifierInput: ElementRef<HTMLInputElement>;
@ViewChild('optionInput') optionInput: ElementRef<HTMLInputElement>;
// @ViewChild('auto') matAutocomplete: MatAutocomplete; //remove this
@ViewChild('autoModifier') autoModifier:MatAutocomplete;
// @ViewChild('autoOption') optionMatAutoComplete: MatAutocomplete;
constructor(
public service: DialogQuestionService,
private router: Router,
private route: ActivatedRoute,
private api: ApiService,
private fb: FormBuilder,
public dialog: MatDialog
){
//if the appropriate filters list is empty THEN open the modal in add()
this.filteredModifiers = this.modifierCtrl.valueChanges.pipe(
startWith(null),
map((modifier: string | null) => modifier ? this._filter(modifier, 'modifierList') : this.modifierList.slice() )
);
this.filteredOptions = this.optionCtrl.valueChanges.pipe(
startWith(null),
map((option: string | null) => option ? this._filter(option, 'optionList') : this.optionList.slice() )
);
this.createForm();
}
ngOnInit(){
this.setupForm(this.id);
}
setupForm(id) {
forkJoin(
this.api.getAll('Category'),
this.api.getAll('Modifier'),
this.api.getAll('Option'),
this.api.getAll('Tax'),
this.api.getAll('Component'),
this.api.getAll('Type')
).subscribe(([Categories, Modifiers, Options, Taxes, Components, Types]) => {
this.categoryList = Categories.map(c => c.name);
this.modifierList = Modifiers.map(c => c.name);
this.optionList = Options.map(c => c.name);
this.taxList = Taxes.map(c => c.name );
this.componentList = Components.map(c => c.name);
this.typeList = Types.map(c => c.name );
});
if(this.route.snapshot.data.update){
this.api.get('Item',id).subscribe( item => this.itemCreateForm.patchValue(item) );
this.pageType = 'Update';
}
}
createForm(){
this.itemCreateForm = this.fb.group({
name: '',
categories: [],
price: 0,
options: [],
components: [],
modifiers: [],
removals: [],
taxes: [],
description: '',
author: '',
});
}
add(event: MatChipInputEvent, selectedItems, allItemsList, ctrl, type, filteredList): void {
const options = {};
const input = event.input;
const value = event.value;
if(!this.autoModifier.isOpen || this[allItemsList].indexOf(value) < 0 ){ // just trying this out for modifiers so far, if it works make a switch or something
console.log('listy',filteredList, this.autoModifier);
// Add our new item to both the DB and local selected items
if ((value || '').trim()) {
if(!this[allItemsList].includes(value.trim())){
//in this case it is new
switch(type){
case 'category':
break;
case 'option':
options['Type'] = this.typeList;
options['Categories'] = this.categoryList;
break;
case 'component':
options['Options'] = this.optionList;
options['Categories'] = this.categoryList;
break;
case 'modifier':
options['Additions'] = this.optionList;
options['Removals'] = this.optionList;
options['Options'] = this.optionList;
options['Categories'] = this.categoryList;
options['Type'] = this.typeList;
break;
case 'tax':
break;
default:
break;
}
let dialogQuestions = this.service.getQuestions(type, options);
//open a modal to create that item
const dialogRef = this.dialog.open(DialogComponent, {
width: '70%',
data: {
dialog: this,
type: type,
details:{
name:value,
questions: dialogQuestions
}
},
});
dialogRef.afterClosed().subscribe(result => {
this[allItemsList].push(result.name);
this[selectedItems].push(result.name);
this.autocomplete.closePanel();//this doesnt work for some reason
//based on the result add it to the allItemsList and the selectedItems
//also create a chip for it
});
}
}
// Reset the input value
if (input) {
input.value = '';
}
this[ctrl].setValue(null);
}
remove(item: string, type): void {
console.log('removing from selected', this, item , type, this[type]);
const index = this[type].indexOf(item);
if (index >= 0) {
this[type].splice(index, 1);
}
}
selected(event: MatAutocompleteSelectedEvent, selectedItems, inputType, ctrl): void {
console.log('SELECTED HAS FIRED')
this[selectedItems].push(event.option.viewValue);
this[inputType].nativeElement.value = '';
this[ctrl].setValue(null);
}
focusOut() {
//change this to pass in the control as argument
this.modifierCtrl.disable();
this.modifierCtrl.enable();
// this seems to help but now im having issues with focus moving
// to the first chip when trying to select the input box.
// its good enough for now
}
private _filter(value: string, type): string[] {
const filterValue = value.toLowerCase();
return this[type].filter(item => item.toLowerCase().indexOf(filterValue) === 0);
}
onFormSubmit(form: NgForm) {
if (this.pageType === 'Create') {
console.log('Creating', form)
this.api.post('Item', form)
.subscribe(res => {
console.log('resres',res)
let id = res['_id'];
this.router.navigate(['menu/details', id]);
}, (err) => {
console.log(err);
});
} else if (this.pageType === 'Update') {
console.log('Updating', form)
this.api.update('Item', this.id, form)
.subscribe(res => {
console.log('resres',res)
let id = res['_id'];
this.router.navigate(['menu/details', id]);
}, (err) => {
console.log(err);
});
}
}
}
Upvotes: 0
Views: 5828
Reputation: 113
I see this is a very old question, but I just ran into a similar issue myself and came up with another work around (quite hacky, but there you go).
In my case, I have methods being called from the template for the Input element's blur
event, as well as the mat-autocomplete optionSelected
event. The called methods take care of adding chips to the formControl attached to the mat-chip-list element...
...<input
#tagInput
[matChipInputFor]="chipList"
type="text"
[formControl]="tagInputControl"
[matAutocomplete]="auto"
#trigger="matAutocompleteTrigger"
(blur)="onInputBlur($event)"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
autoActiveFirstOption
(optionSelected)="onTagSelected($event, trigger, tagInput)"
>...
The issue I saw was that if a user typed into the input field and then selected an option from the autocomplete list, the input blur event fired first, and the optionSelected event never fired.
My hacky workaround is to add a setTimeout to the onBlur method. For whatever reason, while the onBlur method is waiting, the optionSelected event fires.
onInputBlur(event: FocusEvent) {
const input = event.target as HTMLInputElement;
setTimeout(() => {
if (
input.value &&
input.value.length > 1
) {
// Add the value to the chip list source array and do whatever else you need...
}
}, 150);
}
I would love to know WHY this works, but since my component is now working as intended...
Upvotes: 0
Reputation: 56
I had the same issue, I solved it by setting the addOnBlur option to false. Hope this works for you!
Upvotes: 3
Reputation: 418
this seems to be a pretty common issue with the component and it's mainly because when you hit ENTER, the matChipInputTokenEnd and the optionSelected fire. An then the chip will already be added and the input wouldn't have any value to add. Maybe that's why you're not getting the callback for that method. Anyway, check this answer, it may help :
[https://stackoverflow.com/a/52814543/5625648][1]
Hope it helps, cheers.
Upvotes: 0