1

Sorry, I'm not even sure if this is the right title for my question. I tried to keep the code as light as possible.

Basically, I have a FormArray of FormGroups that each consist of an Angular Material Autocomplete. I want the options for the autocompletes to be an Observable so that I can update the entire list of options whenever one of the FormGroups in the array is changed - basically, whenever an option is selected I don't want it to be able to be selected by any of the other FormGroup's in the array.

But, and this is where my new-ness to angular will shine, I am updating the list of options on valueChanges, but when I push a new FormGroup on the array, I need a timeout that triggers a second valueChanges in order for the autocomplete options to show up at all. If I don't have it in there, then the list doesn't show up in the most recently added FormGroup.

Why is this?

The code is here on stackblitz

Just click Add Input and you'll see a list of users in the autocomplete, but if you comment out line 72 and click Add Input, you won't see the list in the most recently added Form Group.

EDIT

It's going to be way easier to view the question by looking at the code on stackblitz, but because I got downvoted and close suggested, here's the component code:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, FormArray } from '@angular/forms';
import { switchMap } from 'rxjs/operators';
import { of } from 'rxjs'

const users = [
    {
        id:1,
        name:'user1',
    },
    {
        id:2,
        name:'user2',
    },
    {
        id:3,
        name:'user3',
    }
]

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  usersForm: FormArray;  
  users = users;
  filteredUsers: Observable<any>;
  selectedUsers = [];
  canAddInput = true;

  constructor(private fb: FormBuilder ) {}

  ngOnInit() {
    this.usersForm = this.fb.array([]);

    this
        .usersForm
        .valueChanges
        .subscribe(data => {
            this.selectedUsers = data.map(d => d.userNameSelector).filter(d => !!d);
            this.canAddInput = this.getUnselectedUsers().length !== 0 && this.usersForm.controls.length < this.users.length
        })

    //filtered users is an observable 
    this.filteredUsers = this
        .usersForm
        .valueChanges
        .pipe(
            switchMap(() => of(this.getUnselectedUsers()))
        )

   }

   getUnselectedUsers()
   {
    //console.log(this.users.filter(u => this._isNotSelectedUser(u.name)));
    return this.users.filter(u => this._isNotSelectedUser(u.name));
   }

  _isNotSelectedUser = user => !this.selectedUsers.includes(user)

  addInput() {
    this.usersForm.push(this.fb.group({userNameSelector:null}));

    //why do I need this - it triggers usersForm.valueChanges and filteredUsers is updated, but won't the code work without it why?
    setTimeout(() => this.usersForm.updateValueAndValidity(),1);
  }

  removeInput = idx => this.usersForm.removeAt(idx)

}

And the HTML:

    <form class="example-form" [formGroup]='usersForm'>
  <div *ngIf="usersForm.controls">
    <div
      *ngFor="let ctrlName of usersForm.controls; let i = index;" [formGroupName]="i">

      <mat-form-field class="example-full-width">
        <input 
          matInput 
          placeholder="Choose a user" 
          [matAutocomplete]="auto"
          formControlName="userNameSelector"
        >
      </mat-form-field>
      <!-- <span>Your choice is: {{usersForm.get(input.name).value | json}}</span> -->

      <mat-autocomplete #auto="matAutocomplete">
          <mat-option 
            <!-- this async pipe is going to be important here -->
            *ngFor="let user of filteredUsers | async" 
            [value]="user.name"
          >
            <span>{{ user.name }}</span>
          </mat-option>
      </mat-autocomplete>
      <button 
        mat-raised-button 
        color="warn"
        (click)="removeInput(i)"
      >Remove Input</button>
    </div>
  </div>
  <button 
    *ngIf="canAddInput"
    mat-raised-button color="primary"
    (click)="addInput()"
  >Add Input</button>
</form>
Adam Jenkins
  • 51,445
  • 11
  • 72
  • 100
  • https://stackoverflow.com/questions/42197806/what-is-updatevalueandvalidity – Chellappan வ Dec 02 '18 at 04:51
  • @Chellappan - doesn't really answer my question. I knew I had to trigger `valueChanges` in my code, but I still don't know **why** I had to trigger `valueChanges`. It's a timing thing, for sure, but I'd like somebody to ELI5 because, from looking at the code, I don't know why the `valueChanges` event should be needed. – Adam Jenkins Dec 02 '18 at 04:59
  • 2
    @Adam, at first time you push the new control, Angular add the autocomplete, but it's necesary a "second round" if you want "fill" the "filteredUsers" of the new control because is a observable and you not subscribe yet. If your filteredUsers was not an observable (simply in the first changeValues.subscribe make this.filteredUsers=this.getUnselectedUsers() - and remove the "| async"- in the mat-autocompleted) you needn't make the updateValueAndValidity – Eliseo Dec 02 '18 at 16:41
  • @Eliseo - you nailed it. I'm new to observables (is it that obvious?). Thanks! – Adam Jenkins Dec 02 '18 at 16:46

0 Answers0