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.
- package/README.md +112 -424
- package/fesm2022/ngx-devextreme-zoneless-core.mjs +116 -303
- package/fesm2022/ngx-devextreme-zoneless-core.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-data-source.mjs +444 -0
- package/fesm2022/ngx-devextreme-zoneless-data-source.mjs.map +1 -0
- package/fesm2022/ngx-devextreme-zoneless-data.mjs +755 -166
- package/fesm2022/ngx-devextreme-zoneless-data.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-editors.mjs +264 -98
- package/fesm2022/ngx-devextreme-zoneless-editors.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-forms.mjs +408 -0
- package/fesm2022/ngx-devextreme-zoneless-forms.mjs.map +1 -0
- package/fesm2022/ngx-devextreme-zoneless-navigation.mjs +301 -38
- package/fesm2022/ngx-devextreme-zoneless-navigation.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-overlays.mjs +40 -33
- package/fesm2022/ngx-devextreme-zoneless-overlays.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-visualization.mjs +172 -38
- package/fesm2022/ngx-devextreme-zoneless-visualization.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless.mjs +10 -6
- package/fesm2022/ngx-devextreme-zoneless.mjs.map +1 -1
- package/package.json +18 -14
- package/types/ngx-devextreme-zoneless-core.d.ts +65 -183
- package/types/ngx-devextreme-zoneless-core.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless-data-source.d.ts +187 -0
- package/types/ngx-devextreme-zoneless-data-source.d.ts.map +1 -0
- package/types/ngx-devextreme-zoneless-data.d.ts +345 -44
- package/types/ngx-devextreme-zoneless-data.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless-editors.d.ts +106 -4
- package/types/ngx-devextreme-zoneless-editors.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless-forms.d.ts +214 -0
- package/types/ngx-devextreme-zoneless-forms.d.ts.map +1 -0
- package/types/ngx-devextreme-zoneless-navigation.d.ts +140 -6
- package/types/ngx-devextreme-zoneless-navigation.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless-overlays.d.ts +9 -2
- package/types/ngx-devextreme-zoneless-overlays.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless-visualization.d.ts +119 -16
- package/types/ngx-devextreme-zoneless-visualization.d.ts.map +1 -1
- package/types/ngx-devextreme-zoneless.d.ts +2 -1
- package/types/ngx-devextreme-zoneless.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://context7.com/caffeinatedcoder/ngx-devextreme-zoneless)
|
|
5
|
-
[](https://www.npmjs.com/package/ngx-devextreme-zoneless)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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** ≥
|
|
28
|
-
- **DevExtreme / devextreme-angular** ≥
|
|
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`, `
|
|
81
|
-
`DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
456
|
-
ngx-devextreme-zoneless/
|
|
457
|
-
ngx-devextreme-zoneless/
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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.
|