Arash
Arash

Reputation: 17

angular material optionSelected does not fire when value in input field

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

Answers (3)

Mark Flegg
Mark Flegg

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

Lucia Cazorla Vicente
Lucia Cazorla Vicente

Reputation: 56

I had the same issue, I solved it by setting the addOnBlur option to false. Hope this works for you!

Upvotes: 3

Alfredo Zamudio
Alfredo Zamudio

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

Related Questions