npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gp-grid/angular

v1.2.0

Published

A high-performance Angular data grid component with virtual scrolling, cell selection, sorting, filtering, and Excel-like editing

Readme

@gp-grid/angular 🏁 🏎️

A high-performance, feature-lean Angular data grid built to handle millions of rows. Thin wrapper around @gp-grid/core — virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle, with a standalone gp-grid component driven by Angular signals.

Installation

pnpm add @gp-grid/angular

Peer requirements: @angular/common and @angular/core >=18.0.0.

Quick Start

import { Component } from "@angular/core";
import { GpGridComponent, createGridData } from "@gp-grid/angular";
import type { AngularColumnDefinition } from "@gp-grid/angular";

interface Person {
  id: number;
  name: string;
  age: number;
}

@Component({
  selector: "app-root",
  standalone: true,
  imports: [GpGridComponent],
  template: `
    <gp-grid
      [columns]="columns"
      [dataSource]="grid.dataSource"
      [rowHeight]="36" />
  `,
})
export class App {
  protected readonly grid = createGridData<Person>(
    [
      { id: 1, name: "Alice", age: 30 },
      { id: 2, name: "Bob", age: 25 },
    ],
    { getRowId: (row) => row.id },
  );

  protected readonly columns: AngularColumnDefinition[] = [
    { field: "id", cellDataType: "number", headerName: "ID", width: 80 },
    { field: "name", cellDataType: "text", headerName: "Name", width: 200 },
    { field: "age", cellDataType: "number", headerName: "Age", width: 100 },
  ];
}

Import the stylesheet once (e.g. in styles.css or angular.json):

@import "@gp-grid/angular/dist/styles.css";

For custom cell, edit, and header renderers, pass ng-template references via the column cellRenderer / editRenderer / headerRenderer fields — see the Angular docs for the full API.

Dependency injection

For components that want lifecycle-managed cleanup or testable seams, use provideGridData and injectGridData. They wire the same mutable data source through Angular's DI, mirroring useGridData in @gp-grid/react and @gp-grid/vue. The service implements OnDestroy and clears the data source automatically when the component is destroyed.

import { Component } from "@angular/core";
import {
  GpGridComponent,
  provideGridData,
  injectGridData,
} from "@gp-grid/angular";
import type { AngularColumnDefinition } from "@gp-grid/angular";

interface Person {
  id: number;
  name: string;
  age: number;
}

const initialRows: Person[] = [
  { id: 1, name: "Alice", age: 30 },
  { id: 2, name: "Bob", age: 25 },
];

@Component({
  selector: "app-root",
  standalone: true,
  imports: [GpGridComponent],
  providers: [
    provideGridData<Person>({
      getRowId: (row) => row.id,
      initialData: initialRows,
    }),
  ],
  template: `
    <gp-grid
      [columns]="columns"
      [dataSource]="grid.dataSource"
      [rowHeight]="36" />
    <button (click)="grid.addRows([{ id: 3, name: 'Carol', age: 28 }])">Add</button>
  `,
})
export class App {
  protected readonly grid = injectGridData<Person>();

  protected readonly columns: AngularColumnDefinition[] = [
    { field: "id", cellDataType: "number", headerName: "ID", width: 80 },
    { field: "name", cellDataType: "text", headerName: "Name", width: 200 },
    { field: "age", cellDataType: "number", headerName: "Age", width: 100 },
  ];
}

provideGridData returns a standard Angular Provider[], so it composes with other provide* functions in the component's providers array. Register it on the consuming component (not on a parent injector) so each component instance gets its own data source. injectGridData returns the GridDataService behind it, and GRID_DATA_OPTIONS is the injection token provideGridData registers its GridDataOptions under, for a test that wants to provide them by hand.

Column state and events

columnState is an optional input of type ColumnStateUpdate[] — { columnId, width?, hidden?, order?, pinned? } — applied through the core whenever it changes.

columnLayout is an optional input of type "fit" | "fixed" (default "fit"). "fit" expands columns without an explicit pixel override so their total reaches the body scroll container's width; "fixed" keeps declared/overridden widths and scrolls horizontally. Changing the input switches the mode without recreating the core. columns.getState() reports width only while an explicit override exists, plus resolvedWidth (displayed CSS px, 0 while hidden).

columnOverscan (default 240 CSS px) is how far past each clip edge center columns stay mounted. The default header cycles through physical left, physical right and unpinned. pinIcon replaces its SVG; labels.pinLeftColumn, labels.pinRightColumn and labels.unpinColumn replace its accessible names.

Pinning: set pinned: "start" or "end" on a definition, or call core.columns.setPinned(columnId, "start" | "end" | null) on the exposed core (grid.core via @ViewChild). A pin is a request — when the viewport cannot fit it the column renders in the scrolling center and is admitted again once there is room, so read region from columns.getState(). Only the admitted pins plus a window of center columns are mounted. See Column pinning.

Frozen rows: freezeRows is an optional input of type { count, maxCount?, minSuffixHeight? } (defaults count 0, maxCount 100, minSuffixHeight 64) that keeps the displayed rows [0, count) fixed below the header. count is a request — the effective count is bounded by maxCount, by the viewport (a frozen prefix always leaves minSuffixHeight for the suffix) and, with a paginated source, by the page budget — so read grid.core.frozenRows.get() for { requestedCount, effectiveCount, limit }. The (onFrozenRowsChanged) output fires whenever either reported field changes, not for the initial resolution and not per scroll. A reduced prefix is announced through labels.frozenRowsLimited in a visually hidden live region. Changing the input after mount applies the new count through core.frozenRows.set without recreating the component or resetting scroll; the runtime setter also corrects the scroll position so the visible suffix stays anchored, and an equal-valued object is silent. See Frozen rows.

Row heights: rows are rowHeight px tall unless you set a height by row identity through the exposed core (grid.core via @ViewChild):

grid.core?.rowHeights.set([{ rowId: 2, height: 96 }]);
grid.core?.rowHeights.reset([2]);   // or reset() for all of them
grid.core?.rowHeights.getOverrides();

A height must be finite and > 0; set is all or nothing, so one invalid entry throws a RangeError and applies nothing. With getRowId a height follows its row through sort, filter, refresh and paging, and an ID whose page has not loaded yet waits for its row. Without getRowId, an integer rowId addresses a view index and is dropped at the next data revision. Applying a height anchors the row at the clip top and corrects the scroll position in the same batch, so the viewport does not jump; row boxes are sized from slot.height and the row drag ghost from rd.sourceRowHeight. Nothing is measured continuously; a fit measures once (below). See Row heights.

Row resize and auto-fit: [rowResize]="true" lets the user drag a row edge, and a double-click on a row or column edge fits it to its rendered content once. [autoFit] ({ maxColumnWidth?, minRowHeight?, maxRowHeight? }, defaults 600, rowHeight and 10 × rowHeight) bounds the fits and the row drag, and (onRowResized) reports each changed row. The commands are on the exposed core:

grid.core?.columns.fit(["name"]); // { status, columns, skipped, ... }
grid.core?.rowHeights.fit();       // every mounted row
grid.core?.columns.setState([{ columnId: "name", width: null }]); // drop the width

A fit reads the mounted cells only and stores the result like a resize. Call it after the render that follows a column change: a fit in the same task returns "stale" and applies nothing. The edge handles are pointer-only (aria-hidden); the keyboard equivalents are Alt+ArrowLeft/Right and Alt+ArrowUp/Down (resize), Alt+Shift+ArrowLeft/Right (move the column), Alt+Enter (fit the column) and Alt+Shift+Enter (fit the row). See Auto-fit and row resize.

Column groups: [columnGroups] (AngularColumnGroupChild[]) nests the headers over the column ids, every column referenced once and ungrouped ones at the root, and [headerBandHeights] sets each band's height (default headerHeight); both apply at runtime. An invalid hierarchy is rejected: the grid keeps the previous one and emits (onColumnSchemaRejected) with { code, source, id?, limit?, message }. The input stays as given and is re-applied with every later columns change, so restore a valid hierarchy. [columnGroupLimits] (maxDepth 64, maxNodes 100,000, maxFragments 100,000) is read at creation. A group's headerRenderer is a TemplateRef receiving ColumnGroupHeaderParams as $implicit, or a key of [headerRenderers]. Under strict templates the published input typings do not accept undefined: cast an absent hierarchy and bind [] for no band heights. See Column groups and header bands.

Row grouping: [rowGrouping] takes a createRowGrouping({ dimensions, measures?, grandTotal? }) and groups the rows of a source loaded in full; keep it in a field, because a new object regroups. Every group starts collapsed. The expander and label sit in [groupLabelColumn] (default: the first displayed column) and each aggregate under the column named by the measure's field; aggregates are displayed unrounded, so give a numeric measure column a valueFormatter. [groupLabelRenderer] is a TemplateRef (GroupLabelRendererTemplate) whose $implicit is { row, viewIndex, label, toggle }. A cell template receives rowKind on an aggregate cell, with rowData undefined, and is not rendered for a group cell without an aggregate.

import { createRowGrouping } from "@gp-grid/angular";

protected readonly grouping = createRowGrouping({
  dimensions: [{ field: "country" }, { field: "city" }],
  measures: [{ field: "amount", aggregate: "sum" }],
  grandTotal: "top",
});
<gp-grid #grid [columns]="columns" [rows]="rows" [rowHeight]="32" [getRowId]="getRowId"
  [rowGrouping]="grouping" groupLabelColumn="country"
  (onRowGroupToggled)="onToggled($event)"
  (onRowGroupingRejected)="onRejected($event)" />
<button (click)="grid.core?.rowGroups.setExpanded(null, true)">Expand all</button>

A pointer down on the expander, a double-click on a group row, and Enter or Space on its active cell toggle it. Editing a measure cell updates the aggregates on its path; editing a dimension cell moves the record, expands its new groups and keeps the active cell on it. Group and total rows are read-only, and row drag is disabled while grouped. A paginated source, an unknown field or an object key without toKey is rejected: the grid stays flat and emits (onRowGroupingRejected). A data source can also return rows it grouped itself as a HierarchicalRowAccess. Under strict templates the published input typings do not accept null or undefined: cast to ungroup (null as unknown as RowGrouping). Angular creates its core on the server, so a server render may already contain the collapsed group rows (role="treegrid"). The label cell's parts, GroupToggleComponent and GroupLabelComponent, are exported. See Row grouping and aggregation.

Server grouping: for a dataset that stays on a server, bind createServerGroupingSource(service, config) to [dataSource], and no [rowGrouping]. The grid asks your service for one group's children or leaves a page at a time and never loads a collapsed group's leaves. Rows not loaded yet render as loading rows, and a group whose page failed collapses with an error and retries when expanded again. The source is read-only unless created with writable: true; keep it in a field. See Server grouping.

<gp-grid [columns]="columns" [dataSource]="source" [rowHeight]="32" groupLabelColumn="country" />

Pivot: createPivotSource(source, config) wraps a flat source that holds every record and answers a read-only hierarchy over columns generated per key tuple and measure. Pass it as [dataSource] and the schema from subscribeSchema as [columns] and [columnGroups], kept in a signal; buildPivotSchema(keys, options) builds the same schema from keys you computed yourself, without the engine. See Pivot.

<gp-grid [columns]="schema().columns" [columnGroups]="schema().columnGroups" [dataSource]="source"
  [rowHeight]="32" [groupLabelColumn]="labelColumnId" />

Writes and history: every edit, paste, fill and cells.setValue is one write command reported through the (onWriteCommitted) output with its changes by record id and the targets it skipped; [history]="{ maxEntries: 100 }" turns on undo and redo of those commands by record id (core.history.undo(), Ctrl+Z, Ctrl+Y). See Write commands and history.

An editor commit is coerced by the column's cellDataType, like paste: "60000" typed into a number column stores 60000, and a draft the type cannot hold writes nothing.

| Output | Payload | | --- | --- | | (onColumnResized) | { columnId, width, viewIndex } | | (onColumnMoved) | { columnId, fromViewIndex, toViewIndex } | | (onColumnPinned) | { columnId, pinned } (null when unpinned) | | (onFrozenRowsChanged) | FrozenRowsState — { requestedCount, effectiveCount, limit } | | (onRowResized) | { rowId, height, viewIndex } per row a drag, a key or a fit changed | | (onColumnSchemaRejected) | ColumnSchemaError — { code, source, id?, limit?, message } | | (onRowGroupToggled) | { rowId, expanded } per group a pointer or key gesture toggled | | (onRowGroupingRejected) | RowGroupingRejection — { reason, field? } | | (onRowDragEnd) | { rowId, fromViewIndex, toViewIndex } | | (onCellValueChanged) | CellValueChangedEvent<TData>; it gained columnId, and colIndex is the current view column index |

<gp-grid
  [columns]="columns"
  [columnState]="columnState"
  [dataSource]="grid.dataSource"
  [rowHeight]="36"
  (onColumnResized)="onResized($event)"
  (onColumnMoved)="onMoved($event)"
  (onColumnPinned)="onPinned($event)" />

Migration from 0.x

The full list of changes from 0.23.x, with before/after code, is in the migration guide.

The old payloads { colIndex, newWidth }, { fromIndex, toIndex } and { source, target } became { columnId, width, viewIndex }, { columnId, fromViewIndex, toViewIndex } and { rowId, fromViewIndex, toViewIndex }. There is no compatibility adapter.

Reassigning columns reconciles the schema by column id (colId ?? field) without recreating the core: retained columns keep their user width/order/visibility, unrelated sort/filter/scroll survives, and removed columns drop their headers and state. A definition width/hidden change only applies when the column has no user override for that property; otherwise call columns.resetState(["id"]) on the exposed core.

The public website documentation for this package lives outside this repository and should be updated by the maintainer.

License

Apache-2.0 — see LICENSE.