如何在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
相关产品推荐
相关产品推荐

