ag-Grid 是一款功能丰富的数据网格库,它不仅提供了内置的编辑器,还允许你轻松创建和使用自定义编辑器。在这篇文章中,我们将深入探讨如何在 React 中创建自定义 ag-Grid 编辑器,并展示一些最佳实践。
内置编辑器
-
**文本框编辑器 (
agTextCellEditor
** ):- 作用: 允许用户直接在单元格中输入文本。
- 用法: 在列的
cellEditor
属性中设置为'agTextCellEditor'
。
typescript
columnDefs: [
{ headerName: '姓名', field: 'name', editable: true, cellEditor: 'agTextCellEditor' },
// 其他列配置...
],
-
大文本编辑器 **(
agLargeTextCellEditor
** ):- 作用: 允许用户直接在单元格中输入大文本。类似 textarea 元素。
- 用法: 在列的
cellEditor
属性中设置为'agTextCellEditor'
。
typescriptcolumnDefs: [ { headerName: '备注', field: 'notes', editable: true, cellEditor: 'agLargeTextCellEditor', cellEditorParams: { // 设置大文本编辑器的最大高度 maxHeight: 200, }, }, // 其他列配置... ],
-
**数字编辑器 (
agNumberCellEditor
** ):- 作用: 用于编辑数字类型的数据。
- 用法: 在列的
cellEditor
属性中设置为'agNumberCellEditor'
。
typescript
columnDefs: [
{ headerName: '年龄', field: 'age', editable: true, cellEditor: 'agNumberCellEditor' },
// 其他列配置...
],
-
**日期编辑器 (
agDateCellEditor
** ):- 作用: 用于编辑日期类型的数据。
- 用法: 在列的
cellEditor
属性中设置为'agDateCellEditor'
。
typescript
columnDefs: [
{ headerName: '生日', field: 'birthday', editable: true, cellEditor: 'agDateCellEditor' },
// 其他列配置...
],
-
**下拉框编辑器 (
agSelectCellEditor
** ):- 作用: 提供一个下拉框,允许用户从预定义的选项中选择。
- 用法: 在列的
cellEditor
属性中设置为'agSelectCellEditor'
,并使用cellEditorParams
传递选项。
typescript
const genderOptions = ['Male', 'Female'];
columnDefs: [
{
headerName: '性别',
field: 'gender',
editable: true,
cellEditor: 'agSelectCellEditor',
cellEditorParams: { values: genderOptions },
},
// 其他列配置...
],
-
**富文本下拉编辑器 (
agRichSelectCellEditor
** ):- 作用: 提供一个富文本下拉框,允许用户选择复杂的选项。
- 用法: 在列的
cellEditor
属性中设置为'agRichSelectCellEditor'
,并使用cellEditorParams
传递选项。
typescript
columnDefs: [
{
headerName: '城市',
field: 'city',
editable: true,
cellEditor: 'agRichSelectCellEditor',
cellEditorParams: {
values: ['New York', 'London', 'Tokyo'],
},
},
// 其他列配置...
],
这些是一些常见的内置编辑器。在配置 ag-Grid 列时,你可以根据数据类型和需求选择合适的编辑器。
agRichSelectCellEditor
和 **agSelectCellEditor
**的区别
agRichSelectCellEditor
提供了一个富文本样式的下拉框,允许用户从预定义的选项中选择。
基本用法:
agRichSelectCellEditor
的基本用法和 agSelectCellEditor
是一样的。
高级用法:
-
**自定义样式:**
agRichSelectCellEditor
支持使用 CSS 来自定义下拉框的样式。你可以通过全局样式或者在列定义中使用cellEditorParams
中的cellStyle
属性来实现。typescriptcellEditorParams: { values: ['Option 1', 'Option 2', 'Option 3'], cellStyle: { backgroundColor: 'lightblue', color: 'darkblue', }, },
-
异步加载选项: 有时,你可能需要在下拉框中异步加载选项。在这种情况下,可以在
cellEditorParams
中使用values
属性的函数形式。typescriptcellEditorParams: { values: () => { return new Promise((resolve) => { // 异步加载选项的逻辑,例如从服务端获取数据 setTimeout(() => { resolve(['Option 1', 'Option 2', 'Option 3']); }, 1000); }); }, },
使用异步加载可以确保编辑器中的选项在需要时动态获取。
-
自定义模板: 你可以通过在
cellEditorParams
中使用cellRenderer
属性来自定义下拉框中每个选项的模板。typescriptcellEditorParams: { values: ['Option 1', 'Option 2', 'Option 3'], cellRenderer: (params) => { // 自定义模板的逻辑,返回一个包含 HTML 的字符串 return `<span style="font-style: italic;">${params.value}</span>`; }, },
使用自定义模板可以实现更灵活的选项显示。
注意事项:
agRichSelectCellEditor
通常在编辑模式下通过单击单元格进入。- 在
cellEditorParams
中的values
属性可以是一个静态数组,也可以是一个返回数组的函数,甚至是一个返回 Promise 的异步函数。 cellEditorParams
中的cellStyle
属性用于设置下拉框的样式,可以根据需要进行定制。agRichSelectCellEditor
是企业版ag-Grid才有的功能,还有一些其他的功能,可以自行查询文档React Data Grid: Provided Cell Editors (ag-grid.com)
cellEditorParams
参数
cellEditorParams
是用于配置 ag-Grid 内置编辑器的参数对象。以下是一些通用常见的属性,可以用于配置不同的内置编辑器组件:
-
**
values
** :- **类型:**
Array
|Function
|Promise
- 描述: 用于提供下拉框选项的数组,可以是一个静态数组,一个返回数组的函数,或者一个返回 Promise 的异步函数。适用于
agRichSelectCellEditor
、agSelectCellEditor
等有下拉框的编辑器。
typescriptcellEditorParams: { values: ['Option 1', 'Option 2', 'Option 3'], },
- **类型:**
-
**
maxHeight
** :- **类型:**
number
- 描述: 用于设置编辑器的最大高度,以防止过度增长。适用于
agLargeTextCellEditor
。
typescriptcellEditorParams: { maxHeight: 200, },
- **类型:**
-
**
cellStyle
** :- **类型:**
Object
- 描述: 用于设置编辑器的样式。可以通过这个属性来自定义编辑器的外观。
typescriptcellEditorParams: { cellStyle: { backgroundColor: 'lightyellow', color: 'darkblue', }, },
- **类型:**
-
**
cellRenderer
** :- **类型:**
Function
- 描述: 用于自定义编辑器中每个选项的模板。可以通过这个属性来实现更灵活的选项显示。
typescriptcellEditorParams: { cellRenderer: (params) => { // 自定义模板的逻辑,返回一个包含 HTML 的字符串 return `<div style="font-style: italic;">${params.value}</div>`; }, },
- **类型:**
cellEditorParams
动态参数
cellEditorParams
属性还可以是一个函数,可以动态调整编辑器的参数,在自定义编辑器时,经常会用到。
typescript
cellEditorParams: params => {
const selectedCountry = params.data.country;
// 根据单元格数据中的 'country' 属性,动态生成了不同的城市列表。
if (selectedCountry === 'Ireland') {
return {
values: ['Dublin', 'Cork', 'Galway']
};
} else {
return {
values: ['New York', 'Los Angeles', 'Chicago', 'Houston']
};
}
}
使用自定义编辑器
在某些情况下,内置编辑器可能无法满足特定的编辑需求。例如,你可能需要一个具有复杂交互、自定义样式或特殊输入控件的编辑器。这时,自定义编辑器就派上用场了。通过自定义编辑器,你可以完全掌控编辑器的外观和行为,以满足项目的特定需求。比如说使用 antd 组件实现自定义编辑器。
步骤一:创建自定义编辑器组件
首先,我们将创建一个简单的自定义编辑器组件。在这个例子中,我们将创建一个带有日期选择器的编辑器。
typescript
// CustomDateEditor.jsx
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
const CustomDateEditor = forwardRef((props, ref) => {
const [date, setDate] = useState(props.value);
useEffect(() => {
setDate(props.value);
}, [props.value]);
const handleDateChange = (e) => {
setDate(e.target.value);
};
// 使用 useImperativeHandle 暴露给父组件的接口
useImperativeHandle(ref, () => ({
// 提供一个名为 getValue 的方法,用于获取编辑器的值,这个方法是必须实现的
getValue: () => date,
// isCancelBeforeStart 和 isCancelAfterEnd 用于控制编辑器状态的取消
// 在初始化后被调用一次,如果返回true,则无法进入编辑状态。可以使用这个方法根据特定条件决定是否开始编辑,例如,仅在某些按键按下时才允许编辑。
isCancelBeforeStart: () => false,
// 如果返回true,则修改的值不会被真正使用
isCancelAfterEnd: () => false,
// 获得焦点时会调用这个方法
focusIn: () => false;
// 失去焦点时调用
focusOut: () => false;
}));
return (
<div>
<input type="date" value={date} onChange={handleDateChange} />
</div>
);
});
export default CustomDateEditor;
在这个组件中,我们使用 forwardRef
和 useImperativeHandle
,以便在外部获取编辑器实例,并提供一个名为 getValue
的方法,用于获取编辑器的值。
步骤二:在 ag-Grid 中使用自定义编辑器
现在,我们将在 ag-Grid 中配置并使用我们的自定义编辑器。
typescript
// YourGridComponent.jsx
import React, { useRef } from 'react';
import { AgGridReact } from 'ag-grid-react';
import CustomDateEditor from './CustomDateEditor';
const YourGridComponent = () => {
const customEditorRef = useRef(null);
const onGridReady = (params) => {
// 获取 CustomDateEditor 的实例
customEditorRef.current = params.api.getCellEditorInstances()[0];
};
const getValueFromCustomEditor = () => {
// 调用 CustomDateEditor 的 getValue 方法
const editorValue = customEditorRef.current.getValue();
console.log('Value from CustomDateEditor:', editorValue);
};
const columnDefs = [
{
headerName: '日期',
field: 'date',
editable: true,
cellEditor: 'CustomDateEditor',
},
// 其他列配置...
];
return (
<div>
<button onClick={getValueFromCustomEditor}>获取自定义编辑器的值</button>
<AgGridReact
// 其他配置...
onGridReady={onGridReady}
columnDefs={columnDefs}
components={{
CustomDateEditor: CustomDateEditor,
}}
/>
</div>
);
};
export default YourGridComponent;
在这个例子中,我们在 columnDefs
中将编辑器配置为 'CustomDateEditor'
,并通过 components
注册我们的自定义编辑器组件。在 onGridReady
中,我们使用 useRef
获取自定义编辑器的实例,然后通过按钮调用 getValueFromCustomEditor
方法获取编辑器的值。
进阶
在实际项目中,自定义编辑器很常见,也很复杂,因为需要考虑的地方太多了。再和其他的一些显示需求糅合在一起,更复杂。例如:
编辑:
实现一个工艺选择器,下拉框形式。
下拉列表数据从后台请求,根据每一行数据中的工厂code来请求不同的工艺列表数据,组件的入参需要动态生成;
每次进入编辑,都需要请求一次数据,是否需要缓存,如果缓存,累计下拉,是否会导致内存过多,影响性能;
当重新选择工艺后,同一行数据中的工艺路线和所属车间都需要一起修改,也就是说修改一个单元格的数据,其他单元格可能也会被修改;
有可能还会涉及到其他行的数据也需要修改;
显示:
后端数据和前端需要的数据可能不一致,比如前端需要的是字符串数组,后端返回的数据是字符串逗号拼接的;
后端返回的数据是拆分为id和name的,而前端可能需要一个对象{id: "xx", name: "name"},这样才能更好的回显。
总结来讲,涉及到编辑,编辑表单需要考虑的东西,这里都需要考虑,总所周知,表单是很复杂的东西。我们需要考虑:
-
异步加载数据:
- 对于下拉框形式的选择器,可以通过
agRichSelectCellEditor
来实现,当然也可以使用自定义编辑器的方式实现; - 关于数据缓存,可以考虑使用缓存来减少不必要的请求,但需要注意缓存策略,确保不会导致内存过多。
- 对于下拉框形式的选择器,可以通过
-
数据的联动:
- 当重新选择后,其他单元格甚至其他行的数据也需要发生变化。
typescript// 在onCellValueChanged监听单元格的变更,setDataValue设置值 const onCellValueChanged = useCallback((params) => { const colId = params.column.getId(); if (colId === 'country') { const selectedCountry = params.data.country; const selectedCity = params.data.city; const allowedCities = countyToCityMap(selectedCountry); const cityMismatch = allowedCities.indexOf(selectedCity) < 0; if (cityMismatch) { // 单独修改当前行其他数据 params.node.setDataValue('city', null); } // 动态批量更新其他的内容 params.api.applyTransaction({ update: [{...}] }); } }, []);
-
前后端数据结构差异处理:
- 合理的使用
valueGetter
和valueSetter
,对数据进行处理。
- 合理的使用
-
数据校验:
- 需要根据具体的业务场景来设计编辑器的行为,考虑用户交互和数据流的整体流程。
可以通过isCancelBeforeStart
和isCancelAfterEnd
实现校验功能。
撤销/回退
在复杂的数据表格应用中,用户可能频繁进行编辑操作,但有时候需要撤销之前的更改或者重新应用已经取消的操作。Ag-Grid 提供了内建的撤销和回退功能。
启用编辑记录
在配置中设置 undoRedoCellEditing
和 undoRedoCellEditingLimit
属性,即可开启撤销和回退功能,并限制历史记录的数量。
typescript
<AgGridReact
// 其他配置...
undoRedoCellEditing={true}
// 可以撤销的次数
undoRedoCellEditingLimit={20}
/>
撤销和回退按钮
撤销操作可以通过调用 api.undoCellEditing()
实现,而回退操作可以通过 api.redoCellEditing()
实现。
typescript
const undo = () => {
gridApi.current.api.undoCellEditing();
};
const redo = () => {
gridApi.current.api.redoCellEditing();
};
撤销和回退记录
也可以获取到撤销和回退的次数,通过次数控制按钮是否可点击
typescript
// 撤销的次数
const undoSize = gridApi.current.api.getCurrentUndoSize();
// 回退的次数
const redoSize = gridApi.current.api.getCurrentRedoSize();
复杂对象
如果单元格值包含复杂对象,则需要执行几个步骤才能进行撤消/重做。
typescript
const [columnDefs, setColumnDefs] = useState([
{
field: 'a',
editable: true,
valueGetter: params => {
// create complex object from data
return {
actualValue: params.data[params.colDef.field],
anotherProperty: params.data.anotherProperty,
}
},
//
valueSetter: params => {
// update data from complex object
params.data[params.colDef.field] = params.newValue.actualValue
return true
},
// 自定义对象是否相同
equals: (valueA, valueB) => {
// compare complex objects
return valueA.actualValue === valueB.actualValue
}
}
]);
结论
通过自定义 ag-Grid 编辑器,我们可以在 React 项目中轻松满足各种编辑需求。后续我会尝试使用formily来实现ag-Grid的编辑器,利用formily完成字段的校验、关联逻辑、异步请求等。