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;
相关推荐
掘金酱24 分钟前
[稀土掘金 × 火山引擎] AI用量周榜冲刺赛|获奖名单公示
前端·人工智能·后端
泡泡oO43 分钟前
“如果你还在用Superpowers,那我不要和你说话”
前端·后端·全栈
维克兜率天1 小时前
趋势过滤:避免被“假突破“反复打脸
前端
8年区块链老兵1 小时前
一篇文章教会你:用区块高度读懂比特币链上的每一笔交易
前端
xixichensh1 小时前
前端转做AI程序员第一步——开发一个AI流式对话Demo
前端
小羊没烦恼!2 小时前
系统内部模块(子系统)之间的耦合以及模块(子系统)划分
java·开发语言·前端·数据库·算法·c#
Leslie1652 小时前
聚合不只是 GROUP BY:HashAggregate 与 SortAggregate 的内存账本
前端
ITOM运维行者2 小时前
PHP性能监控怎么做?从响应时间到慢函数的6个关键指标
前端·javascript·后端
成旭先生2 小时前
AI 文本审核 API:一段中文文本判风险等级、命中标签与处置建议
java·前端·人工智能·api接口·内容风控·文本审核·ugc审核
liangshanbo12153 小时前
前端面试题:AI 对话中超长消息导致内存溢出,怎么解决?
java·开发语言·前端