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

ngx-datawindow

v1.1.2

Published

Angular DataWindow component inspired by PowerBuilder DataWindow — DataStore engine, virtual computed columns, column-level change tracking, offline persistence, and optimistic locking.

Readme

ngx-datawindow

Bringing PowerBuilder DataWindow's design philosophy to the modern web era.

ngx-datawindow is an Angular table component that reimagines the DataWindow — a legendary data management paradigm from 1991 — for today's web applications. It provides zero-config CRUD, virtual computed columns, multi-buffer state management, optimistic offline sync, and column-level change tracking out of the box.

npm version License: MIT Test Status


Screenshots

Basic CRUD Basic CRUD operations with inline editing

Business Demo E-commerce order management with real-time stats

Live Stock Quotes Real-time data feed with sparkline charts


Why ngx-datawindow?

In 1991, PowerBuilder introduced DataWindow — a component that treated data as a first-class citizen with state, history, and traceability. It proved that "data should be managed seriously." Thirty years later, this philosophy is still ahead of most modern frontend approaches.

ngx-datawindow is not a nostalgia project. It is a translation of DataWindow's design principles into modern Angular:

  • Data managed by an engine, not scattered across components
  • Operations are staged (temp → confirm → commit), not instantaneous
  • Changes are traceable to the exact column, not coarse row diffs
  • Validation intercepts at entry, not after submission
  • Every operation has lifecycle hooks for intervention

See doc/DATAWINDOW-SOUL.md for the full design manifesto and doc/DATAWINDOW-MODERN.md for why these ideas remain relevant today.


Features

| Feature | Description | Status | |---------|-------------|--------| | Zero-config CRUD | Built-in create/read/update/delete | Phase 1 | | Virtual Computed Columns | JS function formulas, auto-recompute | Phase 1 | | Multi-buffer Management | main / filter / delete buffers | Phase 1 | | Aggregation | sum / avg / count / min / max with grouping | Phase 1 | | Reactive Design | Angular Signals, real-time updates | Phase 1 | | Row Status Tracking | new / modified / deleted with visual cues | Phase 1 | | Column Filtering | 15 operators, text/number/select/date/boolean | Phase 1 | | Global Search | Cross-column search | Phase 1 | | Sort & Pagination | Material Sort + Paginator | Phase 1 | | Row Selection | Single and multi-select modes | Phase 1 | | Inline Editing | Double-click to edit cells | Phase 1 | | Validation | Required, format (regex), range checks | Phase 1 | | Delta Updates | Generate new/modified/deleted update data | Phase 1 | | Column-level Change Tracking | Old/new value + timestamp per column | Phase 1 | | ItemChanged Rejection | Real-time interception, reject invalid input | Phase 1 | | Undo / Redo | Command Pattern, full stack | Phase 1 | | Complete Event Lifecycle | RetrieveStart → RowFocusChanged → ItemChanged → SaveStart | Phase 1 | | Offline Persistence | IndexedDB storage, works offline | Phase 2 | | Optimistic Locking | rowVersionMap conflict detection (server/client/manual) | Phase 2 | | Sync Metrics | Duration, bytes, synced/conflict counts | Phase 2 | | Virtual Scroll | CDK CdkVirtualScrollViewport for large datasets | Phase 2 |


Installation

npm install ngx-datawindow

Quick Start

1. Import the module

import { DataTableModule } from 'ngx-datawindow';

@NgModule({
  imports: [DataTableModule],
})
export class AppModule {}

2. Basic usage

import { Component } from '@angular/core';
import { DataTableComponent, DataStoreConfig, ColumnConfig, TableConfig } from 'ngx-datawindow';

@Component({
  selector: 'app-employees',
  standalone: true,
  imports: [DataTableComponent],
  template: `
    <ngx-datawindow
      [datastoreConfig]="config"
      [columns]="columns"
      [data]="employees"
      [tableConfig]="tableConfig"
      (rowAdded)="onAdd($event)"
      (rowUpdated)="onUpdate($event)"
      (rowDeleted)="onDelete($event)">
    </ngx-datawindow>
  `,
})
export class EmployeesComponent {
  config: DataStoreConfig = {
    name: 'employees',
    fields: [
      { name: 'id', type: 'number', required: true },
      { name: 'name', type: 'string', required: true },
      { name: 'department', type: 'string' },
      { name: 'salary', type: 'number' },
    ],
  };

  columns: ColumnConfig[] = [
    { field: 'id', header: 'ID', width: '60px' },
    { field: 'name', header: 'Name', editable: true, filterable: true },
    { field: 'department', header: 'Department', editable: true, filterType: 'select',
      filterOptions: [{ value: 'Engineering', label: 'Engineering' }, { value: 'Sales', label: 'Sales' }]
    },
    { field: 'salary', header: 'Salary', editable: true, editType: 'number', align: 'right' },
  ];

  tableConfig: TableConfig = {
    title: 'Employee Management',
    showToolbar: true,
    showGlobalSearch: true,
    selectionMode: 'multiple',
    toolbarActions: { add: true, delete: true, refresh: true, export: true },
    pagination: { pageSizeOptions: [10, 25, 50, 100], defaultPageSize: 10 },
  };

  employees = [
    { id: 1, name: 'Alice', department: 'Engineering', salary: 25000 },
    { id: 2, name: 'Bob', department: 'Sales', salary: 18000 },
    { id: 3, name: 'Charlie', department: 'Engineering', salary: 35000 },
  ];

  onAdd(row) { console.log('Row added:', row); }
  onUpdate(event) { console.log('Row updated:', event); }
  onDelete(rowId) { console.log('Row deleted:', rowId); }
}

3. Virtual Computed Columns

config: DataStoreConfig = {
  name: 'orders',
  fields: [
    { name: 'product', type: 'string' },
    { name: 'quantity', type: 'number' },
    { name: 'price', type: 'number' },
    { name: 'total', type: 'virtual', virtual: true,
      formula: (row) => row.raw['quantity'] * row.raw['price'] },
  ],
};

4. Column-Level Change Tracking

// Modify a field
await store.updateRow(1, { salary: 30000 });

// Get all changes for a row
const changes = store.getRowFieldChanges(1);
// Returns: [{ field: 'salary', change: { oldValue: 25000, newValue: 30000, timestamp: ... } }]

// Get original value (ignoring current modifications)
const original = store.getFieldOriginalValue(1, 'salary'); // 25000

// Undo a single field change
store.undoFieldChange(1, 'salary');

5. ItemChanged Rejection

// Field-level validation
fields: [{
  name: 'salary',
  itemValidate: (oldVal, newVal) => {
    if (newVal < 0) return 'Salary cannot be negative';
    return true;
  }
}]

// Global handler
store.onItemChanged(async (event) => {
  if (event.field === 'salary' && event.newValue > 50000) {
    return 'reject'; // Reject and prevent entry
  }
  return 'accept';
});

// The update will be rejected
const result = await store.updateRow(1, { salary: -1000 });
if (!result.success) {
  console.log('Rejected:', result.rejected.rejectReason.message);
}

6. Undo / Redo

// Basic operations
store.addRow({ name: 'Alice' });
store.undo(); // Undo
store.redo(); // Redo

// Check stack state
const stack = store.getUndoStack();
console.log(`Undoable: ${stack.undoCount}, Redoable: ${stack.redoCount}`);

// Get full history
const history = store.getUndoHistory();
history.forEach(cmd => {
  console.log(`${cmd.type}: ${cmd.description}`);
});

// Clear history
store.clearUndoHistory();

7. Offline Persistence

import { OfflineService } from 'ngx-datawindow';

// Initialize offline sync
const offlineService = new OfflineService(store);

// Sync when online
await offlineService.sync((pending) => {
  // Send to your backend API
  return fetch('/api/sync', {
    method: 'POST',
    body: JSON.stringify(pending),
  }).then(res => res.json());
});

// Conflict resolution strategies
// - server_wins: server version always wins
// - client_wins: local changes always win
// - manual: returns conflicts for manual resolution

API Reference

DataTableComponent

Inputs:

| Input | Type | Description | |-------|------|-------------| | datastoreConfig | DataStoreConfig | Schema definition (name, fields, computed columns) | | columns | ColumnConfig[] | Column display configuration | | data | DataRow[] | Initial data array | | tableConfig | TableConfig | UI settings (title, toolbar, pagination, etc.) | | isLoading | boolean | Loading state indicator |

Outputs:

| Output | Payload | Description | |--------|---------|-------------| | rowAdded | DataRow | Fired when a row is added | | rowUpdated | ChangeEvent | Fired when a row is modified | | rowDeleted | RowId | Fired when a row is deleted | | rowClicked | RowClickEvent | Fired on row click | | rowDoubleClicked | RowClickEvent | Fired on row double-click | | selectionChanged | DataRow[] | Fired when selection changes | | toolbarAction | ToolbarEvent | Fired on toolbar action | | pageChanged | PageEvent | Fired on pagination change |

ColumnConfig

{
  field: string;           // Data field name
  header: string;          // Display header
  width?: string;          // e.g. '120px'
  sortable?: boolean;      // Enable sorting
  filterable?: boolean;    // Enable column filter
  filterType?: 'text' | 'number' | 'select' | 'date' | 'boolean';
  filterOptions?: { value: any; label: string }[];
  editable?: boolean;     // Enable inline editing
  editType?: 'text' | 'number' | 'select' | 'date';
  aggregate?: 'sum' | 'avg' | 'count' | 'min' | 'max';
  sticky?: 'left' | 'right';
  virtual?: boolean;      // Virtual computed column
  format?: { type: 'currency' | 'percent' | 'date'; args?: any };
  align?: 'left' | 'center' | 'right';
}

TableConfig

{
  title?: string;
  showToolbar?: boolean;
  showPaginator?: boolean;
  showColumnFilter?: boolean;
  showGlobalSearch?: boolean;
  toolbarActions?: {
    add?: boolean | { label?: string };
    delete?: boolean | { label?: string };
    refresh?: boolean;
    export?: boolean | 'csv' | 'json' | 'xlsx';
  };
  selectionMode?: 'none' | 'single' | 'multiple';
  pagination?: {
    pageSizeOptions?: number[];
    defaultPageSize?: number;
  };
  virtualScroll?: boolean;
  virtualScrollItemSize?: number;
}

Architecture

┌─────────────────────────────────────────────────┐
│          DataTableComponent (UI Layer)          │
│   Material Table + CDK Virtual Scroll + Signals  │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│            DataTableService (State)             │
│  CRUD, filtering, sorting, aggregation, events   │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│              DataStore (Core Engine)             │
│  Pure TypeScript, framework-agnostic (~50KB)     │
│  Buffers, state, change tracking, validation     │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│          OfflineService (Persistence)             │
│  IndexedDBManager → OfflineStorageAdapter        │
│  → OfflineService (optimistic locking + sync)    │
└─────────────────────────────────────────────────┘

The DataStore engine is written in pure TypeScript with zero Angular dependencies. It can be extracted and used in any framework.


Tech Stack

  • Angular 21 — Component framework
  • Angular Material — UI components
  • Angular CDK — Virtual scrolling, accessibility
  • TypeScript 5.4+ — Strict mode
  • IndexedDB — Offline persistence (via raw API)
  • Jest — Unit and integration testing (107/107 passing)

Roadmap

Phase 3: Developer Experience (in progress)

  • [ ] Visual column config designer
  • [ ] Declarative persistence configuration
  • [ ] Multiple presentation styles (Grid / Form / Card)
  • [ ] PDF / Excel export
  • [ ] Full documentation + StackBlitz demos

Future

  • [ ] Database connection layer (optional backend integration)
  • [ ] Nested datawindows (Master-Detail)
  • [ ] Report engine (grouped reports, crosstabs)
  • [ ] Real-time collaboration

Contributing

We welcome all contributions! See doc/CONTRIBUTING.md for development setup, coding standards, and PR workflow.

# Clone
git clone https://github.com/Sugitter/ngx-datawindow.git
cd ngx-datawindow

# Install
npm install

# Run example
cd example && npm install && ng serve

# Test
npm run test

# Build
npm run build

License

MIT — open source, free for all.


Acknowledgments

This project draws its inspiration from PowerBuilder DataWindow (1991–). We are grateful to Powersoft, Sybase, SAP, and Appeon for keeping DataWindow alive through decades of transition. We hope to carry its philosophy into the web era.


Good design is timeless.