I am encountering two issues with the subtask editing functionality in my application:
When I click the "Edit" button to edit a subtask and make changes, the subtask gets updated even if I don't click the "Save" button. Instead, when I click the "Create/Update" button, the changes to the subtask are saved automatically. This behavior is unexpected since I did not explicitly save the subtask.
After making changes to a subtask without saving then updating the task. After that reopening the task dialog, the save icon (diskette) is displayed instead of the edit icon (pencil). The correct behavior should display the edit icon when the task is reopened without any unsaved changes.
Here is a detailed description of the steps to reproduce these issues:
For the second issue:
I would appreciate any advice on how to address these issues.
main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [TaskFormComponent, CommonModule],
template: `
<!-- <app-task-form></app-task-form> -->
<br><br><br>
<button (click)="updateTask()">Edit</button>
<div>Updated Subtasks:</div>
<ul>
<li *ngFor="let subtask of this.task.subtasks">
{{ subtask.description }}
</li>
</ul>
`,
})
export class App {
name = 'Angular';
task: Task = {
id: 1,
title: 'Task 1',
subtasks: [
{
id: 1,
taskId: 1,
description: 'Subtask 1',
isDone: false,
isEditable: false,
},
{
id: 2,
taskId: 1,
description: 'Subtask 2',
isDone: false,
isEditable: false,
},
],
};
constructor(private dialog: MatDialog) {}
public updateTask() {
this.dialog
.open(TaskFormComponent, {
data: { fromPopup: true, task: this.task },
})
.afterClosed()
.pipe(filter((task) => task))
.subscribe((task) => {});
}
}
bootstrapApplication(App, {
providers: [provideAnimations()],
}).catch((err) => console.error(err));
task-form.component.ts
export class TaskFormComponent {
protected readonly Object = Object;
taskForm!: FormGroup;
fromPopup = false;
subtasks: Subtask[] = [];
constructor(
private fb: FormBuilder,
@Optional() private dialogRef: MatDialogRef<TaskFormComponent>,
@Optional()
@Inject(MAT_DIALOG_DATA)
public data: { fromPopup: boolean; task: Task }
) {
if (!this.data) {
this.data = {
fromPopup: true,
task: {
id: 1,
title: '',
subtasks: [],
},
};
}
}
ngOnInit() {
this.fromPopup = !!this.data?.fromPopup;
this.taskForm = this.fb.group({
id: this.data?.task?.id,
title: new FormControl(''),
subtasks: new FormControl(''),
});
if (this.data?.task) {
this.taskForm.patchValue({
id: this.data.task?.id,
title: this.data.task.title,
});
this.subtasks = this.data.task.subtasks;
}
}
public get subtasksFormControl() {
return this.taskForm.get('subtasks') as FormControl;
}
public addSubtask() {
const subtask = this.taskForm.get('subtasks')?.value;
if (subtask.trim()) {
this.subtasks.push({
id: undefined,
taskId: 1,
description: subtask,
isDone: false,
isEditable: false,
} as Subtask);
this.subtasksFormControl.setValue(this.subtasks);
}
this.clearSubtask();
}
public clearSubtask() {
this.taskForm.patchValue({ subtask: '' });
}
public editSubtask(index: number) {
this.subtasks[index].isEditable = true;
}
public saveSubtask(index: number, subtask: Subtask) {
this.subtasks[index].isEditable = false;
this.subtasks[index] = subtask;
}
public deleteSubtask(index: number) {
this.subtasks.splice(index, 1);
}
public onSubmit() {
if (this.data?.task) {
const taskRawValue = {
...this.taskForm.getRawValue(),
subtasks: this.subtasks,
};
this.onUpdateTask();
} else {
}
this.onReset();
}
public onUpdateTask() {
const taskRawValue = {
...this.taskForm.getRawValue(),
subtasks: this.subtasks,
};
if (this.fromPopup) {
this.dialogRef.close(taskRawValue);
} else {
}
}
public onReset() {
this.taskForm.reset();
this.subtasks = [];
}
}
task-form.component.html
<div mat-dialog-title>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>
<mat-form-field>
<mat-label>Subtasks</mat-label>
<input
matInput
formControlName="subtasks"
type="text"
placeholder="Enter a subtask"
/>
<button mat-icon-button matSuffix (click)="addSubtask()">
<mat-icon>add</mat-icon>
</button>
<div class="vertical-divider" matSuffix></div>
<button mat-icon-button matSuffix (click)="clearSubtask()">
<mat-icon>close</mat-icon>
</button>
<mat-hint>Press enter to add a subtask</mat-hint>
</mat-form-field>
</form>
<!-- SHOW HERE ALL SUBTASKS-->
<div class="subtasks-container">
@for (subtask of subtasks; track subtask){
<div class="subtask-text">
<span>•</span>
<input
[(ngModel)]="subtask.description"
type="text"
name="subtask"
[readonly]="!subtask.isEditable"
[ngClass]="{
editable: subtask.isEditable,
'read-only': !subtask.isEditable
}"
/>
<div class="subtasks-actions-container">
@if(!subtask.isEditable) {
<mat-icon color="primary" (click)="editSubtask($index)">edit</mat-icon>
} @if(subtask.isEditable) {
<mat-icon color="primary" (click)="saveSubtask($index, subtask)"
>save</mat-icon
>
}
<mat-icon color="warn" (click)="deleteSubtask($index)">delete</mat-icon>
</div>
</div>
}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button (click)="onSubmit()" mat-raised-button color="primary" type="submit">
Create/Update
</button>
</mat-dialog-actions>
From here:
this.dialog
.open(TaskFormComponent, {
data: { fromPopup: true, task: this.task },
})
you are sharing the reference of this.task
into the MatDialogData
, which results in whatever changes made in TaskFormComponent
, will reflect your parent component (App
).
You should solve it with a deep copy.
Pre-requisites: Install loadash
import * as _ from 'lodash';
public updateTask() {
this.dialog
.open(TaskFormComponent, {
data: { fromPopup: true, task: _.cloneDeep(this.task) },
})
.afterClosed()
.pipe(filter((task) => task))
.subscribe((task) => (this.task = task));
}
Also, don't forget to re-assign the changed value to task
.
Updated
You save the latest (changed) data without checking the isEditable
status. Before saving and the modal close, you should check the isEditable
status and reflect the original value for those subtasks which are not saved (by index/position). Besides, you should need a deep copy for the subtasks
to retain the original value from the MAT_DIALOG_DATA
.
import * as _ from 'lodash';
ngOnInit() {
...
if (this.data?.task) {
this.taskForm.patchValue({
id: this.data.task?.id,
title: this.data.task.title,
});
this.subtasks = _.cloneDeep(this.data.task.subtasks);
}
}
public deleteSubtask(index: number) {
this.subtasks.splice(index, 1);
this.data.task.subtasks.splice(index, 1);
}
public onSubmit() {
if (this.data?.task) {
const taskRawValue = {
...this.taskForm.getRawValue(),
subtasks: this.subtasks.map((x, i) =>
x.isEditable ? (this.data.task.subtasks.at(i) ?? x) : x
),
};
this.onUpdateTask();
} else {
}
this.onReset();
}
public onUpdateTask() {
const taskRawValue = {
...this.taskForm.getRawValue(),
subtasks: this.subtasks.map((x, i) =>
x.isEditable ? (this.data.task.subtasks.at(i) ?? x) : x
),
};
if (this.fromPopup) {
this.dialogRef.close(taskRawValue);
} else {
}
}
Issue(s) & Concern(s)
Issue 1: I noticed that you have a typo error for the control name which results in the form control's value is not clear after adding the sub-tasks. Correct it as below:
public clearSubtask() {
this.taskForm.patchValue({ subtasks: '' });
}
Issue 2: The default behavior of <button>
element is type="submit"
. As the Add and Close button for subtasks
form control within the form, this will submit the form and close the modal. To avoid it, you have to add the type="button"
attribute to the <button>
elements.
<button mat-icon-button matSuffix (click)="addSubtask()" type="button">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button matSuffix (click)="clearSubtask()" type="button">
<mat-icon>close</mat-icon>
</button>