AgGrid使用经验汇总

2022-05-13 fishedee 前端

0 概述

AgGrid使用经验汇总,AgGrid可以说是前端功能最完备的一个表格组件了,功能包括有:

  • 列合并,分组,聚合,筛选,排序,移动,可调宽度,隐藏,固定,轴模式
  • 行合并,固定,拖放移动,formatter,getter,render
  • 选择,单击或者双击选中,单选或者多选,Checkbox选中
  • 可编辑单元格,键盘导航
  • 主从,表格的
  • 高性能,行与列都是虚拟展示,支持一百万行,和一百列的Grid展示和操作
  • 动画,行与列变化的时候有动画效果
  • 图表和Excel,支持客户端操作生成图表和Excel
  • 服务器端数据模型,这个很屌,按照用户的滚动位置自动加载服务器数据,同时保留排序,分组,聚合,刷新,选择,轴模式

这样一个完备的组件,它的API设计也是相当漂亮。如何在一个React的框架中,兼顾性能,同时保持API的灵活性,是一件不太容易的事情。

官网在这里,整体文档与Demo丰富,是一个成熟的商业库。

0.1 命令式和内置数据源

AgGrid与普通AntdTable最大的不同在于:

  • 命令式,绝大部分的API,通过获取AgGrid的引用AgGridRef以后,执行它的命令式API来实现,包括有配置列,配置行数(原始数据,filter后数据,group后数据,filter以及Group后数据)据,获取选中行,获取filterMode等等。命令式API是高性能设计的关键,避免每次都需要通过Render比较数据来确定具体的Row变化位置。
  • 内置数据源,AntdTable的selection是用value与onChange绑定在一起的,这意味着每选中一行,都需要触发一次Render。AgGrid另辟蹊径,自带selection数据源,不需要外部引入,当用户选中以后仅作通知操作,不需要重新触发Render。如果需要外部设置selection数据,则需要调用它的命令式API。

从某种意义上说,AgGrid更像是一个传统的命令式UI框架。

  • 使用命令式API来获取,和设置内部数据源,包括列信息,列状态,选中信息,筛选信息
  • 使用事件来倾听用户的触发行为,选中变化,筛选变化,分组变化,列宽度变化,列移动变化等等。

0.2 声明式和外部数据源

AgGrid仅推荐在rowData上使用React的声明式的外部数据源,当向AgGrid传入新的rowData时,它通过以下步骤确定数据是否有更新:

  • rowData的数据引用是否有更新,如果数据引用不变的话,就假设数据源没有发生变化,不需要刷新,不管它的实际内容有没有真的变化。
  • rowData数据引用变化以后,通过比较rowData的每个元素的引用是否发生变化,来确定每一行是否有发生变化,来确定是否需要刷新这一行。

因此,如何写入新数据

  • 新数据来源于外部,使用Immutable的方式更新数据,每次更新Array必须创建新的Array引用出来。同时,对于每一行的数据发生变化,对这一行的引用也要发生变化。然后重新Render页面。
  • 新数据来源于内部,就像selection的变化可能是因为用户触发引起的一样,rowData的单元格变化也可能是因为AgGrid的可编辑表格打开了,用户手动编辑单元格导致的。这个时候,AgGrid的思路是,直接在外部数据源中原地写数据,而不是使用Immutable来生成新的引用。这样也不再需要触发Render的操作。开发者可以通过侦听事件的方式,来响应内部数据发生变化了的通知。

声明式和外部数据源的刷新方式,在AgGrid中并不是必要的操作,更多是迁就原来React开发者的开发方式。AgGrid中仅推荐对rowData使用这种方式刷新,尽量不要对colDef使用这种方式刷新。

0.3 两步编辑操作

在现有几乎所有的UI框架中,编辑都是一步操作。例如,我们有上下两个textArea,我们希望上面一个textarea的数据变化的时候,也会产生下面的textArea变化。

const [state,setState] = useState('');
return (
    <div>
        <textarea value={state} onChange={(e)=>{
            setState(e.target.value);
        }}>
        <textarea value={state} onChange={(e)=>{
            setState(e.target.value);
        }}>
    </div>
);

一个直观的做法是,对两个textArea共有一个外部数据源,当其中一个textarea的onChange事件触发的时候,重新render当前页面。这种做法有两个问题:

  • 每一个键盘操作,都需要重新Render整个页面。当页面上有很多组件的时候,Render的操作就会触发得过于频繁。
  • 展示与输入数据被固定为同一种样式,如果一个表格上每个cell都是可以编辑的,这个表格看上去就会很丑,因为每个单元格都是有边框包围起来的Input组件。另外,在实际应用中,我们希望非编辑状态的数字是3位逗号展示法,而编辑状态的数字是没有逗号的。

在实际应用中,我们更多使用两部编辑操作,仅兼顾了性能,同时更加美观。

  • 每个单元格默认是在展示状态,这个时候可以用3位逗号展示法来展示数据。
  • 点击单元格的时候,单元格转换为编辑状态。处于编辑状态的单元格,当value发生变化的时候,不会触发其他单元格的关联更新。这样避免了,每次的键盘操作,都需要频繁的整页面Render操作。
  • 当编辑状态的单元格丢失焦点的时候,输入组件的数据自动更新到表格中,并触发其他单元格的关联更新。丢失焦点的时候,才需要进行整页面的Render操作。

如果你仔细留意以上流程就会发现,Excel中的编辑也是相同的两步操作,而不是一步操作。

具体的代码可以看这里的第6节

0.4 付费

企业版需要付费,就其所提供的功能而言,这个价格并不贵,甚至说物超所值。而且,这样的组件功能几乎不会过时,凡是做Web管理系统的都离不开AgGrid的这些功能,这家公司的商业模式其实很棒,属于小而美的方向,唯一的缺陷在于盗版横行。

1 依赖

npm install ag-grid-community --save
npm install ag-grid-enterprise --save
npm install ag-grid-react --save

使用Package模式来安装全部依赖,

Ag-Grid提供了按需Module安装依赖的方式,可以大幅减少包大小,看这里

2 列

代码在这里

2.1 基础

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { ColDef, GetRowIdFunc } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: '9.23', date: '2022-01-02' },
        { id: 2, make: "Ford", model: "Mondeo", price: '31.2', date: '2021-01-02' },
        { id: 3, make: "Porsche", model: "Boxter", price: '188.7', date: '2022-03-02' }
    ]);

    const [columnDefs] = useState([
        //headerName是名称
        { headerName: '品牌', field: 'make' },
        //field是字段名
        { headerName: '型号', field: 'model' },
        //定义col的ID,以及类型
        { colId: 'price1', headerName: '价格', field: 'price', type: 'numberColumn' },
        //width没有定义,defaultColDef定义为170,dateColumn定义为200,最终值为200
        { headerName: '日期', field: 'date', type: 'dateColumn' },
        //width是宽度,这里定义的width为500,不会被defaultColDef的width为170覆盖
        { colId: 'price2', headerName: '价格2', field: 'price', type: 'numberColumn', width: 500 },
        //使用groupId与children,创建一个列分组
        {
            headerName: 'Medals',
            groupId: 'medalsGroup',
            children: [
                { colId: 'price3', headerName: '价格3', field: 'price', type: 'numberColumn' },
                { headerName: '型号', field: 'model' },
                //hide为隐藏该列
                { headerName: '品牌', field: 'make', hide: true },
            ]
        }
    ])

    //列的属性默认属性,由columnDefs + defaultColDef(和defaultColGroupDef) + columnType合并为最终类型
    //合并规则为:
    //原columnDefs的值中非undefined的属性不会被覆盖,只有undefined的属性会被覆盖
    //defaultColDef的优先级,比columnType的优先级要低
    const defaultColDef = useMemo(() => {
        return {
            width: 170,
            //可调宽度
            resizable: true,
        };
    }, []);

    const columnTypes = useMemo(() => {
        return {
            //定义数字类型
            numberColumn: {
                headerClass: 'ag-right-aligned-header',
                cellClass: 'ag-right-aligned-cell'
            },
            //定义日期类型
            dateColumn: {
                width: 200,
            },
        };
    }, []);

    const getRowId: GetRowIdFunc = useCallback((props) => {
        return props.data.id;
    }, []);
    const onGridReady = useCallback((params) => {
        console.log('grid ready');
    }, []);
    return (
        <div style={{ width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    getRowId={getRowId}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    columnTypes={columnTypes}
                    onGridReady={onGridReady} />
            </div>
        </div>
    );
};

export default App;

字段为:

  • headerName,名称
  • field,字段名,
  • colId,列ID。如果colId不存在的话,默认使用field作为colId。
  • width,宽度
  • groupId,children,创建一个列分组

列的属性默认属性,由columnDefs + defaultColDef(和defaultColGroupDef) + columnType合并为最终类型,合并规则为:

  • 原columnDefs的值中非undefined的属性不会被覆盖,只有undefined的属性会被覆盖
  • defaultColDef的优先级,比columnType的优先级要低

2.2 宽度

2.2.1 用户可调宽度

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { ColDef, ColumnResizedEvent, GetRowIdFunc } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: '9.23', date: '2022-01-02' },
        { id: 2, make: "Ford", model: "Mondeo", price: '31.2', date: '2021-01-02' },
        { id: 3, make: "Porsche", model: "Boxter", price: '188.7', date: '2022-03-02' }
    ]);

    const [columnDefs] = useState([
        //minWidth最小宽度
        { headerName: '品牌', field: 'make', minWidth: 100 },
        //maxWidth最大宽度
        { headerName: '型号', field: 'model', maxWidth: 300 },
        //width当前宽度
        { colId: 'price1', headerName: '价格', field: 'price', type: 'numberColumn', width: 500 },
        { headerName: '日期', field: 'date', type: 'dateColumn' },
    ])

    const defaultColDef = useMemo(() => {
        return {
            width: 170,
            //可调宽度
            resizable: true,
        };
    }, []);

    const columnTypes = useMemo(() => {
        return {
            //定义数字类型
            numberColumn: {
                headerClass: 'ag-right-aligned-header',
                cellClass: 'ag-right-aligned-cell'
            },
            //定义日期类型
            dateColumn: {
                width: 200,
            },
        };
    }, []);

    const getRowId: GetRowIdFunc = useCallback((props) => {
        return props.data.id;
    }, []);
    const onGridReady = useCallback((params) => {
        console.log('grid ready');
    }, []);
    const onColmnResized = useCallback((param: ColumnResizedEvent) => {
        console.log("column resize", param.column);
        if (param.finished) {
            //只处理那些finish以后的事件
            console.log("column resize finish", param.column);
        }
    }, []);
    return (
        <div style={{ width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    getRowId={getRowId}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    columnTypes={columnTypes}
                    onGridReady={onGridReady}
                    onColumnResized={onColmnResized} />
            </div>
        </div>
    );
};

export default App;
  • 使用resizable,实现用户自定义宽度
  • 使用onColumnResized,来接收宽度变化事件
  • 可调宽度依然受到minWidth,maxWidth的影响

2.2.2 flex宽度

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { ColDef, ColumnResizedEvent, GetRowIdFunc } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: '9.23', date: '2022-01-02' },
        { id: 2, make: "Ford", model: "Mondeo", price: '31.2', date: '2021-01-02' },
        { id: 3, make: "Porsche", model: "Boxter", price: '188.7', date: '2022-03-02' }
    ]);

    const [columnDefs] = useState([
        { headerName: '品牌', field: 'make', width: 100 },
        { headerName: '型号', field: 'model', width: 200 },
        //flex是自动获取当前的剩余宽度,使得刚好填充屏幕宽度
        { colId: 'price1', headerName: '价格', field: 'price', type: 'numberColumn', flex: 1 },
        //flex为2,所以日期列的宽度,刚好为价格列宽度的2倍
        { headerName: '日期', field: 'date', type: 'dateColumn', flex: 2 },
    ])

    const defaultColDef = useMemo(() => {
        return {
            width: 170,
            //可调宽度
            resizable: true,
        };
    }, []);

    const columnTypes = useMemo(() => {
        return {
            //定义数字类型
            numberColumn: {
                headerClass: 'ag-right-aligned-header',
                cellClass: 'ag-right-aligned-cell'
            },
            //定义日期类型
            dateColumn: {
                width: 200,
            },
        };
    }, []);

    const getRowId: GetRowIdFunc = useCallback((props) => {
        return props.data.id;
    }, []);
    const onGridReady = useCallback((params) => {
        console.log('grid ready');
    }, []);
    const onColmnResized = useCallback((param: ColumnResizedEvent) => {
        console.log("column resize", param.column);
        if (param.finished) {
            //只处理那些finish以后的事件
            console.log("column resize finish", param.column);
        }
    }, []);
    return (
        <div style={{ width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    getRowId={getRowId}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    columnTypes={columnTypes}
                    onGridReady={onGridReady}
                    onColumnResized={onColmnResized} />
            </div>
        </div>
    );
};

export default App;

flex是自动获取当前的剩余宽度,使得刚好填充屏幕宽度,和flex布局里面的flexGrow参数很相似,没啥好说的

2.2.3 自动宽度

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<React.CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete', width: 150, suppressSizeToFit: true },
        {
            field: 'age',
            headerName: 'Age of Athlete',
            width: 90,
            minWidth: 50,
            maxWidth: 150,
        },
        { field: 'country', width: 120 },
        { field: 'year', width: 90 },
        { field: 'date', width: 110 },
        { field: 'sport', width: 110 },
        { field: 'gold', width: 100 },
        { field: 'silver', width: 100 },
        { field: 'bronze', width: 100 },
        { field: 'total', width: 100 },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            resizable: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const sizeToFit = useCallback(() => {
        //根据屏幕宽度,调整列宽,刚好为屏幕宽度
        gridRef.current!.api.sizeColumnsToFit();
    }, []);

    const autoSizeAll = useCallback((skipHeader: boolean) => {
        //根据数据内容长度,调整列宽,使得每列的内容都能显示得到,skipHeader为true代表不考虑列头名称的宽度
        const allColumnIds: string[] = [];
        gridRef.current!.columnApi.getAllColumns()!.forEach((column) => {
            allColumnIds.push(column.getId());
        });
        gridRef.current!.columnApi.autoSizeColumns(allColumnIds, skipHeader);
    }, []);

    return (
        <div style={containerStyle}>
            <div className="button-bar">
                <button onClick={sizeToFit}>Size to Fit</button>
                <button onClick={() => autoSizeAll(false)}>Auto-Size All</button>
                <button onClick={() => autoSizeAll(true)}>
                    Auto-Size All (Skip Header)
                </button>
            </div>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid提供了自动调整宽度的两种方式:

  • sizeColumnsToFit,根据屏幕宽度,调整列宽,刚好为屏幕宽度
  • autoSizeColumns,根据数据内容长度,调整列宽,使得每列的内容都能显示得到,skipHeader为true代表不考虑列头名称的宽度

2.3 移动

2.3.1 用户可移动

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { ColDef, ColumnMovedEvent, ColumnResizedEvent, GetRowIdFunc } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: '9.23', date: '2022-01-02' },
        { id: 2, make: "Ford", model: "Mondeo", price: '31.2', date: '2021-01-02' },
        { id: 3, make: "Porsche", model: "Boxter", price: '188.7', date: '2022-03-02' }
    ]);

    const [columnDefs] = useState([
        { headerName: '品牌', field: 'make', minWidth: 100 },
        {
            headerName: '型号', field: 'model', maxWidth: 300,
            //禁止该列拖着移动,但是允许被其他列推动着移动
            suppressMovable: true
        },
        {
            colId: 'price1', headerName: '价格', field: 'price', type: 'numberColumn', width: 500,
            //禁止该列拖到Grid外面隐藏
            lockVisible: true,
        },
        { headerName: '日期', field: 'date', type: 'dateColumn' },
    ])

    const defaultColDef = useMemo(() => {
        return {
            width: 170,
            //默认就支持
        };
    }, []);

    const columnTypes = useMemo(() => {
        return {
            //定义数字类型
            numberColumn: {
                headerClass: 'ag-right-aligned-header',
                cellClass: 'ag-right-aligned-cell'
            },
            //定义日期类型
            dateColumn: {
                width: 200,
            },
        };
    }, []);

    const getRowId: GetRowIdFunc = useCallback((props) => {
        return props.data.id;
    }, []);
    const onGridReady = useCallback((params) => {
        console.log('grid ready');
    }, []);
    const onColmnMoved = useCallback((param: ColumnMovedEvent) => {
        console.log("column move", param);
    }, []);
    return (
        <div style={{ width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '80%', marginTop: '10%', width: '100%' }}>
                <AgGridReact
                    getRowId={getRowId}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    columnTypes={columnTypes}
                    onGridReady={onGridReady}
                    onColumnMoved={onColmnMoved}
                //默认情况下,移动列有动画效果
                //suppressColumnMoveAnimation={false}
                //默认情况下,就支持拖动列来移动列位置,禁用的话需要使用suppressMovableColumns
                //suppressMovableColumns={true}
                //默认情况下,就支持将列拖出去屏幕以外来消失列,禁用的话需要使用suppressDragLeaveHidesColumns
                //suppressDragLeaveHidesColumns={true}
                />
            </div>
        </div>
    );
};

export default App;
  • 默认情况下,AgGrid允许用户自己移动列的顺序,以及将列移出Grid以外(隐藏该列)
  • suppressMovable,指定禁止该列拖着移动,但是允许被其他列推动着移动
  • lockVisible,指定禁止该列拖到Grid外面隐藏
  • suppressMovableColumns,全局禁止所有列移动
  • suppressDragLeaveHidesColumns,全局禁止所有列移动隐藏
  • suppressColumnMoveAnimation,全局禁止列移动的动画效果

2.3.2 锁紧列排序

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', dispaly: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete',
            width: 200,
        },
        {
            //该列总是在最左边
            field: 'age', lockPosition: 'left', cellClass: 'locked-col',
            width: 500
        },
        { field: 'country', width: 500 },
        { field: 'year', width: 500 },
        {
            field: 'total',
            //该列总是在最右边
            lockPosition: 'right', cellClass: 'locked-col', width: 500
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    suppressDragLeaveHidesColumns={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;
  • lockPosition,可以设置该列总是在最左侧,或者最右侧。注意,lock依然会被水平滚动条影响,消失到grid以外

2.4 固定

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    ColumnPinnedEvent,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', dispaly: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete',
            width: 200,
        },
        {
            //该列总是在最左边,固定左侧,有自己独立的滚动条
            field: 'age', pinned: 'left', cellClass: 'locked-col',
            width: 500
        },
        { field: 'country', width: 500 },
        { field: 'year', width: 500 },
        {
            field: 'total',
            //该列总是在最右边,固定右侧,有自己独立的滚动条
            pinned: 'right', cellClass: 'locked-col', width: 500
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            //默认允许通过拖动方式,将部分列pinned到左侧或者右侧,使用lockPinned以后能禁止这种操作
            //lockPinned:true
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onColumnPinned = useCallback((params: ColumnPinnedEvent) => {
        console.log('column pinned', params);
    }, []);
    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    suppressDragLeaveHidesColumns={true}
                    onGridReady={onGridReady}
                    onColumnPinned={onColumnPinned}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;
  • pinned,与locked不同,它是固定在grid的左侧或者右侧,不受水平滚动条影响,不会移出到grid以外
  • AgGrid默认允许用户通过拖动的方式,将部分列动态固定在左侧,或者固定在右侧。
  • lockPinned,指定禁止该列通过拖动的方式固定
  • onColumnPinned,用户手动拖动导致固定列的事件

2.5 合并

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete', pinned: 'left' },
        { field: 'age', pinned: 'left' },
        {
            field: 'country',
            //colSpan是一个函数,根据不同行,决定合并多少列
            colSpan: function (params) {
                const country = params.data.country;
                if (country === 'Russia') {
                    // have all Russia age columns width 2
                    return 2;
                } else if (country === 'United States') {
                    // have all United States column width 4
                    return 4;
                } else {
                    // all other rows should be just normal
                    return 1;
                }
            },
        },
        { field: 'year' },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 150,
            resizable: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

使用colSpan就能实现,根据每一行数据,动态地合并多列

2.6 更新

2.6.1 全列更新

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';
import { Button } from 'antd';

const GridExample = () => {
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<(ColDef | ColGroupDef)[]>([
        // using default ColDef
        { field: 'athlete' },
        { field: 'sport' },
        // using number column type
        { field: 'age', },
        { field: 'year', },
        // using date and non-editable column types
        { field: 'date', width: 220 },
        { headerName: 'Gold', field: 'gold' },
        { headerName: 'Silver', field: 'silver' },
        { headerName: 'Bronze', field: 'bronze' },
        {
            headerName: 'Total',
            field: 'total',
            columnGroupShow: 'closed',
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 150,
            resizable: true,
        };
    }, []);
    const defaultColGroupDef = useMemo<Partial<ColGroupDef>>(() => {
        return {
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const toggleSport = () => {
        console.log(columnDefs);
        let sportIndex = columnDefs.findIndex((single: any) => {
            return single.field == 'sport';
        });
        let newColumnDefs: any;
        if (sportIndex != -1) {
            newColumnDefs = columnDefs.filter((single: any) => {
                return single.field != 'sport';
            });
        } else {
            newColumnDefs = [
                ...columnDefs,
                { field: 'sport' },
            ];
        }
        //将本地的ColumnDef获取了,然后用React的方式设置进去,这个方法也是可以的
        setColumnDefs(newColumnDefs);

        //这种方法不好的地方在于,当以UI方式进行Table分组、筛选或者调整顺序以后,AgGrid并没有对本地的ColumnDef进行修改。
        //也就是说,setColumnDefs总是拿着陈旧的数据在修改
        //而且,使用filter的方式直接删除列,会丢失原来列的状态信息,例如是原来的width信息
    }

    const toggleSport2 = () => {
        //getColumnDefs是列信息里面经过最终合并后的数据,它反应的是最新的列信息数据
        const oldColumnDefs = gridRef.current!.api.getColumnDefs()!;
        console.log('columnDefs ', oldColumnDefs);
        let newColumnDefs = oldColumnDefs.map((single: any) => {
            if (single.field == 'sport') {
                return {
                    ...single,
                    hide: !single.hide,
                }
            } else {
                return single;
            }
        });
        /*
        * columnDefs的数据
        * aggFunc: null
        * colId: "athlete"
        * editable: true
        * field: "athlete"
        * filter: "agTextColumnFilter"
        * floatingFilter: true
        * hide: undefined
        * pinned: null
        * pivot: false
        * pivotIndex: null
        * resizable: true
        * rowGroup: false
        * rowGroupIndex: null
        * sort: null
        * sortIndex: null
        * sortable: true
        * width: 150
        */
        //以命令的方式写入列信息,使用hide的方式,不会丢失原来列的状态信息
        //update的比较方式,
        gridRef.current!.api.setColumnDefs(newColumnDefs);
    }


    const toggleSport3 = () => {
        //columnState比columnDefs少的内容有:
        //editable,sortable,filter,floatFilter等等

        /*columnState数据
        * aggFunc: null
        * colId: "athlete"
        * flex: null
        * hide: false
        * pinned: null
        * pivot: false
        * pivotIndex: null
        * rowGroup: false
        * rowGroupIndex: null
        * sort: null
        * sortIndex: null
        * width: 150
        */
        const savedState = gridRef.current!.columnApi.getColumnState();
        console.log('columnState ', savedState);
        savedState.forEach(single => {
            if (single.colId == 'sport') {
                single.hide = !single.hide;
            }
        })
        gridRef.current!.columnApi.applyColumnState({
            state: savedState
        });
    }

    const gridRef = useRef<AgGridReact>(null);
    return (
        <div style={containerStyle}>
            <div>
                <Button onClick={toggleSport}>{'Toggle Sport列'}</Button>
                <Button onClick={toggleSport2}>{'Toggle Sport列2'}</Button>
                <Button onClick={toggleSport3}>{'Toggle Sport列3'}</Button>
            </div>
            <div style={{ flex: '1', boxSizing: 'border-box' }}>
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        defaultColGroupDef={defaultColGroupDef}
                        onGridReady={onGridReady}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;
  • 尽量不要使用声明方式更新列,而是要用命令方式更新列,这样性能更好,也能获取到最新的列信息,不会丢掉列状态信息
  • setColumnDefs,需要传入全部列信息。搭配着getColumnDefs使用。
  • applyColumnState,可以只更新局部列信息,但是状态数量更少。搭配着getColumnState使用。

2.6.2 部分列更新

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    SideBarDef,
} from 'ag-grid-community';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete' },
        { field: 'age' },
        { field: 'country' },
        { field: 'sport' },
        { field: 'year' },
        { field: 'date' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            sortable: true,
            resizable: true,
            width: 150,
            enableRowGroup: true,
            enablePivot: true,
            enableValue: true,
        };
    }, []);
    const sideBar = useMemo<
        SideBarDef | string | string[] | boolean | null
    >(() => {
        return {
            toolPanels: ['columns'],
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onBtSortAthlete = useCallback(() => {
        //setColumnDefs,每次都要传入所有列,没有传入的列看成被删掉了
        //applyColumnState,可以只传入一个列,没有传入的列看成保持不变
        /*columnState数据
       * aggFunc: null
       * colId: "athlete"
       * flex: null
       * hide: false
       * pinned: null
       * pivot: false
       * pivotIndex: null
       * rowGroup: false
       * rowGroupIndex: null
       * sort: null
       * sortIndex: null
       * width: 150
       */
        //只对athlete列,设置为升序排列
        gridRef.current!.columnApi.applyColumnState({
            state: [{ colId: 'athlete', sort: 'asc' }],
        });
    }, []);

    const onBtSortCountryThenSportClearOthers = useCallback(() => {
        //对country,sport组合为升序排列
        //defaultState用来设置其他列,表示为其他列的排序去掉
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'country', sort: 'asc', sortIndex: 0 },
                { colId: 'sport', sort: 'asc', sortIndex: 1 },
            ],
            defaultState: { sort: null },
        });
    }, []);

    const onBtClearAllSorting = useCallback(() => {
        //所有列的排序去掉
        gridRef.current!.columnApi.applyColumnState({
            defaultState: { sort: null },
        });
    }, []);

    const onBtRowGroupCountryThenSport = useCallback(() => {
        //对country,sport列组合为分组
        //将其他列设置为不分组
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'country', rowGroupIndex: 0 },
                { colId: 'sport', rowGroupIndex: 1 },
            ],
            defaultState: { rowGroup: false },
        });
    }, []);

    const onBtRemoveCountryRowGroup = useCallback(() => {
        //对country列的分组去掉
        gridRef.current!.columnApi.applyColumnState({
            state: [{ colId: 'country', rowGroup: false }],
        });
    }, []);

    const onBtClearAllRowGroups = useCallback(() => {
        //对所有列的分组去掉
        gridRef.current!.columnApi.applyColumnState({
            defaultState: { rowGroup: false },
        });
    }, []);

    const onBtOrderColsMedalsFirst = useCallback(() => {
        //applyColumnState默认传入的列,不对顺序进行操作
        //对grid的列顺序要与state顺序一致的时候,需要指定applyOrder
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'gold' },
                { colId: 'silver' },
                { colId: 'bronze' },
                { colId: 'total' },
                { colId: 'athlete' },
                { colId: 'age' },
                { colId: 'country' },
                { colId: 'sport' },
                { colId: 'year' },
                { colId: 'date' },
            ],
            applyOrder: true,
        });
    }, []);

    const onBtOrderColsMedalsLast = useCallback(() => {
        //另外一个列排序
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'athlete' },
                { colId: 'age' },
                { colId: 'country' },
                { colId: 'sport' },
                { colId: 'year' },
                { colId: 'date' },
                { colId: 'gold' },
                { colId: 'silver' },
                { colId: 'bronze' },
                { colId: 'total' },
            ],
            applyOrder: true,
        });
    }, []);

    const onBtHideMedals = useCallback(() => {
        //设置hide属性
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'gold', hide: true },
                { colId: 'silver', hide: true },
                { colId: 'bronze', hide: true },
                { colId: 'total', hide: true },
            ],
        });
    }, []);

    const onBtShowMedals = useCallback(() => {
        //设置hide属性
        gridRef.current!.columnApi.applyColumnState({
            state: [
                { colId: 'gold', hide: false },
                { colId: 'silver', hide: false },
                { colId: 'bronze', hide: false },
                { colId: 'total', hide: false },
            ],
        });
    }, []);

    return (
        <div style={containerStyle}>
            <div className="test-header">
                <table>
                    <tbody>
                        <tr>
                            <td>Sort:</td>
                            <td>
                                <button onClick={onBtSortAthlete}>Sort Athlete</button>
                                <button onClick={onBtSortCountryThenSportClearOthers}>
                                    Sort Country, then Sport - Clear Others
                                </button>
                                <button onClick={onBtClearAllSorting}>
                                    Clear All Sorting
                                </button>
                            </td>
                        </tr>
                        <tr>
                            <td>Column Order:</td>
                            <td>
                                <button onClick={onBtOrderColsMedalsFirst}>
                                    Show Medals First
                                </button>
                                <button onClick={onBtOrderColsMedalsLast}>
                                    Show Medals Last
                                </button>
                            </td>
                        </tr>
                        <tr>
                            <td>Column Visibility:</td>
                            <td>
                                <button onClick={onBtHideMedals}>Hide Medals</button>
                                <button onClick={onBtShowMedals}>Show Medals</button>
                            </td>
                        </tr>
                        <tr>
                            <td>Row Group:</td>
                            <td>
                                <button onClick={onBtRowGroupCountryThenSport}>
                                    Group Country then Sport
                                </button>
                                <button onClick={onBtRemoveCountryRowGroup}>
                                    Remove Country
                                </button>
                                <button onClick={onBtClearAllRowGroups}>
                                    Clear All Groups
                                </button>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>

            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    sideBar={sideBar}
                    rowGroupPanelShow={'always'}
                    pivotPanelShow={'always'}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

使用applyColumnState的关键点在:

  • 使用state传入局部列的状态信息,这个可以不传
  • 使用defaultState来指定其他列的状态信息,这个可以不传
  • 使用applyOrder,来确定是否同步列顺序

2.6.3 列顺序无关更新

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

function getColumnDefsA() {
    return [
        { field: 'athlete', headerName: 'A Athlete' },
        { field: 'age', headerName: 'A Age' },
        { field: 'country', headerName: 'A Country' },
        { field: 'sport', headerName: 'A Sport' },
        { field: 'year', headerName: 'A Year' },
        { field: 'date', headerName: 'A Date' },
        { field: 'gold', headerName: 'A Gold' },
        { field: 'silver', headerName: 'A Silver' },
        { field: 'bronze', headerName: 'A Bronze' },
        { field: 'total', headerName: 'A Total' },
    ];
}

function getColumnDefsB() {
    return [
        { field: 'gold', headerName: 'B Gold' },
        { field: 'silver', headerName: 'B Silver' },
        { field: 'bronze', headerName: 'B Bronze' },
        { field: 'total', headerName: 'B Total' },
        { field: 'athlete', headerName: 'B Athlete' },
        { field: 'age', headerName: 'B Age' },
        { field: 'country', headerName: 'B Country' },
        { field: 'sport', headerName: 'B Sport' },
        { field: 'year', headerName: 'B Year' },
        { field: 'date', headerName: 'B Date' },
    ];
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            initialWidth: 100,
            sortable: true,
            resizable: true,
            filter: true,
        };
    }, []);
    const [columnDefs, setColumnDefs] = useState<ColDef[]>(getColumnDefsA());

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const setColsA = useCallback(() => {
        gridRef.current!.api.setColumnDefs(getColumnDefsA());
    }, []);

    const setColsB = useCallback(() => {
        gridRef.current!.api.setColumnDefs(getColumnDefsB());
    }, []);

    const clearColDefs = useCallback(() => {
        gridRef.current!.api.setColumnDefs([]);
    }, []);

    return (
        <div style={containerStyle}>
            <div className="test-header">
                <button onClick={setColsA}>Column Set A</button>
                <button onClick={setColsB}>Column Set B</button>
                <button onClick={clearColDefs}>Clear</button>
            </div>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    defaultColDef={defaultColDef}
                    //默认情况下,setColumnDefs不仅会考虑列状态本身,还会考虑列的顺序
                    //如果,我们在调用setColumnDefs告知ag-grid,只需要考虑列状态,不需要列顺序,那么就打开maintainColumnOrder的开关就可以了
                    //maintainColumnOrder={true}
                    columnDefs={columnDefs}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

默认情况下,setColumnDefs不仅会考虑列状态本身,还会考虑列的顺序。如果,我们在调用setColumnDefs告知ag-grid,只需要考虑列状态,不需要考虑列顺序,那么就打开maintainColumnOrder的开关就可以了

2.6.4 列匹配与合并原则

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    ValueGetterParams,
} from 'ag-grid-community';

const athleteColumn = {
    headerName: 'Athlete',
    valueGetter: function (params: ValueGetterParams) {
        return params.data.athlete;
    },
};

function getColDefsMedalsIncluded() {
    return [
        //3.没有colId,也没有field的时候,使用object引用来标识这个列
        athleteColumn,
        //1,优先使用colId来标识这个列
        {
            colId: 'myAgeCol',
            headerName: 'Age',
            valueGetter: function (params: ValueGetterParams) {
                return params.data.age;
            },
        },
        //4,都不匹配的时候,ag-grid会认为这个是一个新的列,所以每次都会进行刷新
        {
            headerName: 'Country',
            headerClass: 'country-header',
            valueGetter: function (params: ValueGetterParams) {
                return params.data.country;
            },
        },
        //2,没有colId的时候,使用field来标识这个列
        { field: 'sport' },
        { field: 'year' },
        { field: 'date' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ];
}

function getColDefsMedalsExcluded() {
    return [
        athleteColumn,
        {
            colId: 'myAgeCol',
            headerName: 'Age',
            valueGetter: function (params: ValueGetterParams) {
                return params.data.age;
            },
        },
        {
            headerName: 'Country',
            headerClass: 'country-header',
            valueGetter: function (params: ValueGetterParams) {
                return params.data.country;
            },
        },
        { field: 'sport' },
        { field: 'year' },
        { field: 'date' },
        //标识了这个列以后,ag-grid对属性的合并规则是:
        //如果新属性的值为undefined,那么原来的属性值就保留,
        //如果新属性的值为null,那么原来的属性值就要清除,例如清除sort,清除group等等
        //如果新属性的值为具体值(非undefined且非null),那么原来的属性值就要被覆盖。
    ];
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            initialWidth: 100,
            sortable: true,
            resizable: true,
        };
    }, []);
    const [columnDefs, setColumnDefs] = useState<ColDef[]>(
        getColDefsMedalsIncluded()
    );

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onBtExcludeMedalColumns = useCallback(() => {
        gridRef.current!.api.setColumnDefs(getColDefsMedalsExcluded());
    }, []);

    const onBtIncludeMedalColumns = useCallback(() => {
        gridRef.current!.api.setColumnDefs(getColDefsMedalsIncluded());
    }, []);

    return (
        <div style={containerStyle}>
            <div className="test-header">
                <button onClick={onBtIncludeMedalColumns}>
                    Include Medal Columns
                </button>
                <button onClick={onBtExcludeMedalColumns}>
                    Exclude Medal Columns
                </button>
            </div>

            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    defaultColDef={defaultColDef}
                    columnDefs={columnDefs}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

列匹配的原则是:

  • 1,优先使用colId来标识这个列
  • 2,没有colId的时候,使用field来标识这个列
  • 3,没有colId,也没有field的时候,使用object引用来标识这个列
  • 4,都不匹配的时候,ag-grid会认为这个是一个新的列,所以每次都会进行刷新

列合并值得原则是:

  • 如果新属性的值为undefined,那么原来的属性值就保留不变,
  • 如果新属性的值为null,那么原来的属性值就要清除,例如清除sort,清除group等等
  • 如果新属性的值为具体值(非undefined且非null),那么原来的属性值就要被覆盖。

3 筛选,排序,分组,支点

代码在这里

3.1 筛选

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { Button } from 'antd';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import moment from 'moment';
import { ColDef, FilterChangedEvent, ValueGetterFunc, ValueGetterParams } from 'ag-grid-community';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete',
            filter: 'agTextColumnFilter',
        },
        {
            colId: 'age2',
            field: 'age',
            maxWidth: 100,
            filter: 'agNumberColumnFilter',
        },
        {
            field: 'country',
            filter: 'agTextColumnFilter',
        },
        {
            //显示为country2,filter的数据为age
            colId: 'country2',
            field: 'country(以age来刷新)',
            filter: 'agNumberColumnFilter',
            filterValueGetter: (params: ValueGetterParams) => {
                return params.data.age;
            }
        },
        {
            field: 'date',
            filter: 'agNumberColumnFilter',
            maxWidth: 100,
        },
        {
            field: 'year',
            filter: 'agDateColumnFilter',
            filterParams: {
                //agDateColumnFilter需要一个comparator配置,因为要对输入数据与内容数据进行比较
                comparator: function (filterLocalDateAtMidnight: Date, cellValue: string) {
                    let left = moment(filterLocalDateAtMidnight);
                    let right = moment(cellValue, 'YYYY');
                    let result = left.year() - right.year();
                    if (result < 0) {
                        return 1;
                    } else if (result > 0) {
                        return -1;
                    } else {
                        return 0;
                    }
                },
                //默认inRange不包含两个边界点,需要指定这个选项
                inRangeInclusive: true,
            },
            width: 100,
        },
        { field: 'sport' },
        { field: 'gold', filter: 'agNumberColumnFilter' },
        { field: 'silver', filter: 'agNumberColumnFilter' },
        { field: 'bronze', filter: 'agNumberColumnFilter' },
        { field: 'total', filter: 'agNumberColumnFilter' },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            minWidth: 150,
            //所有列均可筛选
            filter: true,
            //要一个固定行,输入filter数据
            floatingFilter: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    const clearFilter = () => {
        gridRef.current!.api.setFilterModel(null);
    }

    const getFilter = () => {
        const filterModel = gridRef.current!.api.getFilterModel();
        /*
        filterModel是object类型,key为colId,不是field,value为filter配置
        搜索age为19的rows
        {
            "age2": {
                "filterType": "number",
                "type": "equals",
                "filter": 19
            }
        }
        */
        /*
        搜索age为19岁和2岁的rows
        {
            "age2": {
                "filterType": "number",
                "operator": "AND",
                "condition1": {
                    "filterType": "number",
                    "type": "equals",
                    "filter": 19
                },
                "condition2": {
                    "filterType": "number",
                    "type": "equals",
                    "filter": 2
                }
            }
        }
        */
        /*
        搜索age为19至80的rows
        {
            "age2": {
                "filterType": "number",
                "type": "inRange",
                "filter": 19,
                "filterTo": 80
            }
        }
          */
        /*
        搜索age为19,country包含United的rows
       {
           "age2": {
               "filterType": "number",
               "type": "equals",
               "filter": 19
           },
           "country": {
               "filterType": "text",
               "type": "contains",
               "filter": "United"
           }
       }
       */
        console.log('filterModel', filterModel);
    }
    const setFilter = useCallback(() => {
        gridRef.current!.api.setFilterModel({
            "age2": {
                "filterType": "number",
                "type": "equals",
                "filter": 19
            }
        });
    }, []);
    const addRows = useCallback(() => {
        let rows = [];
        gridRef.current!.api.forEachNode(single => {
            rows.push(single.data);
        });
        rows.push({
            athlete: 'FishGold',
            age: 19,
            country: new Date().toLocaleString(),
        });
        gridRef.current!.api.setRowData(rows);
    }, []);
    const onFilterChanged = useCallback((params: FilterChangedEvent) => {
        console.log('filter Changed', params);
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={clearFilter}>{'清除筛选'}</Button>
                <Button onClick={getFilter}>{'获取筛选数据'}</Button>
                <Button onClick={setFilter}>{'剔除其他条件,只搜索19岁的群众'}</Button>
                <Button onClick={addRows}>{'添加19岁的数据'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                    onFilterChanged={onFilterChanged}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • filter,指定filter的方式,默认为agTextColumnFilter。可以为字符串,也可以为一个简单的true
  • filterParams,agDateColumnFilter需要一个comparator配置,因为要对输入数据与内容数据进行比较。inRangeInclusive,默认inRange不包含两个边界点,需要指定这个选项
  • floatingFilter,展示一个固定行,输入filter数据
  • filterModel,是一个string,any的结果,其中,key为colId,不是field
  • filterValueGetter,可以自定义filter的来源数据与valueGetter的数据是不同的

API为:

  • getFilterModel,获取当前的filter配置
  • setFilterModel,设置当前的filter配置
  • onFilterChanged,filter变化时触发的事件

3.2 排序


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import {
    ColDef,
    ColumnRowGroupChangedEvent,
    SortChangedEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        //对athlete asc,country asc的方式进行排序,sortIndex是排序的顺序
        { field: 'country', sortable: true, sort: 'asc', sortIndex: 1, hide: false },
        { field: 'athlete', sortable: true, sort: 'asc', sortIndex: 0, hide: false },
        { field: 'year' },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        //sortable就是是否允许在UI中自定义排序
        { field: 'sport', sortable: true, },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
            //所有列均可排序
            sortable: true,
            //没有排序的列,也要显示可以排序的图标
            unSortIcon: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,
        };
    }, []);

    const gridRef = useRef<AgGridReact>(null);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    const clearSort = () => {
        gridRef.current!.columnApi.applyColumnState({
            defaultState: {
                sort: null,
            }
        });
    }

    const getSort = () => {
        const columnState = gridRef.current!.columnApi.getColumnState();
        console.log('columnState', columnState);
    }

    const onSortChanged = useCallback((params: SortChangedEvent) => {
        console.log('SortChangedEvent', params);
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={clearSort}>{'清除排序'}</Button>
                <Button onClick={getSort}>{'获取排序'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ width: '100%', flex: '1' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onSortChanged={onSortChanged}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • sortable,指定该列是否可以排序
  • unSortIcon,当列没有排序的时候,也要显示可以排序的图标
  • getColumnState,根据state中的getColumnState,sort属性(asc或者desc),sortIndex属性(数字)来获取排序信息
  • applyColumnState,sort为null可以清除排序信息,或者设置排序信息
  • onSortChanged,用户触发排序的回调

3.3 分组


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import {
    ColDef,
    ColumnRowGroupChangedEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        //对country列进行分组rowGroup为true,分组后该列不消失hide为false
        //rowGroupIndex为分组的顺序
        { field: 'country', enableRowGroup: true, rowGroup: true, rowGroupIndex: 1, hide: false },
        { field: 'athlete', enableRowGroup: true, rowGroup: true, rowGroupIndex: 0, hide: false },
        { field: 'year' },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        //enableRowGroup就是是否允许在panel上进行自定义分组
        { field: 'sport', enableRowGroup: true, },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,
        };
    }, []);

    const gridRef = useRef<AgGridReact>(null);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    const clearGroup = () => {
        gridRef.current!.columnApi.applyColumnState({
            defaultState: {
                rowGroup: false,
            }
        });
    }

    const getGroup = () => {
        const columnState = gridRef.current!.columnApi.getColumnState();
        console.log('columnState', columnState);
    }

    const onColumnRowGroupChanged = useCallback((params: ColumnRowGroupChangedEvent) => {
        console.log('rowGroupChanged', params);
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={clearGroup}>{'清除分组'}</Button>
                <Button onClick={getGroup}>{'获取分组'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ width: '100%', flex: '1' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onColumnRowGroupChanged={onColumnRowGroupChanged}
                    rowGroupPanelShow={'always'}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

分组其实与排序很相似,只不过是将sort换成了rowGroup,sortIndex换成了rowGroupIndex。另外,AgGrid支持用户自定义分组,所以有enableRowGroup的配置,是否允许用户通过拖放列来对列进行分组。

  • rowGroup,当前列是否仅需分组
  • rowGroupIndex,分组的顺序
  • enableRowGroup,是否允许用户对该列执行分组,需要打开rowGroupPanelShow
  • onColumnRowGroupChanged,用户自定义分组触发后的事件
  • getColumnState,获取分组信息
  • applyColumnState,清除或者设置分组信息

3.4 支点


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import {
    ColDef,
    ColumnPivotChangedEvent,
    ColumnRowGroupChangedEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        //对country列进行分组rowGroup为true,分组后该列不消失hide为false
        //rowGroupIndex为分组的顺序,以country作为分组
        { field: 'country', enableRowGroup: true, rowGroup: true, rowGroupIndex: 0 },
        { field: 'athlete' },
        //将year的数据行,转换为year列
        { field: 'year', pivot: true, pivotIndex: 0 },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total' },
        { field: 'age' },
        { field: 'date' },
        //enablePivot就是是否允许在panel上进行自定义分组
        { field: 'sport', enablePivot: true, },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            sortable: true,
            flex: 1,
            minWidth: 150,
            resizable: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,
        };
    }, []);

    const gridRef = useRef<AgGridReact>(null);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    const clearPivot = () => {
        gridRef.current!.columnApi.applyColumnState({
            defaultState: {
                pivot: false,
            }
        });
    }

    const getPivot = () => {
        const columnState = gridRef.current!.columnApi.getColumnState();
        console.log('columnState', columnState);
    }

    const onColumnPivotChanged = useCallback((params: ColumnPivotChangedEvent) => {
        console.log('rowGroupChanged', params);
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={clearPivot}>{'清除Pivot'}</Button>
                <Button onClick={getPivot}>{'获取Pivot'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ width: '100%', flex: '1' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onColumnPivotChanged={onColumnPivotChanged}
                    rowGroupPanelShow={'always'}
                    //打开pivotMode
                    pivotMode={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

Pivot,支点模式,是一个相当有用的分析工具,用来将行转为列。

  • rowGroup和rowGroupIndex,需要先对部分列进行分组
  • aggFunc,然后对部分列执行聚合函数
  • pivot和pivotIndex,最后指定部分列的行数据,转换为列
  • pivotMode,需要打开PivotMode才能看到行转列的效果

API有:

  • enablePivot,是否允许用户自定义Pivot,通过右侧Panel,拖动Column到下方来执行
  • onColumnPivotChanged,用户自定义Pivot以后的事件回调

4 行

4.1 基础

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { GetRowIdParams } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: 35000 },
        { id: 2, make: "Ford", model: "Mondeo", price: 32000 },
        { id: 3, make: "Porsche", model: "Boxter", price: 72000 }
    ]);

    const [columnDefs] = useState([
        { field: 'make' },
        { field: 'model' },
        { field: 'price' }
    ])

    const defaultColDef = useMemo(() => {
        return {
            sortable: true,
        };
    }, []);
    const gridRef = useRef<AgGridReact>(null);

    const add = () => {
        let maxId = -1;
        rowData.forEach(single => {
            if (maxId <= single.id) {
                maxId = single.id;
            }
        })
        let newRowData = [
            ...rowData,
            {
                id: maxId + 1,
                make: "M" + maxId,
                model: "C" + maxId,
                price: maxId,
            },
        ]
        setRowData(newRowData);
    }

    let del = () => {
        let selectedRows = gridRef.current!.api.getSelectedNodes();
        let selectionRowIds = selectedRows.map(single => {
            return single.data.id;
        });
        let newRowData = rowData.filter((single) => {
            return selectionRowIds.indexOf(single.id) < 0;
        });
        console.log(rowData, selectionRowIds, newRowData);
        setRowData(newRowData);
    }

    //需要指定getRowId,才能启用animateRows,而且在重新刷新数据的时候更少地触发render
    const getRowId = useCallback((props: GetRowIdParams) => {
        return props.data.id;
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={add}>{'添加一行'}</Button>
                <Button onClick={del}>{'删除一行'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ width: '100%', flex: '1' }}>
                <AgGridReact
                    ref={gridRef}
                    getRowId={getRowId}
                    rowData={rowData}
                    defaultColDef={defaultColDef}
                    rowSelection={'multiple'}
                    columnDefs={columnDefs}
                    animateRows={true}>
                </AgGridReact>
            </div>
        </div>
    );
};

export default App;

要点如下:

  • getRowId,尽可能对所有的Grid都设置getRowId,这样能加快更新数据的速度,而且数据更新变化的时候有对应的动画效果
  • animateRows,动画效果
  • 声明式,默认rowData就支持使用声明方式来修改数据,我们也可以用命令方式的setRowData来修改数据。

4.2 高度

4.2.1 可变高度

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    RowHeightParams,
} from 'ag-grid-community';

var swimmingHeight: number;

var groupHeight: number;

var russiaHeight: number;

function getData() {
    return [
        {
            athlete: 'Ryan Lochte',
            age: 27,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 2,
            silver: 2,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Yekaterina Lobaznyuk',
            age: 17,
            country: 'Russia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Gymnastics',
            gold: 0,
            silver: 2,
            bronze: 1,
            total: 3,
        },
        {
            athlete: 'Ryan Lochte',
            age: 20,
            country: 'United States',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 1,
            silver: 1,
            bronze: 0,
            total: 2,
        },
        {
            athlete: 'Ericka Lorenz',
            age: 23,
            country: 'United States',
            year: 2004,
            date: '29/08/2004',
            sport: 'Waterpolo',
            gold: 0,
            silver: 0,
            bronze: 1,
            total: 1,
        },
        {
            athlete: 'Ericka Lorenz',
            age: 19,
            country: 'United States',
            year: 2000,
            date: '01/10/2000',
            sport: 'Waterpolo',
            gold: 0,
            silver: 1,
            bronze: 0,
            total: 1,
        },
        {
            athlete: 'Nikita Lobintsev',
            age: 23,
            country: 'Russia',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 0,
            silver: 0,
            bronze: 1,
            total: 1,
        },
        {
            athlete: 'Tatyana Logunova',
            age: 24,
            country: 'Russia',
            year: 2004,
            date: '29/08/2004',
            sport: 'Fencing',
            gold: 1,
            silver: 0,
            bronze: 0,
            total: 1,
        },
        {
            athlete: 'Tatyana Logunova',
            age: 20,
            country: 'Russia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Fencing',
            gold: 1,
            silver: 0,
            bronze: 0,
            total: 1,
        },
        {
            athlete: 'Nelson Loyola',
            age: 32,
            country: 'Cuba',
            year: 2000,
            date: '01/10/2000',
            sport: 'Fencing',
            gold: 0,
            silver: 0,
            bronze: 1,
            total: 1,
        },
    ];
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'country', rowGroup: true },
        { field: 'athlete' },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ]);

    const setSwimmingHeight = useCallback((height: number) => {
        //修改了高度以后,需要使用restRowHeights,能触发ag-grid调用getRowHeight来更新高度
        swimmingHeight = height;
        gridRef.current!.api.resetRowHeights();
    }, []);

    const setGroupHeight = useCallback((height: number) => {
        groupHeight = height;
        gridRef.current!.api.resetRowHeights();
    }, []);

    const setRussiaHeight = useCallback((height: number) => {
        // 对rowNode直接调用setRowHeight,并不能触发更新高度
        //1.要么是用resetRowHeights,触发的是getRowHeight的回调
        //2.要么是用onRowHeightChanged,手动批量通知ag-grid,它的rowNode中的rowHeight变更了
        russiaHeight = height;
        gridRef.current!.api.forEachNode(function (rowNode) {
            //使用rowNode的触发是一次性的,当下一次的getRowHeight的数据还是旧数据的话,依然会用旧数据的高度
            if (rowNode.data && rowNode.data.country === 'Russia') {
                rowNode.setRowHeight(height);
            }
        });
        gridRef.current!.api.onRowHeightChanged();
    }, []);

    //getRowHeight是一个回调,可以用来指定每个group,或者每一个行的高度
    const getRowHeight = useCallback(
        (params: RowHeightParams): number | undefined | null => {
            if (params.node.group && groupHeight != null) {
                //指定group的高度
                return groupHeight;
            } else if (
                params.data &&
                params.data.country === 'Russia' &&
                russiaHeight != null
            ) {
                //指定data为Russia的高度
                return russiaHeight;
            } else if (
                params.data &&
                params.data.sport === 'Swimming' &&
                swimmingHeight != null
            ) {
                //指定data为Swimming的高度
                return swimmingHeight;
            }
        },
        [groupHeight, russiaHeight, swimmingHeight]
    );

    return (
        <div style={containerStyle}>
            <div
                style={{
                    marginBottom: '5px',
                    fontFamily: 'Verdana, Geneva, Tahoma, sans-serif',
                    fontSize: '13px',
                }}
            >
                <div>
                    Top Level Groups:
                    <button onClick={() => setGroupHeight(42)}>42px</button>
                    <button onClick={() => setGroupHeight(75)}>75px</button>
                    <button onClick={() => setGroupHeight(125)}>125px</button>
                </div>
                <div style={{ marginTop: '5px' }}>
                    Swimming Leaf Rows:
                    <button onClick={() => setSwimmingHeight(42)}>42px</button>
                    <button onClick={() => setSwimmingHeight(75)}>75px</button>
                    <button onClick={() => setSwimmingHeight(125)}>125px</button>
                </div>
                <div style={{ marginTop: '5px' }}>
                    Russia Leaf Rows:
                    <button onClick={() => setRussiaHeight(42)}>42px</button>
                    <button onClick={() => setRussiaHeight(75)}>75px</button>
                    <button onClick={() => setRussiaHeight(125)}>125px</button>
                </div>
            </div>

            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    animateRows={true}
                    groupDefaultExpanded={1}
                    getRowHeight={getRowHeight}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要配置每个行的不同高度有两种方式,

  • 外部数据源,由开发者自己存储行高信息,然后提供getRowHeight给AgGrid,当外部数据源发生变化的时候,手动触发resetRowHeights方法。
  • 内部数据源,由AgGrid存储行高信息,我们使用RowNode的setRowHeight来配置每个行的行高信息。调用了setRowHeight以后,需要手动触发一次onRowHeightChanged来批量刷新页面。

4.2.2 自动高度

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    SideBarDef,
} from 'ag-grid-community';

function getData() {
    var latinSentence =
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu.';
    var latinWords = latinSentence.split(' ');

    var rowData = [];

    function generateRandomSentence(row: any, col: any) {
        var wordCount = ((row + 1) * (col + 1) * 733 * 19) % latinWords.length;
        var parts = [];
        for (var i = 0; i < wordCount; i++) {
            parts.push(latinWords[i]);
        }
        var sentence = parts.join(' ');
        return sentence + '.';
    }

    // create 100 rows
    for (var i = 0; i < 100; i++) {
        var item = {
            rowNumber: 'Row ' + i,
            autoA: generateRandomSentence(i, 1),
            autoB: generateRandomSentence(i, 2),
            autoC: generateRandomSentence(i, 3),
        };
        rowData.push(item);
    }

    return rowData;
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            headerName: 'Row #',
            field: 'rowNumber',
            width: 120,
        },
        {
            field: 'autoA',
            width: 300,
            //设置wrapText,会产生一个white-space: normal的CSS属性
            wrapText: true,
            //设置autoHeight,行高与内容高度有关
            autoHeight: true,
            headerName: 'A) Auto Height',
        },
        {
            width: 300,
            field: 'autoB',
            //只设置了wrapText,但是没有autoHeight,因此仅有一行高度
            wrapText: true,
            headerName: 'B) Normal Height',
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            sortable: true,
            resizable: true,
        };
    }, []);
    const sideBar = useMemo<
        SideBarDef | string | string[] | boolean | null
    >(() => {
        return {
            toolPanels: [
                {
                    id: 'columns',
                    labelDefault: 'Columns',
                    labelKey: 'columns',
                    iconKey: 'columns',
                    toolPanel: 'agColumnsToolPanel',
                    toolPanelParams: {
                        suppressRowGroups: true,
                        suppressValues: true,
                        suppressPivots: true,
                        suppressPivotMode: true,
                        suppressSideButtons: true,
                        suppressColumnFilter: true,
                        suppressColumnSelectAll: true,
                        suppressColumnExpandAll: true,
                    },
                },
            ],
            defaultToolPanel: 'columns',
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        // in this example, the CSS styles are loaded AFTER the grid is created,
        // so we put this in a timeout, so height is calculated after styles are applied.
        setTimeout(function () {
            //初始化数据是空的,生成数据是后续加载的
            setRowData(getData());
        }, 500);
    }, []);

    /**
     * 自动高度的缺点是:
     * 鼠标拖动垂直滚动条会有滞后,因为行高的计算需要在展示内容的时候动态计算出来,不能在屏幕外计算出来(虚拟滚动的特性)
     * 显示数据会有回跳,因为行高是动态计算的,默认为一行,当前面行的高度发生变化了,当前行的数据就会往下跳
     * 无法开启列的虚拟滚动,对于大量列的场景不适用
     * 自动行高消耗较大的计算资源,切勿在所有列中设置autoHeight
     * pinnedRow的动态行哥不能马上执行
     */
    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    sideBar={sideBar}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

自动高度,是根据列的内容来自动调整行高,要点如下:

  • wrapText,设置wrapText,会产生一个white-space: normal的CSS属性
  • autoHeight,设置autoHeight,行高与内容高度有关

自动高度的缺点是:

  • 鼠标拖动垂直滚动条会有滞后,因为行高的计算需要在展示内容的时候动态计算出来,不能在屏幕外计算出来(虚拟滚动的特性)
  • 显示数据会有回跳,因为行高是动态计算的,默认为一行,当前面行的高度发生变化了,当前行的数据就会往下跳
  • 无法开启列的虚拟滚动,对于大量列的场景不适用
  • 自动行高消耗较大的计算资源,切勿在所有列中设置autoHeight
  • pinnedRow的动态行哥不能马上执行

4.3 移动

4.3.1 受控移动

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    RowDragEndEvent,
    RowDragEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        //对该列增加一个手型按钮,可以拖动移行
        { field: 'athlete', rowDrag: true },
        { field: 'country' },
        { field: 'year', width: 100 },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 170,
            sortable: true,
            filter: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onRowDragEnd = useCallback((params: RowDragEndEvent) => {
        console.log('rowDrag end', params);
        //拖动以后,这个数据依然是旧的
        console.log('data', rowData?.slice(0, 10));
        //forEachNode里面的才是新数据
        const result: any[] = [];
        params.api.forEachNode((single, index) => {
            if (index <= 10) {
                result.push(single.data);
            }
        });
        console.log('allData', result);
    }, [rowData]);
    /**
     * rowDragManaged模式的限制点:
     * 只能在Client模式中使用
     * 无法在分页,排序,筛选,分组,支点模式中使用
     */
    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //拖动移行模式,将移行以后自动更新数据,默认是需要手动更新数据的
                    rowDragManaged={true}
                    onRowDragEnd={onRowDragEnd}
                    //移行的过程中,其他行不产生动画变化
                    suppressMoveWhenRowDragging={true}
                    animateRows={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

将移动对RowNode的变化,交给AgGrid来处理:

  • rowDrag,对该列增加一个手型按钮,可以拖动移行
  • rowDragManaged,拖动移行模式,将移行以后自动更新数据,默认是需要手动更新数据的
  • suppressMoveWhenRowDragging,默认移动行的过程中,会有动画效果,可以设置为没有动画效果。
  • onRowDragEnd,移动行结束的时候的事件回调

要注意的一点是,在移动行以后,AgGrid并没有修改rowData的引用,只修改了RowNode的顺序。所以,不要直接拿rowData的数据,而是重新拿forEachNode里面的数据。

rowDragManaged模式的限制点:

  • 只能在Client模式中使用
  • 无法在分页,排序,筛选,分组,支点模式中使用

4.3.2 非受控移动

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    GetRowIdFunc,
    GetRowIdParams,
    Grid,
    GridOptions,
    GridReadyEvent,
    RowDragMoveEvent,
} from 'ag-grid-community';

function getData() {
    return [
        {
            athlete: 'Michael Phelps',
            age: 23,
            country: 'United States',
            year: 2008,
            date: '24/08/2008',
            sport: 'Swimming',
            gold: 8,
            silver: 0,
            bronze: 0,
            total: 8,
        },
        {
            athlete: 'Michael Phelps',
            age: 19,
            country: 'United States',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 6,
            silver: 0,
            bronze: 2,
            total: 8,
        },
        {
            athlete: 'Michael Phelps',
            age: 27,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 4,
            silver: 2,
            bronze: 0,
            total: 6,
        },
        {
            athlete: 'Natalie Coughlin',
            age: 25,
            country: 'United States',
            year: 2008,
            date: '24/08/2008',
            sport: 'Swimming',
            gold: 1,
            silver: 2,
            bronze: 3,
            total: 6,
        },
        {
            athlete: 'Aleksey Nemov',
            age: 24,
            country: 'Russia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Gymnastics',
            gold: 2,
            silver: 1,
            bronze: 3,
            total: 6,
        },
        {
            athlete: 'Alicia Coutts',
            age: 24,
            country: 'Australia',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 1,
            silver: 3,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Missy Franklin',
            age: 17,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 4,
            silver: 0,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Ryan Lochte',
            age: 27,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 2,
            silver: 2,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Allison Schmitt',
            age: 22,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 3,
            silver: 1,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Natalie Coughlin',
            age: 21,
            country: 'United States',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 2,
            silver: 2,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Ian Thorpe',
            age: 17,
            country: 'Australia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 3,
            silver: 2,
            bronze: 0,
            total: 5,
        },
        {
            athlete: 'Dara Torres',
            age: 33,
            country: 'United States',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 2,
            silver: 0,
            bronze: 3,
            total: 5,
        },
        {
            athlete: 'Cindy Klassen',
            age: 26,
            country: 'Canada',
            year: 2006,
            date: '26/02/2006',
            sport: 'Speed Skating',
            gold: 1,
            silver: 2,
            bronze: 2,
            total: 5,
        },
        {
            athlete: 'Nastia Liukin',
            age: 18,
            country: 'United States',
            year: 2008,
            date: '24/08/2008',
            sport: 'Gymnastics',
            gold: 1,
            silver: 3,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Marit Bjørgen',
            age: 29,
            country: 'Norway',
            year: 2010,
            date: '28/02/2010',
            sport: 'Cross Country Skiing',
            gold: 3,
            silver: 1,
            bronze: 1,
            total: 5,
        },
        {
            athlete: 'Sun Yang',
            age: 20,
            country: 'China',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Kirsty Coventry',
            age: 24,
            country: 'Zimbabwe',
            year: 2008,
            date: '24/08/2008',
            sport: 'Swimming',
            gold: 1,
            silver: 3,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Libby Lenton-Trickett',
            age: 23,
            country: 'Australia',
            year: 2008,
            date: '24/08/2008',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Ryan Lochte',
            age: 24,
            country: 'United States',
            year: 2008,
            date: '24/08/2008',
            sport: 'Swimming',
            gold: 2,
            silver: 0,
            bronze: 2,
            total: 4,
        },
        {
            athlete: 'Inge de Bruijn',
            age: 30,
            country: 'Netherlands',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 1,
            silver: 1,
            bronze: 2,
            total: 4,
        },
        {
            athlete: 'Petria Thomas',
            age: 28,
            country: 'Australia',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 3,
            silver: 1,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Ian Thorpe',
            age: 21,
            country: 'Australia',
            year: 2004,
            date: '29/08/2004',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Inge de Bruijn',
            age: 27,
            country: 'Netherlands',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 3,
            silver: 1,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Gary Hall Jr.',
            age: 25,
            country: 'United States',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Michael Klim',
            age: 23,
            country: 'Australia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 2,
            silver: 2,
            bronze: 0,
            total: 4,
        },
        {
            athlete: "Susie O'Neill",
            age: 27,
            country: 'Australia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 1,
            silver: 3,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Jenny Thompson',
            age: 27,
            country: 'United States',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 3,
            silver: 0,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Pieter van den Hoogenband',
            age: 22,
            country: 'Netherlands',
            year: 2000,
            date: '01/10/2000',
            sport: 'Swimming',
            gold: 2,
            silver: 0,
            bronze: 2,
            total: 4,
        },
        {
            athlete: 'An Hyeon-Su',
            age: 20,
            country: 'South Korea',
            year: 2006,
            date: '26/02/2006',
            sport: 'Short-Track Speed Skating',
            gold: 3,
            silver: 0,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Aliya Mustafina',
            age: 17,
            country: 'Russia',
            year: 2012,
            date: '12/08/2012',
            sport: 'Gymnastics',
            gold: 1,
            silver: 1,
            bronze: 2,
            total: 4,
        },
        {
            athlete: 'Shawn Johnson',
            age: 16,
            country: 'United States',
            year: 2008,
            date: '24/08/2008',
            sport: 'Gymnastics',
            gold: 1,
            silver: 3,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Dmitry Sautin',
            age: 26,
            country: 'Russia',
            year: 2000,
            date: '01/10/2000',
            sport: 'Diving',
            gold: 1,
            silver: 1,
            bronze: 2,
            total: 4,
        },
        {
            athlete: 'Leontien Zijlaard-van Moorsel',
            age: 30,
            country: 'Netherlands',
            year: 2000,
            date: '01/10/2000',
            sport: 'Cycling',
            gold: 3,
            silver: 1,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Petter Northug Jr.',
            age: 24,
            country: 'Norway',
            year: 2010,
            date: '28/02/2010',
            sport: 'Cross Country Skiing',
            gold: 2,
            silver: 1,
            bronze: 1,
            total: 4,
        },
        {
            athlete: 'Ole Einar Bjørndalen',
            age: 28,
            country: 'Norway',
            year: 2002,
            date: '24/02/2002',
            sport: 'Biathlon',
            gold: 4,
            silver: 0,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Janica Kostelic',
            age: 20,
            country: 'Croatia',
            year: 2002,
            date: '24/02/2002',
            sport: 'Alpine Skiing',
            gold: 3,
            silver: 1,
            bronze: 0,
            total: 4,
        },
        {
            athlete: 'Nathan Adrian',
            age: 23,
            country: 'United States',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 0,
            total: 3,
        },
        {
            athlete: 'Yannick Agnel',
            age: 20,
            country: 'France',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 2,
            silver: 1,
            bronze: 0,
            total: 3,
        },
        {
            athlete: 'Brittany Elmslie',
            age: 18,
            country: 'Australia',
            year: 2012,
            date: '12/08/2012',
            sport: 'Swimming',
            gold: 1,
            silver: 2,
            bronze: 0,
            total: 3,
        },
    ];
}

var immutableStore: any[] = getData();

var sortActive = false;

var filterActive = false;

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete', rowDrag: true },
        { field: 'country' },
        { field: 'year', width: 100 },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 170,
            sortable: true,
            filter: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        // add id to each item, needed for immutable store to work
        immutableStore.forEach(function (data, index) {
            data.id = index;
        });
        setRowData(immutableStore);
    }, []);

    // listen for change on sort changed
    const onSortChanged = useCallback(() => {
        var colState = gridRef.current!.columnApi.getColumnState() || [];
        sortActive = colState.some((c) => c.sort);
        // suppress row drag if either sort or filter is active
        var suppressRowDrag = sortActive || filterActive;
        console.log(
            'sortActive = ' +
            sortActive +
            ', filterActive = ' +
            filterActive +
            ', allowRowDrag = ' +
            suppressRowDrag
        );
        //有sort或者有filter都需要禁止行拖动
        gridRef.current!.api.setSuppressRowDrag(suppressRowDrag);
    }, [filterActive]);

    // listen for changes on filter changed
    const onFilterChanged = useCallback(() => {
        filterActive = gridRef.current!.api.isAnyFilterPresent();
        // suppress row drag if either sort or filter is active

        //有sort或者有filter都需要禁止行拖动
        var suppressRowDrag = sortActive || filterActive;
        console.log(
            'sortActive = ' +
            sortActive +
            ', filterActive = ' +
            filterActive +
            ', allowRowDrag = ' +
            suppressRowDrag
        );
        gridRef.current!.api.setSuppressRowDrag(suppressRowDrag);
    }, [filterActive]);

    const onRowDragMove = useCallback(
        (event: RowDragMoveEvent) => {
            var movingNode = event.node;
            var overNode = event.overNode;
            var rowNeedsToMove = movingNode !== overNode;
            if (rowNeedsToMove) {
                // the list of rows we have is data, not row nodes, so extract the data
                var movingData = movingNode.data;
                var overData = overNode!.data;
                var fromIndex = immutableStore.indexOf(movingData);
                var toIndex = immutableStore.indexOf(overData);
                console.log('move data', fromIndex, toIndex);
                var newStore = immutableStore.slice();
                moveInArray(newStore, fromIndex, toIndex);
                immutableStore = newStore;
                gridRef.current!.api.setRowData(newStore);
                gridRef.current!.api.clearFocusedCell();
            }
            function moveInArray(arr: any[], fromIndex: number, toIndex: number) {
                var element = arr[fromIndex];
                //这个性能较差
                arr.splice(fromIndex, 1);
                arr.splice(toIndex, 0, element);
            }
        },
        [immutableStore]
    );

    const getRowId = useCallback((params: GetRowIdParams) => {
        return params.data.id;
    }, []);

    /*
    * unmanage的性能较差,不太建议使用
    */
    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    animateRows={true}
                    getRowId={getRowId}
                    onGridReady={onGridReady}
                    onSortChanged={onSortChanged}
                    onFilterChanged={onFilterChanged}
                    onRowDragMove={onRowDragMove}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

非受控移动,就是AgGrid只告诉移动的过程,但是不对RowNode和RowData进行更新,需要开发者手动更新。

非受控移动注意不要使用Immutable的更新方式,性能很差,需要用原地更新的方式。

4.3.3 自定义拖动组件

'use strict';

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    ICellRendererParams,
} from 'ag-grid-community';
import './style.css';

const CustomCellRenderer = (props: ICellRendererParams) => {
    const myRef = useRef(null);

    useEffect(() => {
        //获取year的span,然后告诉ag-grid这个是一个拖动手柄
        props.registerRowDragger(myRef.current!);
    });

    return (
        <div className="my-custom-cell-renderer">
            <div className="athlete-info">
                <span>{props.data.athlete}</span>
                <span>{props.data.country}</span>
            </div>
            <span className="year" ref={myRef}>{props.data.year}</span>
        </div>
    );
};

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete',
            cellClass: 'custom-athlete-cell',
            cellRenderer: CustomCellRenderer,
        },
        { field: 'country' },
        { field: 'year', width: 100 },
        { field: 'date' },
        { field: 'sport' },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 170,
            sortable: true,
            filter: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    rowDragManaged={true}
                    animateRows={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

自定义拖动的组件

  • 使用CustomCellRenderer来重新定义列的Render
  • 在列的Render中注册registerRowDragger

4.4 固定

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    RowClassParams,
    RowStyle,
} from 'ag-grid-community';
import './style.css';

//自定义渲染器
const CustomPinnedRowRenderer: React.FC<any> = (props) => {
    return <span style={props.style}>{props.value}</span>;
}

function createData(count: number, prefix: string) {
    var result = [];
    for (var i = 0; i < count; i++) {
        result.push({
            athlete: prefix + ' Athlete ' + i,
            age: prefix + ' Age ' + i,
            country: prefix + ' Country ' + i,
            year: prefix + ' Year ' + i,
            date: prefix + ' Date ' + i,
            sport: prefix + ' Sport ' + i,
        });
    }
    return result;
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete',
            cellRendererSelector: function (params) {
                //pinned数据与普通数据,使用相同的列方式渲染,用node.rowPinned来区分
                if (params.node.rowPinned) {
                    return {
                        component: CustomPinnedRowRenderer,
                        params: {
                            //自定义渲染器的默认参数
                            style: { color: 'blue' },
                        },
                    };
                } else {
                    // rows that are not pinned don't use any cell renderer
                    return undefined;
                }
            },
        },
        {
            field: 'age',
            cellRendererSelector: function (params) {
                if (params.node.rowPinned) {
                    return {
                        component: CustomPinnedRowRenderer,
                        params: {
                            style: { 'font-style': 'italic' },
                        },
                    };
                } else {
                    // rows that are not pinned don't use any cell renderer
                    return undefined;
                }
            },
        },
        { field: 'country' },
        { field: 'year' },
        { field: 'date' },
        { field: 'sport' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            width: 200,
            sortable: true,
            filter: true,
            resizable: true,
        };
    }, []);
    const getRowStyle = useCallback(function (
        params: RowClassParams
    ): RowStyle | undefined {
        //行的style设置
        if (params.node.rowPinned) {
            return { 'font-weight': 'bold' };
        }
    },
        []);
    const pinnedTopRowData = useMemo<any[]>(() => {
        return createData(1, 'Top');
    }, []);
    const pinnedBottomRowData = useMemo<any[]>(() => {
        return createData(1, 'Bottom');
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onPinnedRowTopCount = useCallback(() => {
        var headerRowsToFloat = (document.getElementById('top-row-count') as any)
            .value;
        var count = Number(headerRowsToFloat);
        var rows = createData(count, 'Top');
        //命令方式地设置pinnedTopRowData,与pinnedBottomRowData
        gridRef.current!.api.setPinnedTopRowData(rows);
    }, []);

    const onPinnedRowBottomCount = useCallback(() => {
        var footerRowsToFloat = (document.getElementById('bottom-row-count') as any)
            .value;
        var count = Number(footerRowsToFloat);
        var rows = createData(count, 'Bottom');
        gridRef.current!.api.setPinnedBottomRowData(rows);
    }, []);

    return (
        <div style={containerStyle}>
            <div className="example-wrapper">
                <div className="example-header">
                    <span>Rows to Pin on Top:</span>
                    <select
                        onChange={onPinnedRowTopCount}
                        id="top-row-count"
                        style={{ marginLeft: '10px', marginRight: '20px' }}
                    >
                        <option value="0">0</option>
                        <option value="1" selected={true}>
                            1
                        </option>
                        <option value="2">2</option>
                        <option value="3">3</option>
                        <option value="4">4</option>
                    </select>
                    <span>Rows to Pin on Bottom:</span>
                    <select
                        onChange={onPinnedRowBottomCount}
                        id="bottom-row-count"
                        style={{ marginLeft: '10px' }}
                    >
                        <option value="0">0</option>
                        <option value="1" selected={true}>
                            1
                        </option>
                        <option value="2">2</option>
                        <option value="3">3</option>
                        <option value="4">4</option>
                    </select>
                </div>

                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        getRowStyle={getRowStyle}
                        //pinnedTopRowData相当有用,它们与普通grid共用列,但是不同的数据源
                        //但是pinned数据,不参与排序,筛选,分组,选择
                        pinnedTopRowData={pinnedTopRowData}
                        pinnedBottomRowData={pinnedBottomRowData}
                        onGridReady={onGridReady}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

固定行时一个相当有用的功能,常常用来显示合计行的信息。要点如下:

  • 固定行,与普通行,都是占用相同的列信息,相同的列渲染,只是固定行不会随滚动条滚动而已。
  • pinnedTopRowData,和pinnedBottomRowData都是通过内部数据源的命令方式更新,需要setPinnedTopRowData和setPinnedBottomRowData来进行配套设置。

4.5 合并

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ICellRendererComp,
    ICellRendererParams,
    RowSpanParams,
} from 'ag-grid-community';
import getData from './data';
import './style.css';

function rowSpan(params: RowSpanParams) {
    if (params.data.show) {
        return 4;
    } else {
        return 1;
    }
}

const ShowCellRenderer: React.FC<any> = (props) => {
    const cellBlank = !props.value;
    if (cellBlank) {
        return null;
    }
    return (
        <div>
            <div className="show-name">{props.value.name}</div>
            <div className="show-presenter">{props.value.presenter}</div>
        </div>
    );
}
const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'localTime' },
        {
            field: 'show',
            cellRenderer: ShowCellRenderer,
            //ag-grid的rowSpan是一个掩眼法,它没有真的在table里面使用rowSpan
            //它是根据rowSpan的数值来设置每个单元格的高度
            //例如,当rowSpan为2的时候,这个单元格的高度刚好为2倍的行高。而那个被rowSpan覆盖的单元格依然会被正常渲染出来
            rowSpan: rowSpan,
            cellClassRules: {
                //所以,rowSpan都需要一个特别的操作,将rowSpan的单元格设置background为一个具体的颜色,来掩盖下面的单元格
                //你可以试试更改show-cell为show-cell2的类名就知道了
                //cellClassRules的意思是,value(指当前的show字段)不为空的时候,该单元格就赋予一个show-cell的类名
                'show-cell': 'value !== undefined',
            },
            width: 200,
        },
        { field: 'a' },
        { field: 'b' },
        { field: 'c' },
        { field: 'd' },
        { field: 'e' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            resizable: true,
            width: 170,
        };
    }, []);

    /**
     * AgGrid的rowSpan有很多限制,包括有:
     * 不要在最后一行进行rowSpan,否则会产生span过高留有空行
     * 需要设置cellClassRules来设置背景,保证rowSpan的下面行看不到
     * 覆盖行肉眼上看不到,但是依然有效果,例如焦点转移的时候依然会转到它身上,(这简直太糟糕了,看不到的单元格还会占据焦点位置)
     * 动态行高,自动行高都用不了
     * 排序和筛选都有奇怪的展示效果
     * 范围选择也会有奇怪的效果,不能正常工作
     */
    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //使用rowSpan的时候,我们都会加这个suppressRowTransform属性
                    //这个属性的意思,虚拟列表使用top来实现,而不是使用transform来实现
                    suppressRowTransform={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid的合并行方法:

  • rowSpan,使用rowSpan来配置每个cell占据的高度。
  • cellClassRules,使用cellClassRules来配置有rowSpan数据的className,用来将含有rowSpan的数据覆盖下面的数据。
  • suppressRowTransform,使用top,而不是transform来实现虚拟化列表

AgGrid的rowSpan有很多限制,包括有:

  • 不要在最后一行进行rowSpan,否则会产生span过高留有空行
  • 需要设置cellClassRules来设置背景,保证rowSpan的下面行看不到
  • 覆盖行肉眼上看不到,但是依然有效果,例如焦点转移的时候依然会转到它身上,(这简直太糟糕了,看不到的单元格还会占据焦点位置)
  • 动态行高,自动行高都用不了
  • 排序和筛选都有奇怪的展示效果
  • 范围选择也会有奇怪的效果,不能正常工作

4.6 排序

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { GetRowIdParams } from 'ag-grid-community';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 2, make: "Ford", model: "Mondeo", price: '91.2', price2: 91.2 },
        { id: 1, make: "Toyota", model: "Celica", price: '3.23', price2: 3.23 },
        { id: 3, make: "Porsche", model: "Boxter", price: '188.7', price2: 188.7 }
    ]);

    const [columnDefs] = useState([
        { field: 'make' },
        { field: 'model' },
        { headerName: '价格(默认排序)', field: 'price' },
        {
            headerName: '价格(自定义排序)', field: 'price',
            //注意这个comparator与filter中的comparator的不同
            //sort的comparator是为了grid数组中的两两比较
            //filter中的comparator的是为了与目标数据的比较
            comparator: (valueA: any, valueB: any, nodeA: any, nodeB: any, isInverted: boolean) => {
                let v1Number = Number.parseFloat(valueA);
                let v2Number = Number.parseFloat(valueB);
                if (v1Number == v2Number) return 0;
                return (v1Number > v2Number) ? 1 : -1;
            }
        }
    ])

    const defaultColDef = useMemo(() => {
        return {
            sortable: true,
        };
    }, []);
    const gridRef = useRef<AgGridReact>(null);

    const add = () => {
        let maxId = -1;
        rowData.forEach(single => {
            if (maxId <= single.id) {
                maxId = single.id;
            }
        })
        const rand = Math.floor(Math.random() * 10000) / 100;
        let newRowData = [
            ...rowData,
            {
                id: maxId + 1,
                make: "M" + maxId,
                model: "C" + maxId,
                price: rand + '',
                price2: rand,
            },
        ]
        setRowData(newRowData);
    }

    let del = () => {
        let selectedRows = gridRef.current!.api.getSelectedNodes();
        let selectionRowIds = selectedRows.map(single => {
            return single.data.id;
        });
        let newRowData = rowData.filter((single) => {
            return selectionRowIds.indexOf(single.id) < 0;
        });
        console.log(rowData, selectionRowIds, newRowData);
        setRowData(newRowData);
    }

    //需要指定getRowId,才能启用animateRows,而且在重新刷新数据的时候更少地触发render
    const getRowId = useCallback((props: GetRowIdParams) => {
        return props.data.id;
    }, []);
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={add}>{'添加一行'}</Button>
                <Button onClick={del}>{'删除一行'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ width: '100%', flex: '1' }}>
                <AgGridReact
                    ref={gridRef}
                    getRowId={getRowId}
                    rowData={rowData}
                    defaultColDef={defaultColDef}
                    rowSelection={'multiple'}
                    columnDefs={columnDefs}
                    animateRows={true}
                    //默认不支持多列排序,需要用multiSortKey来指定
                    multiSortKey={'ctrl'}>
                </AgGridReact>
            </div>
        </div>
    );
};

export default App;

行排序的要点:

  • 自定义comparator,部分情况下需要加入一个comparator,注意这个comparator与filter中的comparator的不同。sort的comparator是为了grid数组中的两两比较,而filter中的comparator的是为了与目标数据的比较
  • multiSortKey,使用快捷键来实现多列排序。

5 样式

5.1 行样式

import styles from './index.less';

import { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useState } from 'react';
import 'ag-grid-community/dist/styles/ag-grid.css';
//引入主题
import 'ag-grid-community/dist/styles/ag-theme-balham.css';
import { Button } from 'antd';
import { GetRowIdFunc, RowClassParams } from 'ag-grid-community';
import './style.css';

const App: React.FC<any> = () => {
    const [rowData, setRowData] = useState([
        { id: 1, make: "Toyota", model: "Celica", price: 35000 },
        { id: 2, make: "Ford", model: "Mondeo", price: 32000 },
        { id: 3, make: "Porsche", model: "Boxter", price: 72000 }
    ]);

    const [columnDefs] = useState([
        { field: 'make' },
        { field: 'model' },
        { field: 'price' }
    ])

    const add = () => {
        let newRowData = [
            ...rowData,
            {
                id: rowData.length + 1,
                make: "M1",
                model: "C2",
                price: 23000,
            },
        ]
        setRowData(newRowData);
    }
    //每行的style
    const getRowStyle = (params: any) => {
        if (params.node.rowIndex % 2 === 0) {
            return { background: 'red' };
        }
    };
    //每行的class
    const getRowClass = (params: any) => {
        if (params.node.rowIndex % 3 === 0) {
            return 'my-shaded-effect';
        }
    };

    const getRowId: GetRowIdFunc = useCallback((props) => {
        return props.data.id;
    }, []);

    const rowClassRules = useMemo(() => {
        //输入row,输出boolean,指定是否有这个class
        return {
            // apply green to 2008
            'rag-green-outer': function (params: RowClassParams) {
                let result = (params.data.make == 'M1');
                console.log(params.data, result);
                return result;
            },
        };
    }, []);

    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={add}>{'添加一行'}</Button>
            </div>
            <div className="ag-theme-balham" style={{ flex: '1', width: '100%' }}>
                <AgGridReact
                    getRowId={getRowId}
                    getRowStyle={getRowStyle}
                    getRowClass={getRowClass}
                    rowData={rowData}
                    rowClassRules={rowClassRules}
                    columnDefs={columnDefs}>
                </AgGridReact>
            </div>
        </div>
    );
};

export default App;

行样式的三种方法:

  • getRowStyle,直接设置行的style
  • getRowClass,直接设置行的className
  • rowClassRules,对多个className进行判断和组合。

行样式都是配置在AgGrid的属性上的。

5.2 单元格样式

'use strict';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import React, { useCallback, useMemo, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import {
    CellClassParams,
    CellClassRules,
    ColDef,
    GridReadyEvent,
    ICellRendererParams,
    ValueParserParams,
} from 'ag-grid-community';

const ragCellClassRules: CellClassRules = {
    'rag-green-outer': (params) => params.value === 2008,
    'rag-amber-outer': (params) => params.value === 2004,
    'rag-red-outer': (params) => params.value === 2000,
};


const cellStyle = (params: CellClassParams) => {
    const color = numberToColor(params.value);
    return {
        backgroundColor: color,
    };
};

const numberToColor = (val: number) => {
    if (val === 0) {
        return '#ffaaaa';
    } else if (val == 1) {
        return '#aaaaff';
    } else {
        return '#aaffaa';
    }
};

const ragRenderer = (params: ICellRendererParams) => {
    return <span className="rag-element">{params.value}</span>;
};

const numberParser = (params: ValueParserParams) => {
    const newValue = params.newValue;
    let valueAsNumber;
    if (newValue === null || newValue === undefined || newValue === '') {
        valueAsNumber = null;
    } else {
        valueAsNumber = parseFloat(params.newValue);
    }
    return valueAsNumber;
};

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete' },
        {
            field: 'age',
            maxWidth: 90,
            valueParser: numberParser,
            //输入cell的value,输出boolean,是否需要这个class
            cellClassRules: {
                'rag-green': 'x < 20',
                'rag-amber': 'x >= 20 && x < 25',
                'rag-red': 'x >= 25',
            },
        },
        { field: 'country' },
        {
            field: 'year',
            maxWidth: 90,
            valueParser: numberParser,
            cellClassRules: ragCellClassRules,
            cellRenderer: ragRenderer,
        },
        { field: 'date', cellClass: 'rag-amber' },
        {
            field: 'sport',
            //输入cell的value,输出cell的class
            cellClass: (params: CellClassParams) => {
                return params.value === 'Swimming' ? 'rag-green' : 'rag-amber';
            },
        },
        {
            field: 'gold',
            valueParser: numberParser,
            //cell的样式,一个固定的常量
            cellStyle: {
                // you can use either came case or dashes, the grid converts to whats needed
                backgroundColor: '#aaffaa', // light green
            },
        },
        {
            field: 'silver',
            valueParser: numberParser,
            //cell的样式,一个函数,传入不同cell的value,输出样式
            cellStyle: (params: CellClassParams) => {
                const color = numberToColor(params.value);
                return {
                    backgroundColor: color,
                };
            },
        },
        {
            field: 'bronze',
            valueParser: numberParser,
            // same as above, but demonstrating dashes in the style, grid takes care of converting to/from camel case
            cellStyle: cellStyle,
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 150,
            editable: true,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

单元格样式,要点:

  • cellStyle,直接配置样式,可以为一个object,也可以为一个函数
  • cellClass,直接配置className,可以为一个string,也可以为一个函数
  • cellClassRules,,对多个className进行判断和组合

单元格样式,都是配置在column上的

6 客户端数据模型

Client-Data是支持特性最为丰富的模式,我们应尽可能使用这种数据模式。

代码在这里

6.1 外部数据源

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { ColDef, ColGroupDef, GetRowIdParams, Grid, GridOptions } from 'ag-grid-community';
import './style.css'
import getData from './getData';
import { Button } from 'antd';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<React.CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'firstName' },
        { field: 'lastName' },
        { field: 'gender' },
        { field: 'age' },
        { field: 'mood' },
        { field: 'country' },
        { field: 'address', minWidth: 550 },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            //flex: 1,
            minWidth: 110,
            //全部单元格默认是可编辑的,数据会直接在rowData上进行修改
            editable: true,
            resizable: true,
            sortable: true,
        };
    }, []);

    const getAllData = useCallback(() => {
        console.log('allRowData', rowData);
        gridRef.current!.api.forEachNode((rowNode, index) => {
            console.log('node ' + index + " : " + JSON.stringify(rowNode.data) + " is in the grid");
        });

        // iterate only nodes that pass the filter
        //forEachNodeAfterFilter

        //iterate only nodes that pass the filter and ordered by the sort order
        //forEachNodeAfterFilterAndSort

        //iterate through every leaf node in the grid
        //forEachLeafNode
    }, [rowData]);

    const refreshAllData = useCallback(() => {
        //将Ag-Grid看成是渲染器,不存放数据相关操作
        setRowData(getData());
    }, [rowData]);

    const reverseData = useCallback(() => {
        let newData = [...rowData].reverse();
        setRowData(newData);
    }, [rowData]);
    const pushData = useCallback(() => {
        let maxId = -1;
        for (let i in rowData) {
            if (rowData[i].id > maxId) {
                maxId = rowData[i].id;
            }
        }
        let id = maxId + 1;
        let newData = [
            ...rowData,
            {
                id: id,
                firstName: 'Fish_' + id,
                lastName: 'Fish_' + id,
                gender: 'Male',
                age: id,
            }
        ];
        setRowData(newData);
    }, [rowData]);

    const popData = useCallback(() => {
        let newData = rowData.filter((single, index) => {
            return index != 0;
        })
        setRowData(newData);
    }, [rowData]);

    const removeSelected = useCallback(() => {
        const selectedRowNodes = gridRef.current!.api.getSelectedNodes();
        const selectedIds = selectedRowNodes.map(function (rowNode) {
            //id总是为string类型
            return rowNode.id;
        });
        const newData = rowData.filter((single) => {
            return selectedIds.indexOf(single.id + '') < 0;
        });
        setRowData(newData);
    }, [rowData]);

    const allSetAge = useCallback(() => {
        const newData = rowData.map((single) => {
            let age = Math.floor(Math.random() * 100);
            return {
                ...single,
                age: age,
            };
        })
        setRowData(newData);
    }, [rowData]);

    const getRowId = useCallback((params: GetRowIdParams) => {
        return params.data.id;
    }, []);
    return (
        <div style={containerStyle}>
            <div>
                <Button onClick={getAllData}>{'获取数据'}</Button>
                <Button onClick={refreshAllData}>{'重置全部数据'}</Button>
                <Button onClick={reverseData}>{'reverse数据'}</Button>
                <Button onClick={pushData}>{'push数据'}</Button>
                <Button onClick={popData}>{'pop数据'}</Button>
                <Button onClick={removeSelected}>{'删除选中行'}</Button>
                <Button onClick={allSetAge}>{'随机设置Age'}</Button>
            </div>
            <div className="grid-wrapper">
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        //需要设置getRowId,才能得到更好的刷新效果
                        getRowId={getRowId}
                        rowSelection={'multiple'}
                        animateRows={true}
                        //默认的Enter键进入或者退出编辑状态,并不会去转移光标

                        //鼠标单击进入编辑状态,但是会丢失导航能力,并不好用
                        singleClickEdit={true}
                        //当表格丢失焦点的时候,自动停止编辑并保存,这点非常有用
                        stopEditingWhenCellsLoseFocus={true}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

外部数据源是React推荐的数据流模式,AgGrid也支持这种模式。另外:

  • forEachNode,获取所有RowNode数据
  • forEachNodeAfterFilter,获取所有筛选后的RowNode数据
  • forEachNodeAfterFilterAndSort,获取所有排序和筛选后的RowNode数据
  • forEachLeafNode,获取所有分组中的叶子数据

外部数据源模式,在数据更新的时候,AgGrid也是采用类似React的方式进行合并更新

  • 根据rowId对新旧两个数组进行diff操作,如果数组的引用不变,就看成数组无变动
  • 根据数组元素的引用进行比较,如果元素引用不变,就看成元素无变动。

6.2 内部数据源

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { ColDef, ColGroupDef, GetRowIdParams, Grid, GridOptions } from 'ag-grid-community';
import './style.css'
import getData from './getData';
import { Button } from 'antd';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<React.CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'firstName' },
        { field: 'lastName' },
        { field: 'gender' },
        { field: 'age' },
        { field: 'mood' },
        { field: 'country' },
        { field: 'address', minWidth: 550 },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            //flex: 1,
            minWidth: 110,
            //全部单元格默认是可编辑的,数据会直接在rowData上进行修改
            editable: true,
            resizable: true,
            sortable: true,
        };
    }, []);

    const getAllData = useCallback(() => {
        let rowData: any[] = [];
        gridRef.current!.api.forEachNode((rowNode, index) => {
            rowData.push(rowNode.data);
        });
        return rowData;
    }, []);

    const refreshAllData = useCallback(() => {
        //setRowData没有Immutable的限制,可以传递不同或者相同的引用进去也能更新数据
        //但是这样做会导致可能会导致困惑,rowData里面的引用没有改变了,但是数据改变了
        //另外,从当前的rowData拿数据也会产生困惑
        //这其实是将Ag-Grid看成是数据容器,以命令的方式写入数据到数据容器中
        //只不过当Ag-Grid的数据引用与初始rowData保持一致的话,我们能更少地出现bug
        //gridRef.current!.api.setRowData(getData());

        //更好的做法是,坚持不更改rowData的引用传递进去
        let newData = getAllData();
        let rows = getData();
        newData.splice(0, newData.length);
        for (let i in rows) {
            newData.push(rows[i]);
        }
        gridRef.current!.api.setRowData(newData);

        //另外一种办法是,坚持不是用api.setRowData,而是使用React的setRowData来设置数据,但是这样做需要Immutable的设置
        //看index2的实现
    }, []);

    const reverseData = useCallback(() => {
        let newData = getAllData();
        newData.reverse();
        gridRef.current!.api.setRowData(newData);
    }, []);
    const pushData = useCallback(() => {
        let newData = getAllData();
        let maxId = -1;
        for (let i in newData) {
            if (newData[i].id > maxId) {
                maxId = newData[i].id;
            }
        }
        let id = maxId + 1;
        newData.push({
            id: id,
            firstName: 'Fish_' + id,
            lastName: 'Fish_' + id,
            gender: 'Male',
            age: id,
        });
        gridRef.current!.api.setRowData(newData);
    }, []);

    const popData = useCallback(() => {
        let newData = getAllData();
        if (newData.length != 0) {
            newData.splice(0, 1);
        }
        gridRef.current!.api.setRowData(newData);
    }, []);

    const removeSelected = useCallback(() => {
        const selectedRowNodes = gridRef.current!.api.getSelectedNodes();
        const selectedIds = selectedRowNodes.map(function (rowNode) {
            return rowNode.id;
        });
        let newData = getAllData();
        selectedIds.forEach(single => {
            const delIndex = newData.findIndex((data) => {
                return data.id == single;
            });
            if (delIndex != -1) {
                newData.splice(delIndex, 1);
            }
        })
        gridRef.current!.api.setRowData(newData);
    }, []);

    const allSetAge = useCallback(() => {
        //修改一个单元格的信息时,需要将整个row的引用改掉
        let newData = getAllData();
        for (let i in newData) {
            let single = newData[i];
            //这样做是正确的,修改需要将整个row的引用改掉
            newData[i] = {
                ...single,
                age: Math.floor(Math.random() * 200),
            }
        }
        gridRef.current!.api.setRowData(newData);
    }, []);

    const failAllSetAge = useCallback(() => {
        //修改一个单元格的信息时,需要将整个row的引用改掉
        let newData = getAllData();
        for (let i in newData) {
            let single = newData[i];
            //这样做是错误的,因为key对应的object引用不变,所以Grid不进行修改
            single.age = Math.floor(Math.random() * 200);
        }
        gridRef.current!.api.setRowData(newData);
    }, []);

    const getRowId = useCallback((params: GetRowIdParams) => {
        return params.data.id;
    }, []);
    return (
        <div style={containerStyle}>
            <div>
                <Button onClick={getAllData}>{'获取数据'}</Button>
                <Button onClick={refreshAllData}>{'重置全部数据'}</Button>
                <Button onClick={reverseData}>{'reverse数据'}</Button>
                <Button onClick={pushData}>{'push数据'}</Button>
                <Button onClick={popData}>{'pop数据'}</Button>
                <Button onClick={removeSelected}>{'删除选中行'}</Button>
                <Button onClick={allSetAge}>{'随机设置Age'}</Button>
                <Button onClick={failAllSetAge}>{'错误随机设置Age'}</Button>
            </div>
            <div className="grid-wrapper">
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        //需要设置getRowId,才能得到更好的刷新效果
                        getRowId={getRowId}
                        rowSelection={'multiple'}
                        animateRows={true}
                        //默认的Enter键进入或者退出编辑状态,并不会去转移光标

                        //鼠标单击进入编辑状态,但是会丢失导航能力,并不好用
                        singleClickEdit={true}
                        //当表格丢失焦点的时候,自动停止编辑并保存,这点非常有用
                        stopEditingWhenCellsLoseFocus={true}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

内部数据源是由React来存放数据自身,开发者在外部也不进行数据存储,这种方式对AgGrid的支持度最高,性能相对外部数据源也好一点。要点如下:

  • 数组里面的每个元素都要赋新值,否则可能不刷新
  • setRowData,使用setRowData来设置数据,新的数组可以是相同引用的,也可以是不同引用的。但是依然按照rowId来diff,以及按照元素引用比较来确定元素是否变化。
  • forEachNode,forEachNodeAfterFilter,forEachNodeAfterFilterAndSort,forEachLeafNode等方法也是正常使用的。

6.3 事务提交

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    RowNodeTransaction,
} from 'ag-grid-community';
import getData from './getData2';

let newCount = 1;

function createNewRowData() {
    const newData = {
        make: 'Toyota ' + newCount,
        model: 'Celica ' + newCount,
        price: 35000 + newCount * 17,
        zombies: 'Headless',
        style: 'Little',
        clothes: 'Airbag',
    };
    newCount++;
    return newData;
}

function printResult(res: RowNodeTransaction) {
    console.log('---------------------------------------');
    if (res.add) {
        res.add.forEach(function (rowNode) {
            console.log('Added Row Node', rowNode);
        });
    }
    if (res.remove) {
        res.remove.forEach(function (rowNode) {
            console.log('Removed Row Node', rowNode);
        });
    }
    if (res.update) {
        res.update.forEach(function (rowNode) {
            console.log('Updated Row Node', rowNode);
        });
    }
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ flex: '1', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'make' },
        { field: 'model' },
        { field: 'price' },
        { field: 'zombies' },
        { field: 'style' },
        { field: 'clothes' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
        };
    }, []);

    const getRowData = useCallback(() => {
        const rowData: any[] = [];
        gridRef.current!.api.forEachNode(function (node) {
            rowData.push(node.data);
        });
        console.log('Row Data:');
        console.table(rowData);
    }, []);

    const clearData = useCallback(() => {
        gridRef.current!.api.setRowData([]);
    }, []);

    const addItems = useCallback((addIndex: number | undefined) => {
        const newItems = [
            createNewRowData(),
            createNewRowData(),
            createNewRowData(),
        ];
        //添加行
        const res = gridRef.current!.api.applyTransaction({
            add: newItems,
            //addIndex为undefined的时候,就是尾部插入的意思
            addIndex: addIndex,
        })!;
        printResult(res);
    }, []);

    const updateItems = useCallback(() => {
        // update the first 2 items
        const itemsToUpdate: any[] = [];
        gridRef.current!.api.forEachNodeAfterFilterAndSort(function (
            rowNode,
            index
        ) {
            // only do first 2
            if (index >= 2) {
                return;
            }
            const data = rowNode.data;
            data.price = Math.floor(Math.random() * 20000 + 20000);
            itemsToUpdate.push(data);
        });
        //对于数据update,优先使用rowId来判断行,没有rowId就用对象引用来判断行,都没有的话就看成没有相同行
        const res = gridRef.current!.api.applyTransaction({
            //更新行
            update: itemsToUpdate,
        })!;
        printResult(res);
    }, []);

    const onRemoveSelected = useCallback(() => {
        const selectedData = gridRef.current!.api.getSelectedRows();
        //删除行
        const res = gridRef.current!.api.applyTransaction({
            remove: selectedData,
        })!;
        printResult(res);
    }, []);

    return (
        <div style={containerStyle}>
            <div style={{ marginBottom: '4px' }}>
                <button onClick={() => addItems(undefined)}>Add Items</button>
                <button onClick={() => addItems(2)}>Add Items addIndex=2</button>
                <button onClick={updateItems}>Update Top 2</button>
                <button onClick={onRemoveSelected}>Remove Selected</button>
                <button onClick={getRowData}>Get Row Data</button>
                <button onClick={clearData}>Clear Data</button>
            </div>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    rowSelection={'multiple'}
                    animateRows={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

无论是setRowData的内部数据源方式,还是React的外部数据源方式,他们都离不开传入一个新数组,然后由AgGrid来做diff操作更新。在一些对性能特别敏感的场景中,diff操作可能会成为操作的瓶颈,因此,AgGrid提供了更为底层的Transaction的事务提交方式。

  • applyTransaction,add与addIndex,可以批量添加元素
  • applyTransaction,remove,可以批量删除元素
  • applyTransaction,update,可以批量更新元素

applyTransaction,传入的都是data数据,而不是RowNode数据,但是避免了diff操作,性能更好。返回值是RowNode数据。

  • RowNode.setData({xxxx}),对RowNode设置整行数据
  • RowNode.setDataValue(‘field’,value),对RowNode的元素一部分进行更新

RowNode上也可以用setData,和setDataValue来做细粒度的更新操作,但是这种操作不是立即更新的。在数据更新以后,sort,filter,group,pivot这些操作都不会自动刷新,需要手动刷新,这里就没有Demo了。

6.4 批量异步提交

AgGrid还提供了周期性异步刷新的工作方式,看这里,能大幅提高数据更新速度,付出代价是数据的展示有少量延迟。这里也不做Demo了。

6.5 脏检查

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef, ColGroupDef, GetRowIdParams, Grid, GridOptions, ICellRendererParams,
    ValueGetterParams
} from 'ag-grid-community';
import './style.css'
import getData from './getData';
import { Button } from 'antd';

const RowIndexRender: React.FC<ICellRendererParams> = (props) => {
    const rowIndex = props.rowIndex + 1;
    console.log('RowIndexRender');
    return <span>{rowIndex}</span>
}

const AllNameRender: React.FC<ICellRendererParams> = (props) => {
    console.log('allNameRender');
    const data = props.data;
    const allName = 'AllName:[' + data.firstName + " " + data.lastName + "]";
    return <span>{allName}</span>
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<React.CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            colId: 'checkbox',
            checkboxSelection: true,
            width: 100,
        },
        {
            //无脏检查
            headerName: '行号(错误示范)',
            colId: 'rowId1',
            cellRenderer: RowIndexRender,
        },
        {
            //valueGetter作脏检查
            headerName: '行号(valueGetter作脏检查,错误示范)',
            colId: 'rowId2',
            valueGetter: (params: ValueGetterParams) => {
                console.log('checkRender', params);
                return params.node?.rowIndex;
            },
            cellRenderer: RowIndexRender,
        },
        { field: 'firstName', sortable: true, editable: true },
        { field: 'lastName', sortable: true, editable: true },
        {
            //原数据没有allName字段,自动脏检查会失败
            colId: 'all1',
            headerName: '全名(错误示范)',
            field: 'allName',
            cellRenderer: AllNameRender,
        },
        {
            //用valueGetter来反馈ag-grid,脏检查使用的是valueGetter反馈的结果
            colId: 'all2',
            headerName: '全名(valueGetter作脏检查)',
            valueGetter: (params: ValueGetterParams) => {
                const data = params.data;
                return data.firstName + "_" + data.lastName;
            },
            cellRenderer: AllNameRender,
        },
        {
            //用valueGetter和equals来反馈ag-grid,脏检查使用的是equals反馈的结果
            colId: 'all3',
            headerName: '全名(equals作脏检查,错误示范)',
            valueGetter: (params: ValueGetterParams) => {
                const data = params.data;
                return data;
            },
            equals: (left: any, right: any) => {
                //left与right的类型来自于field,或者valueGetter的结果
                if (left.firstName != right.firstName) {
                    return false;
                }
                if (left.lastName != right.lastName) {
                    return false;
                }
                return true;
            },
            cellRenderer: AllNameRender,
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            //flex: 1,
            minWidth: 110,
            resizable: true,
        };
    }, []);

    const getAllData = useCallback(() => {
        let rowData: any[] = [];
        gridRef.current!.api.forEachNode((rowNode, index) => {
            rowData.push(rowNode.data);
        });
        return rowData;
    }, []);

    const refreshAllData = useCallback(() => {
        const allData = getData();
        gridRef.current!.api.setRowData(allData);
    }, []);

    const setLastNameInPlace = useCallback(() => {
        const rows = gridRef.current!.api.getSelectedRows();
        rows.forEach(single => {
            single.lastName = 'jj';
        })
        gridRef.current!.api.applyTransaction({
            update: rows,
        });
    }, []);

    const setLastNameNoInPlace = useCallback(() => {
        const rows = gridRef.current!.api.getSelectedRows();
        let updatedRows = rows.map(single => {
            return {
                ...single,
                lastName: 'jj',
            }
        })
        gridRef.current!.api.applyTransaction({
            update: updatedRows,
        });
    }, []);

    const refreshForce = useCallback(() => {
        gridRef.current!.api.refreshCells({
            force: true,
        });
    }, []);

    const refreshNotForce = useCallback(() => {
        gridRef.current!.api.refreshCells({
        });
    }, []);

    const getRowId = useCallback((params: GetRowIdParams) => {
        return params.data.id;
    }, []);
    return (
        <div style={containerStyle}>
            <div>
                <Button onClick={getAllData}>{'获取数据'}</Button>
                <Button onClick={refreshAllData}>{'重置全部数据'}</Button>
                <Button onClick={setLastNameInPlace}>{'原地选中行设置lastName(错误示范)'}</Button>
                <Button onClick={setLastNameNoInPlace}>{'非原地选中行设置lastName'}</Button>
                <Button onClick={refreshForce}>{'强制刷新'}</Button>
                <Button onClick={refreshNotForce}>{'非强制刷新'}</Button>
            </div>
            <div className="grid-wrapper">
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        onGridReady={refreshAllData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        getRowId={getRowId}
                        rowSelection={'multiple'}
                        animateRows={true}
                        singleClickEdit={true}
                        stopEditingWhenCellsLoseFocus={true}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid脏检查的流程:

  • 检查Row的每个引用有没有改变,引用不变则将Row看成没有变更,结束脏检查。引用变化了,继续下一步。
  • 调用Column的field,或者valueGetter来取值,然后跟原值进行比较。对比过不变以后,结束脏检查。引用变化了,继续下一步。
  • 调用Column的equals来判断脏检查,根据equals的返回值true/false来判断。

refreshCells的force区别

  • 无force的时候,忽略Row的引用是否不变,直接进行Column的field/valueGetter/equals来进行脏检查。也就是说,无force情况下,依然会进行脏检查,只是忽略了Row引用不变而已。
  • 有force的时候,忽略Row的引用是否不变,绕过Column的field/valueGetter/equals来判断脏检查,强制运行所有Component的刷新。

系统自带操作的表示:

  • 排序的时候,不触发任何脏检查,所以rowId2列即使有valueGetter作脏检查,依然会失败,更好的做法是倾听排序事件,手动做无force刷新。
  • cellEditing的时候,新数据原地修改,row引用不变,但会自动触发该行的无force刷新。从而实现其他cell的跟随变动。

常见的坑:

  • 数据原地更新,这会导致脏检查失败。
  • all3列的操作,valueGetter返回的是row引用本身,然后在equals进行判断。这样是不对的,因为cellEditing的时候,新数据是原地修改的。valueGetter返回的依然是旧row引用,导致脏检查判断为不需刷新,直接绕过了equals的判断。

7 选择

7.1 单选,多选与checkbox

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    RowNode,
} from 'ag-grid-community';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'athlete', minWidth: 150,
            //把该列加入一个选择框,方便进行勾选操作,特别是在多选操作的时候
            checkboxSelection: true,
            //头部是否有一个勾选全部的按钮
            headerCheckboxSelection: true,
            //当数据进入筛选模式的时候,勾选全部是指勾选所有数据,还是只勾选筛选后的数据
            //headerCheckboxSelectionFilteredOnly: true,
        },
        { field: 'age', maxWidth: 90 },
        { field: 'country', minWidth: 150 },
        { field: 'year', maxWidth: 90 },
        { field: 'date', minWidth: 150 },
        { field: 'sport', minWidth: 150 },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 100,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    const onSelectionChanged = useCallback(() => {
        //selection changed的事件
        //getSelectedRows来获取选中数据
        //getSelectedNodes来获取选中的Node
        var selectedRows = gridRef.current!.api.getSelectedRows();
        var selectedRowsString = '';
        var maxToShow = 5;
        selectedRows.forEach(function (selectedRow, index) {
            if (index >= maxToShow) {
                return;
            }
            if (index > 0) {
                selectedRowsString += ', ';
            }
            selectedRowsString += selectedRow.athlete;
        });
        if (selectedRows.length > maxToShow) {
            var othersCount = selectedRows.length - maxToShow;
            selectedRowsString +=
                ' and ' + othersCount + ' other' + (othersCount !== 1 ? 's' : '');
        }
        (document.querySelector(
            '#selectedRows'
        ) as any).innerHTML = selectedRowsString;
    }, []);

    //设置该行是否可以被选择
    const isRowSelectable = useCallback((rowNode: RowNode) => {
        return rowNode.data.age > 25;
    }, []);


    const selectAllAmerican = useCallback(() => {
        //通过node的setSelected来设置选中行
        gridRef.current!.api.forEachNode(function (node) {
            node.setSelected(node.data.country === 'United States');
        });
    }, []);

    return (
        <div style={containerStyle}>
            <div className="example-wrapper">
                <div className="example-header">
                    Selection:
                    <span id="selectedRows"></span>
                </div>
                <button onClick={selectAllAmerican}>Select All American</button>


                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        //rowSelection默认为单选,可以设置为多选
                        rowSelection={'multiple'}
                        onGridReady={onGridReady}
                        isRowSelectable={isRowSelectable}
                        onSelectionChanged={onSelectionChanged}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • checkboxSelection,把该列加入一个选择框,方便进行勾选操作,特别是在多选操作的时候
  • headerCheckboxSelection,头部是否有一个勾选全部的按钮
  • headerCheckboxSelectionFilteredOnly,当数据进入筛选模式的时候,勾选全部是指勾选所有数据,还是只勾选筛选后的数据
  • rowSelection,单选还是多选模式
  • isRowSelectable,设置该行是否可以被选择
  • onSelectionChanged,selection changed的事件

API

  • getSelectedRows,获取选中的数据
  • getSelectedNodes,获取选中的RowNode
  • selectAll,选中所有RowNode
  • deselectAll,清除所有的选中RowNode
  • rowNode.setSelected,设置RowNode的选中状态

选中显示是一个内部数据源的模式

8 筛选

8.1 筛选面板

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import moment from 'moment';

const GridExample = () => {
    const gridRef = useRef<any>();
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        {
            field: 'athlete',
            filter: 'agTextColumnFilter',
            //有filterParams的都不能自动刷新
            filterParams: {
                //弹出filter框的按钮有哪些,重置按钮,和应用按钮
                buttons: ['reset', 'apply'],
                //默认情况后,点击按钮并不会自动退出面板
            },
        },
        {
            field: 'age',
            maxWidth: 100,
            filter: 'agNumberColumnFilter',
            filterParams: {
                buttons: ['reset', 'apply'],
                //点击filter按钮后是否自动关闭filter框,且应用执行
                closeOnApply: true,
            },
        },
        {
            field: 'country',
            filter: 'agTextColumnFilter',
            filterParams: {
                //Clear按钮与Reset按钮不同的是,Clear按钮只是清除面板数据,并不会更新Filter数据
                buttons: ['clear', 'apply'],
            },
        },
        {
            field: 'year',
            filter: 'agNumberColumnFilter',
            filterParams: {
                //Cancel按钮就是取消了
                buttons: ['apply', 'cancel'],
                closeOnApply: true,
            },
            maxWidth: 100,
        },
        {
            headerName: '年',
            field: 'year',
            filter: 'agDateColumnFilter',
            filterParams: {
                comparator: function (filterLocalDateAtMidnight: Date, cellValue: string) {
                    let left = moment(filterLocalDateAtMidnight);
                    let right = moment(cellValue, 'YYYY');
                    let result = left.year() - right.year();
                    if (result < 0) {
                        return 1;
                    } else if (result > 0) {
                        return -1;
                    } else {
                        return 0;
                    }
                },
                //默认inRange不包含两个边界点,需要指定这个选项
                inRangeInclusive: true,
            },
            maxWidth: 100,
        },
        { field: 'sport' },
        { field: 'gold', filter: 'agNumberColumnFilter' },
        { field: 'silver', filter: 'agNumberColumnFilter' },
        { field: 'bronze', filter: 'agNumberColumnFilter' },
        { field: 'total', filter: 'agNumberColumnFilter' },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            minWidth: 150,
            filter: true,
            floatingFilter: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    return (
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '80%', width: '80%' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • filterParams的buttons,筛选面板的按钮
  • filterParams的closeOnApply,点击filter按钮后是否自动关闭filter框,且应用执行。默认是不会自动关闭的

8.2 集合筛选

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';

const GridExample = () => {
    const gridRef = useRef<any>();
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        {
            field: 'athlete',
            //设置按照集合来筛选
            filter: 'agSetColumnFilter',
            //默认会从数据里面拿,我们也可以手动指定
            filterParams: {
                buttons: ['reset', 'apply'],
                //默认是由AgGrid自己抓取所有可能数据,也可以自己提供set的数据
                //values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
            }
        },
        {
            field: 'age',
            maxWidth: 100,
            filter: 'agNumberColumnFilter',
        },
        {
            colId: 'country2',
            field: 'country',
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,允许分批多次勾选,操作的步骤就应该是,先剔除所有勾选,然后逐次勾选集合
                buttons: ['clear', 'apply'],
            },
        },
        {
            field: 'country',
            filter: 'agSetColumnFilter',
            filterParams: {
                //这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                //操作步骤应该是,直接输入text,然后勾选
                applyMiniFilterWhileTyping: true,
            },
        },
        {
            field: 'year',
            filter: 'agNumberColumnFilter',
            maxWidth: 100,
        },
        {
            field: 'sport',
            //多筛选器
            filter: 'agMultiColumnFilter',
            filterParams: {
                filters: [
                    {
                        filter: 'agTextColumnFilter',
                    },
                    {
                        filter: 'agSetColumnFilter',
                        filterParams: {
                            //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                            applyMiniFilterWhileTyping: true,
                        },
                    }
                ]
            },
        },
        { field: 'gold', filter: 'agNumberColumnFilter' },
        { field: 'silver', filter: 'agNumberColumnFilter' },
        { field: 'bronze', filter: 'agNumberColumnFilter' },
        { field: 'total', filter: 'agNumberColumnFilter' },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            minWidth: 150,
            filter: true,
            floatingFilter: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);


    const reset = useCallback(() => {
        gridRef.current.api.setFilterModel(null);
    }, []);

    return (
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '80%', width: '80%' }}>
                <Button onClick={reset}>重置筛选</Button>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                    //加入侧边栏的筛选
                    sideBar={'filters'}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

集合筛选是AgGrid中的下拉列表筛选方式,要点如下:

  • filter改为agSetColumnFilter,默认自动提取数据作为下拉列表。也可以自己手动指定values来作为下拉列表。
  • agSetColumnFitler。默认是多次勾选一个集合的,允许分批多次勾选,操作的步骤就应该是,先剔除所有勾选,然后逐次勾选集合。当指定applyMiniFilterWhileTyping的时候,只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合,操作步骤应该是,直接输入text,然后勾选

可以将多个筛选器组合在一起,agMultiColumnFilter,看代码吧

9 渲染

AgGrid对单元格的数据,从field取出来以后,有多个步骤,分别为:

  • valueGetter,默认为从field配置中拿数据,我们也能自定义从data中拉取数据的方式。value是排序,筛选,分组和支点的依据数据。
  • valueFormatter,默认为直接展示,我们也能对数据进行格式化,变形后输出。formatter以后的数据仅仅改变展示方式,不影响排序,筛选,分组和支点的数据依据。
  • valueRender,默认为转换为字符串显示,我们也能通过自定义组件来展示,这是最为强大的展示方式,但是性能要差一点,因为DOM更多。

9.1 valueGetter

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ValueGetterParams,
} from 'ag-grid-community';

function createRowData() {
    var rowData = [];
    for (var i = 0; i < 100; i++) {
        rowData.push({
            a: Math.floor(i % 4),
            b: Math.floor(i % 7),
        });
    }
    return rowData;
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(createRowData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            headerName: '#',
            maxWidth: 100,
            valueGetter: (params: ValueGetterParams) => {
                //使用node参数
                return params.node ? params.node.rowIndex : null;
            },
        },
        { field: 'a' },
        { field: 'b' },
        {
            headerName: 'A + B',
            colId: 'a&b',
            valueGetter: (params: ValueGetterParams) => {
                //多个参数
                return params.data.a + params.data.b;
            },
        },
        {
            headerName: 'A * 1000',
            minWidth: 95,
            valueGetter: (params: ValueGetterParams) => {
                //单个参数
                return params.data.a * 1000;
            },
        },
        {
            headerName: 'B * 137',
            minWidth: 90,
            valueGetter: (params: ValueGetterParams) => {
                return params.data.b * 137;
            },
        },
        {
            headerName: 'Random',
            minWidth: 90,
            valueGetter: () => {
                //随机数
                //在跳出viewPort重新进入以后会刷新
                return Math.floor(Math.random() * 1000);
            },
        },
        {
            headerName: 'Chain',
            valueGetter: (params: ValueGetterParams) => {
                //相当于a+b,然后*1000,没啥意义
                return params.getValue('a&b') * 1000;
            },
        },
        {
            headerName: 'Const',
            minWidth: 85,
            valueGetter: () => {
                //常量
                return 99999;
            },
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 75,
            sortable: true,
            // cellClass: 'number-cell'
        };
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

valueGetter就是从data中提取data value的方式。

9.2 valueFormatter

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ValueFormatterParams,
} from 'ag-grid-community';

function bracketsFormatter(params: ValueFormatterParams) {
    return '(' + params.value + ')';
}

function currencyFormatter(params: ValueFormatterParams) {
    return '£' + formatNumber(params.value);
}

//3位数字分割法
function formatNumber(number: number) {
    // this puts commas into the number eg 1000 goes to 1,000,
    // i pulled this from stack overflow, i have no idea how it works
    return Math.floor(number)
        .toString()
        .replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}

function createRowData() {
    var rowData = [];
    for (var i = 0; i < 100; i++) {
        rowData.push({
            a: Math.floor(((i + 2) * 173456) % 10000),
            b: Math.floor(((i + 7) * 373456) % 10000),
        });
    }
    return rowData;
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(createRowData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { headerName: 'A', field: 'a' },
        { headerName: 'B', field: 'b' },
        //valueFormatter与valueGetter的不同在于,
        //valueFormatter只对数据进行装饰处理,不改变数据的value,所以是用旧value来sort
        //valueGetter确实是更改了数据本身,所以可以用新的value来sort,
        { headerName: '£A', field: 'a', valueFormatter: currencyFormatter },
        { headerName: '£B', field: 'b', valueFormatter: currencyFormatter },
        { headerName: '(A)', field: 'a', valueFormatter: bracketsFormatter },
        { headerName: '(B)', field: 'b', valueFormatter: bracketsFormatter },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            cellClass: 'number-cell',
            resizable: true,
        };
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

valueFormatter是将data value转换到UI value的方式。

9.3 valueRender与headerComponent

'use strict';

import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    CellEditingStartedEvent,
    CellEditingStoppedEvent,
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ICellRendererParams,
    RowEditingStartedEvent,
    RowEditingStoppedEvent,
} from 'ag-grid-community';
import GenderRenderer from './genderRenderer';
import MoodRenderer from './moodRenderer';

const ValueRender: React.FC<ICellRendererParams> = (props) => {
    console.log('value', props.value);
    console.log('title', (props as any).title);
    console.log('ctx', props.context);
    console.log('data', props.data);
    console.log('rowIndex', props.rowIndex);
    return (<div style={{ background: 'blue', color: 'white' }}>{props.value}</div>);
}

const SortingHeader = memo((props: any) => {

    const [sortState, setSortState] = useState<string>();

    const onClick = useCallback(() => {
        props.progressSort();
    }, []);

    useEffect(() => {
        const listener = () => {
            if (props.column.isSortAscending()) {
                setSortState('ASC');
            } else if (props.column.isSortDescending()) {
                setSortState('DESC');
            } else {
                setSortState(undefined);
            }
        };

        props.column.addEventListener('sortChanged', listener);

        return () => props.column.removeEventListener('sortChanged', listener);;
    }, []);

    return (
        <span className="my-header" onClick={onClick}>
            <img style={{ width: '30px', height: '30px' }} src="https://d1yk6z6emsz7qy.cloudfront.net/static/images/loading.gif" className="my-spinner" />
            {props.displayName} {sortState}
        </span>
    );
});


const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>([
        { value: 14, type: 'age' },
        { value: 'female', type: 'gender' },
        { value: 'Happy', type: 'mood' },
        { value: 21, type: 'age' },
        { value: 'male', type: 'gender' },
        { value: 'Sad', type: 'mood' },
    ]);
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'value',
            //指定该列的行头render
            headerComponent: SortingHeader,
            //对该列使用一个指定的render
            cellRenderer: ValueRender,
            cellRendererParams: {
                title: "Fish",
            }
        },
        {
            headerName: 'Rendered Value',
            field: 'value',
            cellRendererSelector: (params: ICellRendererParams) => {
                //根据cell的值不同,采用不用的Render
                const moodDetails = {
                    component: MoodRenderer,
                };
                const genderDetails = {
                    component: GenderRenderer,
                    params: { values: ['Male', 'Female'] },
                };
                if (params.data.type === 'gender') return genderDetails;
                else if (params.data.type === 'mood') return moodDetails;
                else return undefined;
            },
        },
        { field: 'type' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
        };
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    context={{
                        'Name': "Cat",
                    }}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • valueRender是从data value或者(UI value)转换到Component。要注意valueRender中的renderParams,context的这些传递数据的方式
  • headerComponent是header Component。

10 编辑

valueGetter valueFormatter valueRender
data -> data value -> ui value -> component
<- <- <-
valueSetter valueParser valueEditor

编辑是渲染的逆过程,他们刚好是一个对称的关系

10.1 可编辑

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef, ColGroupDef, Grid, GridOptions,
    CellEditingStartedEvent,
    CellEditingStoppedEvent,
    RowEditingStartedEvent,
    RowEditingStoppedEvent,
} from 'ag-grid-community';
import './style.css'

function getData() {
    return [
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Bob',
            lastName: 'Harrison',
            gender: 'Male',
            address:
                '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Mary',
            lastName: 'Wilson',
            gender: 'Female',
            age: 11,
            address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
            mood: 'Sad',
            country: 'Ireland',
        },
        {
            firstName: 'Zahid',
            lastName: 'Khan',
            gender: 'Male',
            age: 12,
            address:
                '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
            mood: 'Happy',
            country: 'Ireland',
        },
        {
            firstName: 'Jerry',
            lastName: 'Mane',
            gender: 'Male',
            age: 12,
            address:
                '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
            mood: 'Happy',
            country: 'Ireland',
        },
    ];
}

function getPinnedTopData() {
    return [
        {
            firstName: '##',
            lastName: '##',
            gender: '##',
            address: '##',
            mood: '##',
            country: '##',
        },
    ];
}

function getPinnedBottomData() {
    return [
        {
            firstName: '##',
            lastName: '##',
            gender: '##',
            address: '##',
            mood: '##',
            country: '##',
        },
    ];
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'firstName' },
        { field: 'lastName' },
        { field: 'gender' },
        { field: 'age' },
        { field: 'mood' },
        { field: 'country' },
        { field: 'address', minWidth: 550 },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            //flex: 1,
            minWidth: 110,
            //全部单元格默认是可编辑的
            editable: true,
            resizable: true,
        };
    }, []);
    const pinnedTopRowData = useMemo<any[]>(() => {
        return getPinnedTopData();
    }, []);
    const pinnedBottomRowData = useMemo<any[]>(() => {
        return getPinnedBottomData();
    }, []);

    const onBtStopEditing = useCallback(() => {
        gridRef.current!.api.stopEditing();
    }, []);

    const onBtStartEditing = useCallback(
        (key?: string, char?: string, pinned?: string) => {
            gridRef.current!.api.setFocusedCell(0, 'lastName', pinned);
            gridRef.current!.api.startEditingCell({
                rowIndex: 0,
                colKey: 'lastName',
                // set to 'top', 'bottom' or undefined
                //哪些,顶部行,还是尾部行,还是中行
                rowPinned: pinned,
                //进入编辑状态的键盘Key,Delete是删除当前单元格后进入
                key: key,
                //进入单元格状态的默认数据
                charPress: char,
            });
        },
        []
    );

    const onBtNextCell = useCallback(() => {

        //移到前一个
        gridRef.current!.api.tabToNextCell();
    }, []);

    const onBtPreviousCell = useCallback(() => {
        //移到后一个
        gridRef.current!.api.tabToPreviousCell();
    }, []);

    const onBtWhich = useCallback(() => {
        //获取当前的编辑单元格
        var cellDefs = gridRef.current!.api.getEditingCells();
        if (cellDefs.length > 0) {
            var cellDef = cellDefs[0];
            console.log(
                'editing cell is: row = ' +
                cellDef.rowIndex +
                ', col = ' +
                cellDef.column.getId() +
                ', floating = ' +
                cellDef.rowPinned
            );
        } else {
            console.log('no cells are editing');
        }
    }, []);


    const onRowEditingStarted = useCallback((event: RowEditingStartedEvent) => {
        console.log('never called - not doing row editing');
    }, []);

    const onRowEditingStopped = useCallback((event: RowEditingStoppedEvent) => {
        console.log('never called - not doing row editing');
    }, []);

    const onCellEditingStarted = useCallback((event: CellEditingStartedEvent) => {
        console.log('cellEditingStarted');
    }, []);

    const onCellEditingStopped = useCallback((event: CellEditingStoppedEvent) => {
        console.log('cellEditingStopped');
    }, []);

    //AgGrid的分为选择和编辑状态
    //选择状态可以使用上下左右,Tab键,ShiftTab键移动光标,Enter键会进入编辑状态,输入Del或者字母数字会直接进入编辑状态并修改内容
    //编辑状态不能导航,可以输入数据,Enter键输入多一次会退出编辑状态
    //鼠标单击会进入选择状态
    //鼠标双击会进入编辑状态
    return (
        <div style={containerStyle}>
            <div className="example-wrapper">
                <div
                    style={{
                        marginBottom: '5px',
                        display: 'flex',
                        justifyContent: 'space-between',
                    }}
                >
                    <div>
                        <button onClick={() => onBtStartEditing(undefined)}>
                            edit (0)
                        </button>
                        <button onClick={() => onBtStartEditing('Delete')}>
                            edit (0, Delete)
                        </button>
                        <button onClick={() => onBtStartEditing(undefined, 'T')}>
                            edit (0, 'T')
                        </button>
                        <button
                            onClick={() => onBtStartEditing(undefined, undefined, 'top')}
                        >
                            edit (0, Top)
                        </button>
                        <button
                            onClick={() => onBtStartEditing(undefined, undefined, 'bottom')}
                        >
                            edit (0, Bottom)
                        </button>
                    </div>
                    <div>
                        <button onClick={onBtStopEditing}>stop ()</button>
                        <button onClick={onBtNextCell}>next ()</button>
                        <button onClick={onBtPreviousCell}>previous ()</button>
                    </div>
                    <div>
                        <button onClick={onBtWhich}>which ()</button>
                    </div>
                </div>
                <div className="grid-wrapper">
                    <div style={gridStyle} className="ag-theme-alpine">
                        <AgGridReact
                            ref={gridRef}
                            rowData={rowData}
                            columnDefs={columnDefs}
                            defaultColDef={defaultColDef}
                            //设置的是数组,多行
                            pinnedTopRowData={pinnedTopRowData}
                            pinnedBottomRowData={pinnedBottomRowData}
                            //默认的Enter键进入或者退出编辑状态,并不会去转移光标

                            //输入Enter键的时候转移到下一行的光标
                            enterMovesDown={true}
                            //输入Enter键的时候转移到下一行的光标,即使当前单元格处于编辑状态
                            enterMovesDownAfterEdit={false}
                            //鼠标单击进入编辑状态,但是会丢失导航能力,并不好用
                            //singleClickEdit={true}
                            //当表格丢失焦点的时候,自动停止编辑并保存,这点非常有用
                            stopEditingWhenCellsLoseFocus={true}

                            //各种事件
                            onRowEditingStarted={onRowEditingStarted}
                            onRowEditingStopped={onRowEditingStopped}
                            onCellEditingStarted={onCellEditingStarted}
                            onCellEditingStopped={onCellEditingStopped}
                        ></AgGridReact>
                    </div>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid的可编辑表格需要理解以下几点:

  • 编辑状态和展示状态,有展示状态,和编辑状态两种。展示状态只能展示数据和切换焦点(方向键和Tab键),编辑状态可以编辑,但不能切换焦点。当单元格处于展示状态的时候,输入Enter键,或者可见字符,或者退格字符,能转换到编辑状态。当单元格处于编辑状态的时候,再次输入Enter键,能切换到展示状态。
  • 切换焦点,在展示状态,输入Tab键或者方向键,都能切换焦点。在编辑状态,方向键无法切换焦点,只能用Tab键切换焦点。

其实这种模式与Excel的默认设计也是一样的,可以仔细对比一下

要点如下:

  • editable,指定该列是可以编辑的

API

  • setFocusedCell,设置当前的焦点在哪里,但是这不代表进入了编辑状态
  • getFocusedCell,获取当前的焦点在哪里
  • startEditingCell,设置单元格进入编辑状态,进入编辑状态的按键也是可以配置的。
  • stopEditingCell,设置当前编辑状态的单元格退出编辑状态。
  • getEditingCells,获取当前处于编辑状态的单元格
  • tabToNextCell,切换到下一个焦点,只切换,不进入编辑状态
  • tabToPreviousCell,切换到上一个焦点,只切换,不进入编辑状态

事件

  • onRowEditingStarted,行编辑的开始
  • onRowEditingStopped,行编辑的结束
  • onCellEditingStarted,单元格编辑的开始
  • onCellEditingStopped,单元格编辑的结束

其他的一些API

  • enterMovesDown,输入Enter键的时候转移到下一行的光标
  • enterMovesDownAfterEdit,输入Enter键的时候转移到下一行的光标,即使当前单元格处于编辑状态
  • singleClickEdit,鼠标单击进入编辑状态,但是会丢失导航能力,并不好用
  • stopEditingWhenCellsLoseFocus,当表格丢失焦点的时候,自动停止编辑并保存,这点非常有用

10.2 valueParser与valueSetter

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    CellValueChangedEvent,
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ValueGetterParams,
    ValueParserParams,
    ValueSetterParams,
} from 'ag-grid-community';

function getData() {
    var rowData = [];
    var firstNames = ['Niall', 'John', 'Rob', 'Alberto', 'Bas', 'Dimple', 'Sean'];
    var lastNames = [
        'Pink',
        'Black',
        'White',
        'Brown',
        'Smith',
        'Smooth',
        'Anderson',
    ];

    for (var i = 0; i < 100; i++) {
        rowData.push({
            a: Math.floor(Math.random() * 100),
            b: Math.floor(Math.random() * 100),
            firstName: firstNames[i % firstNames.length],
            lastName: lastNames[i % lastNames.length],
        });
    }

    return rowData;
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            headerName: 'Name',
            //getter从data中拉取
            valueGetter: function (params: ValueGetterParams) {
                return params.data.firstName + ' ' + params.data.lastName;
            },
            //从输入数据中拉取data,返回true代表有所改变,返回false代表没有改变
            //valueSetter解决的是数据如何保存原始数据的问题,需要保存到数据的哪些字段中
            //valueGetter是valueSetter的逆过程,从原始数据中提取数据
            valueSetter: function (params: ValueSetterParams) {
                var fullName = params.newValue;
                var nameSplit = fullName.split(' ');
                var newFirstName = nameSplit[0];
                var newLastName = nameSplit[1];
                var data = params.data;
                if (data.firstName !== newFirstName || data.lastName !== newLastName) {
                    data.firstName = newFirstName;
                    data.lastName = newLastName;
                    // return true to tell grid that the value has changed, so it knows
                    // to update the cell
                    return true;
                } else {
                    // return false, the grid doesn't need to update
                    return false;
                }
            },
        },

        {
            headerName: 'FirstName',
            field: 'firstName',
        },

        {
            headerName: 'LastName',
            field: 'lastName',
        },
        {
            headerName: 'Age',
            field: 'age',
            //默认的输入数据为string,需要用parser转换为number
            //valueParser主要解决的是数据如何获取的问题,从editorValue返回到value
            //valueFormatter是valueParser的过程,它描述的是从value到showValue的转换
            valueParser: function (params: ValueParserParams) {
                return Number.parseInt(params.newValue);
            }
        },
        {
            headerName: 'B',
            valueGetter: function (params: ValueGetterParams) {
                return params.data.b;
            },
            valueSetter: function (params: ValueSetterParams) {
                var newValInt = parseInt(params.newValue);
                var valueChanged = params.data.b !== newValInt;
                if (valueChanged) {
                    params.data.b = newValInt;
                }
                return valueChanged;
            },
        },
        {
            headerName: 'C.X',
            valueGetter: function (params: ValueGetterParams) {
                if (params.data.c) {
                    return params.data.c.x;
                } else {
                    return undefined;
                }
            },
            valueSetter: function (params: ValueSetterParams) {
                if (!params.data.c) {
                    params.data.c = {};
                }
                params.data.c.x = params.newValue;
                return true;
            },
        },
        {
            headerName: 'C.Y',
            valueGetter: function (params: ValueGetterParams) {
                if (params.data.c) {
                    return params.data.c.y;
                } else {
                    return undefined;
                }
            },
            valueSetter: function (params: ValueSetterParams) {
                if (!params.data.c) {
                    params.data.c = {};
                }
                params.data.c.y = params.newValue;
                return true;
            },
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            resizable: true,
            editable: true,
        };
    }, []);

    const onCellValueChanged = useCallback((event: CellValueChangedEvent) => {
        console.log('Data after change is', event.data);
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onCellValueChanged={onCellValueChanged}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • valueParser,是从UIComponent的ui value转换到data value
  • valueSetter,是从data value转换到data

API

  • onCellValueChanged,是经历了parser与setter以后的事件触发,而不仅仅是stopEditing

10.3 valueEditor

import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { ICellEditorParams } from 'ag-grid-community';

import './style.css';
const KEY_BACKSPACE = 'Backspace';
const KEY_DELETE = 'Delete';
const KEY_ENTER = 'Enter';
const KEY_TAB = 'Tab';

export default forwardRef((props: ICellEditorParams, ref) => {
    const createInitialState = () => {
        let startValue;

        if (props.key === KEY_BACKSPACE || props.key === KEY_DELETE) {
            // if backspace or delete pressed, we clear the cell
            startValue = '';
        } else if (props.charPress) {
            // if a letter was pressed, we start with the letter
            startValue = props.charPress;
        } else {
            // otherwise we start with the current value
            startValue = props.value;
        }

        return {
            value: startValue,
        };
    };

    const initialState = createInitialState();
    const [value, setValue] = useState(initialState.value);
    const refInput = useRef<HTMLInputElement>(null);

    // focus on the input
    useEffect(() => {
        // get ref from React component
        window.setTimeout(() => {
            const eInput = refInput.current!;
            eInput.focus();
            eInput.select();
        }, 0);
    }, []);

    /* Component Editor Lifecycle methods */
    useImperativeHandle(ref, () => {
        return {
            // the final value to send to the grid, on completion of editing
            getValue() {
                return value;
            },

            // Gets called once before editing starts, to give editor a chance to
            // cancel the editing before it even starts.
            //默认情况下,输入字母和数字都会进入到编辑状态
            //isCancelBeforeStart可以指定只有输入数字的时候才进入到编辑状态
            isCancelBeforeStart() {
                return false;
            },

            // Gets called once when editing is finished (eg if Enter is pressed).
            // If you return true, then the result of the edit will be ignored.
            //当数据大于某个值的时候,取消返回数据
            isCancelAfterEnd() {
                // will reject the number if it greater than 1,000,000
                // not very practical, but demonstrates the method.
                return false;
            },
        };
    });

    return (
        <input
            ref={refInput}
            className={'my-input'}
            value={value}
            onChange={(event: any) => setValue(event.target.value)}
        />
    );
});

我们先自定义一个NumberEditor,注意如何初始化数据,以及使用useImperativeHandle来返回数据

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    CellEditingStartedEvent,
    CellEditingStoppedEvent,
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    ICellEditorParams,
    RowEditingStartedEvent,
    RowEditingStoppedEvent,
} from 'ag-grid-community';
import getData from './data';

import numberEditor from './numberEditor';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'name',
            editable: true,
            cellEditor: numberEditor,
            //Enter事件默认会被Ag-Grid处理掉,不会冒泡到触发位置
            //Enter事件默认会被Ag-Grid处理为进入和退出编辑状态的方法
            //但是当cellEditor本身也需要去处理Enter事件的时候,就会出现问题
            suppressKeyboardEvent: (params) => {
                if (!params.editing) return false
                if (params.event.key == 'Enter') {
                    return true;
                } else {
                    return false;
                }
            }
        },
        {
            field: 'age',
            editable: true,
            cellEditor: numberEditor,
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
        };
    }, []);

    const onRowEditingStarted = useCallback((event: RowEditingStartedEvent) => {
        console.log('never called - not doing row editing');
    }, []);

    const onRowEditingStopped = useCallback((event: RowEditingStoppedEvent) => {
        console.log('never called - not doing row editing');
    }, []);

    const onCellEditingStarted = useCallback((event: CellEditingStartedEvent) => {
        console.log('cellEditingStarted');
    }, []);

    const onCellEditingStopped = useCallback((event: CellEditingStoppedEvent) => {
        console.log('cellEditingStopped');
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onRowEditingStarted={onRowEditingStarted}
                    onRowEditingStopped={onRowEditingStopped}
                    onCellEditingStarted={onCellEditingStarted}
                    onCellEditingStopped={onCellEditingStopped}
                    singleClickEdit={true}
                //stopEditingWhenCellsLoseFocus={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

然后我们使用cellEditor来注入自己的Editor,注意,如果组件中需要处理Enter事件,那么就需要用suppressKeyboardEvent来避免Enter事件被AgGrid处理掉了。

10.4 enter切换焦点

代码看这里

使用Enter键切换焦点的方法,就不贴代码了。

10.5 外部数据源更新

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    CellEditRequestEvent,
    ColDef,
    ColGroupDef,
    GetRowIdFunc,
    GetRowIdParams,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

let rowImmutableStore: any[];

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'athlete', minWidth: 160 },
        { field: 'age' },
        { field: 'country', minWidth: 140 },
        { field: 'year' },
        { field: 'date', minWidth: 140 },
        { field: 'sport', minWidth: 160 },
        { field: 'gold' },
        { field: 'silver' },
        { field: 'bronze' },
        { field: 'total' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 100,
            editable: true,
        };
    }, []);
    const getRowId = useCallback((params: GetRowIdParams) => params.data.id, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => {
                data.forEach((item, index) => (item.id = index));
                rowImmutableStore = data;
                setRowData(rowImmutableStore);
            });
    }, []);

    const onCellEditRequest = useCallback(
        (event: CellEditRequestEvent) => {
            //数据本身
            const data = event.data;
            //字段名
            const field = event.colDef.field;
            //字段值
            const newValue = event.newValue;
            const newItem = { ...data };
            newItem[field!] = event.newValue;
            console.log('onCellEditRequest, updating ' + field + ' to ' + newValue);
            rowImmutableStore = rowImmutableStore.map((oldItem) =>
                oldItem.id == newItem.id ? newItem : oldItem
            );
            setRowData(rowImmutableStore);
        },
        [rowImmutableStore]
    );

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    getRowId={getRowId}
                    //默认editor是原地修改数据的,打开readOnlyEdit以后,数据不能原地修改,而是通过发送请求来修改
                    readOnlyEdit={true}
                    onGridReady={onGridReady}
                    //cellEditRequest,数据更改后事件
                    onCellEditRequest={onCellEditRequest}
                ></AgGridReact>
            </div>
        </div>
    );
};

默认情况下,AgGrid会将新数据写入到data的字段本身,也就是原地更新。当打开readOnlyEdit开关以后,AgGrid就不会去直接修改数据,然后通过事件回调来通知数据被更改了,onCellEditRequest就是数据被更新的通知。

11 分组

11.1 分组显示


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        //对country列进行分组rowGroup为true,分组后该列不消失hide为false
        { field: 'country', rowGroup: true, hide: false },
        { field: 'athlete', rowGroup: true, hide: false },
        { field: 'year' },
        //分组后的合并函数,聚合函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        { field: 'sport' },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,
            cellRendererParams: {
                //默认在分组列显示数字
                suppressCount: false,
            }
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    return (
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //生成的分组列
                    autoGroupColumnDef={autoGroupColumnDef}
                    //分组的显示方式,将所有分组合并到一列来显示,这个方法好看一点
                    groupDisplayType={'singleColumn'}
                    //分组后,分组列数据是否保留,hide为false的话不需要设置这一个
                    //showOpenedGroup={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • 使用rowGroup与rowIndex,来设置默认分组
  • 使用aggFunc,来设置分组后的聚合函数列
  • autoGroupColumnDef,分组后会自动新增一个分组列,显示当前的分组列信息
  • groupDisplayType,分组的显示方式有多种,将多个分组合并到一列(singleColumn),以及每个分组一个列(multiplyColumn)

11.2 顶部分组工具栏


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        {
            field: 'country',
            //打开了enableRowGroup才能放入groupPanel中
            enableRowGroup: true,
        },
        {
            field: 'athlete',
            enableRowGroup: true,
        },
        {
            field: 'year',
            enableRowGroup: true,
        },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        {
            field: 'sport',
        },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
            sortable: true,
            unSortIcon: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);


    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,

            //似乎没啥用
            // enables filtering on the group column
            //filter: true,
        };
    }, []);

    return (
        <div style={{ display: 'flex', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //showOpenedGroup={true}
                    onGridReady={onGridReady}
                    autoGroupColumnDef={autoGroupColumnDef}
                    //打开自定义group面板,并保留列
                    //顶部的分组列,可以通过拖动列的方式自定义分组
                    rowGroupPanelShow={'always'}
                    suppressDragLeaveHidesColumns={true}
                    suppressMakeColumnVisibleAfterUnGroup={true}
                    //子合计,没啥用,别用
                    //groupIncludeFooter={true}
                    //主合计
                    groupIncludeTotalFooter={true}
                    animateRows={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • rowGroupPanelShow,设置always的方式,能打开顶部分组工具栏,用户可以自定义分组。注意,只有那些enableRowGroup的列才能允许自定义分组。
  • suppressMakeColumnVisibleAfterUnGroup,默认情况下,对该列进行自定义分组以后,列就会消失了。这个选项可以保证列一直显示。
  • showOpenedGroup,默认情况下,分组以后自动生成一列分组列,分组列只会在分组行进行数据显示。showOpendGroup打开以后,在非分组行也会显示出分组信息。但是,在分组行只能显示最后一个分组的信息,不能显示多个分组的信息。
  • groupIncludeTotalFooter,主合计
  • groupIncludeFooter,子合计

11.3 右侧工具栏


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        {
            //打开了enableRowGroup才能放入groupPanel中
            field: 'country',
            enableRowGroup: true,

            //只展示聚合数据,不展示明细数据
            //pivot: true,
            //筛选器的提供
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        {
            field: 'athlete', enableRowGroup: true,

            //只展示聚合数据,不展示明细数据
            //pivot: true,
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        {
            field: 'year', enableRowGroup: true,
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        {
            field: 'sport',
            filter: 'agTextColumnFilter',
        },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
            sortable: true,
            unSortIcon: true,
            floatingFilter: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);


    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,
            //分组列也能进行筛选操作
            filter: true,
            // supplies 'country' values to the filter 
            filterValueGetter: (params: any) => params.data.country,
        };
    }, []);

    return (
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '80%', width: '80%' }}>
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //分组后,分组列数据是否保留,hide为false的话不需要设置这一个
                    //showOpenedGroup={true}
                    onGridReady={onGridReady}
                    autoGroupColumnDef={autoGroupColumnDef}
                    //打开sideBar,可以自定义聚合方法,展示的列
                    sideBar={true}
                    //打开自定义group面板,并保留列
                    rowGroupPanelShow={'always'}
                    suppressDragLeaveHidesColumns={true}
                    suppressMakeColumnVisibleAfterUnGroup={true}
                    //子合计,没啥用,别用
                    //groupIncludeFooter={true}
                    //主合计
                    groupIncludeTotalFooter={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • sideBar,右侧工具栏,这个工具栏可以配置的,看这里

11.4 底部状态栏


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState([
        {
            //打开了enableRowGroup才能放入groupPanel中
            field: 'country',
            enableRowGroup: true,

            //只展示聚合数据,不展示明细数据
            //pivot: true,
            //筛选器的提供
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        {
            field: 'athlete', enableRowGroup: true,

            //只展示聚合数据,不展示明细数据
            //pivot: true,
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        {
            field: 'year', enableRowGroup: true,
            filter: 'agSetColumnFilter',
            filterParams: {
                //默认是多次勾选一个集合的,这个是只筛选UI看到的集合,这个也好用,缺点是只能单选一个UI集合
                applyMiniFilterWhileTyping: true,
            },
        },
        //分组后的合并函数
        { field: 'gold', aggFunc: 'sum' },
        { field: 'silver', aggFunc: 'sum' },
        { field: 'bronze', aggFunc: 'sum' },
        { field: 'total', aggFunc: 'sum' },
        { field: 'age' },
        { field: 'date' },
        {
            field: 'sport',
            filter: 'agTextColumnFilter',
        },
    ]);
    const defaultColDef = useMemo(() => {
        return {
            flex: 1,
            minWidth: 150,
            resizable: true,
            sortable: true,
            unSortIcon: true,
            floatingFilter: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);


    const statusBar = {
        statusPanels: [
            {
                statusPanel: 'agTotalAndFilteredRowCountComponent',
                align: 'left',
            },
            { statusPanel: 'agAggregationComponent' },
        ]
    };

    const autoGroupColumnDef = useMemo(() => {
        return {
            headerName: 'MyGroup',
            minWidth: 300,

            //似乎没啥用
            // enables filtering on the group column
            //filter: true,
        };
    }, []);

    return (
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100vh' }}>
            <div className="ag-theme-alpine" style={{ height: '80%', width: '80%' }}>
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    //分组后,分组列数据是否保留,hide为false的话不需要设置这一个
                    //showOpenedGroup={true}
                    onGridReady={onGridReady}
                    autoGroupColumnDef={autoGroupColumnDef}
                    //打开sideBar,可以自定义聚合方法,展示的列
                    sideBar={true}
                    //打开自定义group面板,并保留列
                    rowGroupPanelShow={'always'}
                    suppressDragLeaveHidesColumns={true}
                    suppressMakeColumnVisibleAfterUnGroup={true}
                    //子合计,没啥用,别用
                    //groupIncludeFooter={true}
                    //主合计
                    groupIncludeTotalFooter={true}
                    //statusBar只能显示总数,以及圈起来部分的数据
                    statusBar={statusBar}
                    enableRangeSelection={true}
                    //打开图表
                    enableCharts={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • statusBar,加入statusBar配置打开statusBar
  • enableRangeSelection,加入范围选择以后,自动进行数据聚合操作,显示在statusBar
  • enableCharts,打开自定义生成图表的功能

11.5 聚合函数

11.5.1 自定义聚合函数

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    IAggFunc,
    IAggFuncParams,
    SideBarDef,
} from 'ag-grid-community';

function oneTwoThreeFunc(params: IAggFuncParams) {
    // this is just an example, rather than working out an aggregation,
    // we just return 123 each time, so you can see in the example 22 is the result
    return 123;
}

function xyzFunc(params: IAggFuncParams) {
    // this is just an example, rather than working out an aggregation,
    // we just return 22 each time, so you can see in the example 22 is the result
    return 'xyz';
}

// sum function has no advantage over the built in sum function.
// it's shown here as it's the simplest form of aggregation and
// showing it can be good as a starting point for understanding
// hwo the aggregation functions work.
function sumFunction(params: IAggFuncParams) {
    let result = 0;
    params.values.forEach((value) => {
        if (typeof value === 'number') {
            result += value;
        }
    });
    return result;
}

// min and max agg function. the leaf nodes are just numbers, like any other
// value. however the function returns an object with min and max, thus the group
// nodes all have these objects.
function minAndMaxAggFunction(params: IAggFuncParams) {
    // this is what we will return
    const result = {
        min: null,
        max: null,
        //聚合函数,可以返回一个类型,但是要定义toString方法
        // because we are returning back an object, this would get rendered as [Object,Object]
        // in the browser. we could get around this by providing a valueFormatter, OR we could
        // get around it in a customer cellRenderer, however this is a trick that will also work
        // with clipboard.
        toString: function () {
            return '(' + this.min + '..' + this.max + ')';
        },
    };
    // update the result based on each value
    params.values.forEach((value) => {
        const groupNode =
            value !== null && value !== undefined && typeof value === 'object';
        const minValue = groupNode ? value.min : value;
        const maxValue = groupNode ? value.max : value;
        // value is a number, not a 'result' object,
        // so this must be the first group
        result.min = min(minValue, result.min);
        result.max = max(maxValue, result.max);
    });
    return result;
}

// the average function is tricky as the multiple levels require weighted averages
// for the non-leaf node aggregations.
function avgAggFunction(params: IAggFuncParams) {
    // the average will be the sum / count
    let sum = 0;
    let count = 0;
    params.values.forEach((value) => {
        const groupNode =
            value !== null && value !== undefined && typeof value === 'object';
        if (groupNode) {
            //在聚合函数中,判断是否为分组的方法
            // we are aggregating groups, so we take the
            // aggregated values to calculated a weighted average
            sum += value.avg * value.count;
            count += value.count;
        } else {
            // skip values that are not numbers (ie skip empty values)
            if (typeof value === 'number') {
                sum += value;
                count++;
            }
        }
    });
    // avoid divide by zero error
    let avg = null;
    if (count !== 0) {
        avg = sum / count;
    }
    // the result will be an object. when this cell is rendered, only the avg is shown.
    // however when this cell is part of another aggregation, the count is also needed
    // to create a weighted average for the next level.
    const result = {
        count: count,
        avg: avg,
        // the grid by default uses toString to render values for an object, so this
        // is a trick to get the default cellRenderer to display the avg value
        toString: function () {
            return `${this.avg}`;
        },
    };
    return result;
}

function roundedAvgAggFunction(params: IAggFuncParams) {
    const result = avgAggFunction(params);
    if (result.avg) {
        result.avg = Math.round(result.avg * 100) / 100;
    }
    return result;
}

// similar to Math.min() except handles missing values, if any value is missing, then
// it returns the other value, or 'null' if both are missing.
function min(a: any, b: any) {
    const aMissing = typeof a !== 'number';
    const bMissing = typeof b !== 'number';
    if (aMissing && bMissing) {
        return null;
    } else if (aMissing) {
        return b;
    } else if (bMissing) {
        return a;
    } else if (a > b) {
        return b;
    } else {
        return a;
    }
}

// similar to Math.max() except handles missing values, if any value is missing, then
// it returns the other value, or 'null' if both are missing.
function max(a: any, b: any) {
    const aMissing = typeof a !== 'number';
    const bMissing = typeof b !== 'number';
    if (aMissing && bMissing) {
        return null;
    } else if (aMissing) {
        return b;
    } else if (bMissing) {
        return a;
    } else if (a < b) {
        return b;
    } else {
        return a;
    }
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        { field: 'country', rowGroup: true, hide: true },
        { field: 'year', rowGroup: true, hide: true },
        // this column uses min and max func
        { headerName: 'minMax(age)', field: 'age', aggFunc: minAndMaxAggFunction },
        // here we use an average func and specify the function directly
        {
            headerName: 'avg(age)',
            field: 'age',
            //指定默认的聚合函数
            aggFunc: avgAggFunction,
            //是否允许用户在sideBar自定义执行聚合函数
            enableValue: true,
            minWidth: 200,
        },
        {
            headerName: 'roundedAvg(age)',
            field: 'age',
            aggFunc: roundedAvgAggFunction,
            enableValue: true,
            minWidth: 200,
        },
        // here we use a custom sum function that was registered with the grid,
        // which overrides the built in sum function
        {
            headerName: 'sum(gold)',
            field: 'gold',
            aggFunc: 'sum',
            enableValue: true,
        },
        // and these two use the built in sum func
        {
            headerName: 'abc(silver)',
            field: 'silver',
            aggFunc: '123',
            enableValue: true,
        },
        {
            headerName: 'xyz(bronze)',
            field: 'bronze',
            aggFunc: 'xyz',
            enableValue: true,
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 150,
            filter: true,
            sortable: true,
            resizable: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo<ColDef>(() => {
        return {
            headerName: 'Athlete',
            field: 'athlete',
            minWidth: 250,
        };
    }, []);
    //定义自己的分组函数,分组函数只能对单列进行操作
    const aggFuncs = useMemo<{
        [key: string]: IAggFunc;
    }>(() => {
        return {
            // this overrides the grids built in sum function
            sum: sumFunction,
            // this adds another function called 'abc'
            '123': oneTwoThreeFunc,
            // and again xyz
            xyz: xyzFunc,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));

        // we could also register functions after the grid is created,
        // however because we are providing the columns in the grid options,
        // it will be to late (eg remove 'xyz' from aggFuncs, and you will
        // see the grid complains).
        params.api.addAggFunc('xyz', xyzFunc);
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    autoGroupColumnDef={autoGroupColumnDef}
                    enableRangeSelection={true}
                    //默认在HeaderName中包含了聚合函数的名称,可以指定用这个来显示原名称
                    suppressAggFuncInHeader={true}
                    //定义自己的聚合函数,用户可以在sideBar中选择
                    aggFuncs={aggFuncs}
                    sideBar={true}
                    onGridReady={onGridReady}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • aggFuncs,自定义一个聚合函数的集合,定义以后,在column中也可以直接引用这些聚合函数。聚合函数的结果既可以是一个string,也可以是一个含有toString的object类型。
  • suppressAggFuncInHeader,默认聚合函数的列,在分组以后,列名称会发生变化,设置为true以后可以避免修改列名称的问题。

11.5.2 多列聚合函数

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    IAggFuncParams,
    ValueFormatterParams,
    ValueGetterParams,
} from 'ag-grid-community';

const numberFormatter: (params: ValueFormatterParams) => string = (
    params: ValueFormatterParams
) => {
    if (!params.value || params.value === 0) return '0';
    return '' + Math.round(params.value * 100) / 100;
};

function ratioValueGetter(params: ValueGetterParams) {
    if (!(params.node && params.node.group)) {
        // no need to handle group levels - calculated in the 'ratioAggFunc'
        return createValueObject(params.data.gold, params.data.silver);
    }
}

function ratioAggFunc(params: IAggFuncParams) {
    let goldSum = 0;
    let silverSum = 0;
    params.values.forEach((value) => {
        if (value && value.gold) {
            goldSum += value.gold;
        }
        if (value && value.silver) {
            silverSum += value.silver;
        }
    });
    return createValueObject(goldSum, silverSum);
}

function createValueObject(gold: number, silver: number) {
    return {
        gold: gold,
        silver: silver,
        //输出的时候才做除法操作
        toString: () => `${gold && silver ? gold / silver : 0}`,
    };
}

function ratioFormatter(params: ValueFormatterParams) {
    if (!params.value || params.value === 0) return '';
    return '' + Math.round(params.value * 100) / 100;
}

const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'country',
            rowGroup: true,
            hide: true,
            //该列不允许在sideBar中进行手工调整
            suppressColumnsToolPanel: true,
        },
        {
            field: 'sport',
            rowGroup: true,
            hide: true,
            suppressColumnsToolPanel: true,
        },
        {
            field: 'year',
            pivot: true,
            hide: true,
            suppressColumnsToolPanel: true,
        },
        { field: 'gold', aggFunc: 'sum', valueFormatter: numberFormatter },
        { field: 'silver', aggFunc: 'sum', valueFormatter: numberFormatter },
        {
            headerName: 'Ratio',
            colId: 'goldSilverRatio',

            //多列聚合,首先要做的是,将多列数据转换为object数据
            valueGetter: ratioValueGetter,
            //然后,指定我们的聚合函数,注意聚合函数的输入和输出都是object
            aggFunc: ratioAggFunc,
            //最后将结果展示出来,将object,转换为string
            valueFormatter: ratioFormatter,
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            minWidth: 150,
            sortable: true,
            filter: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo<ColDef>(() => {
        return {
            minWidth: 220,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => setRowData(data));
    }, []);

    return (
        <div style={containerStyle}>
            <div style={gridStyle} className="ag-theme-alpine">
                <AgGridReact
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    autoGroupColumnDef={autoGroupColumnDef}
                    suppressAggFuncInHeader={true}
                    onGridReady={onGridReady}
                    sideBar={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

大部分的聚合函数都是关于单列里面的数据,例如是sum,max,min,avg。但是少量情况下,聚合函数的计算是基于多个列的。例如计算订单的平均单价,它需要计算订单总金额的sum,以及订单总数量的sum,然后两者相除。

多列聚合函数,AgGrid使用一个较为取巧的办法来实现,没有提供额外的机制。

  • valueGetter,首先要做的是,将多列数据转换为object数据
  • aggFunc,然后,我们的聚合函数,注意聚合函数的输入和输出都是object。计算多列的sum,然后放入到一个object中。
  • valueFormatter,最后,将一个object的结果,在toString的时候,做除法操作。注意,除法不是在aggFunc里面做,而是在valueFormatter里面做。

11.6 树形数据

11.6.1 只读树形数据

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    GetDataPath,
    Grid,
    GridOptions,
} from 'ag-grid-community';
import { getData } from './data';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        //显示数据只有jobTitle与employmentType两个列
        { field: 'jobTitle' },
        { field: 'employmentType' },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
        };
    }, []);

    //自动生成的分组列,每个元素为dataPath里面的数据
    const autoGroupColumnDef = useMemo<ColDef>(() => {
        return {
            headerName: 'Organisation Hierarchy',
            minWidth: 300,
            cellRendererParams: {
                suppressCount: true,
            },
        };
    }, []);

    //返回数据的树形地址
    const getDataPath = useCallback((data: any) => {
        return data.orgHierarchy;
    }, []);

    const onFilterTextBoxChanged = useCallback(() => {
        gridRef.current!.api.setQuickFilter(
            (document.getElementById('filter-text-box') as any).value
        );
    }, []);

    return (
        <div style={containerStyle}>
            <div className="example-wrapper">
                <div style={{ marginBottom: '5px' }}>
                    <input
                        type="text"
                        id="filter-text-box"
                        placeholder="Filter..."
                        onInput={onFilterTextBoxChanged}
                    />
                </div>

                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        //定义自动生成的列
                        autoGroupColumnDef={autoGroupColumnDef}
                        //打开treeData,getDataPath,getDataPath是返回每个row树形数据的地址
                        treeData={true}
                        animateRows={true}
                        //默认展开所有group
                        groupDefaultExpanded={-1}
                        getDataPath={getDataPath}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid对树形数据的支持更为彻底,输入的是一个扁平的rowData数据,输出展示的是树形的数据。前提是,每个rowData都提供了自己的完整路径信息。

  • treeData,打开treeData开关
  • getDataPath,根据每个row,返回自己的dataPath信息,注意是完整路径信息,不是简单的parent信息。
  • autoGroupColumnDef,treeData的显示与分组的显示也是一样的,默认分组显示的数据会有括号展示合计子行数量信息,使用suppressCount可以避免这个问题。

11.6.2 可编辑树形数据

'use strict';

import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    GetDataPath,
    GetRowIdFunc,
    GetRowIdParams,
    Grid,
    GridOptions,
    ICellRendererComp,
    ICellRendererParams,
    RowNode,
} from 'ag-grid-community';
import { getData } from './data2';

declare var window: any;

function getNextId() {
    if (!window.nextId) {
        window.nextId = 15;
    } else {
        window.nextId++;
    }
    return window.nextId;
}

const FileCellRenderer: React.FC<ICellRendererParams> = (props) => {
    var icon = getFileIcon(props.value);
    const value = props.value;
    return (<div>
        {icon ? <span>
            <i className={icon}></i>
            <span className="filename"></span>{value}
        </span>
            : value}
    </div>);
}

//删除的时候,连子树都一起删除
function getRowsToRemove(node: RowNode) {
    var res: any[] = [];
    const children = node.childrenAfterGroup || [];
    for (var i = 0; i < children.length; i++) {
        res = res.concat(getRowsToRemove(children[i]));
    }
    // ignore nodes that have no data, i.e. 'filler groups'
    return node.data ? res.concat([node.data]) : res;
}

function isSelectionParentOfTarget(selectedNode: RowNode, targetNode: RowNode) {
    var children = selectedNode.childrenAfterGroup || [];
    for (var i = 0; i < children.length; i++) {
        if (targetNode && children[i].key === targetNode.key) return true;
        isSelectionParentOfTarget(children[i], targetNode);
    }
    return false;
}

//设置node的filePath,递归设置
function getRowsToUpdate(node: RowNode, parentPath: string[]) {
    var res: any[] = [];
    var newPath = parentPath.concat([node.key!]);
    if (node.data) {
        // groups without data, i.e. 'filler groups' don't need path updated
        node.data.filePath = newPath;
    }
    var children = node.childrenAfterGroup || [];
    for (var i = 0; i < children.length; i++) {
        var updatedChildRowData = getRowsToUpdate(children[i], newPath);
        res = res.concat(updatedChildRowData);
    }
    // ignore nodes that have no data, i.e. 'filler groups'
    return node.data ? res.concat([node.data]) : res;
}

function getFileIcon(name: string) {
    return endsWith(name, '.mp3') || endsWith(name, '.wav')
        ? 'far fa-file-audio'
        : endsWith(name, '.xls')
            ? 'far fa-file-excel'
            : endsWith(name, '.txt')
                ? 'far fa-file'
                : endsWith(name, '.pdf')
                    ? 'far fa-file-pdf'
                    : 'far fa-folder';
}

function endsWith(str: string | null, match: string | null) {
    var len;
    if (str == null || !str.length || match == null || !match.length) {
        return false;
    }
    len = str.length;
    return str.substring(len - match.length, len) === match;
}

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo(() => ({ width: '100%', height: '100vh' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState<any[]>(getData());
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'dateModified',
            minWidth: 250,
            comparator: (d1, d2) => {
                return new Date(d1).getTime() < new Date(d2).getTime() ? -1 : 1;
            },
        },
        {
            field: 'size',
            aggFunc: 'sum',
            valueFormatter: (params) => {
                return params.value
                    ? Math.round(params.value * 10) / 10 + ' MB'
                    : '0 MB';
            },
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            flex: 1,
            filter: true,
            sortable: true,
            resizable: true,
        };
    }, []);
    const autoGroupColumnDef = useMemo<ColDef>(() => {
        return {
            headerName: 'Files',
            minWidth: 330,
            cellRendererParams: {
                suppressCount: true,
                //自定义一个FileCellRenderer
                innerRenderer: FileCellRenderer,
            },
        };
    }, []);
    const getDataPath = useCallback((data: any) => {
        return data.filePath;
    }, []);
    const getRowId = useCallback((params: GetRowIdParams) => {
        return params.data.id;
    }, []);

    const addNewGroup = useCallback(() => {
        //添加到一个固定的filePath位置
        var newGroupData = [
            {
                id: getNextId(),
                filePath: ['Music', 'wav', 'hit_' + new Date().getTime() + '.wav'],
                dateModified: 'Aug 23 2017 11:52:00 PM',
                size: 58.9,
            },
        ];
        gridRef.current!.api.applyTransaction({ add: newGroupData });
    }, []);

    const removeSelected = useCallback(() => {
        var selectedNode = gridRef.current!.api.getSelectedNodes()[0]; // single selection
        if (!selectedNode) {
            console.warn('No nodes selected!');
            return;
        }

        //删除该node以及node下面children的数据
        gridRef.current!.api.applyTransaction({
            remove: getRowsToRemove(selectedNode),
        });
    }, []);

    const moveSelectedNodeToTarget = useCallback((targetRowId: string) => {
        var selectedNode = gridRef.current!.api.getSelectedNodes()[0]; // single selection
        if (!selectedNode) {
            console.warn('No nodes selected!');
            return;
        }
        var targetNode = gridRef.current!.api.getRowNode(targetRowId)!;
        var invalidMove =
            selectedNode.key === targetNode.key ||
            isSelectionParentOfTarget(selectedNode, targetNode);
        if (invalidMove) {
            console.warn('Invalid selection - must not be parent or same as target!');
            return;
        }
        var rowsToUpdate = getRowsToUpdate(selectedNode, targetNode.data.filePath);
        gridRef.current!.api.applyTransaction({ update: rowsToUpdate });
    }, []);

    return (
        <div style={containerStyle}>
            <div className="example-wrapper">
                <div style={{ marginBottom: '5px' }}>
                    <button onClick={addNewGroup}>Add New Group</button>
                    <button onClick={() => moveSelectedNodeToTarget('9')}>
                        Move Selected to 'stuff'
                    </button>
                    <button onClick={removeSelected}>Remove Selected</button>
                </div>

                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        autoGroupColumnDef={autoGroupColumnDef}
                        treeData={true}
                        animateRows={true}
                        groupDefaultExpanded={-1}
                        rowSelection={'single'}
                        getDataPath={getDataPath}
                        getRowId={getRowId}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • rowSelection,树形数据也支持rowSelection功能。
  • addGroup,添加的时候指定一下dataPath就可以了,和平时一样的方式添加数据。
  • removeGroup,删除的时候也是平时一样的方式删除,注意删除整个子树,而不只是一个节点。
  • move,更新节点位置的时候,需要注意更新一下整个子树的dataPath就可以了。

这里,AgGrid也提供了如何对树形数据进行拖动的实现。

12 导出Excel

12.1 手动导出

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
} from 'ag-grid-community';

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();
    const [columnDefs, setColumnDefs] = useState<(ColDef | ColGroupDef)[]>([
        {
            headerName: 'Group A',
            children: [
                { field: 'athlete', minWidth: 200 },
                { field: 'country', minWidth: 200 },
            ],
        },
        {
            headerName: 'Group B',
            children: [
                { field: 'sport', minWidth: 150 },
                { field: 'gold' },
                { field: 'silver' },
                { field: 'bronze' },
                { field: 'total' },
            ],
        },
    ]);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            sortable: true,
            filter: true,
            resizable: true,
            minWidth: 100,
            flex: 1,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/small-olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => {
                setRowData(data);
            });
    }, []);

    const onBtExport = useCallback(() => {
        //exportDataAsExcel直接导出
        gridRef.current!.api.exportDataAsExcel();
        //getDataAsExcel返回Blob对象
        //gridRef.current!.api.getDataAsExcel();
    }, []);

    return (
        <div style={containerStyle}>
            <div>
                <button
                    onClick={onBtExport}
                    style={{ marginBottom: '5px', fontWeight: 'bold' }}
                >
                    Export to Excel
                </button>
            </div>
            <div className="grid-wrapper">
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        onGridReady={onGridReady}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid提供了获取Excel的blob功能

  • exportDataAsExcel,直接导出数据
  • getDataAsExcel,获取数据的Blob

使用AgGrid的导出功能,好处在于,所见即所得的导出Excel,不需要额外的代码开发。

12.2 样式与额外内容

'use strict';

import React, { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import {
    ColDef,
    ColGroupDef,
    Grid,
    GridOptions,
    GridReadyEvent,
    ExcelCell,
    ExcelStyle,
} from 'ag-grid-community';


const getRows: () => ExcelCell[][] = () => [
    //第一个空行
    [],
    //第二个行
    [
        {
            styleId: 'coverHeading',
            data: {
                //数据
                value: 'Here is a comma, and a some "quotes".',
                //类型
                type: 'String'
            },
        },
    ],
    //第三个行
    [
        {
            data: {
                value:
                    'They are visible when the downloaded file is opened in Excel because custom content is properly escaped.',
                type: 'String',
            },
        },
    ],
    [
        {
            data: { value: 'this cell:', type: 'String' },
            //合并两列
            mergeAcross: 1
        },
        {
            styleId: 'coverText',
            data: {
                value: 'is empty because the first cell has mergeAcross=1',
                type: 'String',
            },
        },
    ],
    [],
];

const GridExample = () => {
    const gridRef = useRef<AgGridReact>(null);
    const containerStyle = useMemo<CSSProperties>(() => ({ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%', flex: '1' }), []);
    const [rowData, setRowData] = useState<any[]>();

    const [columnDefs, setColumnDefs] = useState<(ColDef | ColGroupDef)[]>([
        {
            headerName: 'Group A',
            children: [
                { field: 'athlete', minWidth: 200 },
                { field: 'country', minWidth: 200 },
            ],
        },
        {
            headerName: 'Group B',
            children: [
                { field: 'sport', minWidth: 150 },
                { field: 'gold' },
                { field: 'silver' },
                { field: 'bronze' },
                { field: 'total' },
            ],
        },
    ]);
    const excelStyles = useMemo<ExcelStyle[]>(() => {
        return [
            {
                id: 'coverHeading',
                font: {
                    italic: true,
                    size: 26,
                    bold: true,
                    underline: 'Single',
                },
            },
            {
                id: 'coverText',
                font: {
                    size: 14,
                },
            },
        ];
    }, []);
    const defaultColDef = useMemo<ColDef>(() => {
        return {
            sortable: true,
            filter: true,
            resizable: true,
            minWidth: 100,
            flex: 1,
        };
    }, []);

    const onGridReady = useCallback((params: GridReadyEvent) => {
        fetch('https://www.ag-grid.com/example-assets/small-olympic-winners.json')
            .then((resp) => resp.json())
            .then((data: any[]) => {
                setRowData(data);
            });
    }, []);

    const onBtExport = useCallback(() => {
        //exportDataAsExcel直接导出
        gridRef.current!.api.exportDataAsExcel({
            sheetName: "表单名",
            prependContent: getRows(),
            appendContent: getRows(),
        });
        //getDataAsExcel返回Blob对象
        //gridRef.current!.api.getDataAsExcel();
    }, []);

    return (
        <div style={containerStyle}>
            <div>
                <button
                    onClick={onBtExport}
                    style={{ marginBottom: '5px', fontWeight: 'bold' }}
                >
                    Export to Excel
                </button>
            </div>
            <div className="grid-wrapper">
                <div style={gridStyle} className="ag-theme-alpine">
                    <AgGridReact
                        ref={gridRef}
                        rowData={rowData}
                        columnDefs={columnDefs}
                        defaultColDef={defaultColDef}
                        onGridReady={onGridReady}
                        excelStyles={excelStyles}
                    ></AgGridReact>
                </div>
            </div>
        </div>
    );
};

export default GridExample;

要点如下:

  • excelStyles,注册excel的样式
  • prependContent,表格前的内容
  • appendContent,表格后的内容
  • sheetName,表格名称

额外的内容就是一个简单的二维数组

  • 第一维,是行,数组类型
  • 第二维,是单元格,对象类型。每个单元格的属性有data,styleId,styleId等等。

13 图表

看11.4有聚合图表的功能,不多说了

14 滚动


import React, { useCallback, useMemo, useRef, useState } from 'react';
import { render } from 'react-dom';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-enterprise';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { Button } from 'antd';
import { ColDef, ICellRendererParams } from 'ag-grid-community';

const IdRenderer: React.FC<ICellRendererParams> = (props) => {
    return (<span>{props.rowIndex + 1}</span>);
}
const GridExample = () => {
    const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
    const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
    const [rowData, setRowData] = useState();
    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
        {
            field: 'id',
            cellRenderer: IdRenderer,
        },
        {
            //打开了enableRowGroup才能放入groupPanel中
            field: 'country',
            width: 300,
        },
        {
            field: 'athlete', enableRowGroup: true,
            width: 300,
        },
        {
            field: 'year', enableRowGroup: true,
            width: 300,
        },
        {
            field: 'sport',
            filter: 'agTextColumnFilter',
        },
        { field: 'gold', aggFunc: 'sum', width: 300 },
        { field: 'silver', aggFunc: 'sum', width: 300 },
        { field: 'bronze', aggFunc: 'sum', width: 300 },
        { field: 'total', aggFunc: 'sum', width: 300 },
        { field: 'age', width: 300 },
        { field: 'date', width: 300 },

    ]);
    const defaultColDef = useMemo(() => {
        return {
            minWidth: 150,
            resizable: true,
        };
    }, []);

    const onGridReady = useCallback((params) => {
        fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
            .then((resp) => resp.json())
            .then((data) => setRowData(data));
    }, []);

    const gridRef = useRef<AgGridReact>(null);
    const jumpRow = () => {
        gridRef.current!.api.ensureIndexVisible(100, 'middle');
    }
    const jumpCol = () => {
        gridRef.current!.api.ensureColumnVisible('age');
    }
    return (
        <div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100vh' }}>
            <div>
                <Button onClick={jumpRow}>{'跳转到第100行'}</Button>
                <Button onClick={jumpCol}>{'跳转age列'}</Button>
            </div>
            <div className="ag-theme-alpine" style={{ height: '100%', width: '100%' }}>
                <AgGridReact
                    ref={gridRef}
                    rowData={rowData}
                    columnDefs={columnDefs}
                    defaultColDef={defaultColDef}
                    onGridReady={onGridReady}
                    animateRows={true}
                ></AgGridReact>
            </div>
        </div>
    );
};

export default GridExample;

AgGrid有跳转到指定行或者指定列的方法。

  • ensureIndexVisible,跳转到指定行数
  • ensureNodeVisible,跳转到指定行的RowNode,或者RowData,或者判断闭包
  • ensureColumnVisible,跳转到指定列,参数为colId

20 总结

一个重要,但没有做Demo的功能是,多表格对齐,看这里

以上介绍的仅为AgGrid的主要功能,大概还有40%的功能没有覆盖到。从这次学习中,我们得到了:

  • DataGrid的功能,客户端选择,编辑,筛选,分组,排序,支点,导入和导出Excel,内设图表模式,这些都是管理系统中成熟的表格操作。类似的有DevExppress公司的GridView,GrapeCity公司的FlexGrid
  • 命令式与内部数据源,命令式API是UI操作中不可或缺的部分,如果所有的UI都只有类似React的声明式操作,那么性能会是个大问题。React和Vue的开发模式虽然很爽,但是还需要看场景地解决问题。
  • 虚拟化滚动的设计,RowNode与RowData的映射,看这里,实现高性能虚拟化滚动的前提是,有一套UI背后的数据结构,描述了每个Row的高度信息。
  • 灵活的API设计,如何用一套固定的API适应各种各样的Grid场景,对架构者来说是相当大的考验。既保持灵活性,同时不失简单。

相关文章