ngx-devextreme-zoneless 2.1.0 → 4.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.
Files changed (38) hide show
  1. package/README.md +112 -424
  2. package/fesm2022/ngx-devextreme-zoneless-core.mjs +116 -303
  3. package/fesm2022/ngx-devextreme-zoneless-core.mjs.map +1 -1
  4. package/fesm2022/ngx-devextreme-zoneless-data-source.mjs +444 -0
  5. package/fesm2022/ngx-devextreme-zoneless-data-source.mjs.map +1 -0
  6. package/fesm2022/ngx-devextreme-zoneless-data.mjs +755 -166
  7. package/fesm2022/ngx-devextreme-zoneless-data.mjs.map +1 -1
  8. package/fesm2022/ngx-devextreme-zoneless-editors.mjs +264 -98
  9. package/fesm2022/ngx-devextreme-zoneless-editors.mjs.map +1 -1
  10. package/fesm2022/ngx-devextreme-zoneless-forms.mjs +408 -0
  11. package/fesm2022/ngx-devextreme-zoneless-forms.mjs.map +1 -0
  12. package/fesm2022/ngx-devextreme-zoneless-navigation.mjs +301 -38
  13. package/fesm2022/ngx-devextreme-zoneless-navigation.mjs.map +1 -1
  14. package/fesm2022/ngx-devextreme-zoneless-overlays.mjs +40 -33
  15. package/fesm2022/ngx-devextreme-zoneless-overlays.mjs.map +1 -1
  16. package/fesm2022/ngx-devextreme-zoneless-visualization.mjs +172 -38
  17. package/fesm2022/ngx-devextreme-zoneless-visualization.mjs.map +1 -1
  18. package/fesm2022/ngx-devextreme-zoneless.mjs +10 -6
  19. package/fesm2022/ngx-devextreme-zoneless.mjs.map +1 -1
  20. package/package.json +18 -14
  21. package/types/ngx-devextreme-zoneless-core.d.ts +65 -183
  22. package/types/ngx-devextreme-zoneless-core.d.ts.map +1 -1
  23. package/types/ngx-devextreme-zoneless-data-source.d.ts +187 -0
  24. package/types/ngx-devextreme-zoneless-data-source.d.ts.map +1 -0
  25. package/types/ngx-devextreme-zoneless-data.d.ts +345 -44
  26. package/types/ngx-devextreme-zoneless-data.d.ts.map +1 -1
  27. package/types/ngx-devextreme-zoneless-editors.d.ts +106 -4
  28. package/types/ngx-devextreme-zoneless-editors.d.ts.map +1 -1
  29. package/types/ngx-devextreme-zoneless-forms.d.ts +214 -0
  30. package/types/ngx-devextreme-zoneless-forms.d.ts.map +1 -0
  31. package/types/ngx-devextreme-zoneless-navigation.d.ts +140 -6
  32. package/types/ngx-devextreme-zoneless-navigation.d.ts.map +1 -1
  33. package/types/ngx-devextreme-zoneless-overlays.d.ts +9 -2
  34. package/types/ngx-devextreme-zoneless-overlays.d.ts.map +1 -1
  35. package/types/ngx-devextreme-zoneless-visualization.d.ts +119 -16
  36. package/types/ngx-devextreme-zoneless-visualization.d.ts.map +1 -1
  37. package/types/ngx-devextreme-zoneless.d.ts +2 -1
  38. package/types/ngx-devextreme-zoneless.d.ts.map +1 -1
package/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
4
  [![Context7](https://img.shields.io/badge/Context7-Indexed-3B82F6)](https://context7.com/caffeinatedcoder/ngx-devextreme-zoneless)
5
- [![NPM](https://img.shields.io/badge/NPM-2.0.0-3B82F6)](https://www.npmjs.com/package/ngx-devextreme-zoneless)
5
+ [![NPM](https://img.shields.io/npm/v/ngx-devextreme-zoneless?label=NPM&color=3B82F6)](https://www.npmjs.com/package/ngx-devextreme-zoneless)
6
6
 
7
- <p align="center">
8
- <img width="400" height="400" align="center" alt="ngx-devextreme-zoneless-logo" src="package-logo.png" />
9
- </p>
7
+ > **Independent community project.** DevExtreme is a registered trademark of
8
+ > Developer Express Inc. This library is not affiliated with, endorsed by, or
9
+ > sponsored by Developer Express Inc.
10
10
 
11
11
  Strongly-typed **signal & zoneless change-detection adapters** for
12
12
  [DevExtreme Angular](https://js.devexpress.com/Angular/) components.
@@ -24,8 +24,8 @@ Angular signal ──effect──▶ widget option / method
24
24
  Angular signal ◀──set──── widget event (optionChanged, selectionChanged, …)
25
25
  ```
26
26
 
27
- - **Angular** ≥ 20 (built and verified against Angular 22, zoneless by default)
28
- - **DevExtreme / devextreme-angular** ≥ 25.1 (built and verified against 26.1)
27
+ - **Angular** ≥ 22.1 < 23 (built and verified against Angular 22.1.3, zoneless by default)
28
+ - **DevExtreme / devextreme-angular** ≥ 26.1 (built and verified against 26.1.4)
29
29
  - **TypeScript** 6.0, `strict`, zero `any` in the public surface
30
30
 
31
31
  > **Why this library exists:** DevExpress officially still requires zone.js.
@@ -77,428 +77,116 @@ export class OrdersComponent {
77
77
  ```
78
78
 
79
79
  Or import everything at once with `DX_SIGNAL_DIRECTIVES` (also available per
80
- family: `DX_VALUE_DIRECTIVES`, `DX_NAVIGATION_DIRECTIVES`,
81
- `DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`, `DX_VISUALIZATION_DIRECTIVES`).
82
-
83
- Components without a dedicated directive (PivotGrid, Sortable, Menu, …) are
84
- covered too — see [Composition API](#composition-api): the same engine works
85
- with **any** option and event of **any** DevExtreme component, fully typed.
86
-
87
- ## Directive catalog
88
-
89
- ### Editors — `[(dxValue)]`
90
-
91
- Every value editor gets a `[(dxValue)]` two-way model plus `dxValueDebounce`
92
- (ms, widget → signal direction) and `dxValueEqual` (custom comparer) inputs.
93
-
94
- | Component | Model type |
95
- |---|---|
96
- | `dx-text-box`, `dx-text-area`, `dx-html-editor` | `string` |
97
- | `dx-number-box` | `number \| null` |
98
- | `dx-check-box` | `boolean \| null` |
99
- | `dx-switch` | `boolean` |
100
- | `dx-date-box` | `Date \| number \| string \| null` |
101
- | `dx-date-range-box` | `readonly [start: DxDateValue \| null, end: DxDateValue \| null]` |
102
- | `dx-calendar` | `DxDateValue \| readonly DxDateValue[] \| null` |
103
- | `dx-color-box`, `dx-autocomplete` | `string \| null` |
104
- | `dx-slider` | `number` |
105
- | `dx-range-slider` | `readonly [start: number, end: number]` |
106
- | `dx-select-box`, `dx-lookup`, `dx-drop-down-box`, `dx-radio-group` | generic `TValue` — inferred from your signal |
107
- | `dx-tag-box` | generic `readonly TKey[]` |
108
- | `dx-filter-builder` | `DxFilterExpression \| null` |
109
- | `dx-range-selector` | DevExtreme's declared scale range type |
110
-
111
- Where DevExtreme declares `value?: any` (SelectBox & friends) the directive is
112
- **generic** instead, so `[(dxValue)]="status"` with a
113
- `signal<OrderStatus | null>` is checked end-to-end.
114
-
115
- ### Selection & data
116
-
117
- | Selector | Model |
118
- |---|---|
119
- | `dx-data-grid[dxSelectedRowKeys]`, `dx-tree-list[dxSelectedRowKeys]` | `readonly TKey[]` |
120
- | `dx-list[dxSelectedItemKeys]` | `readonly TKey[]` |
121
- | `dx-tree-view[dxSelectedNodeKeys]` | `readonly TKey[]` (method-based: `getSelectedNodeKeys` / `selectItem`) |
122
- | `dx-gantt[dxSelectedRowKey]` | `TKey \| null` |
123
- | `dx-scheduler[dxCurrentDate]` | `Date \| number \| string` |
124
- | `dx-scheduler[dxCurrentView]` | DevExtreme's view union |
125
- | `dx-form[dxFormData]` | generic `TFormData` — emits a fresh shallow copy per field edit |
126
-
127
- ### Grid state (dx-data-grid and dx-tree-list)
128
-
129
- | Selector | Model |
130
- |---|---|
131
- | `[dxFocusedRowKey]` | `TKey \| null` (requires `focusedRowEnabled`; `null` clears) |
132
- | `[dxFilterValue]` | `DxFilterExpression \| null` |
133
- | `[dxPageIndex]`, `[dxPageSize]` | `number` (method-based — paging lives under the nested `paging` option) |
134
- | `[dxGridState]` | `DxGridState \| null \| undefined` — the full layout state (`state()` / `stateStoring`) |
135
-
136
- `[(dxGridState)]` is the layout-persistence story: bind it to a signal you
137
- persist wherever you like. `undefined` pulls the current state on attach,
138
- a saved state object restores the layout, `null` resets to defaults. Widget →
139
- signal reads are debounced (`dxGridStateDebounce`, default 100 ms).
140
-
141
- ### Navigation & overlays
142
-
143
- | Selector | Model |
144
- |---|---|
145
- | `dx-tabs`, `dx-tab-panel`, `dx-accordion`, `dx-gallery` + `[dxSelectedIndex]` | `number` |
146
- | `dx-drawer[dxOpened]` | `boolean` |
147
- | `dx-popup`, `dx-popover`, `dx-tooltip`, `dx-toast`, `dx-load-panel`, `dx-action-sheet` + `[dxVisible]` | `boolean` |
148
-
149
- `[(dxVisible)]` tracks *every* way an overlay can close (shading click, escape,
150
- close button, API) — the classic zoneless pain point.
151
-
152
- ### Visualization
153
-
154
- | Selector | Model |
155
- |---|---|
156
- | `dx-chart[dxSelectedPoints]`, `dx-pie-chart[dxSelectedPoints]` | `readonly DxChartSelectedPoint[]` |
157
-
158
- Chart selection is method-based; selected points are exposed as serializable
159
- `{ seriesName, argument, value }` descriptors that can be stored and restored.
160
- DevExtreme charts do not select points on click by themselves (their demos
161
- wire `onPointClick: e => e.target.select()` manually), so these directives
162
- toggle the clicked point's selection by default — opt out with
163
- `[dxPointClickToggle]="false"` if you want to handle `onPointClick` yourself.
164
-
165
- ## Composition API
166
-
167
- The directives are sugar. The same engine is available as injection-context
168
- functions and covers **every option and event of every DevExtreme component**,
169
- fully typed:
170
-
171
- ```ts
172
- import {
173
- bindDxOption, dxOptionSignal, dxEventSignal, injectDxInstance,
174
- } from 'ngx-devextreme-zoneless';
175
-
176
- @Component({ /* ... */ })
177
- export class MyComponent {
178
- // On the same element (inside a directive):
179
- private readonly textBox = inject(DxTextBoxComponent, { self: true });
180
-
181
- // Two-way signal for any option — names & types from DevExtreme Properties:
182
- readonly placeholder = dxOptionSignal(this.textBox, 'placeholder');
183
- readonly mode = dxOptionSignal(this.textBox, 'mode', { initialValue: 'search' });
184
-
185
- // Latest payload of any widget event:
186
- readonly focusOut = dxEventSignal(this.textBox, 'focusOut');
187
-
188
- // Works with view children too (pass the viewChild signal itself):
189
- private readonly grid = viewChild(DxDataGridComponent<Order, number>);
190
- readonly filter = dxOptionSignal(this.grid, 'filterValue');
191
-
192
- // Bind an existing signal to an option:
193
- readonly keys = signal<number[] | undefined>([]);
194
- constructor() {
195
- bindDxOption(this.grid, 'selectedRowKeys', this.keys);
196
- }
197
-
198
- // The raw widget instance as a signal (undefined until created):
199
- readonly widget = injectDxInstance(DxTextBoxComponent);
200
-
201
- // Same, but for a view child — resets to undefined while the widget is
202
- // gone and follows re-creation:
203
- readonly gridWidget = dxInstanceSignal(this.grid);
204
- }
205
- ```
206
-
207
- ### Widget re-creation
208
-
209
- Composition-API bindings on a **getter host reference** (a `viewChild(...)`
210
- signal) survive widget re-creation: when an `@if`, a tab panel or re-created
211
- popup content destroys the widget and later creates a new one, the binding
212
- re-attaches automatically and the initial synchronization runs again — a
213
- signal holding state therefore **restores it into the new widget**. The
214
- attribute directives don't need any of this; they die and are re-created with
215
- their element.
216
-
217
- `onDxWidgetReady` is the underlying primitive: it fires once per widget
218
- instance, and its callback may return a cleanup that runs when the widget
219
- goes away.
220
-
221
- ### Components without a dedicated directive
222
-
223
- The coverage model is simple: **a directive exists wherever a widget has
224
- genuine user-mutable two-way state** (value, selection, visibility, index,
225
- current date/view, form data, chart points). Every other component — and any
226
- component DevExtreme ships in the future — is still fully supported through
227
- the composition API, because `dxOptionSignal`, `dxEventSignal` and
228
- `bindDxOption` are not written against a component list: their option names,
229
- option value types and event payloads are computed from the `Properties`
230
- declaration of whatever host component you hand them. Support doesn't
231
- cliff-edge at the directive catalog; it just gets one notch less sugary.
232
-
233
- A few real-world cases:
234
-
235
- ```ts
236
- // PivotGrid — no two-way options (its mutable state lives in the
237
- // PivotGridDataSource), but every event and option is reachable, typed:
238
- private readonly pivot = inject(DxPivotGridComponent, { self: true });
239
- readonly cellClick = dxEventSignal(this.pivot, 'cellClick');
240
- readonly contextMenu = dxEventSignal(this.pivot, 'contextMenuPreparing');
241
- ```
242
-
243
- ```ts
244
- // Scheduler beyond [(dxCurrentDate)] / [(dxCurrentView)] — appointment
245
- // lifecycle as signals:
246
- private readonly scheduler = inject(DxSchedulerComponent, { self: true });
247
- readonly appointmentAdded = dxEventSignal(this.scheduler, 'appointmentAdded');
248
- ```
249
-
250
- ```ts
251
- // Sortable (e.g. a Kanban board built from dx-sortable + lists) — purely
252
- // event-driven, so state stays in your signals and events drive mutations:
253
- private readonly sortable = viewChild(DxSortableComponent);
254
- readonly reorder = dxEventSignal(this.sortable, 'reorder');
255
- constructor() {
256
- effect(() => {
257
- const e = this.reorder();
258
- if (e) this.cards.update((cards) => moveItem(cards, e.fromIndex, e.toIndex));
259
- });
260
- }
261
- ```
262
-
263
- ```ts
264
- // Any option of any widget, two-way — e.g. Menu, ButtonGroup, Toolbar, ...:
265
- readonly disabled = dxOptionSignal(this.menu, 'disabled');
266
- ```
267
-
268
- For state that is neither an option nor a single event (method-based APIs),
269
- implement a `DxBindingAdapter` and pass it to `bindDxState` — that is exactly
270
- how the chart-selection and tree-view directives are built. And if you want
271
- template sugar for a component we don't cover, a custom directive is ~10
272
- lines by extending `DxValueDirectiveBase`, `DxVisibleDirectiveBase`, etc.
273
-
274
- ## Data loading (signals → DataSource)
275
-
276
- Two-way state is one half of the zoneless problem; the other half is data
277
- loading — DevExtreme components load through `DataSource`/stores, which know
278
- nothing about signals.
279
-
280
- **Array data in a signal** — `dxArrayStoreSource` wraps a
281
- `Signal<readonly T[]>` in a `DataSource` that reloads whenever the signal
282
- changes:
283
-
284
- ```ts
285
- readonly orders = signal<readonly Order[]>([]);
286
- readonly ordersSource = dxArrayStoreSource(this.orders, { key: 'id' });
287
- // <dx-data-grid [dataSource]="ordersSource" />
288
- ```
289
-
290
- **Remote data with signal-driven filters** — `dxReloadOn` triggers a reload
291
- whenever dependency signals change, so external filter state needs no
292
- hand-written `refresh()` plumbing:
293
-
294
- ```ts
295
- readonly search = signal('');
296
-
297
- // e.g. a DevExtreme.AspNet.Data endpoint:
298
- readonly source = new DataSource({
299
- store: createStore({ loadUrl: '/api/orders', key: 'id' }),
300
- });
301
-
302
- constructor() {
303
- dxReloadOn(this.source, [this.search]);
304
- }
305
- ```
306
-
307
- The target can also be a callback (`dxReloadOn(() => this.grid()?.refresh(), [deps])`)
308
- for anything that is not a `DataSource`.
309
-
310
- ## Remote data (`dxRemoteStore`) <sup>developer preview in 2.1</sup>
311
-
312
- For server-side loading, `dxRemoteStore` wraps a load function in a
313
- `DataSource` and exposes the loading state the widget cannot show for you,
314
- using Angular `resource()` vocabulary — no third dialect. This section is
315
- written from the reference screens in `smoke/src/remote.component.ts`, which
316
- run in CI against a DevExtreme.AspNet.Data protocol stub
317
- (`e2e/stub-server.mjs`) including slow- and failing-endpoint cases.
318
-
319
- ```ts
320
- readonly search = signal('');
321
- readonly status = signal<string | null>(null);
322
-
323
- readonly orders = dxRemoteStore<Order, number>({
324
- key: 'id',
325
- deps: [this.search, this.status], // query state living outside the widget
326
- debounce: 300, // for keystroke-fed dependency signals
327
- load: async (options, signal) => {
328
- // options: DevExtreme LoadOptions (filter/sort/skip/take/…) from the widget;
329
- // merge your external filter signals and go to the server:
330
- const filter = combineFilters(options.filter, myExternalFilter());
331
- const response = await fetch(`/api/orders?${dxLoadOptionsParams({ ...options, filter })}`, { signal });
332
- if (!response.ok) throw new Error(`orders failed: ${response.status}`);
333
- return response.json(); // { data, totalCount } — or a plain array
334
- },
335
- });
336
- ```
337
-
338
- ```html
339
- <dx-data-grid [dataSource]="orders.dataSource" [remoteOperations]="true">…</dx-data-grid>
340
- @if (orders.isLoading()) { <app-spinner /> }
341
- @if (orders.error(); as error) { <button (click)="orders.reload()">retry</button> }
342
- ```
343
-
344
- `dxLoadOptionsParams` serializes `LoadOptions` into the exact query-string
345
- format DevExtreme.AspNet.Data server libraries parse (JSON-encoded
346
- `filter`/`sort`/`group`/`select`/summaries, plain `skip`/`take`/
347
- `requireTotalCount`/`requireGroupCount`) — the same wire format the vendor's
348
- `createStore({ loadUrl })` produces, but through your own `fetch`, so auth
349
- headers, interceptors and `AbortSignal` stay in your hands.
350
-
351
- **The contract**, aligned with `resource()` and verified by e2e:
352
-
353
- - **Cancellation, last write wins.** Every load receives an `AbortSignal`. A
354
- reload — dependency-driven or manual — aborts whatever is in flight;
355
- superseded loads never overwrite newer results, never surface stale errors,
356
- and never trigger the widget's load-error UI.
357
- - **`error` + retry.** `error` holds the rejection of the most recent settled
358
- load and is cleared by the next successful one; `reload()` is the retry.
359
- The widget additionally shows its own load-error row — `error` is for UI
360
- outside the widget.
361
- - **Page ownership.** A dependency change is a new query, so the store resets
362
- to page 0 (mirroring `DataSource.filter()` semantics). Manual `reload()`
363
- refetches the same query and keeps the page. You never manage `pageIndex`
364
- yourself — and a bound `[(dxPageIndex)]` signal follows both cases.
365
- - **Debounce ownership.** The `debounce` option applies to dependency-driven
366
- reloads only (type-ahead filter signals). Widget-driven search (a lookup's
367
- `searchTimeout`) is already debounced by the widget — don't stack a second
368
- debounce on the same keystrokes.
369
- - **Search folds into `filter`.** Widget search options are merged into
370
- `LoadOptions.filter` before `load` runs (vendor-parity `useDefaultSearch`),
371
- so your load function only ever deals in filter expressions.
372
-
373
- **Dependent queries** — a master grid's selection signal drives the detail
374
- store; return an empty result to skip the request entirely:
375
-
376
- ```ts
377
- readonly detailOrders = dxRemoteStore<Order, number>({
378
- key: 'id',
379
- deps: [this.selectedCustomerId],
380
- load: async (options, signal) => {
381
- const customerId = untracked(this.selectedCustomerId);
382
- if (customerId === undefined) return { data: [], totalCount: 0 };
383
- const filter = combineFilters(options.filter, ['customerId', '=', customerId]);
384
- return fetchJson(`/api/orders?${dxLoadOptionsParams({ ...options, filter })}`, signal);
385
- },
386
- });
387
- ```
388
-
389
- **Lookup editors need `byKey`** — a select box showing a programmatically set
390
- value must resolve the item behind the key before any page is loaded. Against
391
- a loadOptions endpoint, `byKey` is a filtered load:
392
-
393
- ```ts
394
- readonly customerLookup = dxRemoteStore<Customer, number>({
395
- key: 'id',
396
- load: (options, signal) =>
397
- fetchJson(`/api/customers?${dxLoadOptionsParams(options)}`, signal),
398
- byKey: async (key, signal) => {
399
- const params = dxLoadOptionsParams({ filter: ['id', '=', key], take: 1 });
400
- const result = await fetchJson<{ data: Customer[] }>(`/api/customers?${params}`, signal);
401
- if (result.data[0] === undefined) throw new Error(`customer ${key} not found`);
402
- return result.data[0];
403
- },
404
- });
405
- ```
406
-
407
- **Scope: load-only, by design.** Create/update/delete deliberately stay out
408
- of this first cut — editing flows couple to validation, conflict handling and
409
- optimistic-update policy that this package should not decide for you. For
410
- editable grids, hand the widget a full `CustomStore` (or the vendor's
411
- `createStore` with `insertUrl`/`updateUrl`/`deleteUrl`) and keep
412
- `dxRemoteStore` for the read paths. If real consumer screens produce a shape
413
- worth hardening, CRUD can graduate the same way this API did.
414
-
415
- ## Semantics
416
-
417
- - **Initial sync** (`initialSync`, default `'auto'`): the signal's value is
418
- pushed into the widget on attach; if it is `undefined`, the widget's current
419
- value is pulled into the signal instead.
420
- - **`undefined` vs `null`**: `undefined` means *absent* — a signal holding
421
- `undefined` is never pushed into the widget (that is what makes `'auto'`
422
- initial sync and late-created widgets work). To *clear* an option through a
423
- binding, use `null`: `status.set(null)` clears a select box,
424
- `status.set(undefined)` does nothing. This mirrors DevExtreme's own
425
- conventions — its clearable editors write `null`, not `undefined`.
426
- - **Debounced typing**: DevExtreme text editors update their `value` option on
427
- *blur* by default. Pair `[(dxValue)]` + `dxValueDebounce` with
428
- `valueChangeEvent="input"` on the editor to get live per-keystroke updates:
429
-
430
- ```html
431
- <dx-text-box [(dxValue)]="search" [dxValueDebounce]="300" valueChangeEvent="input" />
432
- ```
433
-
434
- Without it, the binding appears to only fire on blur — that is the widget's
435
- event timing, not the binding's.
436
- - **Echo suppression**: updates only propagate when the value *structurally*
437
- changed (`Date`-aware, deep for arrays/plain objects). Override per binding
438
- via `equal` / `dxValueEqual`.
439
- - **Debounce** applies to the widget → signal direction only, so typing into a
440
- `dx-text-box` doesn't storm your `computed()` graph.
441
- - **Teardown** is automatic via `DestroyRef` — event handlers are detached and
442
- effects destroyed when the directive/component dies.
443
- - **Re-attach**: with a getter host reference the binding follows widget
444
- re-creation (see [Widget re-creation](#widget-re-creation)); per-widget
445
- resources are disposed on each detach.
446
- - **Zoneless**: no `NgZone`, no `markForCheck` — signal writes are the change
447
- notification.
80
+ family: `DX_VALUE_DIRECTIVES`, `DX_OPENED_DIRECTIVES`, `DX_EDITOR_STATE_DIRECTIVES`,
81
+ `DX_NAVIGATION_DIRECTIVES`, `DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`,
82
+ `DX_VISUALIZATION_DIRECTIVES`).
83
+
84
+ ## Choosing your binding style
85
+
86
+ Three ways to put a value on a DevExtreme editor — the classic one, and the
87
+ two this package opens up:
88
+
89
+ | | A: zone.js + Reactive Forms | B: `[(dxValue)]` | C: `[formField]` + `[dxFieldState]` |
90
+ |---|---|---|---|
91
+ | Change detection | zone.js (opt-in since v21) | zoneless, native | zoneless, native |
92
+ | Vendor support | ✅ the supported config | outside support — netted by this repo's CI | same net |
93
+ | Error display | hand-rolled per field/rule | DIY | the widget's own UI, zero template markup |
94
+ | Schema-driven readonly/hidden/disabled | manual | — | yes, all three |
95
+ | Value type safety | typed forms | end-to-end compile error | restored via `[dxFieldState]` |
96
+ | Boilerplate per field | ~5–8 template lines | ~1 line | ~2 lines, flat |
97
+
98
+ Use **B** for state outside a form (filters, toolbars, settings), **C** for
99
+ forms, and **A** only where zone.js is staying anyway. Full code for all
100
+ three, the trade-offs, and the two rules that keep them honest:
101
+ **[Choosing your binding style](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/binding-styles.md)**.
102
+ The smoke app renders them side by side (`npm run smoke` →
103
+ http://localhost:4299/compare) — including the classic syntax's measured
104
+ zoneless behaviour: field state arrives (Angular's reactive directives mark
105
+ for check on their own); the widget state a CVA never carries is what stays
106
+ invisible, and is what the rest of this package covers.
107
+
108
+ ## What is covered
109
+
110
+ Two-way directives exist wherever a widget has genuine user-mutable state —
111
+ `[(dxValue)]` and `[(dxOpened)]` on the editors, selection, focus, editing
112
+ state, deferred selection and layout state on the grids and the card view,
113
+ navigation, selection and focus on the file manager, expansion on the trees,
114
+ `[(dxVisible)]`/`[(dxOpened)]`/`[(dxSelectedIndex)]`/`[(dxSelectedItemKeys)]`
115
+ on overlays and navigation, pane sizes and dimensions on the splitter and the
116
+ resizable, chart point selection and both axes' visual ranges — each typed to
117
+ the widget's real value type. The
118
+ **[directive catalog](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/directives.md)**
119
+ is the authoritative table. Components without a dedicated directive
120
+ (PivotGrid, Sortable, Menu, …) are fully covered by the
121
+ **[composition API](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/composition-api.md)**:
122
+ `dxOptionSignal`, `dxEventSignal` and `bindDxOption` work with **any** option
123
+ and event of **any** DevExtreme component, fully typed.
124
+
125
+ For data, `dxArrayStoreSource` puts a signal-held array behind a `DataSource`,
126
+ and `dxRemoteStore` wraps a server load function in `resource()` vocabulary —
127
+ loading/error signals, cancellation, signal-driven dependencies — speaking the
128
+ complete DevExtreme.AspNet.Data loadOptions wire protocol through your own
129
+ `fetch`:
130
+ **[data loading and remote data](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/remote-data.md)**.
448
131
 
449
132
  ## Entry points & tree-shaking
450
133
 
451
- The package ships per-family secondary entry points:
452
-
453
134
  ```
454
135
  ngx-devextreme-zoneless → everything (re-exports the below)
455
- ngx-devextreme-zoneless/core → engine, types, bases, data-source/remote helpers
456
- ngx-devextreme-zoneless/editors → [(dxValue)] directives
457
- ngx-devextreme-zoneless/navigation → [(dxSelectedIndex)], [(dxOpened)], …
136
+ ngx-devextreme-zoneless/core → engine, types, bases, signal factories
137
+ ngx-devextreme-zoneless/data-source → dxArrayStoreSource, dxReloadOn, dxRemoteStore, dxLoadOptionsParams
138
+ ngx-devextreme-zoneless/editors → [(dxValue)], [(dxOpened)], [(dxZoomLevel)], [(dxInputFieldText)]
139
+ ngx-devextreme-zoneless/navigation → [(dxSelectedIndex)], [(dxOpened)], splitter and resizable layout, …
458
140
  ngx-devextreme-zoneless/overlays → [(dxVisible)] directives
459
- ngx-devextreme-zoneless/data → grid/form/scheduler/tree directives
141
+ ngx-devextreme-zoneless/data → grid/card-view/file-manager/form/scheduler/tree directives
460
142
  ngx-devextreme-zoneless/visualization → chart selection, range selector
461
- ```
462
-
463
- Importing from the main entry is fine: bundlers drop the families you don't
464
- use at module level (CI proves it — a probe app importing a single editor
465
- directive is asserted to contain no Gantt, Scheduler, HtmlEditor, chart or
466
- grid modules). Import from a family entry point when you want the split to be
467
- explicit.
468
-
469
- ## Type helpers
470
-
471
- All building blocks are exported, e.g.:
472
-
473
- ```ts
474
- DxHostOptionName<DxTextBoxComponent> // 'value' | 'placeholder' | 'mode' | ...
475
- DxHostOptionValue<DxTextBoxComponent, 'mode'> // 'email' | 'password' | ... | undefined
476
- DxHostEventName<DxDataGridComponent> // 'rowClick' | 'selectionChanged' | ...
477
- DxHostEventArg<DxTextBoxComponent, 'valueChanged'>
478
- DxHostValue<DxCheckBoxComponent> // boolean | null
479
- ```
480
-
481
- ## Development
482
-
483
- ```bash
484
- npm run build # ng-packagr build → dist/ (all entry points)
485
- npm run typecheck:examples # strict-template check of examples/ + type contract tests
486
- npm run verify # both
487
- npm run smoke # runtime smoke test app on http://localhost:4299
488
- npm run e2e # Playwright suite against the smoke app
489
- npm run probe # bundle tree-shaking probe (requires a prior build)
490
- ```
491
-
492
- The smoke app ([smoke/src/smoke.component.ts](smoke/src/smoke.component.ts))
493
- boots with `provideZonelessChangeDetection()` — zone.js is not even installed
494
- in this workspace — and exercises text box, select box, check box, slider,
495
- tabs, grid selection/paging/focus, signal-driven data loading, popup
496
- visibility, chart point selection, the composition API and widget
497
- re-creation in a real browser. The Playwright suite
498
- ([e2e/smoke.spec.ts](e2e/smoke.spec.ts)) asserts both directions of every
499
- binding against the `data-testid` readouts and runs in CI on every push.
500
-
501
- See [examples/order-dashboard.component.ts](examples/order-dashboard.component.ts)
502
- for a complete signal-driven dashboard and
503
- [examples/type-tests.ts](examples/type-tests.ts) for the compile-time contract
504
- tests.
143
+ ngx-devextreme-zoneless/forms → [dxFieldState] (Signal Forms; NOT re-exported by the main entry)
144
+ ```
145
+
146
+ No entry point of this package carries widget runtime: every directive holds
147
+ its `devextreme-angular` wrapper class as a type-only import and resolves the
148
+ host through the wrapper's own `NestedOptionHost` provider, so the only widget
149
+ code in your bundle is what you import yourself. That matters under code
150
+ splitting, where bundlers assign files to chunks by reachability rather than by
151
+ use — a directive FESM that imported the grid wrapper would put the grid into
152
+ every lazy route touching that FESM as soon as one route used the grid. CI
153
+ measures both halves: a static scan of the built entry points rejects any
154
+ runtime `devextreme-angular/ui/` import, and a six-chunk probe app asserts that
155
+ a chunk importing TreeView selection next to a grid page, or a text-box
156
+ directive next to an html-editor one, keeps only its own widget in its static
157
+ closure. Importing from the main entry is fine — its `export *` barrel over
158
+ separate FESMs was measured not to re-couple them — and the family entry points
159
+ exist to make the split explicit.
160
+
161
+ ## Documentation
162
+
163
+ | Page | What it covers |
164
+ |---|---|
165
+ | **[Choosing your binding style](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/binding-styles.md)** | The same form three ways — classic zone.js, `[(dxValue)]`, Signal Forms — with syntax, boilerplate and capability compared |
166
+ | **[Directive catalog](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/directives.md)** | Every two-way directive with its model type: editors, selection, grid state, navigation, overlays, visualization |
167
+ | **[Signal Forms](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/signal-forms.md)** | `[dxFieldState]`: what crosses the vendor CVA, what the directive adds, why the field is named twice, debounce ownership |
168
+ | **[Composition API](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/composition-api.md)** | `dxOptionSignal`/`dxEventSignal`/`bindDxOption` for any component, widget re-creation, custom adapters |
169
+ | **[Data loading & remote data](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/remote-data.md)** | `dxArrayStoreSource`, `dxReloadOn`, and the `dxRemoteStore` contract: cancellation, error/retry, paging, `byKey`, `langParams` |
170
+ | **[Binding semantics & type helpers](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/semantics.md)** | The engine contract: `undefined` vs `null`, initial sync, echo suppression, debounce, teardown, the exported type helpers |
171
+
172
+ Working on the package itself:
173
+ [development](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/development.md)
174
+ — builds, the smoke app's six screens, the e2e suite, the tree-shaking
175
+ probe and the server-render measurement.
176
+
177
+ ## Verified, not assumed
178
+
179
+ Running DevExtreme zoneless is outside the vendor-supported configuration, so
180
+ the claim this package makes is exactly as strong as its suite: a Playwright
181
+ run drives every directive in the catalog through a real zoneless app — both
182
+ directions on every widget, widget re-creation, signal-driven `*dxTemplate`
183
+ content, slow and failing endpoints against a wire-protocol stub — on every
184
+ push, and a server render of every screen under `@angular/platform-server`
185
+ pins that the same app bootstraps, settles and raises nothing on the server
186
+ with the vendor's `DxServerModule` in place (measured, not assumed: the vendor
187
+ constructs every widget there, and its viz widgets are empty stand-ins — see
188
+ [development](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/development.md)).
189
+ The compile-time surface has its own contract tests
190
+ ([examples/type-tests.ts](examples/type-tests.ts)): option names, event names
191
+ and value types are pinned so a vendor rename fails the build instead of
192
+ shipping.