Search code examples
angularangular-materialangular-reactive-forms

How do I add values ​from an array to a form?


When you click on "Edit Task" a form opens that already contains the values ​​of a task. This already works for simple strings, but not for values ​​that have an array of values.

Problem:

A task can be assigned to several people. However, the people who have already been assigned are not selected.

I've already researched, but couldn't implement the solution on my code:

patchValue on a FormArray object?

How to use FormArray in angular

A Stackblitz Demo:

https://stackblitz.com/edit/stackblitz-starters-1ksmca?file=src%2Fmain.ts

Most relevant Code:

ts-file:

export class TaskFormComponent {
  protected readonly Object = Object;
  taskForm!: FormGroup;
  fromPopup = false;

  constructor(
    @Optional()
    @Inject(MAT_DIALOG_DATA)
    public data: { fromPopup: boolean; task: Task }
  ) {}

  ngOnInit() {
    this.fromPopup = !!this.data?.fromPopup;
    this.taskForm = new FormGroup({
      assignedTo: new FormControl(''),
    });

    if (this.data?.task) {
      this.taskForm.setValue({
        assignedTo: this.data.task.contacts,
      });
    }
  }

  public onSubmit() {
    console.log('Submitted');
  }
}

html-file:

<div>
  <div>
    <div mat-dialog-title>Edit Task</div>
  </div>

  <mat-dialog-content>
    <form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
      <mat-form-field>
        <mat-label>Assigned to</mat-label>
        <mat-icon color="primary" matSuffix>group_add</mat-icon>
        <mat-select formControlName="assignedTo" multiple>
          @for (contact of this.data.task.contacts; track contact.id) {
          <mat-option [value]="contact.id">{{ contact.name }}</mat-option>
          }
        </mat-select>
      </mat-form-field>
    </form>
  </mat-dialog-content>

  <mat-dialog-actions>
    <button mat-raised-button color="primary" type="submit">
      {{ data?.task ? 'Update Task' : 'Create Task' }}
    </button>
    <button mat-raised-button mat-dialog-close type="button" color="warn">
      Close
    </button>
  </mat-dialog-actions>
</div>

Solution

  • Since you have set the value of the options of the select to be id field

    ...
    <mat-option [value]="contact.id">{{ contact.name }}</mat-option>
    ...
    

    We must also patchValue the ids as an array, for this we use the javascript map operator, to fetch only the ids from the array of objects!

    ...
    if (this.data?.task) {
      this.taskForm.setValue({
        assignedTo: this.data.task.contacts.map((x) => x.id),
      });
    }
    ...
    

    FULL CODE:

    TS

    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { Inject, Optional } from '@angular/core';
    import { MatButtonModule } from '@angular/material/button';
    import { MatCheckbox } from '@angular/material/checkbox';
    import {
      MatError,
      MatFormField,
      MatFormFieldModule,
      MatLabel,
    } from '@angular/material/form-field';
    import {
      FormControl,
      FormGroup,
      ReactiveFormsModule,
      Validators,
    } from '@angular/forms';
    import { MatDatepickerModule } from '@angular/material/datepicker';
    import { MatInputModule } from '@angular/material/input';
    import { MatOption, provideNativeDateAdapter } from '@angular/material/core';
    import { MatButtonToggleModule } from '@angular/material/button-toggle';
    import { TitleCasePipe } from '@angular/common';
    import { MatSelect } from '@angular/material/select';
    import { MatIconModule } from '@angular/material/icon';
    import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
    import {
      MAT_DIALOG_DATA,
      MatDialog,
      MatDialogModule,
      MatDialogRef,
    } from '@angular/material/dialog';
    import { Task } from '../../app/task';
    
    @Component({
      selector: 'app-task-form',
      standalone: true,
      imports: [
        CommonModule,
        MatButtonModule,
        MatCheckbox,
        MatError,
        MatFormField,
        MatLabel,
        ReactiveFormsModule,
        MatDatepickerModule,
        MatInputModule,
        MatFormFieldModule,
        MatButtonToggleModule,
        TitleCasePipe,
        MatSelect,
        MatOption,
        MatRadioGroup,
        MatRadioButton,
        MatDialogModule,
        MatIconModule,
      ],
      templateUrl: './task-form.component.html',
      styleUrl: './task-form.component.css',
    })
    export class TaskFormComponent {
      protected readonly Object = Object;
      taskForm!: FormGroup;
      fromPopup = false;
    
      constructor(
        @Optional()
        @Inject(MAT_DIALOG_DATA)
        public data: { fromPopup: boolean; task: Task }
      ) {}
    
      ngOnInit() {
        this.fromPopup = !!this.data?.fromPopup;
        this.taskForm = new FormGroup({
          assignedTo: new FormControl(''),
        });
    
        if (this.data?.task) {
          this.taskForm.setValue({
            assignedTo: this.data.task.contacts.map((x) => x.id),
          });
        }
      }
    
      public onSubmit() {
        console.log('Submitted');
      }
    }
    

    HTML

    <div>
      <div>
        <div mat-dialog-title>Edit Task</div>
      </div>
    
      <mat-dialog-content>
        <form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
          <mat-form-field>
            <mat-label>Assigned to</mat-label>
            <mat-icon color="primary" matSuffix>group_add</mat-icon>
            <mat-select formControlName="assignedTo" multiple>
              @for (contact of this.data.task.contacts; track contact.id) {
              <mat-option [value]="contact.id">{{ contact.name }}</mat-option>
              }
            </mat-select>
          </mat-form-field>
        </form>
      </mat-dialog-content>
    
      <mat-dialog-actions>
        <button mat-raised-button color="primary" type="submit">
          {{ data?.task ? 'Update Task' : 'Create Task' }}
        </button>
        <button mat-raised-button mat-dialog-close type="button" color="warn">
          Close
        </button>
      </mat-dialog-actions>
    </div>
    

    PARENT:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { TaskFormComponent } from './app/task-form/task-form.component';
    import { filter } from 'rxjs';
    import 'zone.js';
    import { MatDialog } from '@angular/material/dialog';
    import { Task } from './app/task';
    import { provideAnimations } from '@angular/platform-browser/animations';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [TaskFormComponent],
      template: `
        <button (click)="editTask()">Edit Task</button>
      `,
    })
    export class App {
      name = 'Angular';
    
      constructor(private dialog: MatDialog) {}
    
      public editTask() {
        let task: Task = {
          id: 1,
          contacts: [
            {
              id: 1,
              name: 'John Doe',
            },
            {
              id: 2,
              name: 'Jane Doe',
            },
          ],
        };
        this.dialog
          .open(TaskFormComponent, {
            data: { fromPopup: true, task: task },
          })
          .afterClosed()
          .pipe(filter((task) => task))
          .subscribe((task) => {
            console.log('Edit Task');
          });
      }
    }
    
    bootstrapApplication(App, {
      providers: [provideAnimations()],
    }).catch((err) => console.error(err));
    

    Stackblitz Demo