ngx-devextreme-zoneless 3.0.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 +40 -20
- package/fesm2022/ngx-devextreme-zoneless-core.mjs +50 -398
- 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 +432 -29
- package/fesm2022/ngx-devextreme-zoneless-data.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-editors.mjs +207 -41
- package/fesm2022/ngx-devextreme-zoneless-editors.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-forms.mjs +40 -40
- package/fesm2022/ngx-devextreme-zoneless-forms.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-navigation.mjs +284 -21
- package/fesm2022/ngx-devextreme-zoneless-navigation.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-overlays.mjs +22 -15
- package/fesm2022/ngx-devextreme-zoneless-overlays.mjs.map +1 -1
- package/fesm2022/ngx-devextreme-zoneless-visualization.mjs +131 -11
- 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 +6 -12
- package/types/ngx-devextreme-zoneless-core.d.ts +40 -170
- 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 +197 -4
- 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 +15 -4
- package/types/ngx-devextreme-zoneless-forms.d.ts.map +1 -1
- 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 +85 -5
- 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
|
@@ -77,8 +77,9 @@ 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`,
|
|
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`).
|
|
82
83
|
|
|
83
84
|
## Choosing your binding style
|
|
84
85
|
|
|
@@ -107,10 +108,13 @@ invisible, and is what the rest of this package covers.
|
|
|
107
108
|
## What is covered
|
|
108
109
|
|
|
109
110
|
Two-way directives exist wherever a widget has genuine user-mutable state —
|
|
110
|
-
`[(dxValue)]`
|
|
111
|
-
|
|
112
|
-
navigation,
|
|
113
|
-
|
|
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
|
|
114
118
|
**[directive catalog](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/directives.md)**
|
|
115
119
|
is the authoritative table. Components without a dedicated directive
|
|
116
120
|
(PivotGrid, Sortable, Menu, …) are fully covered by the
|
|
@@ -129,20 +133,30 @@ complete DevExtreme.AspNet.Data loadOptions wire protocol through your own
|
|
|
129
133
|
|
|
130
134
|
```
|
|
131
135
|
ngx-devextreme-zoneless → everything (re-exports the below)
|
|
132
|
-
ngx-devextreme-zoneless/core → engine, types, bases,
|
|
133
|
-
ngx-devextreme-zoneless/
|
|
134
|
-
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, …
|
|
135
140
|
ngx-devextreme-zoneless/overlays → [(dxVisible)] directives
|
|
136
|
-
ngx-devextreme-zoneless/data → grid/form/scheduler/tree directives
|
|
141
|
+
ngx-devextreme-zoneless/data → grid/card-view/file-manager/form/scheduler/tree directives
|
|
137
142
|
ngx-devextreme-zoneless/visualization → chart selection, range selector
|
|
138
143
|
ngx-devextreme-zoneless/forms → [dxFieldState] (Signal Forms; NOT re-exported by the main entry)
|
|
139
144
|
```
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
146
160
|
|
|
147
161
|
## Documentation
|
|
148
162
|
|
|
@@ -157,16 +171,22 @@ explicit.
|
|
|
157
171
|
|
|
158
172
|
Working on the package itself:
|
|
159
173
|
[development](https://github.com/CaffeinatedCoder/ngx-devextreme-zoneless/blob/main/docs/development.md)
|
|
160
|
-
— builds, the smoke app's
|
|
161
|
-
probe.
|
|
174
|
+
— builds, the smoke app's six screens, the e2e suite, the tree-shaking
|
|
175
|
+
probe and the server-render measurement.
|
|
162
176
|
|
|
163
177
|
## Verified, not assumed
|
|
164
178
|
|
|
165
179
|
Running DevExtreme zoneless is outside the vendor-supported configuration, so
|
|
166
180
|
the claim this package makes is exactly as strong as its suite: a Playwright
|
|
167
|
-
run drives every
|
|
168
|
-
widget re-creation,
|
|
169
|
-
|
|
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
|
|
170
190
|
([examples/type-tests.ts](examples/type-tests.ts)): option names, event names
|
|
171
191
|
and value types are pinned so a vendor rename fails the build instead of
|
|
172
192
|
shipping.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { assertInInjectionContext, inject, Injector, DestroyRef, afterNextRender, effect, untracked, signal,
|
|
3
|
-
import
|
|
4
|
-
import DataSource from 'devextreme/data/data_source';
|
|
2
|
+
import { assertInInjectionContext, inject, Injector, DestroyRef, afterNextRender, effect, untracked, signal, Directive, model, input } from '@angular/core';
|
|
3
|
+
import { NestedOptionHost } from 'devextreme-angular/core';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Structural equality used to break the two-way binding echo cycle
|
|
@@ -425,6 +424,34 @@ function dxInstanceSignal(hostRef, injector) {
|
|
|
425
424
|
}, resolvedInjector);
|
|
426
425
|
return instance.asReadonly();
|
|
427
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Injects the devextreme-angular component hosting the *current element*
|
|
429
|
+
* without naming its class — and therefore without a runtime import of
|
|
430
|
+
* `devextreme-angular/ui/<widget>`:
|
|
431
|
+
*
|
|
432
|
+
* ```ts
|
|
433
|
+
* const host = injectDxHost<DxTextBoxComponent>(); // `DxTextBoxComponent` is a type-only import
|
|
434
|
+
* ```
|
|
435
|
+
*
|
|
436
|
+
* Every generated wrapper lists `NestedOptionHost` in its own `providers` and
|
|
437
|
+
* calls `setHost(this)` in its constructor (verified against devextreme-angular
|
|
438
|
+
* 26.1.4 for all forty adapted widgets), so the token resolves at the widget's
|
|
439
|
+
* element injector and points at the wrapper itself. Nested option components
|
|
440
|
+
* (`dxo-*`, `dxi-*`) get their *own* instance on their own element and never
|
|
441
|
+
* overwrite the widget's. The injection is strictly `{ self: true }`.
|
|
442
|
+
*
|
|
443
|
+
* This is what every shipped directive uses, and the reason is bundle
|
|
444
|
+
* isolation rather than taste: a value import of the wrapper class is a
|
|
445
|
+
* module-level edge from the directive's FESM to the widget, and esbuild
|
|
446
|
+
* assigns files to chunks by reachability — so a widget that is live for one
|
|
447
|
+
* lazy route becomes part of every other route's closure that shares the FESM.
|
|
448
|
+
* With the class as a type-only import the edge does not exist. The caller
|
|
449
|
+
* names the type; the directive's selector (`dx-text-box[dxValue]`) is what
|
|
450
|
+
* makes it true.
|
|
451
|
+
*/
|
|
452
|
+
function injectDxHost() {
|
|
453
|
+
return inject(NestedOptionHost, { self: true }).getHost();
|
|
454
|
+
}
|
|
428
455
|
/**
|
|
429
456
|
* Injects a devextreme-angular component from the *current element* and
|
|
430
457
|
* exposes its widget instance as a signal that becomes non-`undefined` once
|
|
@@ -444,400 +471,6 @@ function injectDxInstance(token) {
|
|
|
444
471
|
return dxInstanceSignal(inject(token, { self: true }));
|
|
445
472
|
}
|
|
446
473
|
|
|
447
|
-
/**
|
|
448
|
-
* Internal: invokes `run` whenever one of the dependency signals changes.
|
|
449
|
-
* The first execution only collects dependencies — reloads happen on changes.
|
|
450
|
-
* With `debounceMs > 0` the invocation is trailing-debounced; the pending
|
|
451
|
-
* timer is cleared both by the next change and by injector destruction (the
|
|
452
|
-
* effect's cleanup runs in either case). `run` executes untracked, so it can
|
|
453
|
-
* read signals without creating subscriptions.
|
|
454
|
-
*
|
|
455
|
-
* Returns a function that drops a debounced run still waiting to fire,
|
|
456
|
-
* reporting whether there was one. That lets a caller with its own refetch
|
|
457
|
-
* path *adopt* a pending dependency change instead of racing it — see
|
|
458
|
-
* `dxRemoteStore`'s `reload()`.
|
|
459
|
-
*
|
|
460
|
-
* Not exported from the public API — this is the one implementation behind
|
|
461
|
-
* `dxReloadOn` and `dxRemoteStore`'s dependency-driven reloads.
|
|
462
|
-
*/
|
|
463
|
-
function watchDxSignals(dependencies, run, options) {
|
|
464
|
-
const debounceMs = options.debounceMs ?? 0;
|
|
465
|
-
let first = true;
|
|
466
|
-
let pending;
|
|
467
|
-
effect((onCleanup) => {
|
|
468
|
-
for (const dependency of dependencies) {
|
|
469
|
-
dependency();
|
|
470
|
-
}
|
|
471
|
-
if (first) {
|
|
472
|
-
first = false;
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
if (debounceMs > 0) {
|
|
476
|
-
const timer = setTimeout(() => {
|
|
477
|
-
pending = undefined;
|
|
478
|
-
untracked(run);
|
|
479
|
-
}, debounceMs);
|
|
480
|
-
pending = timer;
|
|
481
|
-
onCleanup(() => {
|
|
482
|
-
clearTimeout(timer);
|
|
483
|
-
if (pending === timer) {
|
|
484
|
-
pending = undefined;
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
else {
|
|
489
|
-
untracked(run);
|
|
490
|
-
}
|
|
491
|
-
}, { injector: options.injector });
|
|
492
|
-
return () => {
|
|
493
|
-
if (pending === undefined) {
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
clearTimeout(pending);
|
|
497
|
-
pending = undefined;
|
|
498
|
-
return true;
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Wraps a `Signal<readonly T[]>` in a DevExtreme `DataSource` that reloads
|
|
504
|
-
* whenever the signal changes — bind it to any `[dataSource]` input and the
|
|
505
|
-
* widget follows the signal:
|
|
506
|
-
*
|
|
507
|
-
* ```ts
|
|
508
|
-
* readonly orders = signal<readonly Order[]>([]);
|
|
509
|
-
* readonly ordersSource = dxArrayStoreSource(this.orders, { key: 'id' });
|
|
510
|
-
* // <dx-data-grid [dataSource]="ordersSource" />
|
|
511
|
-
* ```
|
|
512
|
-
*
|
|
513
|
-
* The store loads in `'raw'` mode, so DevExtreme applies filtering, sorting
|
|
514
|
-
* and paging client-side exactly as for a plain array. The source is disposed
|
|
515
|
-
* when the injector is destroyed.
|
|
516
|
-
*/
|
|
517
|
-
function dxArrayStoreSource(source, options = {}) {
|
|
518
|
-
if (options.injector === undefined) {
|
|
519
|
-
assertInInjectionContext(dxArrayStoreSource);
|
|
520
|
-
}
|
|
521
|
-
const injector = options.injector ?? inject(Injector);
|
|
522
|
-
const dataSource = new DataSource({
|
|
523
|
-
store: new CustomStore({
|
|
524
|
-
key: options.key,
|
|
525
|
-
loadMode: 'raw',
|
|
526
|
-
// `untracked` — loads must not create reactive subscriptions;
|
|
527
|
-
// reloading is driven by the effect below.
|
|
528
|
-
load: () => untracked(source),
|
|
529
|
-
}),
|
|
530
|
-
});
|
|
531
|
-
dxReloadOn(dataSource, [source], injector);
|
|
532
|
-
injector.get(DestroyRef).onDestroy(() => dataSource.dispose());
|
|
533
|
-
return dataSource;
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Triggers a reload whenever one of the dependency signals changes — the
|
|
537
|
-
* missing link between external filter state held in signals and a
|
|
538
|
-
* remote-loading `DataSource` (or grid), without hand-written `refresh()`
|
|
539
|
-
* plumbing in every component:
|
|
540
|
-
*
|
|
541
|
-
* ```ts
|
|
542
|
-
* readonly search = signal('');
|
|
543
|
-
* readonly source = new DataSource({ store: createStore({ loadUrl: '/api/orders' }) });
|
|
544
|
-
* constructor() {
|
|
545
|
-
* dxReloadOn(this.source, [this.search]);
|
|
546
|
-
* }
|
|
547
|
-
* ```
|
|
548
|
-
*
|
|
549
|
-
* The first run only collects dependencies; reloads happen on changes. For a
|
|
550
|
-
* target that is not a `DataSource` (e.g. a grid instance living in a
|
|
551
|
-
* signal), pass a callback: `dxReloadOn(() => this.grid()?.refresh(), [deps])`.
|
|
552
|
-
*/
|
|
553
|
-
function dxReloadOn(target, dependencies, injector) {
|
|
554
|
-
if (injector === undefined) {
|
|
555
|
-
assertInInjectionContext(dxReloadOn);
|
|
556
|
-
}
|
|
557
|
-
const resolvedInjector = injector ?? inject(Injector);
|
|
558
|
-
const trigger = typeof target === 'function'
|
|
559
|
-
? target
|
|
560
|
-
: 'reload' in target
|
|
561
|
-
? () => target.reload()
|
|
562
|
-
: () => target.refresh();
|
|
563
|
-
watchDxSignals(dependencies, () => void trigger(), { injector: resolvedInjector });
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Wraps a remote load function in a DevExtreme `DataSource` with signal-based
|
|
568
|
-
* loading state:
|
|
569
|
-
*
|
|
570
|
-
* ```ts
|
|
571
|
-
* readonly search = signal('');
|
|
572
|
-
* readonly orders = dxRemoteStore<Order, number>({
|
|
573
|
-
* key: 'id',
|
|
574
|
-
* deps: [this.search],
|
|
575
|
-
* debounce: 300,
|
|
576
|
-
* load: async (options, signal) => {
|
|
577
|
-
* const params = dxLoadOptionsParams(options);
|
|
578
|
-
* params.set('search', untracked(this.search));
|
|
579
|
-
* const response = await fetch(`/api/orders?${params}`, { signal });
|
|
580
|
-
* if (!response.ok) throw new Error(`orders load failed: ${response.status}`);
|
|
581
|
-
* return response.json();
|
|
582
|
-
* },
|
|
583
|
-
* });
|
|
584
|
-
* // <dx-data-grid [dataSource]="orders.dataSource" [remoteOperations]="true" />
|
|
585
|
-
* // @if (orders.error()) { <button (click)="orders.reload()">retry</button> }
|
|
586
|
-
* ```
|
|
587
|
-
*
|
|
588
|
-
* Semantics, aligned with Angular's `resource()`:
|
|
589
|
-
* - every load gets an `AbortSignal`; a reload (dependency-driven or manual)
|
|
590
|
-
* aborts whatever is still in flight — last write wins, stale responses
|
|
591
|
-
* never overwrite newer ones and never surface stale errors;
|
|
592
|
-
* - `error` reflects the most recent settled load and is cleared by the next
|
|
593
|
-
* successful one; `reload()` is the retry;
|
|
594
|
-
* - dependency changes reset to page 0 (a new query is a new result set);
|
|
595
|
-
* manual `reload()` keeps the page (same query, fresh data) — unless a
|
|
596
|
-
* dependency change is still pending in the debounce window, which it adopts
|
|
597
|
-
* rather than races;
|
|
598
|
-
* - widget search options are folded into `LoadOptions.filter` before `load`
|
|
599
|
-
* runs (`useDefaultSearch`, matching the vendor's own remote client), so
|
|
600
|
-
* the load function only ever deals in filter expressions.
|
|
601
|
-
*
|
|
602
|
-
* The store is load-only by design: create/update/delete intentionally stay
|
|
603
|
-
* out of this API — pass a full `CustomStore` to the widget where you need
|
|
604
|
-
* editing.
|
|
605
|
-
*/
|
|
606
|
-
/**
|
|
607
|
-
* Normalizes the counts in a `LoadResult` envelope.
|
|
608
|
-
*
|
|
609
|
-
* DevExtreme types `totalCount` and `groupCount` as `number`, but the wire
|
|
610
|
-
* protocol's reference server implementations aggregate counts into a decimal
|
|
611
|
-
* and spell a decimal as a **quoted JSON string** — so a conformant endpoint
|
|
612
|
-
* may answer `"120"` where the typings promise `120`. Forwarded as-is, the
|
|
613
|
-
* grid's pager arithmetic silently concatenates instead of adding: a measured
|
|
614
|
-
* 120-row result renders as "Page 1 of 120 (1200 items)".
|
|
615
|
-
*
|
|
616
|
-
* Only these two fields are coerced, and only when the string parses as a
|
|
617
|
-
* finite number. `summary` is deliberately left alone: it is positional and
|
|
618
|
-
* may legitimately carry non-numeric aggregates (a `min` over a string or date
|
|
619
|
-
* column), so blind coercion there would corrupt real values.
|
|
620
|
-
*/
|
|
621
|
-
function normalizeCounts(result) {
|
|
622
|
-
if (Array.isArray(result) || result === null || typeof result !== 'object') {
|
|
623
|
-
return result;
|
|
624
|
-
}
|
|
625
|
-
let normalized;
|
|
626
|
-
for (const key of ['totalCount', 'groupCount']) {
|
|
627
|
-
const value = result[key];
|
|
628
|
-
if (typeof value !== 'string') {
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
const parsed = Number(value);
|
|
632
|
-
if (value.trim() === '' || !Number.isFinite(parsed)) {
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
normalized ??= { ...result };
|
|
636
|
-
normalized[key] = parsed;
|
|
637
|
-
}
|
|
638
|
-
return (normalized ?? result);
|
|
639
|
-
}
|
|
640
|
-
function dxRemoteStore(options) {
|
|
641
|
-
if (options.injector === undefined) {
|
|
642
|
-
assertInInjectionContext(dxRemoteStore);
|
|
643
|
-
}
|
|
644
|
-
const injector = options.injector ?? inject(Injector);
|
|
645
|
-
const pendingCount = signal(0, /* @ts-ignore */
|
|
646
|
-
...(ngDevMode ? [{ debugName: "pendingCount" }] : /* istanbul ignore next */ []));
|
|
647
|
-
const lastError = signal(undefined, /* @ts-ignore */
|
|
648
|
-
...(ngDevMode ? [{ debugName: "lastError" }] : /* istanbul ignore next */ []));
|
|
649
|
-
let loadSequence = 0;
|
|
650
|
-
/** Query-scoped loads: superseded by `reload()`, aborted on destroy. */
|
|
651
|
-
const inFlightLoads = new Set();
|
|
652
|
-
/** Key-scoped lookups: aborted on destroy only — see `byKey` above. */
|
|
653
|
-
const inFlightByKey = new Set();
|
|
654
|
-
const trackedLoad = (loadOptions) => {
|
|
655
|
-
const sequence = ++loadSequence;
|
|
656
|
-
const controller = new AbortController();
|
|
657
|
-
inFlightLoads.add(controller);
|
|
658
|
-
pendingCount.update((count) => count + 1);
|
|
659
|
-
const settle = () => {
|
|
660
|
-
inFlightLoads.delete(controller);
|
|
661
|
-
pendingCount.update((count) => count - 1);
|
|
662
|
-
};
|
|
663
|
-
// A load function that is not declared `async` can throw *synchronously*
|
|
664
|
-
// while it builds its request (a bad dependency, a filter that will not
|
|
665
|
-
// serialize, …). Funnel that into the same rejection path — otherwise
|
|
666
|
-
// the tracking never settles and `isLoading` stays `true` forever while
|
|
667
|
-
// the reason never reaches `error`.
|
|
668
|
-
let pending;
|
|
669
|
-
try {
|
|
670
|
-
pending = options.load(loadOptions, controller.signal);
|
|
671
|
-
}
|
|
672
|
-
catch (reason) {
|
|
673
|
-
pending = Promise.reject(reason);
|
|
674
|
-
}
|
|
675
|
-
return Promise.resolve(pending).then((result) => {
|
|
676
|
-
settle();
|
|
677
|
-
if (sequence === loadSequence) {
|
|
678
|
-
lastError.set(undefined);
|
|
679
|
-
}
|
|
680
|
-
return normalizeCounts(result);
|
|
681
|
-
}, (reason) => {
|
|
682
|
-
settle();
|
|
683
|
-
if (controller.signal.aborted) {
|
|
684
|
-
// Superseded load: reject with DevExtreme's own
|
|
685
|
-
// cancellation token (the literal `'canceled'`), which the
|
|
686
|
-
// DataSource recognizes and drops without firing
|
|
687
|
-
// `loadError` — the widget never flashes its error UI for
|
|
688
|
-
// a load this store aborted on purpose.
|
|
689
|
-
throw 'canceled';
|
|
690
|
-
}
|
|
691
|
-
if (sequence === loadSequence) {
|
|
692
|
-
lastError.set(reason);
|
|
693
|
-
}
|
|
694
|
-
throw reason;
|
|
695
|
-
});
|
|
696
|
-
};
|
|
697
|
-
const userByKey = options.byKey;
|
|
698
|
-
const trackedByKey = userByKey === undefined
|
|
699
|
-
? undefined
|
|
700
|
-
: (key) => {
|
|
701
|
-
const controller = new AbortController();
|
|
702
|
-
inFlightByKey.add(controller);
|
|
703
|
-
// Same synchronous-throw guard as `trackedLoad`, so a
|
|
704
|
-
// non-`async` `byKey` cannot leak its controller.
|
|
705
|
-
let pending;
|
|
706
|
-
try {
|
|
707
|
-
pending = userByKey(key, controller.signal);
|
|
708
|
-
}
|
|
709
|
-
catch (reason) {
|
|
710
|
-
pending = Promise.reject(reason);
|
|
711
|
-
}
|
|
712
|
-
return Promise.resolve(pending).finally(() => inFlightByKey.delete(controller));
|
|
713
|
-
};
|
|
714
|
-
const dataSource = new DataSource({
|
|
715
|
-
langParams: options.langParams,
|
|
716
|
-
store: new CustomStore({
|
|
717
|
-
key: options.key,
|
|
718
|
-
loadMode: 'processed',
|
|
719
|
-
useDefaultSearch: true,
|
|
720
|
-
load: trackedLoad,
|
|
721
|
-
byKey: trackedByKey,
|
|
722
|
-
}),
|
|
723
|
-
});
|
|
724
|
-
/** Aborts the query-scoped loads only — `byKey` deliberately survives. */
|
|
725
|
-
const abortInFlightLoads = () => {
|
|
726
|
-
for (const controller of inFlightLoads) {
|
|
727
|
-
controller.abort();
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
/** Drops a debounced dependency run still waiting; `true` if there was one. */
|
|
731
|
-
let cancelPendingDepsRun;
|
|
732
|
-
const reload = () => {
|
|
733
|
-
// Abort in-flight transport before starting over — the aborted loads
|
|
734
|
-
// reject as `'canceled'` (see trackedLoad), which DevExtreme drops
|
|
735
|
-
// silently. Deliberately NOT `dataSource.cancelAll()`: that method
|
|
736
|
-
// resets the internal operation-id counter, and the id reuse makes
|
|
737
|
-
// grid adapters discard the results of every subsequent load.
|
|
738
|
-
abortInFlightLoads();
|
|
739
|
-
// A dependency change still inside the debounce window describes the
|
|
740
|
-
// very refetch this call is asking for, so adopt it rather than race
|
|
741
|
-
// it: take the page-0 reset it implies and drop its timer. Racing was
|
|
742
|
-
// measured at four round trips against at most two on the same path,
|
|
743
|
-
// and moved the page out from under the user a few hundred
|
|
744
|
-
// milliseconds after they had already asked for fresh data. The
|
|
745
|
-
// adopted path costs one round trip or two: resetting the page and
|
|
746
|
-
// refetching are separate `DataSource` operations, and a widget
|
|
747
|
-
// sitting off page 0 usually interposes its current-page load before
|
|
748
|
-
// the page-0 one — though its internal load scheduling can collapse
|
|
749
|
-
// the pair into the page-0 load alone (measured both ways, 26.1.4).
|
|
750
|
-
if (cancelPendingDepsRun?.() === true) {
|
|
751
|
-
dataSource.pageIndex(0);
|
|
752
|
-
}
|
|
753
|
-
return Promise.resolve(dataSource.reload()).then(() => undefined, () => undefined);
|
|
754
|
-
};
|
|
755
|
-
const deps = options.deps ?? [];
|
|
756
|
-
if (deps.length > 0) {
|
|
757
|
-
// Dependencies describe the query, so a change starts a new result
|
|
758
|
-
// set: reset to the first page (mirroring `DataSource.filter()`
|
|
759
|
-
// semantics) and reload. The helper owns the debounce timer,
|
|
760
|
-
// including clearing it on injector destruction.
|
|
761
|
-
cancelPendingDepsRun = watchDxSignals(deps, () => {
|
|
762
|
-
dataSource.pageIndex(0);
|
|
763
|
-
void reload();
|
|
764
|
-
}, { debounceMs: options.debounce, injector });
|
|
765
|
-
}
|
|
766
|
-
injector.get(DestroyRef).onDestroy(() => {
|
|
767
|
-
abortInFlightLoads();
|
|
768
|
-
for (const controller of inFlightByKey) {
|
|
769
|
-
controller.abort();
|
|
770
|
-
}
|
|
771
|
-
dataSource.dispose();
|
|
772
|
-
});
|
|
773
|
-
return {
|
|
774
|
-
dataSource,
|
|
775
|
-
isLoading: computed(() => pendingCount() > 0),
|
|
776
|
-
error: lastError.asReadonly(),
|
|
777
|
-
reload,
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
function normalizeSorting(value) {
|
|
781
|
-
const list = Array.isArray(value) ? value : [value];
|
|
782
|
-
return list.map((entry) => {
|
|
783
|
-
if (typeof entry === 'object' && entry !== null && 'selector' in entry) {
|
|
784
|
-
return entry;
|
|
785
|
-
}
|
|
786
|
-
return { selector: entry, desc: false };
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Serializes `LoadOptions` into the query parameters understood by the
|
|
791
|
-
* DevExtreme.AspNet.Data server libraries — the same wire format the
|
|
792
|
-
* vendor's own `createStore({ loadUrl })` client produces: structured
|
|
793
|
-
* options JSON-encoded (`filter`, `sort`, `group`, `select`, `totalSummary`,
|
|
794
|
-
* `groupSummary`, `langParams`, with string `sort`/`group` entries normalized
|
|
795
|
-
* to `{ selector }` objects), scalars plain (`skip`, `take`,
|
|
796
|
-
* `requireTotalCount`, `requireGroupCount`).
|
|
797
|
-
*
|
|
798
|
-
* ```ts
|
|
799
|
-
* const response = await fetch(`/api/orders?${dxLoadOptionsParams(options)}`, { signal });
|
|
800
|
-
* ```
|
|
801
|
-
*/
|
|
802
|
-
function dxLoadOptionsParams(loadOptions) {
|
|
803
|
-
const params = new URLSearchParams();
|
|
804
|
-
for (const key of ['skip', 'take', 'requireTotalCount', 'requireGroupCount']) {
|
|
805
|
-
const value = loadOptions[key];
|
|
806
|
-
if (value !== undefined && value !== null) {
|
|
807
|
-
params.set(key, String(value));
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
// `parentIds` carries the keys of the rows being expanded — the only
|
|
811
|
-
// loadOptions member a TreeList adds, and the one the protocol's reference
|
|
812
|
-
// .NET server does not parse (verified against 5.1.0: it declares no
|
|
813
|
-
// `ParentIds`). A remote hierarchy is therefore a contract between the
|
|
814
|
-
// widget and *your* endpoint; the client's job is simply not to drop what
|
|
815
|
-
// the widget produced, which is what this line guarantees.
|
|
816
|
-
for (const key of ['filter', 'select', 'totalSummary', 'groupSummary', 'parentIds']) {
|
|
817
|
-
const value = loadOptions[key];
|
|
818
|
-
if (value !== undefined && value !== null) {
|
|
819
|
-
params.set(key, JSON.stringify(value));
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
for (const key of ['sort', 'group']) {
|
|
823
|
-
const value = loadOptions[key];
|
|
824
|
-
if (value !== undefined && value !== null) {
|
|
825
|
-
params.set(key, JSON.stringify(normalizeSorting(value)));
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// `langParams` rides the loadOptions object at runtime but is absent from
|
|
829
|
-
// DevExtreme's `LoadOptions` typing — it is declared on `DataSourceOptions`
|
|
830
|
-
// only — so it is read through an explicit cast rather than from the typed
|
|
831
|
-
// lists above. Dropping it silently changes collation between client and
|
|
832
|
-
// server, which is why it is carried even though the typings do not admit
|
|
833
|
-
// it exists.
|
|
834
|
-
const langParams = loadOptions.langParams;
|
|
835
|
-
if (langParams !== undefined && langParams !== null) {
|
|
836
|
-
params.set('langParams', JSON.stringify(langParams));
|
|
837
|
-
}
|
|
838
|
-
return params;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
474
|
/**
|
|
842
475
|
* Abstract base directives for the concrete per-component adapter directives.
|
|
843
476
|
*
|
|
@@ -928,6 +561,25 @@ class DxSelectedIndexDirectiveBase extends DxBindingDirectiveBase {
|
|
|
928
561
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DxSelectedIndexDirectiveBase, decorators: [{
|
|
929
562
|
type: Directive
|
|
930
563
|
}], ctorParameters: () => [{ type: undefined }, { type: undefined }], propDecorators: { dxSelectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "dxSelectedIndex", required: true }] }, { type: i0.Output, args: ["dxSelectedIndexChange"] }] } });
|
|
564
|
+
/**
|
|
565
|
+
* `[(dxOpened)]` — for widgets with an open/closed state: the Drawer, the
|
|
566
|
+
* drop-down editors (SelectBox, Lookup, DateBox, TagBox, ...) and the
|
|
567
|
+
* DropDownButton. Every way the widget closes — outside click, escape, an
|
|
568
|
+
* item pick, tab-out — lands back in the signal.
|
|
569
|
+
*/
|
|
570
|
+
class DxOpenedDirectiveBase extends DxBindingDirectiveBase {
|
|
571
|
+
dxOpened = model.required(/* @ts-ignore */
|
|
572
|
+
...(ngDevMode ? [{ debugName: "dxOpened" }] : /* istanbul ignore next */ []));
|
|
573
|
+
constructor(host, optionName) {
|
|
574
|
+
super();
|
|
575
|
+
this.bindOption(host, optionName, this.dxOpened);
|
|
576
|
+
}
|
|
577
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DxOpenedDirectiveBase, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
|
|
578
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: DxOpenedDirectiveBase, isStandalone: true, inputs: { dxOpened: { classPropertyName: "dxOpened", publicName: "dxOpened", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { dxOpened: "dxOpenedChange" }, usesInheritance: true, ngImport: i0 });
|
|
579
|
+
}
|
|
580
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DxOpenedDirectiveBase, decorators: [{
|
|
581
|
+
type: Directive
|
|
582
|
+
}], ctorParameters: () => [{ type: undefined }, { type: undefined }], propDecorators: { dxOpened: [{ type: i0.Input, args: [{ isSignal: true, alias: "dxOpened", required: true }] }, { type: i0.Output, args: ["dxOpenedChange"] }] } });
|
|
931
583
|
/** `[(dxVisible)]` — for overlay widgets (Popup, Popover, Toast, ...). */
|
|
932
584
|
class DxVisibleDirectiveBase extends DxBindingDirectiveBase {
|
|
933
585
|
dxVisible = model.required(/* @ts-ignore */
|
|
@@ -953,5 +605,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
953
605
|
* Generated bundle index. Do not edit.
|
|
954
606
|
*/
|
|
955
607
|
|
|
956
|
-
export { DxBindingDirectiveBase, DxSelectedIndexDirectiveBase, DxSelectedItemKeysDirectiveBase, DxSelectedRowKeysDirectiveBase, DxValueDirectiveBase, DxVisibleDirectiveBase, bindDxOption, bindDxState,
|
|
608
|
+
export { DxBindingDirectiveBase, DxOpenedDirectiveBase, DxSelectedIndexDirectiveBase, DxSelectedItemKeysDirectiveBase, DxSelectedRowKeysDirectiveBase, DxValueDirectiveBase, DxVisibleDirectiveBase, bindDxOption, bindDxState, dxEventSignal, dxInstanceSignal, dxOptionAdapter, dxOptionSignal, dxShallowObjectEquals, dxStructuralClone, dxStructuralEquals, injectDxHost, injectDxInstance, onDxWidgetReady };
|
|
957
609
|
//# sourceMappingURL=ngx-devextreme-zoneless-core.mjs.map
|