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-devextreme-zoneless

v1.0.0

Published

Strongly-typed signal & zoneless change-detection adapters for DevExtreme Angular components

Readme

ngx-devextreme-zoneless

Strongly-typed signal & zoneless change-detection adapters for DevExtreme Angular components.

DevExtreme widgets mutate their own state (user typing, row selection, popup closing, chart point clicks, …) through their internal option system. Under zoneless change detection nothing tells Angular about those mutations. This library bridges the gap with signals: every adapter writes widget changes into a signal (which schedules zoneless CD natively) and pushes signal changes back into the widget — with echo suppression, structural equality, optional debouncing and automatic teardown.

Angular signal  ──effect──▶  widget option / method
Angular signal  ◀──set────  widget event (optionChanged, selectionChanged, …)
  • Angular ≥ 20 (built and verified against Angular 22, zoneless by default)
  • DevExtreme / devextreme-angular ≥ 25.1 (built and verified against 26.1)
  • TypeScript 6.0, strict, zero any in the public surface

Everything is derived from DevExtreme's own Properties declarations via template-literal and conditional types — option names, option value types and event payload types are string-literal unions that automatically follow the installed DevExtreme version. A typo'd option name is a compile error.

Installation

npm i ngx-devextreme-zoneless

Quick start

import { Component, signal } from '@angular/core';
import { DxTextBoxModule } from 'devextreme-angular/ui/text-box';
import { DxDataGridModule } from 'devextreme-angular/ui/data-grid';
import {
  DxTextBoxValueDirective,
  DxDataGridSelectedRowKeysDirective,
} from 'ngx-devextreme-zoneless';

@Component({
  imports: [DxTextBoxModule, DxDataGridModule,
            DxTextBoxValueDirective, DxDataGridSelectedRowKeysDirective],
  template: `
    <dx-text-box [(dxValue)]="search" [dxValueDebounce]="300" />
    <dx-data-grid [(dxSelectedRowKeys)]="selected" [dataSource]="orders" keyExpr="id">
      <dxo-selection mode="multiple" />
    </dx-data-grid>
  `,
})
export class OrdersComponent {
  readonly search = signal('');
  readonly selected = signal<readonly number[]>([]); // TKey inferred as number
  readonly orders = [{ id: 1 }, { id: 2 }];
}

Or import everything at once with DX_SIGNAL_DIRECTIVES (also available per family: DX_VALUE_DIRECTIVES, DX_NAVIGATION_DIRECTIVES, DX_VISIBLE_DIRECTIVES, DX_DATA_DIRECTIVES, DX_VISUALIZATION_DIRECTIVES).

Components without a dedicated directive (PivotGrid, Sortable, Menu, …) are covered too — see Composition API: the same engine works with any option and event of any DevExtreme component, fully typed.

Directive catalog

Editors — [(dxValue)]

Every value editor gets a [(dxValue)] two-way model plus dxValueDebounce (ms, widget → signal direction) and dxValueEqual (custom comparer) inputs.

| Component | Model type | |---|---| | dx-text-box, dx-text-area, dx-html-editor | string | | dx-number-box | number \| null | | dx-check-box | boolean \| null | | dx-switch | boolean | | dx-date-box | Date \| number \| string \| null | | dx-date-range-box | readonly [start: DxDateValue \| null, end: DxDateValue \| null] | | dx-calendar | DxDateValue \| readonly DxDateValue[] \| null | | dx-color-box, dx-autocomplete | string \| null | | dx-slider | number | | dx-range-slider | readonly [start: number, end: number] | | dx-select-box, dx-lookup, dx-drop-down-box, dx-radio-group | generic TValue — inferred from your signal | | dx-tag-box | generic readonly TKey[] | | dx-filter-builder | DxFilterExpression \| null | | dx-range-selector | DevExtreme's declared scale range type |

Where DevExtreme declares value?: any (SelectBox & friends) the directive is generic instead, so [(dxValue)]="status" with a signal<OrderStatus | null> is checked end-to-end.

Selection & data

| Selector | Model | |---|---| | dx-data-grid[dxSelectedRowKeys], dx-tree-list[dxSelectedRowKeys] | readonly TKey[] | | dx-list[dxSelectedItemKeys] | readonly TKey[] | | dx-tree-view[dxSelectedNodeKeys] | readonly TKey[] (method-based: getSelectedNodeKeys / selectItem) | | dx-gantt[dxSelectedRowKey] | TKey \| null | | dx-scheduler[dxCurrentDate] | Date \| number \| string | | dx-scheduler[dxCurrentView] | DevExtreme's view union | | dx-form[dxFormData] | generic TFormData — emits a fresh shallow copy per field edit |

Navigation & overlays

| Selector | Model | |---|---| | dx-tabs, dx-tab-panel, dx-accordion, dx-gallery + [dxSelectedIndex] | number | | dx-drawer[dxOpened] | boolean | | dx-popup, dx-popover, dx-tooltip, dx-toast, dx-load-panel, dx-action-sheet + [dxVisible] | boolean |

[(dxVisible)] tracks every way an overlay can close (shading click, escape, close button, API) — the classic zoneless pain point.

Visualization

| Selector | Model | |---|---| | dx-chart[dxSelectedPoints], dx-pie-chart[dxSelectedPoints] | readonly DxChartSelectedPoint[] |

Chart selection is method-based; selected points are exposed as serializable { seriesName, argument, value } descriptors that can be stored and restored. DevExtreme charts do not select points on click by themselves (their demos wire onPointClick: e => e.target.select() manually), so these directives toggle the clicked point's selection by default — opt out with [dxPointClickToggle]="false" if you want to handle onPointClick yourself.

Composition API

The directives are sugar. The same engine is available as injection-context functions and covers every option and event of every DevExtreme component, fully typed:

import {
  bindDxOption, dxOptionSignal, dxEventSignal, injectDxInstance,
} from 'ngx-devextreme-zoneless';

@Component({ /* ... */ })
export class MyComponent {
  // On the same element (inside a directive):
  private readonly textBox = inject(DxTextBoxComponent, { self: true });

  // Two-way signal for any option — names & types from DevExtreme Properties:
  readonly placeholder = dxOptionSignal(this.textBox, 'placeholder');
  readonly mode = dxOptionSignal(this.textBox, 'mode', { initialValue: 'search' });

  // Latest payload of any widget event:
  readonly focusOut = dxEventSignal(this.textBox, 'focusOut');

  // Works with view children too (pass the viewChild signal itself):
  private readonly grid = viewChild(DxDataGridComponent<Order, number>);
  readonly filter = dxOptionSignal(this.grid, 'filterValue');

  // Bind an existing signal to an option:
  readonly keys = signal<number[] | undefined>([]);
  constructor() {
    bindDxOption(this.grid, 'selectedRowKeys', this.keys);
  }

  // The raw widget instance as a signal (undefined until created):
  readonly widget = injectDxInstance(DxTextBoxComponent);
}

Components without a dedicated directive

The coverage model is simple: a directive exists wherever a widget has genuine user-mutable two-way state (value, selection, visibility, index, current date/view, form data, chart points). Every other component — and any component DevExtreme ships in the future — is still fully supported through the composition API, because dxOptionSignal, dxEventSignal and bindDxOption are not written against a component list: their option names, option value types and event payloads are computed from the Properties declaration of whatever host component you hand them. Support doesn't cliff-edge at the directive catalog; it just gets one notch less sugary.

A few real-world cases:

// PivotGrid — no two-way options (its mutable state lives in the
// PivotGridDataSource), but every event and option is reachable, typed:
private readonly pivot = inject(DxPivotGridComponent, { self: true });
readonly cellClick = dxEventSignal(this.pivot, 'cellClick');
readonly contextMenu = dxEventSignal(this.pivot, 'contextMenuPreparing');
// Scheduler beyond [(dxCurrentDate)] / [(dxCurrentView)] — appointment
// lifecycle as signals:
private readonly scheduler = inject(DxSchedulerComponent, { self: true });
readonly appointmentAdded = dxEventSignal(this.scheduler, 'appointmentAdded');
// Sortable (e.g. a Kanban board built from dx-sortable + lists) — purely
// event-driven, so state stays in your signals and events drive mutations:
private readonly sortable = viewChild(DxSortableComponent);
readonly reorder = dxEventSignal(this.sortable, 'reorder');
constructor() {
  effect(() => {
    const e = this.reorder();
    if (e) this.cards.update((cards) => moveItem(cards, e.fromIndex, e.toIndex));
  });
}
// Any option of any widget, two-way — e.g. Menu, ButtonGroup, Toolbar, ...:
readonly disabled = dxOptionSignal(this.menu, 'disabled');

For state that is neither an option nor a single event (method-based APIs), implement a DxBindingAdapter and pass it to bindDxState — that is exactly how the chart-selection and tree-view directives are built. And if you want template sugar for a component we don't cover, a custom directive is ~10 lines by extending DxValueDirectiveBase, DxVisibleDirectiveBase, etc.

Semantics

  • Initial sync (initialSync, default 'auto'): the signal's value is pushed into the widget on attach; if it is undefined, the widget's current value is pulled into the signal instead.
  • Echo suppression: updates only propagate when the value structurally changed (Date-aware, deep for arrays/plain objects). Override per binding via equal / dxValueEqual.
  • Debounce applies to the widget → signal direction only, so typing into a dx-text-box doesn't storm your computed() graph.
  • Teardown is automatic via DestroyRef — event handlers are detached and effects destroyed when the directive/component dies.
  • Zoneless: no NgZone, no markForCheck — signal writes are the change notification.

Type helpers

All building blocks are exported, e.g.:

DxHostOptionName<DxTextBoxComponent>          // 'value' | 'placeholder' | 'mode' | ...
DxHostOptionValue<DxTextBoxComponent, 'mode'> // 'email' | 'password' | ... | undefined
DxHostEventName<DxDataGridComponent>          // 'rowClick' | 'selectionChanged' | ...
DxHostEventArg<DxTextBoxComponent, 'valueChanged'>
DxHostValue<DxCheckBoxComponent>              // boolean | null

Development

npm run build               # ng-packagr build → dist/
npm run typecheck:examples  # strict-template check of examples/ + type contract tests
npm run verify              # both
npm run smoke               # runtime smoke test app on http://localhost:4299

The smoke app (smoke/src/smoke.component.ts) boots with provideZonelessChangeDetection() — zone.js is not even installed in this workspace — and exercises text box, select box, check box, slider, tabs, grid selection, popup visibility, chart point selection and the composition API in a real browser, with data-testid readouts for scripted verification of both binding directions.

See examples/order-dashboard.component.ts for a complete signal-driven dashboard and examples/type-tests.ts for the compile-time contract tests.