ngx-devextreme-zoneless 2.0.0 → 3.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/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,16 +24,18 @@ 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
- > **Why this library exists:** DevExpress officially still requires zone.js —
32
- > zoneless support for `devextreme-angular` has not been started and has no
33
- > ETA. Running DevExtreme zoneless is therefore outside the vendor-supported
34
- > configuration, and this library is the bridge. Its CI (a Playwright suite
35
- > driving every binding through a real zoneless app on every push) is the
36
- > compatibility net the vendor does not provide.
31
+ > **Why this library exists:** DevExpress officially still requires zone.js.
32
+ > As of 2026-08-10 DevExpress is *"considering zoneless Angular support for a
33
+ > future release"* (ticket T1328486) — no ETA, and that thread is where an
34
+ > invite to test will be announced if a release is set. Running DevExtreme
35
+ > zoneless is therefore outside the vendor-supported configuration, and this
36
+ > library is the bridge. Its CI (a Playwright suite driving every binding
37
+ > through a real zoneless app on every push) is the compatibility net the
38
+ > vendor does not provide.
37
39
 
38
40
  Everything is *derived from DevExtreme's own `Properties` declarations* via
39
41
  template-literal and conditional types — option names, option value types and
@@ -78,315 +80,62 @@ Or import everything at once with `DX_SIGNAL_DIRECTIVES` (also available per
78
80
  family: `DX_VALUE_DIRECTIVES`, `DX_NAVIGATION_DIRECTIVES`,
79
81
  `DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`, `DX_VISUALIZATION_DIRECTIVES`).
80
82
 
81
- Components without a dedicated directive (PivotGrid, Sortable, Menu, …) are
82
- covered too — see [Composition API](#composition-api): the same engine works
83
- with **any** option and event of **any** DevExtreme component, fully typed.
84
-
85
- ## Directive catalog
86
-
87
- ### Editors — `[(dxValue)]`
88
-
89
- Every value editor gets a `[(dxValue)]` two-way model plus `dxValueDebounce`
90
- (ms, widget → signal direction) and `dxValueEqual` (custom comparer) inputs.
91
-
92
- | Component | Model type |
93
- |---|---|
94
- | `dx-text-box`, `dx-text-area`, `dx-html-editor` | `string` |
95
- | `dx-number-box` | `number \| null` |
96
- | `dx-check-box` | `boolean \| null` |
97
- | `dx-switch` | `boolean` |
98
- | `dx-date-box` | `Date \| number \| string \| null` |
99
- | `dx-date-range-box` | `readonly [start: DxDateValue \| null, end: DxDateValue \| null]` |
100
- | `dx-calendar` | `DxDateValue \| readonly DxDateValue[] \| null` |
101
- | `dx-color-box`, `dx-autocomplete` | `string \| null` |
102
- | `dx-slider` | `number` |
103
- | `dx-range-slider` | `readonly [start: number, end: number]` |
104
- | `dx-select-box`, `dx-lookup`, `dx-drop-down-box`, `dx-radio-group` | generic `TValue` — inferred from your signal |
105
- | `dx-tag-box` | generic `readonly TKey[]` |
106
- | `dx-filter-builder` | `DxFilterExpression \| null` |
107
- | `dx-range-selector` | DevExtreme's declared scale range type |
108
-
109
- Where DevExtreme declares `value?: any` (SelectBox & friends) the directive is
110
- **generic** instead, so `[(dxValue)]="status"` with a
111
- `signal<OrderStatus | null>` is checked end-to-end.
112
-
113
- ### Selection & data
114
-
115
- | Selector | Model |
116
- |---|---|
117
- | `dx-data-grid[dxSelectedRowKeys]`, `dx-tree-list[dxSelectedRowKeys]` | `readonly TKey[]` |
118
- | `dx-list[dxSelectedItemKeys]` | `readonly TKey[]` |
119
- | `dx-tree-view[dxSelectedNodeKeys]` | `readonly TKey[]` (method-based: `getSelectedNodeKeys` / `selectItem`) |
120
- | `dx-gantt[dxSelectedRowKey]` | `TKey \| null` |
121
- | `dx-scheduler[dxCurrentDate]` | `Date \| number \| string` |
122
- | `dx-scheduler[dxCurrentView]` | DevExtreme's view union |
123
- | `dx-form[dxFormData]` | generic `TFormData` — emits a fresh shallow copy per field edit |
124
-
125
- ### Grid state (dx-data-grid and dx-tree-list)
126
-
127
- | Selector | Model |
128
- |---|---|
129
- | `[dxFocusedRowKey]` | `TKey \| null` (requires `focusedRowEnabled`; `null` clears) |
130
- | `[dxFilterValue]` | `DxFilterExpression \| null` |
131
- | `[dxPageIndex]`, `[dxPageSize]` | `number` (method-based — paging lives under the nested `paging` option) |
132
- | `[dxGridState]` | `DxGridState \| null \| undefined` — the full layout state (`state()` / `stateStoring`) |
133
-
134
- `[(dxGridState)]` is the layout-persistence story: bind it to a signal you
135
- persist wherever you like. `undefined` pulls the current state on attach,
136
- a saved state object restores the layout, `null` resets to defaults. Widget →
137
- signal reads are debounced (`dxGridStateDebounce`, default 100 ms).
138
-
139
- ### Navigation & overlays
140
-
141
- | Selector | Model |
142
- |---|---|
143
- | `dx-tabs`, `dx-tab-panel`, `dx-accordion`, `dx-gallery` + `[dxSelectedIndex]` | `number` |
144
- | `dx-drawer[dxOpened]` | `boolean` |
145
- | `dx-popup`, `dx-popover`, `dx-tooltip`, `dx-toast`, `dx-load-panel`, `dx-action-sheet` + `[dxVisible]` | `boolean` |
146
-
147
- `[(dxVisible)]` tracks *every* way an overlay can close (shading click, escape,
148
- close button, API) — the classic zoneless pain point.
149
-
150
- ### Visualization
151
-
152
- | Selector | Model |
153
- |---|---|
154
- | `dx-chart[dxSelectedPoints]`, `dx-pie-chart[dxSelectedPoints]` | `readonly DxChartSelectedPoint[]` |
155
-
156
- Chart selection is method-based; selected points are exposed as serializable
157
- `{ seriesName, argument, value }` descriptors that can be stored and restored.
158
- DevExtreme charts do not select points on click by themselves (their demos
159
- wire `onPointClick: e => e.target.select()` manually), so these directives
160
- toggle the clicked point's selection by default — opt out with
161
- `[dxPointClickToggle]="false"` if you want to handle `onPointClick` yourself.
162
-
163
- ## Composition API
164
-
165
- The directives are sugar. The same engine is available as injection-context
166
- functions and covers **every option and event of every DevExtreme component**,
167
- fully typed:
168
-
169
- ```ts
170
- import {
171
- bindDxOption, dxOptionSignal, dxEventSignal, injectDxInstance,
172
- } from 'ngx-devextreme-zoneless';
173
-
174
- @Component({ /* ... */ })
175
- export class MyComponent {
176
- // On the same element (inside a directive):
177
- private readonly textBox = inject(DxTextBoxComponent, { self: true });
178
-
179
- // Two-way signal for any option — names & types from DevExtreme Properties:
180
- readonly placeholder = dxOptionSignal(this.textBox, 'placeholder');
181
- readonly mode = dxOptionSignal(this.textBox, 'mode', { initialValue: 'search' });
182
-
183
- // Latest payload of any widget event:
184
- readonly focusOut = dxEventSignal(this.textBox, 'focusOut');
185
-
186
- // Works with view children too (pass the viewChild signal itself):
187
- private readonly grid = viewChild(DxDataGridComponent<Order, number>);
188
- readonly filter = dxOptionSignal(this.grid, 'filterValue');
189
-
190
- // Bind an existing signal to an option:
191
- readonly keys = signal<number[] | undefined>([]);
192
- constructor() {
193
- bindDxOption(this.grid, 'selectedRowKeys', this.keys);
194
- }
195
-
196
- // The raw widget instance as a signal (undefined until created):
197
- readonly widget = injectDxInstance(DxTextBoxComponent);
198
-
199
- // Same, but for a view child — resets to undefined while the widget is
200
- // gone and follows re-creation:
201
- readonly gridWidget = dxInstanceSignal(this.grid);
202
- }
203
- ```
204
-
205
- ### Widget re-creation
206
-
207
- Composition-API bindings on a **getter host reference** (a `viewChild(...)`
208
- signal) survive widget re-creation: when an `@if`, a tab panel or re-created
209
- popup content destroys the widget and later creates a new one, the binding
210
- re-attaches automatically and the initial synchronization runs again — a
211
- signal holding state therefore **restores it into the new widget**. The
212
- attribute directives don't need any of this; they die and are re-created with
213
- their element.
214
-
215
- `onDxWidgetReady` is the underlying primitive: it fires once per widget
216
- instance, and its callback may return a cleanup that runs when the widget
217
- goes away.
218
-
219
- ### Components without a dedicated directive
220
-
221
- The coverage model is simple: **a directive exists wherever a widget has
222
- genuine user-mutable two-way state** (value, selection, visibility, index,
223
- current date/view, form data, chart points). Every other component — and any
224
- component DevExtreme ships in the future — is still fully supported through
225
- the composition API, because `dxOptionSignal`, `dxEventSignal` and
226
- `bindDxOption` are not written against a component list: their option names,
227
- option value types and event payloads are computed from the `Properties`
228
- declaration of whatever host component you hand them. Support doesn't
229
- cliff-edge at the directive catalog; it just gets one notch less sugary.
230
-
231
- A few real-world cases:
232
-
233
- ```ts
234
- // PivotGrid — no two-way options (its mutable state lives in the
235
- // PivotGridDataSource), but every event and option is reachable, typed:
236
- private readonly pivot = inject(DxPivotGridComponent, { self: true });
237
- readonly cellClick = dxEventSignal(this.pivot, 'cellClick');
238
- readonly contextMenu = dxEventSignal(this.pivot, 'contextMenuPreparing');
239
- ```
240
-
241
- ```ts
242
- // Scheduler beyond [(dxCurrentDate)] / [(dxCurrentView)] — appointment
243
- // lifecycle as signals:
244
- private readonly scheduler = inject(DxSchedulerComponent, { self: true });
245
- readonly appointmentAdded = dxEventSignal(this.scheduler, 'appointmentAdded');
246
- ```
247
-
248
- ```ts
249
- // Sortable (e.g. a Kanban board built from dx-sortable + lists) — purely
250
- // event-driven, so state stays in your signals and events drive mutations:
251
- private readonly sortable = viewChild(DxSortableComponent);
252
- readonly reorder = dxEventSignal(this.sortable, 'reorder');
253
- constructor() {
254
- effect(() => {
255
- const e = this.reorder();
256
- if (e) this.cards.update((cards) => moveItem(cards, e.fromIndex, e.toIndex));
257
- });
258
- }
259
- ```
260
-
261
- ```ts
262
- // Any option of any widget, two-way — e.g. Menu, ButtonGroup, Toolbar, ...:
263
- readonly disabled = dxOptionSignal(this.menu, 'disabled');
264
- ```
265
-
266
- For state that is neither an option nor a single event (method-based APIs),
267
- implement a `DxBindingAdapter` and pass it to `bindDxState` — that is exactly
268
- how the chart-selection and tree-view directives are built. And if you want
269
- template sugar for a component we don't cover, a custom directive is ~10
270
- lines by extending `DxValueDirectiveBase`, `DxVisibleDirectiveBase`, etc.
271
-
272
- ## Data loading (signals → DataSource)
273
-
274
- Two-way state is one half of the zoneless problem; the other half is data
275
- loading — DevExtreme components load through `DataSource`/stores, which know
276
- nothing about signals.
277
-
278
- **Array data in a signal** — `dxArrayStoreSource` wraps a
279
- `Signal<readonly T[]>` in a `DataSource` that reloads whenever the signal
280
- changes:
281
-
282
- ```ts
283
- readonly orders = signal<readonly Order[]>([]);
284
- readonly ordersSource = dxArrayStoreSource(this.orders, { key: 'id' });
285
- // <dx-data-grid [dataSource]="ordersSource" />
286
- ```
287
-
288
- **Remote data with signal-driven filters** — `dxReloadOn` triggers a reload
289
- whenever dependency signals change, so external filter state needs no
290
- hand-written `refresh()` plumbing:
291
-
292
- ```ts
293
- readonly search = signal('');
294
-
295
- // e.g. a DevExtreme.AspNet.Data endpoint:
296
- readonly source = new DataSource({
297
- store: createStore({ loadUrl: '/api/orders', key: 'id' }),
298
- });
299
-
300
- constructor() {
301
- dxReloadOn(this.source, [this.search]);
302
- }
303
- ```
304
-
305
- The target can also be a callback (`dxReloadOn(() => this.grid()?.refresh(), [deps])`)
306
- for anything that is not a `DataSource`.
307
-
308
- **Recipe: full remote loading state as signals.** Pair a `CustomStore` with
309
- signals for loading/error state — the store reads filters `untracked`, and
310
- `dxReloadOn` drives reloads:
311
-
312
- ```ts
313
- readonly search = signal('');
314
- readonly isLoading = signal(false);
315
- readonly loadError = signal<unknown>(undefined);
316
-
317
- readonly source = new DataSource<Order, number>({
318
- store: new CustomStore<Order, number>({
319
- key: 'id',
320
- loadMode: 'raw',
321
- load: async () => {
322
- this.isLoading.set(true);
323
- try {
324
- const query = untracked(this.search);
325
- return await fetchOrders(query);
326
- } catch (error) {
327
- this.loadError.set(error);
328
- throw error;
329
- } finally {
330
- this.isLoading.set(false);
331
- }
332
- },
333
- }),
334
- });
335
-
336
- constructor() {
337
- dxReloadOn(this.source, [this.search]);
338
- }
339
- ```
340
-
341
- This intentionally ships as a recipe rather than API: the shape will be
342
- extracted from real consumer screens before it hardens into code.
343
-
344
- ## Semantics
345
-
346
- - **Initial sync** (`initialSync`, default `'auto'`): the signal's value is
347
- pushed into the widget on attach; if it is `undefined`, the widget's current
348
- value is pulled into the signal instead.
349
- - **`undefined` vs `null`**: `undefined` means *absent* — a signal holding
350
- `undefined` is never pushed into the widget (that is what makes `'auto'`
351
- initial sync and late-created widgets work). To *clear* an option through a
352
- binding, use `null`: `status.set(null)` clears a select box,
353
- `status.set(undefined)` does nothing. This mirrors DevExtreme's own
354
- conventions — its clearable editors write `null`, not `undefined`.
355
- - **Debounced typing**: DevExtreme text editors update their `value` option on
356
- *blur* by default. Pair `[(dxValue)]` + `dxValueDebounce` with
357
- `valueChangeEvent="input"` on the editor to get live per-keystroke updates:
358
-
359
- ```html
360
- <dx-text-box [(dxValue)]="search" [dxValueDebounce]="300" valueChangeEvent="input" />
361
- ```
362
-
363
- Without it, the binding appears to only fire on blur — that is the widget's
364
- event timing, not the binding's.
365
- - **Echo suppression**: updates only propagate when the value *structurally*
366
- changed (`Date`-aware, deep for arrays/plain objects). Override per binding
367
- via `equal` / `dxValueEqual`.
368
- - **Debounce** applies to the widget → signal direction only, so typing into a
369
- `dx-text-box` doesn't storm your `computed()` graph.
370
- - **Teardown** is automatic via `DestroyRef` — event handlers are detached and
371
- effects destroyed when the directive/component dies.
372
- - **Re-attach**: with a getter host reference the binding follows widget
373
- re-creation (see [Widget re-creation](#widget-re-creation)); per-widget
374
- resources are disposed on each detach.
375
- - **Zoneless**: no `NgZone`, no `markForCheck` — signal writes are the change
376
- notification.
83
+ ## Choosing your binding style
84
+
85
+ Three ways to put a value on a DevExtreme editor — the classic one, and the
86
+ two this package opens up:
87
+
88
+ | | A: zone.js + Reactive Forms | B: `[(dxValue)]` | C: `[formField]` + `[dxFieldState]` |
89
+ |---|---|---|---|
90
+ | Change detection | zone.js (opt-in since v21) | zoneless, native | zoneless, native |
91
+ | Vendor support | ✅ the supported config | outside support — netted by this repo's CI | same net |
92
+ | Error display | hand-rolled per field/rule | DIY | the widget's own UI, zero template markup |
93
+ | Schema-driven readonly/hidden/disabled | manual | — | yes, all three |
94
+ | Value type safety | typed forms | end-to-end compile error | restored via `[dxFieldState]` |
95
+ | Boilerplate per field | ~5–8 template lines | ~1 line | ~2 lines, flat |
96
+
97
+ Use **B** for state outside a form (filters, toolbars, settings), **C** for
98
+ forms, and **A** only where zone.js is staying anyway. Full code for all
99
+ three, the trade-offs, and the two rules that keep them honest:
100
+ **[Choosing your binding style](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/binding-styles.md)**.
101
+ The smoke app renders them side by side (`npm run smoke` →
102
+ http://localhost:4299/compare) — including the classic syntax's measured
103
+ zoneless behaviour: field state arrives (Angular's reactive directives mark
104
+ for check on their own); the widget state a CVA never carries is what stays
105
+ invisible, and is what the rest of this package covers.
106
+
107
+ ## What is covered
108
+
109
+ Two-way directives exist wherever a widget has genuine user-mutable state —
110
+ `[(dxValue)]` on every editor, selection and grid state on the data widgets,
111
+ `[(dxVisible)]`/`[(dxOpened)]`/`[(dxSelectedIndex)]` on overlays and
112
+ navigation, chart point selection — each typed to the widget's real value
113
+ type. The
114
+ **[directive catalog](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/directives.md)**
115
+ is the authoritative table. Components without a dedicated directive
116
+ (PivotGrid, Sortable, Menu, …) are fully covered by the
117
+ **[composition API](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/composition-api.md)**:
118
+ `dxOptionSignal`, `dxEventSignal` and `bindDxOption` work with **any** option
119
+ and event of **any** DevExtreme component, fully typed.
120
+
121
+ For data, `dxArrayStoreSource` puts a signal-held array behind a `DataSource`,
122
+ and `dxRemoteStore` wraps a server load function in `resource()` vocabulary —
123
+ loading/error signals, cancellation, signal-driven dependencies — speaking the
124
+ complete DevExtreme.AspNet.Data loadOptions wire protocol through your own
125
+ `fetch`:
126
+ **[data loading and remote data](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/remote-data.md)**.
377
127
 
378
128
  ## Entry points & tree-shaking
379
129
 
380
- The package ships per-family secondary entry points:
381
-
382
130
  ```
383
131
  ngx-devextreme-zoneless → everything (re-exports the below)
384
- ngx-devextreme-zoneless/core → engine, types, bases, data-source helpers
132
+ ngx-devextreme-zoneless/core → engine, types, bases, data-source/remote helpers
385
133
  ngx-devextreme-zoneless/editors → [(dxValue)] directives
386
134
  ngx-devextreme-zoneless/navigation → [(dxSelectedIndex)], [(dxOpened)], …
387
135
  ngx-devextreme-zoneless/overlays → [(dxVisible)] directives
388
136
  ngx-devextreme-zoneless/data → grid/form/scheduler/tree directives
389
137
  ngx-devextreme-zoneless/visualization → chart selection, range selector
138
+ ngx-devextreme-zoneless/forms → [dxFieldState] (Signal Forms; NOT re-exported by the main entry)
390
139
  ```
391
140
 
392
141
  Importing from the main entry is fine: bundlers drop the families you don't
@@ -395,39 +144,29 @@ directive is asserted to contain no Gantt, Scheduler, HtmlEditor, chart or
395
144
  grid modules). Import from a family entry point when you want the split to be
396
145
  explicit.
397
146
 
398
- ## Type helpers
399
-
400
- All building blocks are exported, e.g.:
401
-
402
- ```ts
403
- DxHostOptionName<DxTextBoxComponent> // 'value' | 'placeholder' | 'mode' | ...
404
- DxHostOptionValue<DxTextBoxComponent, 'mode'> // 'email' | 'password' | ... | undefined
405
- DxHostEventName<DxDataGridComponent> // 'rowClick' | 'selectionChanged' | ...
406
- DxHostEventArg<DxTextBoxComponent, 'valueChanged'>
407
- DxHostValue<DxCheckBoxComponent> // boolean | null
408
- ```
409
-
410
- ## Development
147
+ ## Documentation
411
148
 
412
- ```bash
413
- npm run build # ng-packagr build → dist/ (all entry points)
414
- npm run typecheck:examples # strict-template check of examples/ + type contract tests
415
- npm run verify # both
416
- npm run smoke # runtime smoke test app on http://localhost:4299
417
- npm run e2e # Playwright suite against the smoke app
418
- npm run probe # bundle tree-shaking probe (requires a prior build)
419
- ```
420
-
421
- The smoke app ([smoke/src/smoke.component.ts](smoke/src/smoke.component.ts))
422
- boots with `provideZonelessChangeDetection()` — zone.js is not even installed
423
- in this workspace — and exercises text box, select box, check box, slider,
424
- tabs, grid selection/paging/focus, signal-driven data loading, popup
425
- visibility, chart point selection, the composition API and widget
426
- re-creation in a real browser. The Playwright suite
427
- ([e2e/smoke.spec.ts](e2e/smoke.spec.ts)) asserts both directions of every
428
- binding against the `data-testid` readouts and runs in CI on every push.
429
-
430
- See [examples/order-dashboard.component.ts](examples/order-dashboard.component.ts)
431
- for a complete signal-driven dashboard and
432
- [examples/type-tests.ts](examples/type-tests.ts) for the compile-time contract
433
- tests.
149
+ | Page | What it covers |
150
+ |---|---|
151
+ | **[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 |
152
+ | **[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 |
153
+ | **[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 |
154
+ | **[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 |
155
+ | **[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` |
156
+ | **[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 |
157
+
158
+ Working on the package itself:
159
+ [development](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/development.md)
160
+ — builds, the smoke app's four screens, the e2e suite and the tree-shaking
161
+ probe.
162
+
163
+ ## Verified, not assumed
164
+
165
+ Running DevExtreme zoneless is outside the vendor-supported configuration, so
166
+ the claim this package makes is exactly as strong as its suite: a Playwright
167
+ run drives every binding family through a real zoneless app — both directions,
168
+ widget re-creation, slow and failing endpoints against a wire-protocol stub —
169
+ on every push. The compile-time surface has its own contract tests
170
+ ([examples/type-tests.ts](examples/type-tests.ts)): option names, event names
171
+ and value types are pinned so a vendor rename fails the build instead of
172
+ shipping.