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

Angular 6中点击按钮触发撤销/重做(Ctrl+Z/Ctrl+Y)功能的实现

解决Angular 6中Material Textarea的撤销/重做问题

我之前也碰到过一模一样的情况,document.execCommand('undo')和redo失效的核心原因是Angular的数据绑定机制会打断原生textarea的编辑历史链。当你用[(ngModel)]或者FormControl绑定textarea的值时,Angular会频繁同步模型与视图,这会清空原生的undo/redo栈,导致原生命令完全无法触发。下面是两种经过验证的可行方案:

方案一:手动维护撤销/重做栈(推荐适配Angular场景)

这种方式完全脱离原生依赖,自己控制编辑历史,完美适配Angular的表单系统,灵活性也更高。

1. 组件完整代码实现

import { Component, ViewChild } from '@angular/core';
import { MatTextarea } from '@angular/material/input';

@Component({
  selector: 'app-text-editor',
  template: `
    <mat-form-field appearance="fill">
      <textarea 
        matInput 
        #textarea 
        [(ngModel)]="content" 
        (input)="updateEditHistory()"
        rows="6"
      ></textarea>
    </mat-form-field>
    <div style="margin-top:1rem;">
      <button mat-raised-button color="primary" (click)="handleUndo()" [disabled]="undoStack.length === 0">撤销</button>
      <button mat-raised-button color="accent" (click)="handleRedo()" [disabled]="redoStack.length === 0" style="margin-left:0.5rem;">重做</button>
    </div>
  `
})
export class TextEditorComponent {
  @ViewChild('textarea') textarea!: MatTextarea;
  content = '';
  // 存储每一次编辑前的内容
  undoStack: string[] = [];
  // 存储被撤销的内容,用于重做
  redoStack: string[] = [];
  // 标记当前操作是撤销/重做,避免触发输入时的历史更新
  isUndoRedoAction = false;

  updateEditHistory() {
    if (!this.isUndoRedoAction) {
      // 用户输入时,将当前内容存入撤销栈,同时清空重做栈(重做链断裂)
      this.undoStack.push(this.content);
      this.redoStack = [];
    }
    this.isUndoRedoAction = false;
  }

  handleUndo() {
    if (this.undoStack.length === 0) return;
    
    this.isUndoRedoAction = true;
    // 将当前内容存入重做栈
    this.redoStack.push(this.content);
    // 从撤销栈取出上一次的内容
    this.content = this.undoStack.pop() || '';
    // 变更检测完成后,手动定位光标到文本末尾(提升用户体验)
    setTimeout(() => {
      this.textarea.nativeElement.focus();
      const textLength = this.content.length;
      this.textarea.nativeElement.setSelectionRange(textLength, textLength);
    }, 0);
  }

  handleRedo() {
    if (this.redoStack.length === 0) return;
    
    this.isUndoRedoAction = true;
    // 将当前内容存入撤销栈
    this.undoStack.push(this.content);
    // 从重做栈取出最近被撤销的内容
    this.content = this.redoStack.pop() || '';
    setTimeout(() => {
      this.textarea.nativeElement.focus();
      const textLength = this.content.length;
      this.textarea.nativeElement.setSelectionRange(textLength, textLength);
    }, 0);
  }
}

2. 核心逻辑说明

  • 用isUndoRedoAction标记区分用户主动输入和撤销/重做操作,避免操作时重复向栈中存入内容
  • 撤销与重做的栈操作完全对称:撤销时将当前内容推入重做栈,重做时推回撤销栈
  • 用setTimeout确保Angular完成变更检测后再操作DOM光标,避免光标位置异常

方案二:绕过Angular绑定,直接操作原生textarea(仅限简单场景)

如果你的场景不需要复杂的表单验证或模型同步,可以暂时关闭Angular的双向绑定,直接操作原生textarea的value,这样原生的undo/redo就能正常工作:

1. 组件代码示例

import { Component, ViewChild } from '@angular/core';
import { MatTextarea } from '@angular/material/input';

@Component({
  selector: 'app-simple-text-editor',
  template: `
    <mat-form-field appearance="fill">
      <textarea matInput #textarea rows="6"></textarea>
    </mat-form-field>
    <div style="margin-top:1rem;">
      <button mat-raised-button color="primary" (click)="handleUndo()">撤销</button>
      <button mat-raised-button color="accent" (click)="handleRedo()" style="margin-left:0.5rem;">重做</button>
    </div>
  `
})
export class SimpleTextEditorComponent {
  @ViewChild('textarea') textarea!: MatTextarea;

  handleUndo() {
    // 必须先让textarea获得焦点,否则原生命令无法生效
    this.textarea.nativeElement.focus();
    document.execCommand('undo', false, null);
  }

  handleRedo() {
    this.textarea.nativeElement.focus();
    document.execCommand('redo', false, null);
  }
}

2. 注意事项

  • 这种方法不能和[(ngModel)]或FormControl一起使用,否则Angular的绑定会再次打断原生编辑历史
  • 仅适合不需要将textarea的值同步到组件模型的简单场景

额外适配提示

如果你使用的是FormControl而非ngModel,只需要把方案一中的[(ngModel)]="content"替换为[formControl]="contentControl",然后通过contentControl.value获取和设置值,核心栈操作逻辑完全一致。

内容的提问来源于stack exchange,提问作者vinay yadav

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 14:07:52