Подзадача обновляется без сохранения, и при повторном открытии диалогового окна задачи отображается неверный значок

Я столкнулся с двумя проблемами с функцией редактирования подзадач в моем приложении:

  1. Когда я нажимаю кнопку «Изменить», чтобы отредактировать подзадачу и внести изменения, подзадача обновляется, даже если я не нажимаю кнопку «Сохранить». Вместо этого, когда я нажимаю кнопку «Создать/Обновить», изменения в подзадаче сохраняются автоматически. Такое поведение является неожиданным, поскольку я не сохранил подзадачу явно.

  2. После внесения изменений в подзадачу без сохранения, затем обновление задачи. После повторного открытия диалога задачи вместо значка редактирования (карандаш) отображается значок сохранения (дискета). Правильное поведение должно отображать значок редактирования при повторном открытии задачи без каких-либо несохраненных изменений.

Вот подробное описание действий по воспроизведению этих проблем:

  1. Нажмите на кнопку «Редактировать» для подзадачи.
  2. Внесите изменения в подзадачу, не нажимая «Сохранить».
  3. Нажмите кнопку «Создать/Обновить».
  4. Обратите внимание, что подзадача обновляется с изменениями, даже если значок «Сохранить» не был нажат.

По второму вопросу:

  1. Вносите изменения в подзадачу без сохранения.
  2. Обновить задачу без сохранения подзадачи
  3. Снова откройте диалоговое окно задачи.
  4. Обратите внимание, что вместо значка карандаша отображается значок дискеты.

Буду признателен за любые советы по решению этих проблем.

Демо-версия Stackblitz

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));

задача-form.comComponent.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 = [];
  }
}

задача-форма.компонент.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>&#8226;</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>

🤔 А знаете ли вы, что...
Angular поддерживает локализацию и многоязычность приложений.


2
51
1

Ответ:

Решено

Отсюда:

this.dialog
  .open(TaskFormComponent, {
    data: { fromPopup: true, task: this.task },
  })

вы делитесь ссылкой this.task на MatDialogData, в результате чего любые изменения, внесенные в TaskFormComponent, будут отражать ваш родительский компонент (App).

Вы должны решить эту проблему с помощью глубокой копии.

Предварительные требования: Установите 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));
}

Также не забудьте повторно присвоить измененное значение task.


Обновлено

Вы сохраняете последние (измененные) данные, не проверяя статус isEditable. Перед сохранением и модальным закрытием следует проверить статус isEditable и отразить исходное значение для тех подзадач, которые не сохранены (по индексу/позиции). Кроме того, вам понадобится глубокая копия subtasks, чтобы сохранить исходное значение 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 {
  }
}

Проблема(ы) и озабоченность(и)

Проблема 1. Я заметил, что у вас есть опечатка в имени элемента управления, в результате чего после добавления подзадач значение элемента управления формы становится неясным. Исправьте это, как показано ниже:

public clearSubtask() {
  this.taskForm.patchValue({ subtasks: '' });
}

Проблема 2. Поведение элемента <button> по умолчанию — type = "submit". Как кнопка «Добавить и закрыть» для элемента управления формой subtasks внутри формы, она отправит форму и закроет модальное окно. Чтобы избежать этого, вам нужно добавить атрибут type = "button" к элементам <button>.

<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>

Демо @ StackBlitz