You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Ant Design Form.List 中使用 TipTap 富文本编辑器时,文本格式化触发全表单重校验的问题

Ant Design Form.List 中使用 TipTap 富文本编辑器时,文本格式化触发全表单重校验的问题

嘿,我猜你现在头疼的点是:每次在 TipTap 里点加粗、斜体这类格式化按钮,整个表单的校验就会自动跑一遍,对吧?这确实挺影响体验的,我来帮你拆解问题根源,再给你落地的解决方案。


先理清楚你的核心实现

1. Form.List 嵌入富文本编辑器的代码

<Form.List name="tableData">
  {(fields, { add, remove }) => (
    <Table
      dataSource={fields}
      columns={[
        {
          title: "Nội dung mục tiêu",
          render: (_, __, index) => (
            <Form.Item
              name={[fields[index].name, "content"]}
              rules={[{ required: true, message: "Vui lòng nhập nội dung" }]}
              validateTrigger="onBlur"
              getValueFromEvent={(val) => val}
            >
              <RichTextEditor placeholder="Nhập nội dung..." />
            </Form.Item>
          ),
        },
      ]}
    />
  )}
</Form.List>

2. RichTextEditor 核心实现

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import Underline from "@tiptap/extension-underline";
import TextAlign from "@tiptap/extension-text-align";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import { useEffect, useRef } from "react";
import {
  BoldOutlined, ItalicOutlined, UnderlineOutlined,
  OrderedListOutlined, UnorderedListOutlined,
  AlignLeftOutlined, AlignCenterOutlined, AlignRightOutlined,
  LinkOutlined, PictureOutlined
} from "@ant-design/icons";

// 工具栏组件
const MenuBar = ({ editor, disabled, formatHandler }) => {
  if (!editor) return null;

  const insertLink = () => {
    const url = window.prompt("Nhập URL:");
    if (url) editor.chain().focus().setLink({ href: url }).run();
  };

  const insertImage = () => {
    const url = window.prompt("Nhập URL ảnh:");
    if (url) editor.chain().focus().setImage({ src: url }).run();
  };

  return (
    <div className="tiptap-toolbar" style={{ backgroundColor: disabled ? "#e0e0e0" : "#ffffff", borderRadius: "8px", padding: "5px" }} >
      <button onClick={() => formatHandler(() => editor.chain().focus().toggleBold().run())} disabled={disabled} className={editor.isActive("bold") ? "active" : ""} >
        <BoldOutlined />
      </button>
      <button onClick={() => formatHandler(() => editor.chain().focus().toggleItalic().run())} disabled={disabled} className={editor.isActive("italic") ? "active" : ""} >
        <ItalicOutlined />
      </button>
      {/* 其他格式化按钮省略 */}
    </div>
  );
};

// 主编辑器组件
const RichTextEditor = ({ value = "", onChange, onBlur, onTouched, disabled = false, placeholder = "Nhập nội dung...", style = {} }) => {
  const lastHtmlRef = useRef(value);
  const isFormattingRef = useRef(false);

  const formatHandler = (command) => {
    isFormattingRef.current = true;
    command();
    setTimeout(() => isFormattingRef.current = false, 100);
  };

  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder({ placeholder }),
      Underline,
      TextAlign.configure({ types: ["heading", "paragraph"] }),
      Link,
      Image,
    ],
    content: value,
    disabled,
    onUpdate: ({ editor }) => {
      const html = editor.getHTML();
      if (html !== lastHtmlRef.current && !isFormattingRef.current) {
        onChange?.(html);
        lastHtmlRef.current = html;
      }
    },
  });

  useEffect(() => {
    if (!editor) return;
    const handleBlur = () => {
      onBlur?.();
      onTouched?.();
    };
    editor.on("blur", handleBlur);
    return () => editor.off("blur", handleBlur);
  }, [editor, onBlur, onTouched]);

  return (
    <div style={style}>
      <MenuBar editor={editor} disabled={disabled} formatHandler={formatHandler} />
      <EditorContent editor={editor} />
    </div>
  );
};

问题根源分析

你已经用isFormattingRef试图区分格式化操作,但还是出问题,主要有两个核心原因:

  1. TipTap的onUpdate触发太频繁:格式化操作(比如加粗)会改变编辑器的HTML内容,即使你标记了isFormattingRef.current = truesetTimeout的100ms延迟可能赶不上onUpdate的触发时机,导致还是会触发onChange
  2. Form.Item的隐式更新逻辑:即使你设置了validateTrigger="onBlur",AntD Form在字段值变化时,还是可能触发隐式的校验逻辑,尤其是在Form.List的嵌套结构下,单个字段的变化可能会触发整个列表的重渲染和校验。

解决方案(最优组合)

1. 优化RichTextEditor:彻底区分格式化与内容输入

修改编辑器的onUpdateformatHandler逻辑,确保只有用户输入内容(非格式化操作)或编辑器失焦时,才触发onChange

const RichTextEditor = ({ value = "", onChange, onBlur, onTouched, disabled = false, placeholder = "Nhập nội dung...", style = {} }) => {
  const lastHtmlRef = useRef(value);
  const isFormattingRef = useRef(false);
  const prevContentRef = useRef(value);

  const formatHandler = (command) => {
    isFormattingRef.current = true;
    prevContentRef.current = editor?.getHTML() || value;
    command();
    // 格式化是同步操作,立即标记为非格式化状态
    setTimeout(() => {
      isFormattingRef.current = false;
      prevContentRef.current = editor?.getHTML() || value;
    }, 0);
  };

  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder({ placeholder }),
      Underline,
      TextAlign.configure({ types: ["heading", "paragraph"] }),
      Link,
      Image,
    ],
    content: value,
    disabled,
    onUpdate: ({ editor }) => {
      const currentHtml = editor.getHTML();
      // 只有非格式化操作 + 内容真的变化时,才触发onChange
      if (!isFormattingRef.current && currentHtml !== prevContentRef.current) {
        onChange?.(currentHtml);
        prevContentRef.current = currentHtml;
        lastHtmlRef.current = currentHtml;
      }
    },
  });

  // 失焦时强制同步内容并触发校验
  useEffect(() => {
    if (!editor) return;
    const handleBlur = () => {
      const currentHtml = editor.getHTML();
      if (currentHtml !== lastHtmlRef.current) {
        onChange?.(currentHtml);
        lastHtmlRef.current = currentHtml;
      }
      onBlur?.();
      onTouched?.();
    };
    editor.on("blur", handleBlur);
    return () => editor.off("blur", handleBlur);
  }, [editor, onBlur, onTouched, onChange]);

  // 监听外部value变化(比如表单重置)
  useEffect(() => {
    if (editor && value !== prevContentRef.current) {
      prevContentRef.current = value;
      lastHtmlRef.current = value;
      editor.commands.setContent(value);
    }
  }, [editor, value]);

  return (
    <div style={style}>
      <MenuBar editor={editor} disabled={disabled} formatHandler={formatHandler} />
      <EditorContent editor={editor} />
    </div>
  );
};

2. 优化Form.Item:精确控制更新与校验时机

修改Form.Item的配置,确保只有必要时才更新字段和触发校验:

<Form.Item
  name={[fields[index].name, "content"]}
  rules={[{ required: true, message: "Vui lòng nhập nội dung" }]}
  // 明确只在失焦时触发校验
  validateTrigger={["onBlur"]}
  getValueFromEvent={(val) => val}
  // 精确控制Form.Item何时更新,避免不必要的重渲染
  shouldUpdate={(prevValues, currentValues) => {
    const prevVal = prevValues.tableData[index]?.content;
    const currentVal = currentValues.tableData[index]?.content;
    return prevVal !== currentVal;
  }}
>
  <RichTextEditor placeholder="Nhập nội dung..." />
</Form.Item>

额外提示

  • 如果还是有问题,可以在Form组件层面设置validateTrigger="onBlur",统一控制整个表单的校验触发时机。
  • 提交表单时,记得手动调用form.validateFields()来做最终校验,确保数据合法。

内容来源于stack exchange

火山引擎 最新活动