You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Angular中实现支持多子组件的撤销(Undo)与重做(Redo)功能?

Angular 父组件实现撤销/重做功能方案

1. 统一操作记录格式

首先定义标准化的操作记录接口,让父组件能识别所有子组件的变更类型与状态:

// 操作记录接口
interface OperationRecord {
  componentType: 'text-field' | 'checkbox' | 'delete'; // 触发变更的组件类型
  sectionIndex: number; // 所在区块索引
  itemId?: string; // 子项唯一标识
  prevState: any; // 变更前状态
  newState: any; // 变更后状态
}

2. 子组件触发变更事件

所有带交互的子组件(文本框、复选框、删除按钮等),在状态变更时通过@Output向父组件发送操作记录:
以文本框组件为例:

// text-field.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'text-field',
  template: `<input type="text" [(ngModel)]="sectionItem.value" (ngModelChange)="onValueChange($event)">`
})
export class TextFieldComponent {
  @Input() sectionItem: any;
  @Output() stateChanged = new EventEmitter<OperationRecord>();
  private lastValue: string;

  ngOnInit() {
    this.lastValue = this.sectionItem.value;
  }

  onValueChange(newValue: string) {
    this.stateChanged.emit({
      componentType: 'text-field',
      sectionIndex: this.sectionItem.sectionIndex,
      itemId: this.sectionItem.id,
      prevState: this.lastValue,
      newState: newValue
    });
    this.lastValue = newValue;
  }
}

其他组件(复选框、删除图标)遵循同样逻辑,在状态变更时触发stateChanged事件,传递对应操作记录。

3. 父组件维护撤销/重做栈

在父组件中初始化两个栈,限制最大长度为5,负责存储操作历史与撤销历史:

// main.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html'
})
export class MainComponent {
  section: any;
  formType: string;
  sectionIndex: number;
  first: boolean;
  last: boolean;

  private undoStack: OperationRecord[] = []; // 撤销栈:存储最近5次操作
  private redoStack: OperationRecord[] = []; // 重做栈:存储最近5次被撤销的操作
  private readonly MAX_STACK_LIMIT = 5;

  // 接收子组件的变更事件
  handleStateChange(operation: OperationRecord) {
    // 新操作产生时清空重做栈
    this.redoStack = [];
    // 栈满时移除最早的记录
    if (this.undoStack.length >= this.MAX_STACK_LIMIT) {
      this.undoStack.shift();
    }
    this.undoStack.push(operation);
  }

  // 执行撤销
  undo() {
    if (this.undoStack.length === 0) return;
    const lastOperation = this.undoStack.pop()!;
    // 恢复到变更前状态
    this.recoverState(lastOperation.prevState, lastOperation);
    // 将操作推入重做栈,超出长度则移除最早记录
    if (this.redoStack.length >= this.MAX_STACK_LIMIT) {
      this.redoStack.shift();
    }
    this.redoStack.push(lastOperation);
  }

  // 执行重做
  redo() {
    if (this.redoStack.length === 0) return;
    const lastUndoneOp = this.redoStack.pop()!;
    // 恢复到变更后状态
    this.recoverState(lastUndoneOp.newState, lastUndoneOp);
    // 将操作推回撤销栈,超出长度则移除最早记录
    if (this.undoStack.length >= this.MAX_STACK_LIMIT) {
      this.undoStack.shift();
    }
    this.undoStack.push(lastUndoneOp);
  }

  // 根据操作记录恢复对应组件状态
  private recoverState(targetState: any, operation: OperationRecord) {
    // 找到目标子项并更新状态
    const targetItem = this.section.items.find((item: any) => item.id === operation.itemId);
    if (!targetItem) return;

    switch(operation.componentType) {
      case 'text-field':
        targetItem.value = targetState;
        break;
      case 'checkbox':
        targetItem.checked = targetState;
        break;
      case 'delete':
        // 删除操作的恢复:将删除项插回原位置
        this.section.items.splice(operation.sectionIndex, 0, targetState);
        break;
      // 扩展其他组件类型的恢复逻辑
    }
  }
}

4. 模板层事件绑定修改

父组件模板

给子组件绑定状态变更事件,同时将Header的撤销/重做点击事件传递到父组件:

<!-- Main component 修改后 -->
<div>
    <div>
        <div>
            <div>
                <header [section]="section" [formType]="formType" [sectionIndex]="sectionIndex" 
                        [first]="first" [last]="last"
                        (undoClicked)="undo()" (redoClicked)="redo()">
                </header>
            </div>
            <div >
               <item-crud [section]="section" [formType]="formType" [sectionIndex]="sectionIndex"
                          (stateChanged)="handleStateChange($event)">
               </item-crud>
            </div>
        </div>
    </div>
</div>

Header组件

将按钮点击事件向上传递给父组件:

// header.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'header',
  templateUrl: './header.component.html'
})
export class HeaderComponent {
  @Input() section: any;
  @Input() formType: string;
  @Input() sectionIndex: number;
  @Input() first: boolean;
  @Input() last: boolean;
  @Output() undoClicked = new EventEmitter<void>();
  @Output() redoClicked = new EventEmitter<void>();

  undo() {
    this.undoClicked.emit();
  }

  redo() {
    this.redoClicked.emit();
  }
}

Item-crud组件

将内部子组件的变更事件向上传递:

// item-crud.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'item-crud',
  templateUrl: './item-crud.component.html'
})
export class ItemCrudComponent {
  @Input() section: any;
  @Input() formType: string;
  @Input() sectionIndex: number;
  @Output() stateChanged = new EventEmitter<OperationRecord>();

  onChildStateChange(operation: OperationRecord) {
    operation.sectionIndex = this.sectionIndex;
    this.stateChanged.emit(operation);
  }
}

Item-crud模板中给子组件绑定事件:

<!-- item-crud 修改后 -->
<div id="accord">
    <div>
        <div>
            <div>
                <div class="formContainer">
                    <div><date-stamp (stateChanged)="onChildStateChange($event)"></date-stamp></div>
                    <div><text-field [sectionItem]="sectionItem" (stateChanged)="onChildStateChange($event)"></text-field></div>
                    <!-- 复选框、删除按钮等组件同理绑定stateChanged事件 -->
                </div>
            </div>
        </div>
    </div>
</div>

5. 核心注意事项

  • 所有子组件的状态变更必须通过stateChanged事件传递,禁止直接修改父组件数据,确保操作记录的完整性。
  • recoverState方法需根据组件类型实现对应恢复逻辑,比如删除操作需要将子项插回原位置。
  • 通过MAX_STACK_LIMIT严格控制栈长度,超出时自动移除最早的记录,保证只保留最近5次操作/撤销记录。

内容的提问来源于stack exchange,提问作者Prasanna Rao

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:05:20