3.0
Create your own cell template

Introduction

Creating a cell template is the best way to customize data visualization and behaviour in ReactGrid. You can define your own one and then use it as other built-in cell types.

In this example, we will explain how to make it.

Let's imagine that you need to implement a brand new cell for displaying a country flag.

Expected functionality:

  1. In edit mode a cell allows you to type in text without any constraints
  2. When a cell is displayed, it should show a country flag based on the cell text

Let's get started!

1. Define flag cell interface

For this tutorial we are going to use the previous project - handling changes.

type attribute is necessary in any cell template, in our sample we will refer to this cell template by flag. text attribute will store our text and it's also necessary.

export interface FlagCell extends Cell {
  type: "flag";
  text: string;
}

2. Data holders

At the beginning we should update columns and rows displayed by the grid. Cell type in row 1, 2 and 3 will be changed to flag at the end.

const [columns] = React.useState<Column[]>(() => [{ columnId: 0, width: 100 }]);
const [rows, setRows] = React.useState<Row<DefaultCellTypes | FlagCell>[]>(
  () => [
    {
      rowId: 0,
      height: 40,
      cells: [{ type: "header", text: "Flag" }],
    },
    {
      rowId: 1,
      height: 40,
      cells: [{ type: "text", text: "rus" }],
    },
    {
      rowId: 2,
      height: 40,
      cells: [{ type: "text", text: "usa" }],
    },
    {
      rowId: 3,
      height: 40,
      cells: [{ type: "text", text: "" }],
    },
  ]
);

3. Creating necessary files

Create new file named FlagCellTemplate.tsx, add the same imports as in the listing below and make flag-cell-style.cssfile that will contain some CSS styles (don't forget to import it into your project's file).

// FlagCellTemplate.tsx
import * as React from "react";
import {
  CellTemplate,
  Cell,
  Compatible,
  Uncertain,
  UncertainCompatible,
  isNavigationKey,
  getCellProperty,
  isAlphaNumericKey,
  keyCodes,
} from "@silevis/reactgrid";
import "./flag-cell-style.css";

4. Creating FlagCellTemplate class

For all defined methods below it is necessary to display and interface the flag cell with internal ReactGrid model correctly.

  • getCompatibleCell as a parameter gets an incoming cell and returns a compatible cell (with text and value attribute required by Compatible type). In more complex examples getCellProperty may throw an exception if the required cell field is undefined.
getCompatibleCell(uncertainCell: Uncertain<FlagCell>): Compatible<FlagCell> {
    const text = getCellProperty(uncertainCell, 'text', 'string');
    const value = parseFloat(text);
    return { ...uncertainCell, text, value };
}
  • handleKeyDown method handles keyDown event on this cell template. Here it just returns unchanged cell and enables edit mode when a user performed a click or pressed ENTER key.
handleKeyDown(
    cell: Compatible<FlagCell>,
    keyCode: number,
    ctrl: boolean,
    shift: boolean,
    alt: boolean
): { cell: Compatible<FlagCell>; enableEditMode: boolean } {
    if (!ctrl && !alt && isAlphaNumericKey(keyCode))
        return { cell, enableEditMode: true };
    return {
        cell,
        enableEditMode: keyCode === keyCodes.POINTER || keyCode === keyCodes.ENTER
    };
}
  • update - as we are not sure if an incoming cell has the same interface like FlagCell so we mark it as UncertainCompatible (the incoming cell has attributes provided by Compatible but it can have other attributes like date from DateCell). In our case, we just copy cell and replace text value.
update(cell: Compatible<FlagCell>, cellToMerge: UncertainCompatible<FlagCell>): Compatible<FlagCell> {
    return this.getCompatibleCell({ ...cell, text: cellToMerge.text });
}
  • render method returns the content of a cell. In edit mode we will display input element. The change which is made in the cell input is propagated outside ReactGrid by onCellChanged function.
render(
    cell: Compatible<FlagCell>,
    isInEditMode: boolean,
    onCellChanged: (cell: Compatible<FlagCell>, commit: boolean) => void
): React.ReactNode {
  if (!isInEditMode) {
    const flagISO = cell.text.toLowerCase(); // ISO 3166-1, 2/3 letters
    const flagURL = `https://restcountries.eu/data/${flagISO}.svg`;
    const alternativeURL = `https://upload.wikimedia.org/wikipedia/commons/0/04/Nuvola_unknown_flag.svg`;
    return (
      <div
        className="rg-flag-wrapper"
        style={{ backgroundImage: `url(${flagURL}), url(${alternativeURL})` }}
      />
    );
  }
  return (
    <input
      ref={input => {
        input && input.focus();
      }}
      defaultValue={cell.text}
      onChange={e =>
        onCellChanged(
          this.getCompatibleCell({ ...cell, text: e.currentTarget.value }),
          false
        )
      }
      onCopy={e => e.stopPropagation()}
      onCut={e => e.stopPropagation()}
      onPaste={e => e.stopPropagation()}
      onPointerDown={e => e.stopPropagation()}
      onKeyDown={e => {
      if (isAlphaNumericKey(e.keyCode) || isNavigationKey(e.keyCode))
        e.stopPropagation();
      }}
    />
  );
}

5. Styling

To set a flag as the background covering the div element, we created CSS rg-flag-wrapper class. All of the cells have a class name created based on the cell type attribute. In our case, the cell class name will be rg-flag-cell. As rg-cell is a flex element we center its content with justify-content: center; attribute.

.rg-flag-cell {
  justify-content: center;
}
 
.rg-flag-wrapper {
  width: 50%;
  height: 80%;
  background-size: cover;
  border: 1px solid #cccccc;
  background-position: center center;
  background-repeat: no-repeat;
}

6. Finishing

Go back to index.tsx file and add customCellTemplates property as shown below and also change type: "text" to type: "flag":

// ...
const [rows, setRows] = React.useState<Row<DefaultCellTypes | FlagCell>[]>(
  () => [
    {
      rowId: 0,
      height: 40,
      cells: [{ type: "header", text: "Flag" }],
    },
    {
      rowId: 1,
      height: 40,
      cells: [{ type: "flag", text: "rus" }], // highlight-line
    },
    {
      rowId: 2,
      height: 40,
      cells: [{ type: "flag", text: "usa" }], // highlight-line
    },
    {
      rowId: 3,
      height: 40,
      cells: [{ type: "flag", text: "" }], // highlight-line
    },
  ]
);
 
return (
  <ReactGrid
    rows={rows}
    columns={columns}
    onCellsChanged={handleChanges}
    customCellTemplates={{ flag: new FlagCellTemplate() }} //highlight-line
  />
);

Result

ReactGrid

Code


  interface Flag {
  isoCode: string;
}

const getFlags = (): Flag[] => [
{ isoCode: "pl" },
{ isoCode: "de" },
{ isoCode: "mx" },
{ isoCode: "" }
];

const getColumns = (): Column[] => [{ columnId: "flag", width: 150 }];

const headerRow: Row = {
rowId: "header",
height: 40,
cells: [{ type: "header", text: "Flags" }]
};

const getRows = (flags: Flag[]): Row<DefaultCellTypes | FlagCell>[] => [
headerRow,
...flags.map<Row<DefaultCellTypes | FlagCell>>((flag, idx) => ({
rowId: idx,
height: 60,
cells: [{ type: "flag", text: flag.isoCode }]
}))
];

function App() {
const [flags] = React.useState<Flag[]>(getFlags());

const rows = getRows(flags);
const columns = getColumns();

return (
<ReactGrid
rows={rows}
columns={columns}
customCellTemplates={{ flag: new FlagCellTemplate() }}
/>
);
}

render(<App />, document.getElementById("root"));


ReactGrid

Preview