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.
Files changed (38) hide show
  1. package/README.md +40 -20
  2. package/fesm2022/ngx-devextreme-zoneless-core.mjs +50 -398
  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 +432 -29
  7. package/fesm2022/ngx-devextreme-zoneless-data.mjs.map +1 -1
  8. package/fesm2022/ngx-devextreme-zoneless-editors.mjs +207 -41
  9. package/fesm2022/ngx-devextreme-zoneless-editors.mjs.map +1 -1
  10. package/fesm2022/ngx-devextreme-zoneless-forms.mjs +40 -40
  11. package/fesm2022/ngx-devextreme-zoneless-forms.mjs.map +1 -1
  12. package/fesm2022/ngx-devextreme-zoneless-navigation.mjs +284 -21
  13. package/fesm2022/ngx-devextreme-zoneless-navigation.mjs.map +1 -1
  14. package/fesm2022/ngx-devextreme-zoneless-overlays.mjs +22 -15
  15. package/fesm2022/ngx-devextreme-zoneless-overlays.mjs.map +1 -1
  16. package/fesm2022/ngx-devextreme-zoneless-visualization.mjs +131 -11
  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 +6 -12
  21. package/types/ngx-devextreme-zoneless-core.d.ts +40 -170
  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 +197 -4
  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 +15 -4
  30. package/types/ngx-devextreme-zoneless-forms.d.ts.map +1 -1
  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 +85 -5
  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
@@ -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`, `DX_NAVIGATION_DIRECTIVES`,
81
- `DX_VISIBLE_DIRECTIVES`, `DX_DATA_DIRECTIVES`, `DX_VISUALIZATION_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)]` 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
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, data-source/remote helpers
133
- ngx-devextreme-zoneless/editors → [(dxValue)] directives
134
- 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, …
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
- Importing from the main entry is fine: bundlers drop the families you don't
142
- use at module level (CI proves it — a probe app importing a single editor
143
- directive is asserted to contain no Gantt, Scheduler, HtmlEditor, chart or
144
- grid modules). Import from a family entry point when you want the split to be
145
- explicit.
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 four screens, the e2e suite and the tree-shaking
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 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
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, computed, Directive, model, input } from '@angular/core';
3
- import CustomStore from 'devextreme/data/custom_store';
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, dxArrayStoreSource, dxEventSignal, dxInstanceSignal, dxLoadOptionsParams, dxOptionAdapter, dxOptionSignal, dxReloadOn, dxRemoteStore, dxShallowObjectEquals, dxStructuralClone, dxStructuralEquals, injectDxInstance, onDxWidgetReady };
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