Skip to content

Home for our reusable ag-grid component

Notifications You must be signed in to change notification settings

linz/step-ag-grid

Repository files navigation

step-ag-grid

semantic-release: angular

Reusable ag-grid component for LINZ / Toitū te whenua.

Features

  • ag-grid-community based grid with custom popover components implemented using a modified react-menu.
  • Default components
    • Text input
    • Text area
    • Drop-down
    • Multi-select
    • Multi-select-grid
    • Bearing/Bearing Correction
    • Popover message
    • Custom form
    • Context menu

Please note this requires React >=17, ag-grid-community >=28, and SASS.

Install

with npm

npm install @linzjs/step-ag-grid

or with Yarn

yarn add @linzjs/step-ag-grid

Demo

npm run storybook

Storybook demo deployed at: https://master--633cd0dc2fe91d7df3ed32e4.chromatic.com/

Usage

Check src\stories for more usage examples

import { useMemo } from "react";

import "@linzjs/lui/dist/fonts";
import "@linzjs/lui/dist/scss/base.scss";
import {
  ColDefT,
  GridCell,
  GridCellFiller,
  GridContextProvider,
  GridContextMenuComponentProps,
  GridPopoverEditDropDown,
  GridPopoverMessage,
  GridUpdatingContextProvider,
  GridWrapper,
  GridFilters,
  GridFilterQuick,
  GridFilterButtons
} from "@linzjs/step-ag-grid";
// Only required for LINZ themes otherwise import the default theme from ag-grid
import "@linzjs/step-ag-grid/dist/GridTheme.scss";
import "@linzjs/step-ag-grid/dist/index.css";
import { GridFilterDownloadCsvButton } from "./GridFilterDownloadCsvButton";

const GridDemo = () => {
  interface ITestRow {
    id: number;
    name: number;
    position: string;
  }

  const columnDefs: ColDefT<ITestRow>[] = useMemo(
    () => [
      GridCell({
        field: "id",
        headerName: "Id",
        export: false,
      }),
      // This is the flex column that will expand to fit
      GridCell<ITestRow, string>({
        field: "name",
        headerName: "Name",
        flex: 1,
        cellRendererParams: {
          warning: ({ value }) => value === "Tester" && "Testers are testing",
          info: ({ value }) => value === "Developer" && "Developers are awesome",
        },
      }),
      GridPopoverEditDropDown(
        {
          field: "position",
          headerName: "Position",
        },
        {
          multiEdit: false,
          editorParams: {
            options: ["Architect", "Developer", "Product Owner", "Scrum Master", "Tester", MenuSeparator, "(other)"],
          },
        },
      ),
      GridPopoverMessage(
        {
          headerName: "Popout message",
          cellRenderer: () => <>Click me!</>,
        },
        {
          multiEdit: true,
          editorParams: {
            message: async ({selectedRows}) => {
              return `There are ${selectedRows.length} row(s) selected`;
            },
          },
        },
      ),
      // If your flex column gets hidden this will become active
      GridCellFiller(),
    ],
    [],
  );

  const ContextMenu = ({ selectedRows, colDef, close }: GridContextMenuComponentProps<ITestRow>): ReactElement => {
    const onClick = useCallback(() => {
      selectedRows.forEach((row) => {
        switch (colDef.field) {
          case "name":
            row.name = "";
            break;
          case "distance":
            row.distance = null;
            break;
        }
      });
      close();
    }, [close, colDef.field, selectedRows]);

    return (
      <>
        <button onClick={onClick}>Button - Clear cell</button>
        <MenuItem onClick={onClick}>Menu Item - Clear cell</MenuItem>
      </>
    );
  };
  
  const rowData: ITestRow[] = useMemo(
    () => [
      { id: 1000, name: "Tom", position: "Tester" },
      { id: 1001, name: "Sue", position: "Developer" },
    ],
    [],
  );

  return (
    <GridUpdatingContextProvider>
      <GridContextProvider>
        <GridWrapper>
          <GridFilters>
            <GridFilterQuick/>
            <GridFilterButtons<ITestRow>
              options={[
                {
                  label: "All",
                },
                {
                  label: "Developers",
                  filter: (row) => row.position === "Developer",
                },
                {
                  label: "Testers",
                  filter: (row) => row.position === "Tester",
                },
              ]}
            />
            <GridFilterColumnsToggle/>
            <GridFilterDownloadCsvButton fileName={"exportFile"}/>
          </GridFilters>
          <Grid selectable={true}
                columnDefs={columnDefs}
                rowData={rowData}
                contextMenu={contextMenu}
                contextMenuSelectRow={false}
                onContentSize={({ width }) => setPanelSize(width)} />
        </GridWrapper>
      </GridContextProvider>
    </GridUpdatingContextProvider>
  );
};

Bulk editing

If you are editing a cell and tab out of the cell, the grid will edit the next editable cell.

At this point you can send the change to the back-end immediately and then wait for an update response OR you could cache the required change, update then cell locally, and then wait for the callback <Grid onCellEditingComplete={fn}/> which will get invoked when the grid cannot find any more editable cells on the grid row, which will speed up editing.

Grid sizing

Grid uses <Grid sizeColumns="auto"/> which sizes by cell content by default. To ignore cell content use "fit", to disable use "none".

If you are within a resizable window/dialog/container there is a callback parameter <Grid onContentSize={({ width }) => setPanelSize(width)}/> to receive the recommended container width.

CSV Download

CSV download relies on column valueFormatters vs ag-grid's default valueGetter implementation. If you use a customRenderer for a column be sure to include a valueFormatter. To disable this behaviour pass undefined to processCellCallback. <GridFilterDownloadCsvButton processCellCallback={undefined}/>

To exclude a column from CSV download add export: false to the GridCell definition.

Writing tests

The following testing calls can be imported from step-ag-grid:

  • findRow
  • queryRow
  • selectRow
  • deselectRow
  • findCell
  • selectCell
  • editCell
  • findOpenMenu
  • validateMenuOptions
  • queryMenuOption
  • findMenuOption
  • clickMenuOption
  • openAndClickMenuOption
  • getMultiSelectOptions
  • findMultiSelectOption
  • clickMultiSelectOption
  • typeOnlyInput
  • typeInputByLabel
  • typeInputByPlaceholder
  • typeOtherInput
  • typeOtherTextArea
  • closeMenu
  • findActionButton
  • clickActionButton
import { render, screen } from "@testing-library/react";
import { waitFor } from "@testing-library/react";
import { findRow, GridUpdatingContextProvider, openAndClickMenuOption } from "@linzjs/step-ag-grid";

const TestComponent = (): JSX.Element => {
  return (
    <GridUpdatingContextProvider>
      <MyGrid />
    </GridUpdatingContextProvider>
  );
};

test("click Delete menu option removes row from the table", async () => {
  await render(<TestComponent />);
  await screen.findByText("My component header");
  expect((await findRow(12345)).getAttribute("row-index")).toBe("1");
  await openAndClickMenuOption(12345, "actions", "Delete");
  await waitFor(async () => expect((await queryRow(12345)).not.toBeDefined()));
});

Playwright support

If your grid has a data-testid a global will be exposed in window with the helper scrollRowIntoViewById. This will throw an exception if the row id is not found.

window.__stepAgGrid.grids[dataTestId].scrollRowIntoViewById("1000")