Конвейер рендеринга ячеек AG Grid с помощью TypeScript

В LiveLoveApp мы являемся большими поклонниками AG Grid — лучшей JavaScript-сетки в мире. Более того, мы предлагаем услуги по внедрению AG Grid на основе нашего опыта!

Почему?

По двум основным причинам: производительность и расширяемость.
Многие наши клиенты используют AG Grid для удовлетворения требований заказчика по отображению табличных данных.

В этой статье вы узнаете:

  • Конвейер рендеринга ячеек AG Grid
  • Как использовать новые дженерики TypeScript, предоставляемые AG Grid API (выпущены в версии 28)
  • Как создать безопасный с точки зрения типов геттер значения для получения значения ячейки
  • Как создать безопасное для типов значение formatted для форматирования значения ячейки
  • Как создать безопасный для типов и производительный рендерер ячеек

Конвейер рендеринга ячеек AG Grid

Без какой-либо настройки и в самой простой форме каждая ячейка в AG Grid отображается как строка, основанная на поле, указанном в предоставленных данных строки.
Однако часто реализация AG Grid не так проста.
В этом случае мы можем использовать конвейер для рендеринга ячеек:

  1. valueGetter()
  2. valueFormatter()
  3. 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, реализуемому при использовании рендерера ячеек.
Интерфейс имеет два дженерика:

  1. Во-первых, мы определяем дженерик T, который будет предоставлен для данных строки.
  2. Во-вторых, у нас есть дженерик 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 Grid ICellRendererParams. Общий тип 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.
  • Создавайте получатели значений, форматоры значений и рендереры ячеек, которые безопасны для типов.

Оцените статью
devanswers.ru
Добавить комментарий