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

dv-datagrid

v1.0.0

Published

A feature-rich Angular data grid library built with signals and standalone components.

Readme

dv-datagrid

A feature-rich Angular data grid library built with signals and standalone components.

Live Demo →

Features

  • Server-side data model (sorting, filtering, pagination driven by your API)
  • Column sorting (single column)
  • Column filtering — text, number, date, set
  • Pagination with configurable page sizes
  • Row selection — single and multi mode with checkboxes
  • Expandable rows with custom detail templates
  • Cell tooltips — field-based or computed
  • Custom cell renderers — Angular components or TemplateRef
  • Cell value formatters and CSS class callbacks
  • Localization — built-in English and Ukrainian, fully customizable
  • Themes — alpine, material, dark, custom
  • Generic row typing — DvDataGrid<T> for type-safe column definitions

Installation

npm install dv-datagrid

Build the library locally if working in the monorepo:

ng build dv-datagrid

Quick start

app.ts

import { Component } from '@angular/core';
import { DvColDef, DvDataGrid, DvGridApi, DvGridOptions, ServerRequestParams } from 'dv-datagrid';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-root',
  imports: [DvDataGrid],
  template: `
    <dv-datagrid
      [columnDefs]="columnDefs"
      [options]="options"
      (gridReady)="onGridReady($event)"
      (serverDataRequested)="onServerDataRequested($event)"
    />
  `,
})
export class AppComponent {
  columnDefs: DvColDef<User>[] = [
    { field: 'id',    headerName: 'ID',    sortable: true, width: 80 },
    { field: 'name',  headerName: 'Name',  sortable: true, filter: 'text' },
    { field: 'email', headerName: 'Email', sortable: false, filter: 'text' },
  ];

  options: DvGridOptions = {
    pagination: true,
    paginationPageSize: 20,
  };

  private api!: DvGridApi;

  onGridReady(api: DvGridApi) {
    this.api = api;
  }

  onServerDataRequested(params: ServerRequestParams) {
    // fetch from your API and call:
    this.api.setServerData(rows, totalCount);
  }
}

Component inputs & outputs

<dv-datagrid
  [columnDefs]="columnDefs"
  [options]="options"
  [detailTemplate]="myDetailTpl"
  (gridReady)="onGridReady($event)"
  (serverDataRequested)="onServerDataRequested($event)"
  (selectionChanged)="onSelectionChanged($event)"
/>

| Input / Output | Type | Description | |---|---|---| | [columnDefs] | DvColDef<T>[] | Column definitions | | [options] | DvGridOptions | Grid configuration | | [detailTemplate] | TemplateRef<{ $implicit: T; rowIndex: number }> | Template for expandable row detail | | (gridReady) | DvGridApi | Fires once on init; provides the API handle | | (serverDataRequested) | ServerRequestParams | Fires on every sort / filter / page change | | (selectionChanged) | any[] | Selected row IDs when selection changes |


DvGridOptions

interface DvGridOptions {
  // Pagination
  pagination?: boolean;               // default true
  paginationPageSize?: number;        // default 20
  paginationPageSizeOptions?: number[]; // default [10, 20, 50, 100]

  // Theme
  theme?: 'alpine' | 'material' | 'dark' | 'custom';

  // Selection
  rowSelection?: 'single' | 'multi';
  getRowId?: (row: any) => any;       // defaults to row.id then index

  // Row expansion
  rowExpansion?: { enabled: boolean };

  // Tooltip
  tooltipShowDelay?: number;          // ms, default 500

  // Localization
  locale?: Partial<GridLocale>;

  // Other
  rowModelType?: 'serverSide';
  enableColumnResize?: boolean;
}

Example

options: DvGridOptions = {
  pagination: true,
  paginationPageSize: 25,
  paginationPageSizeOptions: [10, 25, 100],
  theme: 'alpine',
  rowSelection: 'multi',
  getRowId: (row) => row.id,
  rowExpansion: { enabled: true },
  tooltipShowDelay: 300,
  locale: UK_LOCALE,
};

DvColDef

interface DvColDef<T = any> {
  field: string;                      // dot-notation supported: 'address.city'
  headerName?: string;                // column header label
  sortable?: boolean;                 // default true
  width?: number;                     // fixed column width in px

  // Filtering
  filter?: boolean | 'text' | 'number' | 'date' | 'set';
  filterValues?: any[];               // required for 'set' filter

  // Cell content
  valueFormatter?: (row: T) => any;
  cellRenderer?: Type<any> | TemplateRef<any>;
  cellRendererParams?: Record<string, any>;

  // Cell styling
  cellClass?: string | ((params: CellClassParams<T>) => string);

  // Tooltip
  tooltipField?: string;
  tooltipValueGetter?: (params: CellClassParams<T>) => string;
}

interface CellClassParams<T> {
  value: any;   // cell value
  row: T;       // full row object
  field: string;
  rowIndex: number;
}

Sorting

Enable sorting per column with sortable: true (the default). Clicking a header cycles through asc → desc → unsorted. Only one column can be sorted at a time.

columnDefs: DvColDef<Product>[] = [
  { field: 'name',  sortable: true },
  { field: 'price', sortable: true },
  { field: 'sku',   sortable: false }, // not sortable
];

The current sort state arrives in ServerRequestParams:

onServerDataRequested(params: ServerRequestParams) {
  const { colId, sort } = params.sortModel[0] ?? {};
  // sort === 'asc' | 'desc'
}

Filtering

Set filter on a column to enable filtering. A filter icon appears in the header and opens a dropdown.

Filter types

| Value | UI | |---|---| | 'text' | Operator (contains / equals / starts with / ends with) + text input | | 'number' | Operator (equals / greater than / less than / between) + number input | | 'date' | Operator (equals / before / after / between) + date picker | | 'set' | Checkbox list of values supplied via filterValues | | true | Shorthand for 'text' |

columnDefs: DvColDef<Order>[] = [
  { field: 'reference', filter: 'text' },
  { field: 'amount',    filter: 'number' },
  { field: 'createdAt', filter: 'date' },
  {
    field: 'status',
    filter: 'set',
    filterValues: ['pending', 'completed', 'cancelled'],
  },
];

The active filters arrive in ServerRequestParams.filterModel:

onServerDataRequested(params: ServerRequestParams) {
  for (const [field, filter] of Object.entries(params.filterModel)) {
    switch (filter.type) {
      case 'text':
        // filter.operator: 'ctns' | 'eq' | 'sw' | 'ew'
        // filter.value: string
        break;
      case 'number':
        // filter.operator: 'eq' | '>' | '<' | 'btw'
        // filter.value: number
        // filter.valueTo?: number  (for 'btw')
        break;
      case 'date':
        // filter.operator: 'eq' | 'before' | 'after' | 'btw'
        // filter.value: string  (ISO date)
        // filter.valueTo?: string  (for 'btw')
        break;
      case 'set':
        // filter.values: any[]
        break;
    }
  }
}

Pagination

Pagination is enabled by default. The grid emits serverDataRequested with the current page and pageSize whenever the user navigates.

options: DvGridOptions = {
  pagination: true,
  paginationPageSize: 20,
  paginationPageSizeOptions: [10, 20, 50, 100],
};

Apply it server-side:

onServerDataRequested(params: ServerRequestParams) {
  const { page, pageSize } = params;
  const start = (page - 1) * pageSize;
  const slice = allData.slice(start, start + pageSize);
  this.api.setServerData(slice, allData.length);
}

Programmatic navigation via the API:

this.api.setPage(3);
this.api.setPageSize(50);

Row selection

Single

Click a row to select it; click again to deselect.

options: DvGridOptions = {
  rowSelection: 'single',
  getRowId: (row) => row.id,
};

Multi

A checkbox column is added automatically. Click a row or its checkbox to toggle it. The header checkbox selects / deselects the current page.

options: DvGridOptions = {
  rowSelection: 'multi',
  getRowId: (row) => row.id,
};

Selection output

<dv-datagrid (selectionChanged)="onSelectionChanged($event)" />
onSelectionChanged(ids: any[]) {
  console.log('Selected IDs:', ids);
}

Selection API

this.api.selectRow(id);
this.api.deselectRow(id);
this.api.toggleRowSelection(id);
this.api.selectAll([id1, id2, id3]);
this.api.clearSelection();
this.api.isRowSelected(id); // boolean
// reactive
this.api.selectedRowIds(); // Signal<Set<any>>

Expandable rows

Enable row expansion to let users expand a row and reveal a custom detail panel below it.

Setup

options: DvGridOptions = {
  rowExpansion: { enabled: true },
  getRowId: (row) => row.id,
};
<dv-datagrid
  [options]="options"
  [columnDefs]="columnDefs"
  [detailTemplate]="detailTpl"
/>

<ng-template #detailTpl let-row let-rowIndex="rowIndex">
  <div style="padding: 16px">
    <p>Details for {{ row.name }}</p>
    <p>Row index: {{ rowIndex }}</p>
  </div>
</ng-template>

In the component, expose the template ref:

import { viewChild, TemplateRef } from '@angular/core';

readonly detailTpl = viewChild.required<TemplateRef<any>>('detailTpl');

Detail template context

| Variable | Type | Description | |---|---|---| | let-row ($implicit) | T | The full row object | | let-rowIndex="rowIndex" | number | Zero-based index of the row on the current page |

Nested grid example

<ng-template #detailTpl let-row>
  <div style="padding: 12px 16px">
    <dv-datagrid
      [columnDefs]="orderColumns"
      [options]="orderOptions"
      (serverDataRequested)="loadOrders(row.id, $event)"
      (gridReady)="onOrderGridReady(row.id, $event)"
    />
  </div>
</ng-template>

Column resizing

Enable interactive column resizing by dragging the handle on the right edge of any header cell.

options: DvGridOptions = {
  enableColumnResize: true,
};

Columns are resizable by default when the option is enabled. Opt individual columns out with resizable: false:

columnDefs: DvColDef<Order>[] = [
  { field: 'id',    width: 60,  resizable: false }, // fixed width, no handle
  { field: 'email' },                               // resizable (default)
];

| Property | Type | Description | |---|---|---| | enableColumnResize (options) | boolean | Enable resize handles on all columns (default: false) | | resizable (colDef) | boolean | Per-column override — opt in/out regardless of global option |


Cell renderers

Component renderer

Create a standalone Angular component with value and row inputs:

@Component({
  selector: 'app-status-cell',
  standalone: true,
  template: `
    <span [class]="'badge badge-' + value">{{ value }}</span>
  `,
})
export class StatusCellComponent {
  readonly value = input<string>('');
  readonly row   = input<any>(null);
}

Register it on the column:

{ field: 'status', cellRenderer: StatusCellComponent }

Pass extra data with cellRendererParams:

{
  field: 'price',
  cellRenderer: PriceCellComponent,
  cellRendererParams: { currency: 'USD' },
}

Inside the component, access params via the input:

readonly currency = input<string>('USD');  // from cellRendererParams

TemplateRef renderer

Declare a template in the same component and pass it via viewChild:

readonly statusTpl = viewChild.required<TemplateRef<any>>('statusTpl');

ngOnInit() {
  this.columnDefs = [
    { field: 'status', cellRenderer: this.statusTpl() },
  ];
}
<ng-template #statusTpl let-value let-row="row" let-rowIndex="rowIndex">
  <span [style.color]="value === 'active' ? 'green' : 'red'">
    {{ value }}
  </span>
</ng-template>

Template context variables:

| Variable | Description | |---|---| | let-value ($implicit) | Formatted cell value | | let-row="row" | Full row object | | let-field="field" | Column field name | | let-rowIndex="rowIndex" | Row index on the current page | | let-params="params" | Value of cellRendererParams |

Value formatter

For simple display-only transformations:

{ field: 'amount', valueFormatter: (row) => `$${row.amount.toFixed(2)}` }

Tooltips

Add a tooltip to any column via tooltipField or tooltipValueGetter.

Field-based tooltip

Displays the value of another field as the tooltip:

{ field: 'name', tooltipField: 'fullName' }

Computed tooltip

Full control over the tooltip string:

{
  field: 'status',
  tooltipValueGetter: (p) => `Last updated: ${p.row.updatedAt}`,
}

p is a CellClassParams<T> with value, row, field, and rowIndex.

Delay

options: DvGridOptions = {
  tooltipShowDelay: 300, // ms, default 500
};

Cell classes

Apply a static or dynamic CSS class to cells:

// static
{ field: 'amount', cellClass: 'text-right' }

// dynamic
{
  field: 'balance',
  cellClass: (p) => p.value < 0 ? 'negative' : 'positive',
}

DvGridApi reference

Obtain the API handle from the gridReady output:

private api!: DvGridApi;

onGridReady(api: DvGridApi) {
  this.api = api;
}

Data

| Method | Description | |---|---| | setServerData(rows, total) | Feed a page of rows and the total record count | | refreshServerData() | Re-emit serverDataRequested with current params |

Reactive state (signals)

| Signal | Type | Description | |---|---|---| | rowData | Signal<any[]> | Current page rows | | isLoading | Signal<boolean> | True while waiting for data | | sortModel | Signal<SortItem[]> | Active sort | | filterModel | Signal<FilterModel> | Active filters | | currentPage | Signal<number> | Current page number | | pageSize | Signal<number> | Rows per page | | totalRecords | Signal<number> | Total records across all pages | | totalPages | Signal<number> | Computed total pages | | selectedRowIds | Signal<Set<any>> | Currently selected row IDs |

Pagination

| Method | Description | |---|---| | setPage(page) | Navigate to a specific page | | setPageSize(size) | Change rows per page (resets to page 1) |

Filtering

| Method | Description | |---|---| | setColumnFilter(field, filter) | Apply a filter programmatically | | clearColumnFilter(field) | Remove a specific column filter | | resetFilters() | Clear all filters |

Sorting

| Method | Description | |---|---| | resetSort() | Clear active sort |

Selection

| Method | Description | |---|---| | selectRow(id) | Select a row by ID | | deselectRow(id) | Deselect a row by ID | | toggleRowSelection(id) | Toggle selection | | selectAll(ids) | Select multiple rows | | clearSelection() | Clear all selections | | isRowSelected(id) | Returns boolean |

Reset

| Method | Description | |---|---| | resetAll() | Clear sort, filters, go to page 1, reload |


ServerRequestParams

Emitted by serverDataRequested on every sort / filter / page change.

interface ServerRequestParams {
  page: number;
  pageSize: number;
  sortModel: SortItem[];      // max 1 item currently
  filterModel: FilterModel;   // Record<field, FilterInstance>
}

Typical handler:

onServerDataRequested(params: ServerRequestParams) {
  this.myService
    .getUsers(params)
    .subscribe(({ rows, total }) => {
      this.api.setServerData(rows, total);
    });
}

Localization

Two built-in locales are provided: EN_LOCALE (default) and UK_LOCALE.

import { EN_LOCALE, UK_LOCALE } from 'dv-datagrid';

options: DvGridOptions = { locale: UK_LOCALE };

Override individual keys:

options: DvGridOptions = {
  locale: {
    ...EN_LOCALE,
    noData: 'Nothing here yet',
    applyFilter: 'Search',
  },
};

Custom locale

Implement the full GridLocale interface:

import { GridLocale } from 'dv-datagrid';

const MY_LOCALE: GridLocale = {
  loading: 'Chargement...',
  noData: 'Aucune donnée',
  filterButtonTitle: 'Filtrer',
  reload: 'Recharger',
  filterMenuTitle: 'Filtrer',
  operator: 'Opérateur',
  value: 'Valeur',
  to: 'À',
  filterValuePlaceholder: 'Valeur...',
  toValuePlaceholder: 'Valeur de fin...',
  noValuesAvailable: 'Aucune valeur disponible',
  clearFilter: 'Effacer',
  applyFilter: 'Appliquer',
  textContains: 'Contient',
  textEquals: 'Égal à',
  textStartsWith: 'Commence par',
  textEndsWith: 'Se termine par',
  numberEquals: 'Égal à',
  numberGreaterThan: 'Supérieur à',
  numberLessThan: 'Inférieur à',
  numberBetween: 'Entre',
  dateEquals: 'Égal à',
  dateBefore: 'Avant',
  dateAfter: 'Après',
  dateBetween: 'Entre',
  noRecords: '0 enregistrement',
  rowsPerPage: 'Lignes :',
  of: 'sur',
  firstPage: 'Première page',
  previousPage: 'Page précédente',
  nextPage: 'Page suivante',
  lastPage: 'Dernière page',
};

Themes

options: DvGridOptions = {
  theme: 'alpine',   // 'alpine' | 'material' | 'dark' | 'custom'
};

| Theme | Description | |---|---| | (none) | Default — clean light theme | | alpine | Slightly bolder borders, blue accent | | material | Flat design, smaller radius | | dark | Dark background, light text | | custom | No overrides; style everything via CSS variables |

CSS variables

Override any design token on the host element:

dv-datagrid {
  --accent-color:      #7c3aed;
  --header-bg:         #1e1b4b;
  --header-text:       #e0e7ff;
  --header-border:     #3730a3;
  --cell-bg:           #f5f3ff;
  --cell-text:         #1e1b4b;
  --cell-border:       #ddd6fe;
  --row-hover:         #ede9fe;
  --row-selected:      #ddd6fe;
  --detail-bg:         #f5f3ff;
  --font-family:       'Inter', sans-serif;
  --font-size:         13px;
  --border-radius:     6px;
}

Generic typing

DvDataGrid is generic over the row type T:

// Typed column definitions
columnDefs: DvColDef<User>[] = [
  {
    field: 'name',
    cellClass: (p) => p.row.isActive ? 'active' : 'inactive', // p.row is User ✓
  },
  {
    field: 'role',
    tooltipValueGetter: (p) => p.row.roleDescription,          // p.row is User ✓
  },
];

The component itself infers T from the columnDefs input — no explicit annotation needed in the template.


Public API exports

// Components
export { DvDataGrid }         from 'dv-datagrid';
export { GridPagination }   from 'dv-datagrid';
export { GridFilterMenu }   from 'dv-datagrid';

// Models
export type {
  DvColDef, CellClassParams,
  DvGridOptions, DvGridApi,
  ServerRequestParams,
  SortItem, SortDirection,
  FilterType, FilterInstance,
  FilterModel,
  TextFilter, TextFilterOperator,
  NumberFilter, NumberFilterOperator,
  DateFilter, DateFilterOperator,
  SetFilter,
} from 'dv-datagrid';

// Localization
export type { GridLocale } from 'dv-datagrid';
export { EN_LOCALE, UK_LOCALE } from 'dv-datagrid';

Full example

// app.ts
import { Component, TemplateRef, viewChild } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import {
  DvColDef, DvDataGrid, FilterInstance,
  DvGridApi, DvGridOptions, ServerRequestParams,
  UK_LOCALE,
} from 'dv-datagrid';
import { StatusCellComponent } from './status-cell.component';

interface Order {
  id: number;
  reference: string;
  customer: string;
  amount: number;
  status: 'pending' | 'completed' | 'cancelled';
  createdAt: string;
  items: { name: string; qty: number; price: number }[];
}

@Component({
  selector: 'app-orders',
  imports: [DvDataGrid, DecimalPipe],
  templateUrl: './app.html',
})
export class OrdersComponent {
  readonly detailTpl = viewChild.required<TemplateRef<any>>('detailTpl');
  readonly refTpl    = viewChild.required<TemplateRef<any>>('refTpl');

  columnDefs: DvColDef<Order>[] = [];

  options: DvGridOptions = {
    pagination: true,
    paginationPageSize: 20,
    theme: 'alpine',
    rowSelection: 'multi',
    getRowId: (row) => row.id,
    rowExpansion: { enabled: true },
    tooltipShowDelay: 400,
    locale: UK_LOCALE,
  };

  private api!: DvGridApi;

  ngOnInit() {
    this.columnDefs = [
      {
        field: 'reference',
        headerName: 'Reference',
        sortable: true,
        filter: 'text',
        cellRenderer: this.refTpl(),
      },
      {
        field: 'customer',
        headerName: 'Customer',
        sortable: true,
        filter: 'text',
        tooltipField: 'customer',
      },
      {
        field: 'amount',
        headerName: 'Amount',
        sortable: true,
        filter: 'number',
        valueFormatter: (row) => `$${row.amount.toFixed(2)}`,
        cellClass: (p) => p.value < 0 ? 'negative' : '',
      },
      {
        field: 'status',
        headerName: 'Status',
        filter: 'set',
        filterValues: ['pending', 'completed', 'cancelled'],
        cellRenderer: StatusCellComponent,
      },
      {
        field: 'createdAt',
        headerName: 'Date',
        sortable: true,
        filter: 'date',
      },
    ];
  }

  onGridReady(api: DvGridApi) {
    this.api = api;
  }

  onServerDataRequested(params: ServerRequestParams) {
    this.ordersService.getOrders(params).subscribe(({ rows, total }) => {
      this.api.setServerData(rows, total);
    });
  }

  onSelectionChanged(ids: any[]) {
    console.log('Selected order IDs:', ids);
  }

  itemsTotal(items: Order['items']): number {
    return items.reduce((s, i) => s + i.qty * i.price, 0);
  }
}
<!-- app.html -->
<dv-datagrid
  [columnDefs]="columnDefs"
  [options]="options"
  [detailTemplate]="detailTpl"
  (gridReady)="onGridReady($event)"
  (serverDataRequested)="onServerDataRequested($event)"
  (selectionChanged)="onSelectionChanged($event)"
/>

<!-- Reference cell -->
<ng-template #refTpl let-value>
  <a [routerLink]="['/orders', value]">{{ value }}</a>
</ng-template>

<!-- Expandable row detail -->
<ng-template #detailTpl let-row>
  <div class="order-detail">
    <table>
      <thead>
        <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
      </thead>
      <tbody>
        @for (item of row.items; track item.name) {
          <tr>
            <td>{{ item.name }}</td>
            <td>{{ item.qty }}</td>
            <td>{{ item.price | number:'1.2-2' }} $</td>
            <td>{{ item.qty * item.price | number:'1.2-2' }} $</td>
          </tr>
        }
      </tbody>
      <tfoot>
        <tr>
          <td colspan="3">Total</td>
          <td>{{ itemsTotal(row.items) | number:'1.2-2' }} $</td>
        </tr>
      </tfoot>
    </table>
  </div>
</ng-template>

Development

# Build the library
ng build dv-datagrid

# Run the demo app
ng serve dv-demo

# Run tests
ng test dv-datagrid

License

MIT © deepvyne