AG Grid Cell Rendering Pipeline with TypeScript
Tags: javascript, typescript, react, angular
Published: 2022-07-27 16:05:00

Here at LiveLoveApp, we’re big fans of AG Grid - the best JavaScript grid in the world. In fact, we offer [AG Grid implementation services](https://liveloveapp.com/services/ag-grid) based on our expertise!

Why?

For two primary reasons: performance and extensibility.
Many of our clients use AG Grid to meet customer requirements for displaying tabular data.

In this article you’ll learn:

- The AG Grid cell rendering pipeline
- How to leverage the new TypeScript generics provided by the AG Grid API (released in version 28)
- How to create a type-safe value getter to retrieve the value for a cell
- How to create a type-safe value formatted to format the value of a cell
- How to create a type-safe and performant cell renderer

## AG Grid Cell Rendering Pipeline

Without any customization and in the simplest form, each cell in AG Grid is rendered as a string based on the `field` specified in the provided row data.
However, often times an AG Grid implementation is not this simple.
This is where we can leverage [the pipeline for rendering cells](https://ag-grid.com/angular-data-grid/cell-content/):

1. `valueGetter()`
2. `valueFormatter()`
3. `cellRenderer()`

## Demo or it didn't happen

Here is a demo using React:

<iframe class="stackblitz" src="https://stackblitz.com/edit/ag-grid-cell-rendering-pipeline-react?embed=1&file=Grid.tsx&view=editor"></iframe>

And, here is a demo using Angular:

<iframe class="stackblitz" src="https://stackblitz.com/edit/ag-grid-cell-rendering-pipeline-angular?embed=1&file=src/app/grid.component.ts&view=editor"></iframe>

## Using the `valueGetter()` callback function

First, we can use a `valueGetter()` to fetch and/or mutate data in a cell using a provided callback function.
Let’s take a quick look at an example.

In this example, the requirement is to create a value getter that is type-safe and uses the data provided to AG Grid to conditionally multiply a value within our data set.

```tsx
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;
    };
```

Let’s review the code above:

- First, we declare the `multiplierValueGetter()` higher-order function. Using a higher-order function enables us to define the generic type `T` that extends a `Record` whose values are of type `number`. The higher-order function will return the value getter function that will be invoked by AG Grid with provided `ValueGetterParams<T>`.
- The `multiplierValueGetter()` has two required parameters, first, the `value` property, and second, the `multiplier` property, both of which are keys of the data provided to the grid that is of type `T`.
- Because we are using AG Grid v28 (or greater) we can specify the generic type of `T` for the `ValueGetterParams`. Prior to version 28, this generic type was not available, and as a result the type definition for the `data` property was `any`.
- Within the value getter function, if `data` is `undefined`, which can be the case when using infinite row model or row grouping in AG Grid, we return `0`.
- Finally, we can round the value after multiplying.

Here is an example implementation of our `multiplierValueGetter()` higher-order function.

```tsx
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}
    />
  );
}
```

## Using the `valueFormatter()` callback function

After the cell value is known, the optional `valueFormatter()` callback function enables us to format the value.
Let’s look at an example of using the `valueFormatter()` callback function.

In this example, the requirement is to declare a reusable `decimalValueFormatter()` higher-order function that is type-safe and formats the specified data property to a specified length.

```tsx
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));
  };
```

Let’s review the code above:

- We have declared a `decimalValueFormatter()` higher-order function. This enables the implementation of this value formatter to specify two generic types: `TData` and `TValue`. The generic of `TData` represents the type for the `data` parameter, and the generic of `TValue` represents the type for the `value` parameter. Our higher-order function has an optional `digits` parameter that specifies the min and maximum number of digits for the decimal formatting. The higher-order function returns a function that is the value getter that is invoked by AG Grid with the `ValueGetterParams<TData, TValue>` object.
- In this value formatter, we are using the `Intl.NumberFormat` class to create a new formatter instance, specifying the minimum and maximum number of fraction digits.
- If the `data` is undefined, which can be the case when using infinite row model or row grouping in AG Grid, then we simply return 0.
- Otherwise, we return the formatted value.

Here is an example implementation of our `decimalValueFormatter()` higher-order function.

```tsx
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}
    />
  );
}
```

## Using the `cellRenderer()` callback function

After the value for a cell is determined, and we have optionally formatted the value, we can use a cell renderer to have full control of how a cell is rendered in AG Grid.
By default, all values are rendered as a string.
In order to render a cell other than a string, we can use a custom cell renderer.

It’s important to note that we should only use a cell renderer when necessary.
By default, the `textContent` of the cell HTML element is set to the (optionally formatted) value.
When we are using a cell renderer we are adding additional elements, event listeners, etc. to the DOM, all of which must be rendered for each cell in the grid.

Finally, we recommend that all cell renderers *strictly use* vanilla JS.
This will improve the paint performance of your application when scrolling the grid.
Why is that?
If you use a framework (e.g. React, Angular, or Vue) then as a result each time the cell needs to be rendered, AG Grid must switch the context to a React (or Angular or Vue) application context in order to render the resulting HTML to the DOM. This can be *very* expensive and is often not necessary.

<aside>
📣 Only use a cell renderer when necessary, limit the elements and event listeners to a minimum, and always use vanilla JS.
</aside>

To configure a cell renderer we can provide AG Grid with:

- A string that references a registered framework component
- A class that implements the `ICellRendererComp` interface
- A function that is invoked with the `ICellRendererParams` object

Let’s look at an example. In this example, the user requirement is to display a column with a name that is optionally abbreviated, and, when a user clicks on the name, we want to open a dialog (which will not be the responsibility of AG Grid, but we need to notify the consumer that the user has clicked on the name).

First, let’s define a new interface that describes the contract between the implementation and the cell renderer for the data that is expected.

```tsx
export interface NameCellRendererData {
  id: string;
  name: string;
}
```

Next, let’s define another interface for the click event that will notify the implementation that the user has clicked on the name.

```tsx
export interface NameCellRendererClickEvent<T, E = Event> {
  event: E;
  data: T;
}
```

The `NameCellRendererClickEvent` describes the event handler object that will be provided to a `click` parameter that is implemented when using the cell renderer.
The interface has two generics:

1. First, we define a generic of `T` that will be provided for the row data.
2. Second, we have a generic of `E` that has a default assignment to the global `Event` interface. In the cell renderer we can set a type that is narrower.

Now, let’s define another interface for the parameters that will be provided to the cell renderer.

```tsx
export interface NameCellRendererParams<T> {
  click: (event: NameCellRendererClickEvent<T>) => void;
  document: Document;
  isAbbreviated?: (params: ValueGetterParams<T>) => boolean;
}
```

A few things to note:

- First, we have declared the generic type of `T` in order to maintain type checking of the `params` object that is invoked for the `isAbbreviated` function.
- The `click` parameter will be a callback function that is invoked by the cell renderer. The callback function is invoked with an `event` parameter that is the `NameCellRendererClickEvent` interface.
- The `isAbbreviated` parameter is another callback function that enables the implementing grid to determine if a specific cell value should be abbreviated. We'll use the `ValueGetterParams` interface provided by AG Grid to keep our API ergonomic (in that we expect the developer to be aware of this existing interface, so it makes sense to use it).

Having described the API, let’s look at the code for the cell renderer.

```tsx
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;
  }
}
```

Ok, phew. Let’s review the code above.

- First, we define a new `Params` type that is a union of our `NameCellRendererParams` interface and the AG Grid provided `ICellRendererParams`. The generic type `T` is the provided type for the AG Grid row data, which we further provide to the `ICellRendererParams` interface. The second typescript generic is explicitly set to `string` as we expect that the `value` of the cell will always be a string.
- We export the `NameCellRenderer` class whose generic type `T` extends our previously defined `NameCellRendererData` interface. This ensures that we have type safety between the row data provided to AG Grid and our cell renderer. As required, our class implements the `ICellRendererComp` interface from AG Grid.
- We have a lot of properties that declared that will have references and values as necessary to pass to the `isAbbreviated` provided callback function.
- Note that the `click` property is the provided callback function from the implementation that is invoked when the user clicks on the name.
- Further, note that the `handleClick` property is an execution-bound function that we’ll use within the cell renderer class for adding and removing the event listener.
- The `valueGetterParams` property accessor method returns a `ValueGetterParams<T>` object that is used by the implementation to determine if a name is abbreviated or not. We have decided to use this interface from AG Grid to keep a consistent API for our users (those developers using our cell renderer in their AG Grid implementations). This is important for API ergonomics.
- The `init()`, `getGui()`, `refresh()`, and `destroy()` methods are all implemented according to the `ICellRendererComp` interface from AG Grid. These methods provide hooks to initialize the cell renderer, provide an HTML element to be appended to the DOM by AG Grid when rendering a cell, and more hooks for when the data is refreshed and when the cell is destroyed. It’s important that we use the `destroy()` lifecycle method to do any necessary cleanup, such as removing event listeners, to prevent memory leaks in our application.

Finally, here is an example implementation of the `NameCellRenderer`.

```tsx
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}
    />
	);
}
```

## Summary

So In summary, we have learned how AG Grid renders a cell, and how we can provide data to a cell, optionally format a cell, and if necessary, customize the rendering of a cell.
The key takeaways are:

- Use the `valueGetter()` callback function to fetch and/or mutate the value of a cell.
- Use the `valueFormatter()` callback function to format a cell’s value.
- If necessary, provide a cell renderer to customize the HTML of a cell.
- Cell renderers can also be interactive, invoke callback functions, and more.
- It’s important to remove event listeners when a cell is destroyed.
- Design an API that is ergonomic.
- Create value getters, value formatters, and cell renderers that are type safe.