В LiveLoveApp мы являемся большими поклонниками AG Grid — лучшей JavaScript-сетки в мире. Более того, мы предлагаем услуги по внедрению AG Grid на основе нашего опыта!
Почему?
По двум основным причинам: производительность и расширяемость.
Многие наши клиенты используют AG Grid для удовлетворения требований заказчика по отображению табличных данных.
В этой статье вы узнаете:
- Конвейер рендеринга ячеек AG Grid
- Как использовать новые дженерики TypeScript, предоставляемые AG Grid API (выпущены в версии 28)
- Как создать безопасный с точки зрения типов геттер значения для получения значения ячейки
- Как создать безопасное для типов значение formatted для форматирования значения ячейки
- Как создать безопасный для типов и производительный рендерер ячеек
Конвейер рендеринга ячеек AG Grid
Без какой-либо настройки и в самой простой форме каждая ячейка в AG Grid отображается как строка, основанная на поле
, указанном в предоставленных данных строки.
Однако часто реализация AG Grid не так проста.
В этом случае мы можем использовать конвейер для рендеринга ячеек:
valueGetter()
valueFormatter()
cellRenderer()
Демонстрация или этого не произошло
Вот демонстрация с использованием React:
А вот демонстрация с использованием Angular:
Использование функции обратного вызова valueGetter()
.
Во-первых, мы можем использовать valueGetter()
для получения и/или изменения данных в ячейке с помощью предоставленной функции обратного вызова.
Давайте рассмотрим пример.
В этом примере требуется создать безопасный с точки зрения типов получатель значения, который использует данные, предоставленные AG Grid, для условного умножения значения в нашем наборе данных.
export const multiplierValueGetter =
<T extends Record<TKey, number>,
TKey extends string | number | symbol = string>(
value: keyof T,
multiplier: keyof T
) =>
(params: ValueGetterParams<T>): number => {
if (params.data === undefined) {
return 0;
}
return Math.round(params.data[value] * params.data[multiplier] * 100) / 100;
};
Давайте рассмотрим приведенный выше код:
- Во-первых, мы объявляем функцию высшего порядка
multiplierValueGetter()
. Использование функции высшего порядка позволяет нам определить общий типT
, который расширяетRecord
, значения которого имеют типnumber
. Функция высшего порядка возвращает функцию получения значения, которая будет вызвана AG Grid с предоставленнымиValueGetterParams<T>
. - Функция
multiplierValueGetter()
имеет два необходимых параметра, во-первых, свойствоvalue
, во-вторых, свойствоmultiplier
, оба из которых являются ключами данных, предоставленных гриду, которые имеют типT
. - Поскольку мы используем AG Grid v28 (или выше), мы можем указать общий тип
T
дляValueGetterParams
. До версии 28 этот общий тип был недоступен, и в результате определение типа для свойстваdata
былоany
. - В функции получения значения, если
data
являетсянеопределенным
, что может произойти при использовании модели бесконечных рядов или группировки рядов в AG Grid, мы возвращаем0
. - Наконец, мы можем округлить значение после умножения.
Вот пример реализации нашей функции высшего порядка multiplierValueGetter()
.
interface RowData {
value: number;
multiplier: number;
}
type Props = {
rowData: RowData[]
}
export default function Grid ({ rowData }: Props) {
const colDefs = [
{
colId: 'value',
headerName: 'Value',
field: 'value'
},
{
colId: 'multiplied',
headerName: 'Multiplied',
valueGetter: multiplierValueGetter<RowData>('value', 'multiplier')
}
] as ColDef<RowData>[];
return (
<AgGridReact
className="ag-theme-material"
columnDefs={colDefs}
rowData={rowData}
/>
);
}
Использование функции обратного вызова valueFormatter()
После того как значение ячейки известно, дополнительная функция обратного вызова valueFormatter()
позволяет нам отформатировать значение.
Давайте рассмотрим пример использования функции обратного вызова valueFormatter()
.
В этом примере требуется объявить многократно используемую функцию высшего порядка decimalValueFormatter()
, которая безопасна для типов и форматирует указанное свойство данных до заданной длины.
export const decimalValueFormatter =
<TData, TValue>(digits = 0) =>
(params: ValueFormatterParams<TData, TValue>): string => {
const formatter = new Intl.NumberFormat('en-US', {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
});
if (params.value === undefined) {
return formatter.format(0);
}
return formatter.format(Number(params.value));
};
Давайте рассмотрим приведенный выше код:
- Мы объявили функцию высшего порядка
decimalValueFormatter()
. Это позволяет реализации этого форматера значений указывать два общих типа:TData
иTValue
. Общий типTData
представляет тип для параметраdata
, а общий типTValue
представляет тип для параметраvalue
. Наша функция высшего порядка имеет необязательный параметрdigits
, который определяет минимальное и максимальное количество цифр для десятичного форматирования. Функция высшего порядка возвращает функцию, которая является геттером значений, вызываемым AG Grid с объектомValueGetterParams<TData, TValue>
. - В этом форматере значений мы используем класс
Intl.NumberFormat
для создания нового экземпляра форматера, указывая минимальное и максимальное количество дробных цифр. - Если
data
не определено, что может быть при использовании модели бесконечных строк или группировки строк в AG Grid, то мы просто возвращаем 0. - В противном случае мы возвращаем отформатированное значение.
Вот пример реализации нашей функции высшего порядка decimalValueFormatter()
.
interface RowData {
value: number;
multiplier: number;
}
type Props = {
rowData: RowData[]
}
export default function DashboardGrid ({ rowData }: Props) {
const colDefs = [
{
colId: 'value',
headerName: 'Value',
field: 'value'
},
{
colId: 'multiplied',
headerName: 'Multiplied',
valueGetter: multiplierValueGetter<RowData>('value', 'multiplier'),
valueFormatter: decimalValueFormatter<RowData, Pick<RowData, 'taxRate'>>(2)
}
] as ColDef<RowData>[];
return (
<AgGridReact
className="ag-theme-material"
colDefs={colDefs}
rowData={rowData}
/>
);
}
Использование функции обратного вызова cellRenderer()
После того, как значение ячейки определено, и мы опционально отформатировали значение, мы можем использовать рендерер ячеек, чтобы иметь полный контроль над тем, как ячейка отображается в AG Grid.
По умолчанию все значения отображаются в виде строки.
Чтобы отобразить ячейку не в виде строки, мы можем использовать пользовательский рендерер ячеек.
Важно отметить, что использовать рендерер ячеек следует только в случае необходимости.
По умолчанию textContent
HTML-элемента ячейки устанавливается в (опционально форматированное) значение.
Когда мы используем рендерер ячеек, мы добавляем в DOM дополнительные элементы, слушатели событий и т.д., и все это должно быть отображено для каждой ячейки сетки.
Наконец, мы рекомендуем, чтобы все рендереры ячеек строго использовали ванильный JS.
Это улучшит производительность рисования вашего приложения при прокрутке сетки.
Почему так?
Если вы используете фреймворк (например, React, Angular или Vue), то в результате каждый раз, когда ячейка должна быть отрисована, AG Grid должен переключить контекст на контекст приложения React (или Angular или Vue), чтобы отрисовать результирующий HTML в DOM. Это может быть очень дорого и часто не является необходимым.
📣 Используйте рендерер ячеек только при необходимости, ограничьте количество элементов и слушателей событий до минимума и всегда используйте ванильный JS.
Для настройки рендерера ячеек мы можем предоставить AG Grid:
- Строка, ссылающаяся на зарегистрированный компонент фреймворка
- Класс, реализующий интерфейс
ICellRendererComp
. - Функция, которая вызывается с помощью объекта
ICellRendererParams
.
Давайте рассмотрим пример. В этом примере пользователь должен отобразить колонку с именем, которое может быть сокращенным, и, когда пользователь нажимает на имя, мы хотим открыть диалог (за который не будет отвечать AG Grid, но нам нужно уведомить потребителя, что пользователь нажал на имя).
Сначала определим новый интерфейс, который описывает контракт между реализацией и рендерером ячеек для ожидаемых данных.
export interface NameCellRendererData {
id: string;
name: string;
}
Далее определим другой интерфейс для события click, которое будет уведомлять реализацию о том, что пользователь нажал на имя.
export interface NameCellRendererClickEvent<T, E = Event> {
event: E;
data: T;
}
Интерфейс NameCellRendererClickEvent
описывает объект обработчика события, который будет предоставлен параметру click
, реализуемому при использовании рендерера ячеек.
Интерфейс имеет два дженерика:
- Во-первых, мы определяем дженерик
T
, который будет предоставлен для данных строки. - Во-вторых, у нас есть дженерик
E
, который по умолчанию назначается глобальному интерфейсуEvent
. В рендере ячеек мы можем установить более узкий тип.
Теперь определим другой интерфейс для параметров, которые будут предоставляться рендереру ячеек.
export interface NameCellRendererParams<T> {
click: (event: NameCellRendererClickEvent<T>) => void;
document: Document;
isAbbreviated?: (params: ValueGetterParams<T>) => boolean;
}
Следует отметить несколько моментов:
- Во-первых, мы объявили общий тип
T
, чтобы поддерживать проверку типа объектаparams
, который вызывается для функцииisAbbreviated
. - Параметр
click
будет функцией обратного вызова, которая вызывается рендерером ячеек. Функция обратного вызова вызывается с параметромevent
, который представляет собой интерфейсNameCellRendererClickEvent
. - Параметр
isAbbreviated
— это еще одна функция обратного вызова, которая позволяет реализующей сетке определить, должно ли конкретное значение ячейки быть сокращенным. Мы будем использовать интерфейсValueGetterParams
, предоставляемый AG Grid, чтобы сохранить эргономичность нашего API (в том смысле, что мы ожидаем, что разработчик будет знать об этом существующем интерфейсе, поэтому имеет смысл использовать его).
Описав API, давайте рассмотрим код для рендеринга ячеек.
type Params<T> = NameCellRendererParams<T> & ICellRendererParams<T, string>;
/**
* AG Grid cell renderer for a user name.
*/
export class NameCellRenderer<T extends NameCellRendererData>
implements ICellRendererComp<T>
{
/** AG Grid API. */
private api: GridApi | null = null;
/** The button element. */
private btnEl: HTMLButtonElement | null = null;
/** Provided callback function that is invoked when the button is clicked. */
private click:
| ((event: NameCellRendererClickEvent<T, MouseEvent>) => void)
| null = null;
/** The column definition. */
private colDef: ColDef;
/** The AG Grid column. */
private column: Column | null = null;
/** AG Grid Column API. */
private columnApi: ColumnApi;
/** AG Grid context. */
private context: any;
/** The provided data. */
private data: T | undefined;
/** The global document. */
private document: Document | null = null;
/** Execution context bound function when the button is clicked. */
private handleClick:
| ((this: NameCellRenderer<T>, event: MouseEvent) => void)
| null = null;
/** Callback function to determinate if the name is abbreviated. */
private isAbbreviated?: (params: ValueGetterParams<T>) => boolean;
/** AG Grid row node. */
private node: RowNode;
/** The user name. */
private value: = '';
/** Value getter params to be provided. */
get valueGetterParams(): ValueGetterParams<T> {
return {
api: this.api,
colDef: this.colDef,
column: this.column,
columnApi: this.columnApi,
context: this.context,
data: this.data,
getValue: (field?: string) =>
this.data && field ? this.data[field] : this.value,
node: this.node,
};
}
init(params: Params<T>): void {
this.updateParams(params);
this.setGui();
}
destroy(): void {
if (this.handleClick !== null && this.btnEl !== null) {
this.btnEl.removeEventListener('click', this.handleClick);
}
}
getGui(): HTMLElement {
return this.btnEl!;
}
refresh(params: Params<T>): boolean {
this.updateParams(params);
const isAbbreviated = this.isAbbreviated?.(this.valueGetterParams) ?? false;
this.value = this.transform(params.value, isAbbreviated);
if (this.btnEl) {
this.btnEl.innerHTML = this.value;
}
return true;
}
private setGui(): void {
this.btnEl = this.document.createElement('button') as HTMLButtonElement;
this.btnEl.classList.add('user-name-cell');
this.handleClick = (event) => {
if (this.click) {
this.click({
event,
data: this.data,
});
}
};
const isAbbreviated = this.isAbbreviated?.(this.valueGetterParams) ?? false;
this.btnEl.innerHTML = this.transform(this.value, isAbbreviated);
this.btnEl.addEventListener('click', this.handleClick);
}
private updateParams(params: Params<T>): void {
this.api = params.api;
this.click = params.click;
this.colDef = params.colDef;
this.column = params.column;
this.columnApi = params.columnApi;
this.context = params.context;
this.data = params.data;
this.document = params.document;
this.isAbbreviated = params.isAbbreviated;
this.node = params.node;
this.value = params.value;
}
private transform(value: string, isAbbreviated: boolean): string {
if (isAbbreviated) {
return value.replace(/^Model/i, '');
}
return value;
}
}
Хорошо. Давайте рассмотрим приведенный выше код.
- Во-первых, мы определяем новый тип
Params
, который является объединением нашего интерфейсаNameCellRendererParams
и предоставляемого AG GridICellRendererParams
. Общий типT
— это тип данных строки AG Grid, которые мы далее предоставляем интерфейсуICellRendererParams
. Второй тип сценария явно установлен вstring
, поскольку мы ожидаем, чтоvalue
ячейки всегда будет строкой. - Мы экспортируем класс
NameCellRenderer
, общий тип которогоT
расширяет наш ранее определенный интерфейсNameCellRendererData
. Это гарантирует, что у нас есть безопасность типов между данными строк, предоставляемыми AG Grid, и нашим рендерером ячеек. Как и требуется, наш класс реализует интерфейсICellRendererComp
из AG Grid. - У нас объявлено множество свойств, которые будут иметь ссылки и значения, необходимые для передачи в функцию обратного вызова
isAbbreviated
. - Обратите внимание, что свойство
click
— это предоставленная функция обратного вызова из реализации, которая вызывается, когда пользователь нажимает на имя. - Далее, обратите внимание, что свойство
handleClick
— это связанная с выполнением функция, которую мы будем использовать в классе рендерера ячеек для добавления и удаления слушателя событий. - Метод доступа к свойству
valueGetterParams
возвращает объектValueGetterParams<T>
, который используется реализацией для определения того, является ли имя сокращенным или нет. Мы решили использовать этот интерфейс из AG Grid, чтобы сохранить согласованный API для наших пользователей (разработчиков, использующих наш рендерер ячеек в своих реализациях AG Grid). Это важно для эргономики API. - Методы
init()
,getGui()
,refresh()
иdestroy()
реализованы в соответствии с интерфейсомICellRendererComp
из AG Grid. Эти методы предоставляют крючки для инициализации рендерера ячеек, предоставляют HTML-элемент, который AG Grid добавляет в DOM при рендеринге ячейки, а также дополнительные крючки для обновления данных и уничтожения ячейки. Важно, чтобы мы использовали метод жизненного циклаdestroy()
для выполнения любой необходимой очистки, например, удаления слушателей событий, чтобы предотвратить утечку памяти в нашем приложении.
Наконец, вот пример реализации NameCellRenderer
.
interface RowData {
id: string;
name: string;
}
export default function DashboardGrid () {
const colDefs = [
{
colId: 'name',
field: 'name',
headerName: 'Name',
cellRenderer: NameCellRenderer,
cellRendererParams: {
click: ({ data }) => {
window.alert(`You clicked: ${data.name}`)
},
document,
isAbbreviated: ({ data }) => {
return data.name.length > 20;
},
} as NameCellRendererParams<RowData>
}
] as ColDef<RowData>[];
return (
<AgGridReact
colDefs={colDefs}
rowData={rowData}
/>
);
}
Резюме
Итак, мы узнали, как AG Grid отображает ячейку, как мы можем предоставлять данные в ячейку, опционально форматировать ячейку и, при необходимости, настраивать отображение ячейки.
Основные выводы:
- Используйте функцию обратного вызова
valueGetter()
для получения и/или изменения значения ячейки. - Используйте функцию обратного вызова
valueFormatter()
для форматирования значения ячейки. - Если необходимо, предоставьте рендерер ячеек для настройки HTML ячейки.
- Рендереры ячеек могут быть интерактивными, вызывать функции обратного вызова и многое другое.
- Важно удалять слушателей событий при уничтожении ячейки.
- Разработайте эргономичный API.
- Создавайте получатели значений, форматоры значений и рендереры ячеек, которые безопасны для типов.