ngx-devextreme-zoneless 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricardo Gross
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # ngx-devextreme-zoneless
2
+
3
+ Strongly-typed **signal & zoneless change-detection adapters** for
4
+ [DevExtreme Angular](https://js.devexpress.com/Angular/) components.
5
+
6
+ DevExtreme widgets mutate their own state (user typing, row selection, popup
7
+ closing, chart point clicks, …) through their internal option system. Under
8
+ zoneless change detection nothing tells Angular about those mutations. This
9
+ library bridges the gap with **signals**: every adapter writes widget changes
10
+ into a signal (which schedules zoneless CD natively) and pushes signal changes
11
+ back into the widget — with echo suppression, structural equality, optional
12
+ debouncing and automatic teardown.
13
+
14
+ ```
15
+ Angular signal ──effect──▶ widget option / method
16
+ Angular signal ◀──set──── widget event (optionChanged, selectionChanged, …)
17
+ ```
18
+
19
+ - **Angular** ≥ 20 (built and verified against Angular 22, zoneless by default)
20
+ - **DevExtreme / devextreme-angular** ≥ 25.1 (built and verified against 26.1)
21
+ - **TypeScript** 6.0, `strict`, zero `any` in the public surface
22
+
23
+ Everything is *derived from DevExtreme's own `Properties` declarations* via
24
+ template-literal and conditional types — option names, option value types and
25
+ event payload types are string-literal unions that automatically follow the
26
+ installed DevExtreme version. A typo'd option name is a compile error.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm i ngx-devextreme-zoneless
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ```ts
37
+ import { Component, signal } from '@angular/core';
38
+ import { DxTextBoxModule } from 'devextreme-angular/ui/text-box';
39
+ import { DxDataGridModule } from 'devextreme-angular/ui/data-grid';
40
+ import {
41
+ DxTextBoxValueDirective,
42
+ DxDataGridSelectedRowKeysDirective,
43
+ } from 'ngx-devextreme-zoneless';
44
+
45
+ @Component({
46
+ imports: [DxTextBoxModule, DxDataGridModule,
47
+ DxTextBoxValueDirective, DxDataGridSelectedRowKeysDirective],
48
+ template: `
49
+ <dx-text-box [(dxValue)]="search" [dxValueDebounce]="300" />
50
+ <dx-data-grid [(dxSelectedRowKeys)]="selected" [dataSource]="orders" keyExpr="id">
51
+ <dxo-selection mode="multiple" />
52
+ </dx-data-grid>
53
+ `,
54
+ })
55
+ export class OrdersComponent {
56
+ readonly search = signal('');
57
+ readonly selected = signal<readonly number[]>([]); // TKey inferred as number
58
+ readonly orders = [{ id: 1 }, { id: 2 }];
59
+ }
60
+ ```
61
+
62
+ Or import everything at once with `DX_SIGNAL_DIRECTIVES` (also available per
63
+ family: `DX_VALUE_DIRECTIVES`, `DX_NAVIGATION_DIRECTIVES`,
64
+ `DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`, `DX_VISUALIZATION_DIRECTIVES`).
65
+
66
+ Components without a dedicated directive (PivotGrid, Sortable, Menu, …) are
67
+ covered too — see [Composition API](#composition-api): the same engine works
68
+ with **any** option and event of **any** DevExtreme component, fully typed.
69
+
70
+ ## Directive catalog
71
+
72
+ ### Editors — `[(dxValue)]`
73
+
74
+ Every value editor gets a `[(dxValue)]` two-way model plus `dxValueDebounce`
75
+ (ms, widget → signal direction) and `dxValueEqual` (custom comparer) inputs.
76
+
77
+ | Component | Model type |
78
+ |---|---|
79
+ | `dx-text-box`, `dx-text-area`, `dx-html-editor` | `string` |
80
+ | `dx-number-box` | `number \| null` |
81
+ | `dx-check-box` | `boolean \| null` |
82
+ | `dx-switch` | `boolean` |
83
+ | `dx-date-box` | `Date \| number \| string \| null` |
84
+ | `dx-date-range-box` | `readonly [start: DxDateValue \| null, end: DxDateValue \| null]` |
85
+ | `dx-calendar` | `DxDateValue \| readonly DxDateValue[] \| null` |
86
+ | `dx-color-box`, `dx-autocomplete` | `string \| null` |
87
+ | `dx-slider` | `number` |
88
+ | `dx-range-slider` | `readonly [start: number, end: number]` |
89
+ | `dx-select-box`, `dx-lookup`, `dx-drop-down-box`, `dx-radio-group` | generic `TValue` — inferred from your signal |
90
+ | `dx-tag-box` | generic `readonly TKey[]` |
91
+ | `dx-filter-builder` | `DxFilterExpression \| null` |
92
+ | `dx-range-selector` | DevExtreme's declared scale range type |
93
+
94
+ Where DevExtreme declares `value?: any` (SelectBox & friends) the directive is
95
+ **generic** instead, so `[(dxValue)]="status"` with a
96
+ `signal<OrderStatus | null>` is checked end-to-end.
97
+
98
+ ### Selection & data
99
+
100
+ | Selector | Model |
101
+ |---|---|
102
+ | `dx-data-grid[dxSelectedRowKeys]`, `dx-tree-list[dxSelectedRowKeys]` | `readonly TKey[]` |
103
+ | `dx-list[dxSelectedItemKeys]` | `readonly TKey[]` |
104
+ | `dx-tree-view[dxSelectedNodeKeys]` | `readonly TKey[]` (method-based: `getSelectedNodeKeys` / `selectItem`) |
105
+ | `dx-gantt[dxSelectedRowKey]` | `TKey \| null` |
106
+ | `dx-scheduler[dxCurrentDate]` | `Date \| number \| string` |
107
+ | `dx-scheduler[dxCurrentView]` | DevExtreme's view union |
108
+ | `dx-form[dxFormData]` | generic `TFormData` — emits a fresh shallow copy per field edit |
109
+
110
+ ### Navigation & overlays
111
+
112
+ | Selector | Model |
113
+ |---|---|
114
+ | `dx-tabs`, `dx-tab-panel`, `dx-accordion`, `dx-gallery` + `[dxSelectedIndex]` | `number` |
115
+ | `dx-drawer[dxOpened]` | `boolean` |
116
+ | `dx-popup`, `dx-popover`, `dx-tooltip`, `dx-toast`, `dx-load-panel`, `dx-action-sheet` + `[dxVisible]` | `boolean` |
117
+
118
+ `[(dxVisible)]` tracks *every* way an overlay can close (shading click, escape,
119
+ close button, API) — the classic zoneless pain point.
120
+
121
+ ### Visualization
122
+
123
+ | Selector | Model |
124
+ |---|---|
125
+ | `dx-chart[dxSelectedPoints]`, `dx-pie-chart[dxSelectedPoints]` | `readonly DxChartSelectedPoint[]` |
126
+
127
+ Chart selection is method-based; selected points are exposed as serializable
128
+ `{ seriesName, argument, value }` descriptors that can be stored and restored.
129
+ DevExtreme charts do not select points on click by themselves (their demos
130
+ wire `onPointClick: e => e.target.select()` manually), so these directives
131
+ toggle the clicked point's selection by default — opt out with
132
+ `[dxPointClickToggle]="false"` if you want to handle `onPointClick` yourself.
133
+
134
+ ## Composition API
135
+
136
+ The directives are sugar. The same engine is available as injection-context
137
+ functions and covers **every option and event of every DevExtreme component**,
138
+ fully typed:
139
+
140
+ ```ts
141
+ import {
142
+ bindDxOption, dxOptionSignal, dxEventSignal, injectDxInstance,
143
+ } from 'ngx-devextreme-zoneless';
144
+
145
+ @Component({ /* ... */ })
146
+ export class MyComponent {
147
+ // On the same element (inside a directive):
148
+ private readonly textBox = inject(DxTextBoxComponent, { self: true });
149
+
150
+ // Two-way signal for any option — names & types from DevExtreme Properties:
151
+ readonly placeholder = dxOptionSignal(this.textBox, 'placeholder');
152
+ readonly mode = dxOptionSignal(this.textBox, 'mode', { initialValue: 'search' });
153
+
154
+ // Latest payload of any widget event:
155
+ readonly focusOut = dxEventSignal(this.textBox, 'focusOut');
156
+
157
+ // Works with view children too (pass the viewChild signal itself):
158
+ private readonly grid = viewChild(DxDataGridComponent<Order, number>);
159
+ readonly filter = dxOptionSignal(this.grid, 'filterValue');
160
+
161
+ // Bind an existing signal to an option:
162
+ readonly keys = signal<number[] | undefined>([]);
163
+ constructor() {
164
+ bindDxOption(this.grid, 'selectedRowKeys', this.keys);
165
+ }
166
+
167
+ // The raw widget instance as a signal (undefined until created):
168
+ readonly widget = injectDxInstance(DxTextBoxComponent);
169
+ }
170
+ ```
171
+
172
+ ### Components without a dedicated directive
173
+
174
+ The coverage model is simple: **a directive exists wherever a widget has
175
+ genuine user-mutable two-way state** (value, selection, visibility, index,
176
+ current date/view, form data, chart points). Every other component — and any
177
+ component DevExtreme ships in the future — is still fully supported through
178
+ the composition API, because `dxOptionSignal`, `dxEventSignal` and
179
+ `bindDxOption` are not written against a component list: their option names,
180
+ option value types and event payloads are computed from the `Properties`
181
+ declaration of whatever host component you hand them. Support doesn't
182
+ cliff-edge at the directive catalog; it just gets one notch less sugary.
183
+
184
+ A few real-world cases:
185
+
186
+ ```ts
187
+ // PivotGrid — no two-way options (its mutable state lives in the
188
+ // PivotGridDataSource), but every event and option is reachable, typed:
189
+ private readonly pivot = inject(DxPivotGridComponent, { self: true });
190
+ readonly cellClick = dxEventSignal(this.pivot, 'cellClick');
191
+ readonly contextMenu = dxEventSignal(this.pivot, 'contextMenuPreparing');
192
+ ```
193
+
194
+ ```ts
195
+ // Scheduler beyond [(dxCurrentDate)] / [(dxCurrentView)] — appointment
196
+ // lifecycle as signals:
197
+ private readonly scheduler = inject(DxSchedulerComponent, { self: true });
198
+ readonly appointmentAdded = dxEventSignal(this.scheduler, 'appointmentAdded');
199
+ ```
200
+
201
+ ```ts
202
+ // Sortable (e.g. a Kanban board built from dx-sortable + lists) — purely
203
+ // event-driven, so state stays in your signals and events drive mutations:
204
+ private readonly sortable = viewChild(DxSortableComponent);
205
+ readonly reorder = dxEventSignal(this.sortable, 'reorder');
206
+ constructor() {
207
+ effect(() => {
208
+ const e = this.reorder();
209
+ if (e) this.cards.update((cards) => moveItem(cards, e.fromIndex, e.toIndex));
210
+ });
211
+ }
212
+ ```
213
+
214
+ ```ts
215
+ // Any option of any widget, two-way — e.g. Menu, ButtonGroup, Toolbar, ...:
216
+ readonly disabled = dxOptionSignal(this.menu, 'disabled');
217
+ ```
218
+
219
+ For state that is neither an option nor a single event (method-based APIs),
220
+ implement a `DxBindingAdapter` and pass it to `bindDxState` — that is exactly
221
+ how the chart-selection and tree-view directives are built. And if you want
222
+ template sugar for a component we don't cover, a custom directive is ~10
223
+ lines by extending `DxValueDirectiveBase`, `DxVisibleDirectiveBase`, etc.
224
+
225
+ ## Semantics
226
+
227
+ - **Initial sync** (`initialSync`, default `'auto'`): the signal's value is
228
+ pushed into the widget on attach; if it is `undefined`, the widget's current
229
+ value is pulled into the signal instead.
230
+ - **Echo suppression**: updates only propagate when the value *structurally*
231
+ changed (`Date`-aware, deep for arrays/plain objects). Override per binding
232
+ via `equal` / `dxValueEqual`.
233
+ - **Debounce** applies to the widget → signal direction only, so typing into a
234
+ `dx-text-box` doesn't storm your `computed()` graph.
235
+ - **Teardown** is automatic via `DestroyRef` — event handlers are detached and
236
+ effects destroyed when the directive/component dies.
237
+ - **Zoneless**: no `NgZone`, no `markForCheck` — signal writes are the change
238
+ notification.
239
+
240
+ ## Type helpers
241
+
242
+ All building blocks are exported, e.g.:
243
+
244
+ ```ts
245
+ DxHostOptionName<DxTextBoxComponent> // 'value' | 'placeholder' | 'mode' | ...
246
+ DxHostOptionValue<DxTextBoxComponent, 'mode'> // 'email' | 'password' | ... | undefined
247
+ DxHostEventName<DxDataGridComponent> // 'rowClick' | 'selectionChanged' | ...
248
+ DxHostEventArg<DxTextBoxComponent, 'valueChanged'>
249
+ DxHostValue<DxCheckBoxComponent> // boolean | null
250
+ ```
251
+
252
+ ## Development
253
+
254
+ ```bash
255
+ npm run build # ng-packagr build → dist/
256
+ npm run typecheck:examples # strict-template check of examples/ + type contract tests
257
+ npm run verify # both
258
+ npm run smoke # runtime smoke test app on http://localhost:4299
259
+ ```
260
+
261
+ The smoke app ([smoke/src/smoke.component.ts](smoke/src/smoke.component.ts))
262
+ boots with `provideZonelessChangeDetection()` — zone.js is not even installed
263
+ in this workspace — and exercises text box, select box, check box, slider,
264
+ tabs, grid selection, popup visibility, chart point selection and the
265
+ composition API in a real browser, with `data-testid` readouts for scripted
266
+ verification of both binding directions.
267
+
268
+ See [examples/order-dashboard.component.ts](examples/order-dashboard.component.ts)
269
+ for a complete signal-driven dashboard and
270
+ [examples/type-tests.ts](examples/type-tests.ts) for the compile-time contract
271
+ tests.