'How to remove the previously selected option from a drop-down menu in a table?

I am making a project on angular 7.It has a table with a column having dropdowns. The dropdown contains various languages. When a language is selected in a particular row, then it shouldn't appear in the dropdown in the subsequent row. How do I do that?

I tried deleting the selected language from the array using splice().But as it deletes the object, it is also not shown in the dropdown.

Following is the html -(it is the row of the table that defines the dropdown, and this row is dynamic)

 <tr *ngFor="let field of fieldArray;let i = index">
  <td><button class="btn" (click)="deleteFieldValue(i)"><i class="fa fa-trash"></i></button></td>
  <td class="select">
    <select #selectLang (change)="selected(selectLang.value,i)">
      <option value="undefined" disabled>Select Language</option>
      <option *ngFor="let lang of languageList" value={{lang.name}} [ngValue]="lang.name">{{lang.name}}</option>
    </select>
  </td>
  <td>
    <input id="fileUpload" name="fileUpload" type="file" name="upload_file" (change)=onFileChange($event)>
  </td>
</tr>

following is the typescript code -

languageList = [{'name': "Dothraki"},{'name': "Japanese"}, 
{'name':"German"},{'name':"French"},{'name': "Spanish"}, {'name': 
"Russian"}, {'name': "Italian"}];
  selectedLang;
  optionLang:string;
  fieldArray: Array<any> = [];
  newAttribute: any = {};
  fileUploadName: any;
  selected(lang:string,index:number){
    console.log(lang);

    // this.languageList.splice(index, 1);
    for(let i =0; i< this.languageList.length; i++) {
      if(this.languageList[i]['name'] === lang) {
        this.languageList.splice(i,1);
        break;
      }
    }
  }
  addFieldValue() {
    this.fieldArray.push("hg");
    this.newAttribute = {};
  }
  deleteFieldValue(index: number) {
    this.fieldArray.splice(index, 1);
}
  openFileBrowser(event:any){
    event.preventDefault();
    let element: HTMLElement = document.getElementById('fileUpload') as 
HTMLElement;
    element.click();
  }

  onFileChange(event:any){
    let files = event.target.files;
    this.fileUploadName = files[0].name;
    console.log(files);
  }


Solution 1:[1]

you can solve the problem by holding a set of selected languages and display options conditionally based on whether an option/language is selected before or not.

create a Set to hold selected langs

selectedLangs = new Set<string>();

create a view query to get a list of all select elements

@ViewChildren("selectLang") langSelects: QueryList<ElementRef<HTMLSelectElement>>;

whenever a selection is made/changed on any of the select elements re-populate the selectedLangs set

  selected() {
    this.selectedLangs.clear();
    this.langSelects.forEach(ls => {
      const selectedVal = ls.nativeElement.value;
      if (selectedVal && selectedVal !== "undefined") this.selectedLangs.add(selectedVal);
    });
 }

whenever a field is deleted just remove that language from selectedLangs

  deleteFieldValue(index: number, lang: string) {
    this.selectedLangs.delete(lang);
    this.fieldArray.splice(index, 1);
  }

and when displaying options for a select check if it is currently selected on current select or already selected in another select *ngIf="selectLang.value === lang.name || !isSelected(lang.name)"

<ng-container *ngFor="let lang of languageList" >
  <option *ngIf="selectLang.value === lang.name || !isSelected(lang.name)" value={{lang.name}} [ngValue]="lang.name">
      {{lang.name}}
  </option>
</ng-container>

where isSelected is defined as

  isSelected(lang: string) {
    return this.selectedLangs.has(lang);
  }

here is a working demo with full source https://stackblitz.com/edit/angular-dqvvf5

Solution 2:[2]

You can store the langs in an array make a function like

  lang = []; //define the array
  getLang(i, languageList) {
    return i == 0 ? languageList :
      this.getLang(i - 1, languageList.filter(x => x.name != this.lang[i-1]))
  }

So, you can has some like

<div *ngFor="let a of languageList;let i=index">
    <select  [(ngModel)]="lang[i]">
      <option value="undefined" disabled>Select Language</option>
      <option *ngFor="let lang of getLang(i,languageList)" 
          [value]="lang.name" >{{lang.name}}</option>
    </select>
</div>

But I don't like because each change force Angular to calculate all the options. So we are going to improve the code using FormArray and an array langList, and make sure that we can not choose the same language

First our variable and our function changed

  langList=[];
  getLangForFormArray(i, languageList) {
    return i == 0 ? languageList :
      this.getLang(i - 1, this.langList[i-1].filter(x => x.name != this.formArray.value[i-1]))
  }

We create a formArray

  formArray=new FormArray(this.languageList.map(()=>new FormControl(null)))

And in ngOnInit

  ngOnInit()
  {
    this.formArray.valueChanges.pipe(startWith(null)).subscribe(()=>{
      //create the langList array
      for (let i=0;i<this.languageList.length;i++)
         this.langList[i]=this.getLangForFormArray(i,this.languageList)

      //check no repeat values
      if (value)
      {
         value.forEach((x,index)=>{
         if (this.formArray.value.findIndex(v=>v==x)!=index)
             this.formArray.at(index).setValue(null,{emitEvent:false})
         })
      }
    })
  }

See that use formArray valueChanges with pipe(startWith(null)) to create at first the langList

The .html

<div *ngFor="let control of formArray.controls;let i=index">
    <select [formControl]="control">
      <option value="null" disabled>Select Language</option>
      <option *ngFor="let lang of langList[i]" 
          [value]="lang.name" >{{lang.name}}</option>
    </select>
</div>

And the demo in stackblitz

Solution 3:[3]

Define template that iterates trough form array controls

<mat-form-field *ngFor="let control of formArray.controls; let idx = index">
  <mat-select [formControl]="formArray.controls[idx]"
    (selectionChange)="onSelectionChange()">
    <mat-option [value]="blankLangId">''</mat-option>
    <mat-option *ngFor="let lang of langMap.get(idx)"
      [value]="lang.id">{{lang.name}}</mat-option>
  </mat-select>
</mat-form-field>

Define an array with all possible langs

public langs: LangModel[] = [// here your data]
public blankLangId = 0; // represents an empty selection

Define a Map that is going to hold an array of langs for specific dropdown

  public langMap= new Map<number, LangModel[]>();

Define a method that going to be execute every time selection is changed.

  public onSelectionChange(): void {
    this.normalizeLangsDropDowns();
  }

What normalizeLangsDropDowns method is going to do?

Inside this method we will loop trough form array controls

For each form control we will get all selected langs ids expect current form control

Then we will filter initial array of langs to remove selected ids and set the result into the Map.

Let's implement this logic.

Define a method that gets selected values from formControls expect provided index of control.

  private getSelectedLangIdsExcludingProvidedIndex(langIndex: number): Set<number> {
    return new Set(this.formArray.controls
      .map<number>(control => control.value)
      .filter((langId: number, index: number) => index!== langIndex && langId!== this.blankLangId)
    );
  }

Define a method that will return filtered array of langs

  private filterLangsForLangIndex(langIndex: number): LangModel[] {
    const langIdsToRemove = this.getSelectedLangIdsExcludingProvidedIndex(langIndex);

    return langIdsToRemove .size > 0
      ? this.langs.filter((lang=> !langIdsToRemove.has(lang.id)))
      : this.langs;
  }

Finally define the normalizeLangsDropDowns method

  private normalizeLangsDropDowns(): void {
    // loop trough each form control
    this.formArray.controls.forEach((langControl: AbstractControl, langIndex: number) => {

      // set filtered langs at current index
      this.langMap.set(langIndex, this.filterLangsForLangIndex(langIndex));
    });
  }

Here you go.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ysf
Solution 2 Eliseo
Solution 3 marc_s