ag-grid 自定义组件开发详解

需求背景

业务中存在需要自定义select,并实现打开弹框新增数据,如图

技术背景

  1. react 18
  2. ag 版本 31.1.1 (32版本提供了onValueChange方法,无需getValue,可自行了解)
  3. 组件 antd
  4. 开启了stopEditingWhenCellsLoseFocus(防止用户点击保存按钮,ag还在编辑态,拿不到最新的数据问题,设置失焦自动完成编辑)

核心点

  1. 设置自定义组件cell 为 popup
  2. 点击select Options、modal框时 阻止 mousedown事件(ag失焦判断监听的是 mousedown事件),否则下拉框、弹框会自动关闭

自定义组件需要注意的点

  1. function组件,必须使用 forward 抛出 ICellEditor 方法,其中getValue是必须的
  2. class组件 需实现 ICellEditorComp(建议使用 function 组件,更简单清晰)
  3. stopEdit是同步方法,而setValue是异步方法(回调更新),导致getValue拿不到最新的值,解决方案,使用useRef包裹一下
  4. 下拉框属于popup类型,ag默认有cell尺寸限制,这里根据你的需求选择
  5. react中如果是popup类型,则 col define时必须添加 cellEditorPopup: true,

ICellEditor 介绍

方法名 时机 用途(举例说明 官方注释
isCancelBeforeStart() 编辑器创建前(init() 后立即调用) 根据初始条件决定是否 取消启动编辑(如按下非法键、字段只读等) "If you return true, the editor will not be used and the grid will continue editing."
isCancelAfterEnd() 编辑器关闭前(getValue() 调用之后) 决定是否丢弃编辑结果,例如用户输入不合法、不满足业务逻辑时取消编辑 "If you return true, then the new value will not be used."
getValue() 编辑完成时 返回最终要写入单元格的值;比如返回 <Select /> 的选中值 "Return the final value - called by the grid once after editing is complete"
afterGuiAttached() 编辑器 DOM 渲染后 用于设置焦点等 DOM 操作;比如自动聚焦 <input /> "Useful for any logic that requires attachment before executing"
focusIn() 在整行编辑时,编辑器获得焦点时 执行一些进入焦点时的动作(例如高亮) "If doing full line edit, then gets called when focus should be put into the editor"
focusOut() 在整行编辑时,编辑器失去焦点时 用于处理失焦逻辑(如验证、清理) "If doing full line edit, then gets called when focus is leaving the editor"
refresh(params) 当 cellEditor 被重复使用,并接收新的参数时 更新内部状态(比如新 row data、新默认值) "Gets called with the latest cell editor params every time they update"
isPopup() 编辑器启动时调用一次 返回 true 则编辑器以 popup 模式展示,不受 cell 尺寸限制 "If you return true, the editor will appear in a popup"
getPopupPosition() 仅当 isPopup() 返回 true 时调用 返回 "over"(覆盖 cell)或 "under"(浮在 cell 下方) "Return 'over' if the popup should cover the cell, or 'under'..."

代码示例

typescript 复制代码
import React, {
  useState,
  useImperativeHandle,
  forwardRef,
  useEffect,
  useRef,
} from "react";
import { Select, Divider, Button, Modal, Input } from "antd";
import { useMemoizedFn } from "ahooks";
import { CustomCellEditorProps } from "ag-grid-react";
import { ICellEditor } from "ag-grid-enterprise";

const { Option } = Select;

function AntdSelectPopupEditor(
  params: CustomCellEditorProps,
  ref: React.Ref<ICellEditor>,
) {
  // 初始值为ag传入的value
  const [value, setV] = useState(params.value || "");
  // 解决stopEdit时,getValue 拿到的不是最新值的问题
  const valueRef = useRef(value);
  const options = ["Apple", "Banana", "Orange"];
  const containerRef = useRef<HTMLDivElement>(null);
  // modal框
  const [open, setOpen] = useState(false);
  const modalRef = useRef<HTMLDivElement>(null);
  const setValue = useMemoizedFn((value: any) => {
    setV(value);
    valueRef.current = value;
  });

  useImperativeHandle(ref, () => ({
    // 重要:自定义组件必须有
    getValue: () => {
      return valueRef.current;
    },
    // 重要: 告诉 AG Grid:这个编辑器是 popup,不要随便失焦
    isPopup: () => true, 
    afterGuiAttached: () => {
      // 我这边设置select 默认open,不需要这个了
      // setTimeout(() => {
      //   containerRef.current?.querySelector(".ant-select-selector")?.focus?.();
      // });
    },
  }));

  const handleAddOption = () => {
    setOpen(true);
  };

  // 👇关键点:防止点击 dropdown 触发失焦
  useEffect(() => {
    const stopMouseDown = (e: any) => {
      // 如果点击在 dropdown、editor或者modal框内部就阻止默认冒泡
      if (
        (containerRef.current && containerRef.current.contains(e.target)) ||
        e.target.closest(".ant-select-popup-editor-modal")
      ) {
        e.stopPropagation();
      }
    };
    document.addEventListener("mousedown", stopMouseDown, true);
    return () => {
      document.removeEventListener("mousedown", stopMouseDown, true);
    };
  }, []);

  return (
    <div ref={containerRef} style={{ padding: 8, minWidth: 200 }}>
      <Select
        value={value}
        onChange={(v) => {
          setValue(v);
          params.stopEditing();
        }}
        dropdownRender={(menu: any) => (
          <>
            {menu}
            <Divider style={{ margin: "8px 0" }} />
            <div style={{ padding: "8px", textAlign: "center" }}>
              <Button
                type="link"
                onClick={handleAddOption}
              >
                添加新选项
              </Button>
            </div>
          </>
        )}
        getPopupContainer={() => containerRef.current}
        style={{ width: "100%" }}
        // 默认打开,ag编辑组件时编辑时加载,这里直接设置为open即可
        open 
      >
        {options.map((opt) => (
          <Option key={opt} value={opt}>
            {opt}
          </Option>
        ))}
      </Select>
      <Modal
        open={open}
        title="规格选择"
        className="ant-select-popup-editor-modal"
        ref={modalRef}
        onOk={() => {
          params.stopEditing();
          setOpen(false);
        }}
        onCancel={() => setOpen(false)}
      >
        <Input value={value} onChange={(e) => setValue(e.target.value)} />
      </Modal>
    </div>
  );
}

export default forwardRef(AntdSelectPopupEditor);

使用示例

typescript 复制代码
import { ColDef } from "ag-grid-enterprise";
import { AgGridReact } from "ag-grid-react";
import { useEffect, useState } from "react";
import AntdSelectPopupEditor from "./AntdSelectPopupEditor";
// Row Data Interface
interface IRow {
  make: string;
  model: string;
  price: number;
  electric: boolean;
  size: string;
}

// Create new GridExample component
const GridExample = () => {
  // Row Data: The data to be displayed.
  const [rowData, setRowData] = useState<IRow[]>([
    {
      make: "Tesla",
      model: "Model Y",
      price: 64950,
      electric: true,
      size: "1",
    },
    {
      make: "Ford",
      model: "F-Series",
      price: 33850,
      electric: false,
      size: "2",
    },
    {
      make: "Toyota",
      model: "Corolla",
      price: 29600,
      electric: false,
      size: "3",
    },
    { make: "Mercedes", model: "EQA", price: 48890, electric: true, size: "4" },
    { make: "Fiat", model: "500", price: 15774, electric: false, size: "4" },
    { make: "Nissan", model: "Juke", price: 20675, electric: false, size: "5" },
    { make: "Fiat", model: "500", price: 15774, electric: false, size: "6" },
  ]);

  // Column Definitions: Defines & controls grid columns.
  const [colDefs, setColDefs] = useState<ColDef<IRow>[]>([
    { field: "make" },
    { field: "model" },
    { field: "price", editable: true },
    { field: "electric", editable: true },
    {
      field: "size",
      cellEditor: AntdSelectPopupEditor,
      editable: true,
      // 重要
      cellEditorPopup: true,
      cellEditorPopupPosition: "over",
    },
  ]);

  return (
    <div
      className={"ag-theme-quartz"}
      style={{ width: "100%", height: "500px" }}
    >
      <AgGridReact
        rowData={rowData}
        columnDefs={colDefs}
        stopEditingWhenCellsLoseFocus={true}
      />
    </div>
  );
};
export default GridExample;

效果展示

封装 useStopPropagation 方便服用

typescript 复制代码
type ClassName = string;
type Container = React.RefObject<any> | ClassName;

function useStopPropagation(contains: Container[]) {
  const stopMouseDown = (e: any) => {
    // 如果点击在 dropdown 或 editor 里,就阻止默认行为
    const isContains = contains.some((container) => {
      if (typeof container === "string") {
        return e.target.closest(
          container.startsWith(".") ? container : `.${container}`,
        );
      }
      return container.current?.contains(e.target);
    });

    if (isContains) {
      e.stopPropagation();
    }
  };

  document.addEventListener("mousedown", stopMouseDown, true);
  return () => {
    document.removeEventListener("mousedown", stopMouseDown, true);
  };
}

export default useStopPropagation;
相关推荐
万少2 小时前
Vibe Coding不停歇,移动端 TRAE SOLO 让你用手机也能编程啦
前端·javascript·后端
kyriewen112 小时前
WebAssembly:前端界的“外挂”,让C++代码在浏览器里跑起来
开发语言·前端·javascript·c++·单元测试·ecmascript
烛衔溟3 小时前
TypeScript 接口的基本使用 —— 定义对象形状
前端·javascript·typescript
铁皮饭盒4 小时前
成为AI全栈 - 第3课:路由 RESTful Elysia 状态码 设计规范
前端·后端·全栈
顾昂_4 小时前
Web 性能优化完全指南
前端·面试·性能优化
前端程序媛-Tian5 小时前
前端 AI 提效实战:从 0 到 1 打造团队专属 AI 代码评审工具
前端·人工智能·ai
支付宝体验科技5 小时前
Ant Design Pro v6.0.0 发布
前端
T畅N5 小时前
审批流设计器(前端)
前端·elementui·vue·html·流程图·js
AlunYegeer5 小时前
JAVA,以后端的视角理解前端。在全栈的路上迈出第一步。
java·开发语言·前端
IT_陈寒6 小时前
Redis这个内存杀手,差点让我们运维半夜追杀我
前端·人工智能·后端