Search code examples
angularangular-materialangular-reactive-forms

How can I retrieve the full object when adding or editing a task instead of just the ID


I am working on a task management system where tasks have associated contacts and subtasks. When I add or edit a task, the system currently only returns the ID of the contacts and the string values of the subtasks. However, I need to retrieve the full objects for both the contacts and the subtasks.

How can I modify my code to retrieve the full objects for contacts and subtasks when adding or editing a task? Any guidance or examples would be greatly appreciated. Thank you!

Demo @ StackBlitz

main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TaskFormComponent],
  template: `
  <button (click)="addTask()">Add Task</button>
  <button (click)="editTask()">Edit Task</button>
  `,
})
export class App {
  name = 'Angular';

  constructor(private dialog: MatDialog) {}

  public addTask() {
    this.dialog
      .open(TaskFormComponent, {})
      .afterClosed()
      .pipe(filter((task) => task))
      .subscribe((task) => {
        console.log('Add Task:');
        console.log(task);
      });
  }

  public editTask() {
    let task: Task = {
      id: 1,
      title: 'Task 1',
      subtasks: [
        {
          id: 1,
          taskId: 1,
          description: 'Subtask 1',
          isDone: true,
        },
        {
          id: 2,
          taskId: 1,
          description: 'Subtask 2',
          isDone: false,
        },
      ],
      contacts: [
        {
          id: 1,
          email: '[email protected]',
          name: 'John Doe',
        },
        {
          id: 2,
          email: '[email protected]',
          name: 'Jane Doe',
        },
      ],
      status: TASK_STATUSES['IN_PROGRESS'],
    };

    this.dialog
      .open(TaskFormComponent, {
        data: { fromPopup: true, task: task },
      })
      .afterClosed()
      .pipe(filter((task) => task))
      .subscribe((task) => {
        console.log('Edit Task:');
        console.log(task);
      });
  }
}

task-form.component.ts

export class TaskFormComponent {
  protected readonly Object = Object;
  taskForm!: FormGroup;
  fromPopup = false;
  keywords!: string[];
  contacts: Contact[] = [
    {
      id: 1,
      email: '[email protected]',
      name: 'John Doe',
    },
    {
      id: 2,
      email: '[email protected]',
      name: 'Jane Doe',
    },
    {
      id: 3,
      email: '[email protected]',
      name: 'Max Max',
    },
    {
      id: 4,
      email: '[email protected]',
      name: 'Anna Anna',
    },
  ];
  @ViewChild(ChipFieldComponent) chipFieldComponent!: ChipFieldComponent;
  constructor(
    private fb: FormBuilder,
    @Optional() private dialogRef: MatDialogRef<TaskFormComponent>,
    @Optional()
    @Inject(MAT_DIALOG_DATA)
    public data: { fromPopup: boolean; task: Task }
  ) {}

  ngOnInit() {
    this.keywords = [];
    this.fromPopup = !!this.data?.fromPopup;

    this.taskForm = this.fb.group({
      id: this.data?.task?.id,
      title: new FormControl(''),
      subTasks: new FormControl(''),
      contacts: new FormControl(''),
      status: TASK_STATUSES['TO_DO'],
    });

    if (this.data?.task) {
      this.taskForm.patchValue({
        id: this.data.task?.id,
        title: this.data.task.title,
        subTasks: this.data.task.subtasks.map((x) => x.description),
        contacts: this.data.task.contacts.map((x) => x.id),
        status: this.data?.task.status,
      });
    }
  }

  public get subTasksFormControl() {
    return this.taskForm.get('subTasks') as FormControl;
  }

  public onSubmit() {
    this.dialogRef.close(this.taskForm.getRawValue());
    this.onReset();
  }

  public onReset() {
    this.taskForm.reset();
    this.chipFieldComponent.keywords = [];
  }
}

task-form.component.html

<div mat-dialog-title>{{ data?.task ? 'Update Task' : 'Create Task' }}</div>

<mat-dialog-content>
  <form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
    <mat-form-field>
      <mat-label>Title</mat-label>
      <input
        matInput
        formControlName="title"
        type="text"
        placeholder="Enter a title"
      />
    </mat-form-field>

    <app-chip-field
      [control]="subTasksFormControl"
      [controlTitle]="'subtask'"
    ></app-chip-field>

    <mat-form-field>
      <mat-label>Contacts</mat-label>
      <mat-select formControlName="contacts" multiple>
        @for (contact of 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 (click)="onSubmit()" 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>

Solution

  • Issue

    Before starting the solution, the first issue is that you need to set an empty array instead of an empty string to your subTasks control to resolve the Mat Chip component not able to add multiple values.

    task-form.component.ts

    this.taskForm = this.fb.group({
      ...,
      subTasks: new FormControl([]),
      ...
    });
    

    Changes

    1. In the same component, you should pass the array of objects for the subTasks and contact controls.

    task-form.component.ts

    if (this.data?.task) {
      this.taskForm.patchValue({
        id: this.data.task?.id,
        title: this.data.task.title,
        subTasks: this.data.task.subtasks,
        contacts: this.data.task.contacts,
        status: this.data?.task.status,
      });
    }
    
    1. Set the <mat-option> value as a contact object for the contacts control.

    task-form.component.html

    <mat-form-field>
      <mat-label>Contacts</mat-label>
      <mat-select formControlName="contacts" multiple [compareWith]="contactCompareWithFn">
        @for (contact of contacts; track contact.id) {
          <mat-option [value]="contact">{{ contact.name }}</mat-option>
        }
      </mat-select>
    </mat-form-field>
    

    task-form.component.ts

    contactCompareWithFn = (contact: Contact, value: Contact) => contact.id == value.id;
    
    1. Pass data?.task?.id to the taskId Input property.

    task-form.component.html

    <app-chip-field
      [control]="subTasksFormControl"
      [controlTitle]="'subtask'"
      [taskId]="data?.task?.id"
    ></app-chip-field>
    

    4.1. Add the taskId @Input property to accept the passed-in value.

    4.2. Modify the keywords, and the methods for the Subtask type.

    chip-field.component.ts

    export class ChipFieldComponent {
      @Input() taskId!: number | undefined;
      keywords: Subtask[] = [];
    
      removeKeyword(keyword: Subtask) {
        const index = this.keywords.indexOf(keyword);
        if (index >= 0) {
          this.keywords.splice(index, 1);
          this.announcer.announce(`removed ${keyword}`);
        }
      }
    
      add(event: MatChipInputEvent): void {
        const value = (event.value || '').trim();
        if (value) {
          this.keywords.push({
            id: 0,
            taskId: this.taskId ?? 0,
            isDone: false,
            description: value,
          } as Subtask);
        }
    
        event.chipInput!.clear();
      }
    }
    
    1. Modify the <mat-chip-row> to supply value with keyword object and display the description.

    chip-form.component.html

    <mat-chip-row (removed)="removeKeyword(keyword)" [value]="keyword">
      {{ keyword.description }}
      <button matChipRemove aria-label="'remove ' + keyword.description">
        <mat-icon>cancel</mat-icon>
      </button>
    </mat-chip-row>
    

    Demo @ StackBlitz