mvc-kit 2.13.2 → 2.14.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 (46) hide show
  1. package/BEST_PRACTICES.md +53 -4
  2. package/agent-config/claude-code/skills/mvc-kit/SKILL.md +1 -1
  3. package/agent-config/claude-code/skills/mvc-kit/anti-patterns.md +47 -0
  4. package/agent-config/claude-code/skills/mvc-kit/patterns.md +11 -0
  5. package/agent-config/claude-code/skills/mvc-kit/recipes.md +103 -22
  6. package/agent-config/claude-code/skills/mvc-kit/testing.md +3 -2
  7. package/agent-config/claude-code/skills/mvc-kit-review/checklist.md +7 -6
  8. package/agent-config/copilot/copilot-instructions.md +34 -0
  9. package/agent-config/cursor/cursorrules +34 -0
  10. package/dist/react/components/DataTable.cjs +5 -9
  11. package/dist/react/components/DataTable.cjs.map +1 -1
  12. package/dist/react/components/DataTable.d.ts.map +1 -1
  13. package/dist/react/components/DataTable.js +5 -9
  14. package/dist/react/components/DataTable.js.map +1 -1
  15. package/dist/react/use-instance.cjs +6 -3
  16. package/dist/react/use-instance.cjs.map +1 -1
  17. package/dist/react/use-instance.d.ts.map +1 -1
  18. package/dist/react/use-instance.js +6 -3
  19. package/dist/react/use-instance.js.map +1 -1
  20. package/dist/react/use-local.cjs +1 -0
  21. package/dist/react/use-local.cjs.map +1 -1
  22. package/dist/react/use-local.js +1 -0
  23. package/dist/react/use-local.js.map +1 -1
  24. package/dist/react/use-model.cjs +34 -8
  25. package/dist/react/use-model.cjs.map +1 -1
  26. package/dist/react/use-model.d.ts.map +1 -1
  27. package/dist/react/use-model.js +34 -8
  28. package/dist/react/use-model.js.map +1 -1
  29. package/dist/react/use-subscribe-only.cjs +3 -2
  30. package/dist/react/use-subscribe-only.cjs.map +1 -1
  31. package/dist/react/use-subscribe-only.d.ts.map +1 -1
  32. package/dist/react/use-subscribe-only.js +3 -2
  33. package/dist/react/use-subscribe-only.js.map +1 -1
  34. package/examples/react/AuthExample/src/components/AdminPage.tsx +3 -1
  35. package/examples/react/AuthExample/src/components/DashboardPage.tsx +4 -2
  36. package/examples/react/AuthExample/src/components/ProfilePage.tsx +2 -1
  37. package/package.json +1 -1
  38. package/src/Model.md +55 -6
  39. package/src/Service.md +4 -1
  40. package/src/react/components/DataTable.tsx +9 -13
  41. package/src/react/use-instance.ts +14 -3
  42. package/src/react/use-local.ts +2 -1
  43. package/src/react/use-model.md +51 -4
  44. package/src/react/use-model.test.tsx +86 -0
  45. package/src/react/use-model.ts +44 -15
  46. package/src/react/use-subscribe-only.ts +3 -2
package/BEST_PRACTICES.md CHANGED
@@ -986,11 +986,59 @@ class UserFormModel extends Model<UserFormState> {
986
986
 
987
987
  ### Models Inside ViewModels
988
988
 
989
- For forms with surrounding page state (submission, server errors), wrap the Model inside a ViewModel:
989
+ For forms with surrounding page state (submission, server errors), wrap the Model inside a ViewModel. There are two shapes; the right choice depends on whether the form's initial data is already in hand:
990
+
991
+ | If the initial data is... | Shape |
992
+ |---|---|
993
+ | **Available at construction** (create modal, edit modal receiving an entity prop) | Create the Model in the **constructor** as `readonly`. Non-nullable, no guard. |
994
+ | **Fetched by ID** after mount (edit page that loads from the server) | Declare the Model as `\| null = null` and create it in `onInit()`. Component must guard. |
995
+
996
+ **Never** write `public model!: FormModel` — the `!` definite-assignment assertion lies to TypeScript about runtime state. First render runs before `onInit()`, `vm.model` is undefined, and any component reading it crashes. Pick one of the two shapes below instead.
997
+
998
+ #### Shape 1 — Data available at construction (preferred when applicable)
990
999
 
991
1000
  ```typescript
992
1001
  class EditUserViewModel extends ViewModel<EditState, EditEvents> {
993
- public model!: UserFormModel;
1002
+ // `readonly` refers to the *reference* — the model's state still changes via set().
1003
+ public readonly model: UserFormModel;
1004
+ private service = singleton(UserService);
1005
+
1006
+ constructor(initialState: EditState) {
1007
+ super(initialState);
1008
+ this.model = new UserFormModel(initialState.existing ?? INITIAL_FORM_STATE);
1009
+ }
1010
+
1011
+ async save() {
1012
+ if (!this.model.valid) return;
1013
+ const result = await this.service.save(this.model.state, this.disposeSignal);
1014
+ this.model.commit();
1015
+ this.emit('saved', { id: result.id });
1016
+ }
1017
+
1018
+ protected onDispose() {
1019
+ this.model.dispose();
1020
+ }
1021
+ }
1022
+ ```
1023
+
1024
+ The component — no guard needed; the model exists from the first render:
1025
+
1026
+ ```tsx
1027
+ function EditUserModal({ existing, onClose }: Props) {
1028
+ const [, vm] = useLocal(EditUserViewModel, { existing });
1029
+ const saveState = vm.async.save;
1030
+
1031
+ useEvent(vm, 'saved', onClose);
1032
+
1033
+ return <EditUserForm model={vm.model} onSave={vm.save} saving={saveState.loading} />;
1034
+ }
1035
+ ```
1036
+
1037
+ #### Shape 2 — Data fetched by ID
1038
+
1039
+ ```typescript
1040
+ class EditUserViewModel extends ViewModel<EditState, EditEvents> {
1041
+ public model: UserFormModel | null = null;
994
1042
  private service = singleton(UserService);
995
1043
 
996
1044
  protected async onInit() {
@@ -1000,7 +1048,7 @@ class EditUserViewModel extends ViewModel<EditState, EditEvents> {
1000
1048
  }
1001
1049
 
1002
1050
  async save() {
1003
- if (!this.model.valid) return;
1051
+ if (!this.model || !this.model.valid) return;
1004
1052
  await this.service.update(this.userId, this.model.state, this.disposeSignal);
1005
1053
  this.model.commit();
1006
1054
  this.emit('saved', { id: this.userId });
@@ -1012,7 +1060,7 @@ class EditUserViewModel extends ViewModel<EditState, EditEvents> {
1012
1060
  }
1013
1061
  ```
1014
1062
 
1015
- The component:
1063
+ The component — **must** guard on `vm.model` before rendering the form:
1016
1064
 
1017
1065
  ```tsx
1018
1066
  function EditUserPage() {
@@ -1388,3 +1436,4 @@ Components emit data attributes for CSS hooks — no class name opinions:
1388
1436
  - `Pending` as a ViewModel own property — it dies on unmount; put it on the singleton Resource
1389
1437
  - Passing `this.disposeSignal` to fetch inside Pending's execute callback — would abort on VM unmount, defeating resilience
1390
1438
  - Using `collection.optimistic()` rollback with Pending — snapshot goes stale during retries
1439
+ - Write `public model!: FormModel` or any other `!` definite-assignment / non-null assertion on a ViewModel / Model field — the `!` lies to TypeScript about runtime state. Either make the field non-nullable by creating it in the constructor, or declare it nullable and guard the component render. Same rule for non-null assertions on any nullable value (`foo!.bar`) — narrow or restructure instead.
@@ -84,7 +84,7 @@ Most features combine multiple classes. These are the key composition patterns:
84
84
 
85
85
  - **Data table**: ViewModel + Collection/Resource + Sorting + Pagination + Selection → getter pipeline (`items → filtered → sorted → paged`). Always reset pagination when filters change.
86
86
  - **Infinite scroll / chat**: ViewModel + Feed + Service → cursor-based server pagination with `appendPage()`. Use `AbortSignal.any()` for per-call cancellation on rapid user interactions.
87
- - **Form with validation**: ViewModel creates Model in `onInit()`, disposes in `onDispose()`. Guard `save()` on `model.valid`. Call `model.commit()` after successful save.
87
+ - **Form with validation**: ViewModel owns a Model. If the initial data is known at construction (create modal, edit modal with entity prop), declare the Model `readonly` and create it in the **constructor**. If the data is fetched by ID, declare it `| null = null` and create it in `onInit()` the component must then guard with `if (!vm.model) return <Spinner />`. Never write `public model!: FormModel` (the `!` lies and crashes the first render). Dispose in `onDispose()`, guard `save()` on `model.valid`, commit after successful save. See Recipe 3 (known data) and Recipe 4 (fetched data).
88
88
  - **Two-tier events**: ViewModel `.emit()` for component reactions (navigate, show inline error). EventBus `.emit()` for app-wide side effects (toasts, analytics).
89
89
  - **Singleton with DEFAULT_STATE**: `static DEFAULT_STATE` lets `singleton()` and `useSingleton()` work without repeating initial state at every call site.
90
90
 
@@ -591,3 +591,50 @@ async deleteItem(id: string) {
591
591
  });
592
592
  }
593
593
  ```
594
+
595
+ ---
596
+
597
+ ## 26. Definite-Assignment Assertion (`!`) on a Model/ViewModel Field
598
+
599
+ ```typescript
600
+ // BAD — `!` lies to TypeScript about runtime state.
601
+ // First render runs before onInit(), so vm.model is undefined and useField/useModel crash.
602
+ class EditVM extends ViewModel<EditState> {
603
+ public model!: FormModel;
604
+
605
+ protected onInit() {
606
+ this.model = new FormModel(this.state.existing);
607
+ }
608
+ }
609
+
610
+ // GOOD (data known at construction) — create in constructor, non-nullable
611
+ class EditVM extends ViewModel<EditState> {
612
+ public readonly model: FormModel;
613
+
614
+ constructor(state: EditState) {
615
+ super(state);
616
+ this.model = new FormModel(state.existing ?? INITIAL);
617
+ }
618
+
619
+ protected onDispose() { this.model.dispose(); }
620
+ }
621
+
622
+ // GOOD (data fetched by ID) — declare nullable, guard in component
623
+ class EditVM extends ViewModel<EditState> {
624
+ public model: FormModel | null = null;
625
+
626
+ protected async onInit() {
627
+ const entity = await this.service.getById(this.state.id, this.disposeSignal);
628
+ this.model = new FormModel(entity);
629
+ }
630
+
631
+ protected onDispose() { this.model?.dispose(); }
632
+ }
633
+ ```
634
+
635
+ ```tsx
636
+ // Component must guard for the nullable shape:
637
+ if (!vm.model) return <Spinner />;
638
+ ```
639
+
640
+ Extends to non-null assertions in general (`foo!.bar`, `arr[0]!`) — don't use `!` to paper over a nullable type when the real fix is either (a) make it non-nullable by construction, or (b) narrow it with a proper check. See Recipe 3 (known data) and Recipe 4 (fetched data).
@@ -608,6 +608,17 @@ class UserFormModel extends Model<UserFormState> {
608
608
  const { state, errors, valid, dirty, model } = useModel(() => new UserFormModel({ name: '', email: '' }));
609
609
  ```
610
610
 
611
+ ### Model inside a ViewModel — two shapes
612
+
613
+ | If the initial data is... | Shape |
614
+ |---|---|
615
+ | **Available at construction** (create modal, edit modal with entity prop) | `readonly model: FormModel;` + assign in constructor. Non-nullable, no guard. |
616
+ | **Fetched by ID** after mount | `model: FormModel \| null = null;` + assign in `onInit()`. Component must guard with `if (!vm.model) return <Spinner />`. |
617
+
618
+ **Never** write `public model!: FormModel`. The `!` definite-assignment assertion lies about runtime state: first render runs before `onInit`, so `vm.model` is `undefined`, and `useField`/`useModel` crash. Always either make the field non-nullable via constructor creation, or declare it nullable and guard.
619
+
620
+ See Recipe 3 (known data) and Recipe 4 (fetched data) in [recipes.md](recipes.md).
621
+
611
622
  ---
612
623
 
613
624
  ## Testing Pattern
@@ -240,9 +240,96 @@ function MessageThread({ conversationId }: { conversationId: string }) {
240
240
 
241
241
  ---
242
242
 
243
- ## Recipe 3: Form with Model Validation
243
+ ## Choosing a Form Recipe
244
244
 
245
- ViewModel creates and owns a Model instance. Model handles validation, dirty tracking, commit/rollback. ViewModel handles async save.
245
+ | If the form's initial data is... | Use |
246
+ |---|---|
247
+ | **Already available** at construction (create modal, edit modal receiving an entity prop, wizard step) | **Recipe 3** — Form with Known Initial Data |
248
+ | **Fetched by ID** after mount (edit page that loads from the server) | **Recipe 4** — Form that Loads by ID |
249
+
250
+ The distinction matters because Recipe 4 has a null guard and a lifecycle trap that Recipe 3 does not. Don't copy Recipe 4 when Recipe 3 applies — you'll write defensive code for a nullability you never needed.
251
+
252
+ ---
253
+
254
+ ## Recipe 3: Form with Known Initial Data
255
+
256
+ The initial data is available when the ViewModel is constructed — passed through `useLocal`'s initial state, or known from a constant. The Model is created in the constructor, so it's non-nullable from the first render onward.
257
+
258
+ ```typescript
259
+ interface EditState {
260
+ existing: AlertConfig | null; // null → create mode, non-null → edit mode
261
+ }
262
+
263
+ interface EditEvents {
264
+ saved: { id: string };
265
+ }
266
+
267
+ class EditAlertConfigViewModel extends ViewModel<EditState, EditEvents> {
268
+ // `readonly` means the reference never changes — the model's *state* still changes via set().
269
+ public readonly model: AlertConfigFormModel;
270
+
271
+ private service = singleton(AlertConfigService);
272
+ private collection = singleton(AlertConfigsCollection);
273
+
274
+ constructor(initialState: EditState) {
275
+ super(initialState);
276
+ this.model = new AlertConfigFormModel(
277
+ initialState.existing
278
+ ? alertConfigToFormState(initialState.existing)
279
+ : INITIAL_FORM_STATE,
280
+ );
281
+ }
282
+
283
+ get canSave(): boolean {
284
+ return this.model.valid && this.model.dirty;
285
+ }
286
+
287
+ protected onDispose() {
288
+ this.model.dispose();
289
+ }
290
+
291
+ async save() {
292
+ if (!this.model.valid) return;
293
+ const result = this.state.existing
294
+ ? await this.service.update(this.state.existing.id, this.model.state, this.disposeSignal)
295
+ : await this.service.create(this.model.state, this.disposeSignal);
296
+ this.collection.upsert(result);
297
+ this.model.commit();
298
+ this.emit('saved', { id: result.id });
299
+ }
300
+ }
301
+ ```
302
+
303
+ ```tsx
304
+ function EditAlertConfigModal({ existing, onClose }: { existing: AlertConfig | null; onClose: () => void }) {
305
+ const [, vm] = useLocal(EditAlertConfigViewModel, { existing });
306
+ const { loading: saving } = vm.async.save;
307
+
308
+ useEvent(vm, 'saved', onClose);
309
+
310
+ // No `!vm.model` guard — the model exists from the first render.
311
+ return (
312
+ <form onSubmit={e => { e.preventDefault(); vm.save(); }}>
313
+ <ModelFields model={vm.model} />
314
+ <button disabled={!vm.canSave || saving}>
315
+ {saving ? 'Saving…' : 'Save'}
316
+ </button>
317
+ </form>
318
+ );
319
+ }
320
+ ```
321
+
322
+ **Key rules:**
323
+ - Declare the field as `readonly model: FormModel` and assign it in the constructor — not `model!:` (the `!` would lie about runtime state)
324
+ - Pair the constructor creation with `this.model.dispose()` in `onDispose()`
325
+ - No null guard needed — the component never sees a half-initialized ViewModel
326
+ - Re-create the ViewModel when the entity changes using `useLocal(..., [existing.id])` — the constructor re-runs with the new data
327
+
328
+ ---
329
+
330
+ ## Recipe 4: Form that Loads by ID
331
+
332
+ The ViewModel only knows an ID at construction; the entity is fetched in `onInit()`. The Model is nullable until the fetch completes, and the component must guard its render.
246
333
 
247
334
  ```typescript
248
335
  interface ProfileState {
@@ -255,32 +342,25 @@ interface ProfileEvents {
255
342
  }
256
343
 
257
344
  class LocationProfileViewModel extends ViewModel<ProfileState, ProfileEvents> {
258
- // Model is created dynamically after data loads — starts null
345
+ // Model is created after the fetch resolves — starts null, component must guard.
259
346
  public model: LocationFormModel | null = null;
260
347
 
261
348
  private service = singleton(LocationService);
262
349
  private collection = singleton(LocationsCollection);
263
350
  private bus = singleton(AppEventBus);
264
351
 
265
- // --- Computed getters ---
266
-
267
352
  get canSave(): boolean {
268
353
  return this.model !== null && this.model.valid && this.model.dirty;
269
354
  }
270
355
 
271
- // --- Lifecycle ---
272
-
273
356
  protected onInit() {
274
357
  this.load();
275
358
  }
276
359
 
277
360
  protected onDispose() {
278
- // Model has its own lifecycle — must dispose
279
361
  this.model?.dispose();
280
362
  }
281
363
 
282
- // --- Actions ---
283
-
284
364
  async load() {
285
365
  const location = await this.service.getById(this.state.locationId, this.disposeSignal);
286
366
 
@@ -303,7 +383,7 @@ class LocationProfileViewModel extends ViewModel<ProfileState, ProfileEvents> {
303
383
  this.disposeSignal,
304
384
  );
305
385
  this.collection.update(this.state.locationId, updated);
306
- this.model.commit(); // Reset dirty state after successful save
386
+ this.model.commit();
307
387
  this.set({ location: updated });
308
388
  this.emit('saved', { id: updated.id });
309
389
  this.bus.emit('toast:show', { message: 'Location saved', severity: 'success' });
@@ -311,7 +391,7 @@ class LocationProfileViewModel extends ViewModel<ProfileState, ProfileEvents> {
311
391
  if (!isAbortError(e)) {
312
392
  this.bus.emit('toast:show', { message: 'Failed to save', severity: 'error' });
313
393
  }
314
- throw e; // Re-throw so async tracking captures it
394
+ throw e;
315
395
  }
316
396
  }
317
397
  }
@@ -319,18 +399,19 @@ class LocationProfileViewModel extends ViewModel<ProfileState, ProfileEvents> {
319
399
 
320
400
  ```tsx
321
401
  function LocationProfile({ locationId }: { locationId: string }) {
322
- const [state, vm] = useLocal(LocationProfileViewModel, { location: null, locationId });
323
- const { loading: saving } = vm.async.save ?? {};
402
+ const [, vm] = useLocal(LocationProfileViewModel, { location: null, locationId });
403
+ const { loading: saving } = vm.async.save;
324
404
 
325
405
  useEvent(vm, 'saved', () => navigate('/locations'));
326
406
 
407
+ // Required: the model is created in onInit() after the fetch resolves.
327
408
  if (!vm.model) return <Spinner />;
328
409
 
329
410
  return (
330
411
  <form onSubmit={e => { e.preventDefault(); vm.save(); }}>
331
412
  <ModelFields model={vm.model} />
332
413
  <button disabled={!vm.canSave || saving}>
333
- {saving ? 'Saving...' : 'Save'}
414
+ {saving ? 'Saving' : 'Save'}
334
415
  </button>
335
416
  </form>
336
417
  );
@@ -338,16 +419,16 @@ function LocationProfile({ locationId }: { locationId: string }) {
338
419
  ```
339
420
 
340
421
  **Key rules:**
341
- - Model is created in `onInit()` (or after async load), disposed in `onDispose()` **always pair create/dispose**
342
- - Guard `save()` with `model.valid`never submit invalid data
343
- - Call `model.commit()` after successful save to reset dirty tracking
344
- - Use `model.rollback()` for cancel/discard
422
+ - Declare the field as `model: FormModel | null = null` — not `model!:` (the `!` would lie about runtime state and crash `useField`/`useModel` on the first render)
423
+ - Guard the form render with `if (!vm.model) return <Spinner />` the model is undefined until `onInit` resolves
424
+ - Dispose with `this.model?.dispose()` (optional chain may never have been created on error paths)
425
+ - Guard `save()` with `!this.model || !this.model.valid`
345
426
  - For large forms: use `useModelRef(factory)` + `useField(model, 'fieldName')` for surgical per-field re-renders
346
427
  - ViewModel events (`emit('saved')`) for navigation; EventBus for app-wide toasts
347
428
 
348
429
  ---
349
430
 
350
- ## Recipe 4: Two-Tier Event System
431
+ ## Recipe 5: Two-Tier Event System
351
432
 
352
433
  ViewModel events are for "this instance did something" (component reactions). EventBus events are for "something happened app-wide" (toasts, analytics, navigation).
353
434
 
@@ -419,7 +500,7 @@ function ToastProvider() {
419
500
 
420
501
  ---
421
502
 
422
- ## Recipe 5: Resource with Singleton DEFAULT_STATE
503
+ ## Recipe 6: Resource with Singleton DEFAULT_STATE
423
504
 
424
505
  Singleton ViewModels that share state across routes need `static DEFAULT_STATE` so `singleton()` and `useSingleton()` work without repeating initial state at every call site.
425
506
 
@@ -454,7 +535,7 @@ const [state, auth] = useSingleton(AuthViewModel); // React (auto-init + subscr
454
535
 
455
536
  ---
456
537
 
457
- ## Recipe 6: Error Handling with isAbortError
538
+ ## Recipe 7: Error Handling with isAbortError
458
539
 
459
540
  Async tracking handles errors automatically. Manual try/catch is only needed when you have **side effects** in the catch block (toasts, event emissions, collection rollbacks).
460
541
 
@@ -81,8 +81,9 @@ it('captures errors in async tracking', async () => {
81
81
 
82
82
  await vm.load().catch(() => {}); // Swallow the re-thrown error
83
83
 
84
- expect(vm.async.load.error).toBeInstanceOf(Error);
85
- expect(vm.async.load.error!.message).toBe('Network error');
84
+ // async.error is a string (message), not an Error. errorCode is the canonical code.
85
+ expect(vm.async.load.error).toBe('Network error');
86
+ expect(vm.async.load.errorCode).toBe('unknown');
86
87
 
87
88
  vm.dispose();
88
89
  });
@@ -166,16 +166,17 @@
166
166
 
167
167
  ---
168
168
 
169
- ## Composition Checks (6)
169
+ ## Composition Checks (7)
170
170
 
171
171
  ### Critical
172
172
  1. **Model lifecycle pairing** — If a ViewModel creates a Model in `onInit()` or an action, it must dispose it in `onDispose()`.
173
+ 2. **No `!` definite-assignment or non-null assertion on a ViewModel/Model field** — `public model!: FormModel` lies to TypeScript about runtime state: first render runs before `onInit`, so `vm.model` is `undefined` and any component reading it crashes. Either (a) create the Model in the constructor so it's non-nullable (`readonly model: FormModel`, assigned in `constructor`), or (b) declare it nullable (`model: FormModel \| null = null`) and guard the component render with `if (!vm.model) return <Spinner />`. Same rule applies to non-null assertions on any other nullable field. See Recipe 3 (known data) and Recipe 4 (fetched data).
173
174
 
174
175
  ### Warning
175
- 2. **Per-call cancellation for rapid interactions** — When users can switch contexts rapidly (conversations, search-as-you-type), use `AbortSignal.any([this.disposeSignal, controller.signal])` instead of `disposeSignal` alone.
176
- 3. **Two-tier events** — ViewModel `.emit()` for component-scoped reactions (navigate, show error). EventBus `.emit()` for app-wide side effects (toasts, analytics). Don't use EventBus for single-component concerns.
177
- 4. **Singleton DEFAULT_STATE** — Singleton ViewModels used with `useSingleton()` should have `static DEFAULT_STATE` to avoid repeating initial state at every call site.
178
- 5. **Smart init** — `onInit()` should check `if (this.collection.length === 0)` before fetching to prevent re-fetching when component re-mounts and data is already loaded.
176
+ 3. **Per-call cancellation for rapid interactions** — When users can switch contexts rapidly (conversations, search-as-you-type), use `AbortSignal.any([this.disposeSignal, controller.signal])` instead of `disposeSignal` alone.
177
+ 4. **Two-tier events** — ViewModel `.emit()` for component-scoped reactions (navigate, show error). EventBus `.emit()` for app-wide side effects (toasts, analytics). Don't use EventBus for single-component concerns.
178
+ 5. **Singleton DEFAULT_STATE** — Singleton ViewModels used with `useSingleton()` should have `static DEFAULT_STATE` to avoid repeating initial state at every call site.
179
+ 6. **Smart init** — `onInit()` should check `if (this.collection.length === 0)` before fetching to prevent re-fetching when component re-mounts and data is already loaded.
179
180
 
180
181
  ### Suggestion
181
- 6. **File naming convention** — Files should follow `{Name}{Role}.ts` pattern: `UsersViewModel.ts`, `UserService.ts`, `UsersCollection.ts`, `UsersResource.ts`, `LocationFormModel.ts`, `AppEventBus.ts`.
182
+ 7. **File naming convention** — Files should follow `{Name}{Role}.ts` pattern: `UsersViewModel.ts`, `UserService.ts`, `UsersCollection.ts`, `UsersResource.ts`, `LocationFormModel.ts`, `AppEventBus.ts`.
@@ -179,6 +179,40 @@ class UserModel extends Model<UserFormState> {
179
179
  }
180
180
  ```
181
181
 
182
+ ### Model owned by a ViewModel — pick one of two shapes
183
+
184
+ If the initial data is available at construction (create modal, edit modal receiving an entity prop):
185
+
186
+ ```typescript
187
+ class EditUserVM extends ViewModel<{ existing: User | null }> {
188
+ public readonly model: UserModel; // non-nullable — set in constructor
189
+
190
+ constructor(state: { existing: User | null }) {
191
+ super(state);
192
+ this.model = new UserModel(state.existing ?? INITIAL);
193
+ }
194
+
195
+ protected onDispose() { this.model.dispose(); }
196
+ }
197
+ ```
198
+
199
+ If the initial data is fetched by ID:
200
+
201
+ ```typescript
202
+ class EditUserVM extends ViewModel<{ userId: string }> {
203
+ public model: UserModel | null = null; // nullable — component must guard
204
+
205
+ protected async onInit() {
206
+ const user = await this.service.getById(this.state.userId, this.disposeSignal);
207
+ this.model = new UserModel(user);
208
+ }
209
+
210
+ protected onDispose() { this.model?.dispose(); }
211
+ }
212
+ ```
213
+
214
+ **Never** write `public model!: UserModel` — the `!` lies to TypeScript about runtime state and causes `useField`/`useModel` to crash on the first render with "Cannot read properties of undefined." Either make the Model non-nullable via constructor creation, or declare it nullable and guard the component with `if (!vm.model) return <Spinner />`.
215
+
182
216
  ## Composable Helpers Pattern
183
217
 
184
218
  Declare helpers as ViewModel instance properties. They have `subscribe()` so they're auto-tracked -- getters that read them recompute when helper state changes.
@@ -179,6 +179,40 @@ class UserModel extends Model<UserFormState> {
179
179
  }
180
180
  ```
181
181
 
182
+ ### Model owned by a ViewModel — pick one of two shapes
183
+
184
+ If the initial data is available at construction (create modal, edit modal receiving an entity prop):
185
+
186
+ ```typescript
187
+ class EditUserVM extends ViewModel<{ existing: User | null }> {
188
+ public readonly model: UserModel; // non-nullable — set in constructor
189
+
190
+ constructor(state: { existing: User | null }) {
191
+ super(state);
192
+ this.model = new UserModel(state.existing ?? INITIAL);
193
+ }
194
+
195
+ protected onDispose() { this.model.dispose(); }
196
+ }
197
+ ```
198
+
199
+ If the initial data is fetched by ID:
200
+
201
+ ```typescript
202
+ class EditUserVM extends ViewModel<{ userId: string }> {
203
+ public model: UserModel | null = null; // nullable — component must guard
204
+
205
+ protected async onInit() {
206
+ const user = await this.service.getById(this.state.userId, this.disposeSignal);
207
+ this.model = new UserModel(user);
208
+ }
209
+
210
+ protected onDispose() { this.model?.dispose(); }
211
+ }
212
+ ```
213
+
214
+ **Never** write `public model!: UserModel` — the `!` lies to TypeScript about runtime state and causes `useField`/`useModel` to crash on the first render with "Cannot read properties of undefined." Either make the Model non-nullable via constructor creation, or declare it nullable and guard the component with `if (!vm.model) return <Spinner />`.
215
+
182
216
  ## Composable Helpers Pattern
183
217
 
184
218
  Declare helpers as ViewModel instance properties. They have `subscribe()` so they're auto-tracked — getters that read them recompute when helper state changes.
@@ -74,20 +74,16 @@ function DataTable({ items, columns, keyOf = defaultKeyOf, pageSize, sort, onSor
74
74
  if (error && renderError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: renderError(error) });
75
75
  if (items.length === 0 && renderEmpty) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: renderEmpty() });
76
76
  const resolvedSelection = selection ? resolveSelectionProp(selection) : void 0;
77
- let resolvedSorts;
78
- let resolvedOnSort;
79
- if (sort) {
80
- const resolved = resolveSortProp(sort, onSort);
81
- resolvedSorts = resolved.sorts;
82
- resolvedOnSort = resolved.onSort;
83
- }
77
+ const resolvedSort = sort ? resolveSortProp(sort, onSort) : void 0;
78
+ const resolvedSorts = resolvedSort?.sorts;
79
+ const resolvedOnSort = resolvedSort?.onSort;
84
80
  let displayItems = items;
85
81
  let paginationInfo;
86
82
  if (pagination) paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);
87
83
  else if (pageSize && !pagination) displayItems = items.slice(0, pageSize);
88
84
  const allKeys = resolvedSelection ? displayItems.map((item) => keyOf(item)) : [];
89
- const allSelected = resolvedSelection && allKeys.length > 0 && allKeys.every((k) => resolvedSelection.selected.has(k));
90
- const someSelected = resolvedSelection && allKeys.some((k) => resolvedSelection.selected.has(k));
85
+ const allSelected = !!resolvedSelection && allKeys.length > 0 && allKeys.every((k) => resolvedSelection.selected.has(k));
86
+ const someSelected = !!resolvedSelection && allKeys.some((k) => resolvedSelection.selected.has(k));
91
87
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
92
88
  "data-component": "data-table",
93
89
  className,
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/react/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type {\n Column,\n SortHeaderProps,\n SelectionState,\n SelectionHelper,\n PaginationState,\n PaginationHelper,\n PaginationInfo,\n SortingHelper,\n AsyncStateProps,\n} from './types';\nimport { isSelectionHelper, isPaginationHelper, isSortingHelper } from './types';\nimport type { SortDescriptor } from '../../Sorting';\n\n// ── Prop resolution helpers ──\n\ninterface ResolvedSelection {\n selected: ReadonlySet<any>;\n onToggle: (key: any) => void;\n onToggleAll: (allKeys: any[]) => void;\n}\n\nfunction resolveSelectionProp(selection: SelectionState | SelectionHelper): ResolvedSelection {\n if (isSelectionHelper(selection)) {\n return {\n selected: selection.selected,\n onToggle: (key) => selection.toggle(key),\n onToggleAll: (allKeys) => selection.toggleAll(allKeys),\n };\n }\n return {\n selected: selection.selected,\n onToggle: selection.onToggle,\n onToggleAll: (allKeys) => selection.onToggleAll(allKeys),\n };\n}\n\nfunction resolveSortProp(\n sort: readonly SortDescriptor[] | SortingHelper,\n onSort?: (key: string) => void,\n): { sorts: readonly SortDescriptor[]; onSort: ((key: string) => void) | undefined } {\n if (isSortingHelper(sort)) {\n return { sorts: sort.sorts, onSort: (key) => sort.toggle(key) };\n }\n return { sorts: sort, onSort };\n}\n\nfunction resolvePaginationProp(\n pagination: PaginationState | PaginationHelper,\n pageSize?: number,\n paginationTotal?: number,\n): PaginationInfo {\n if (isPaginationHelper(pagination)) {\n const total = paginationTotal ?? 0;\n const ps = pagination.pageSize;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: (p) => pagination.setPage(p),\n goPrev: () => pagination.setPage(page - 1),\n goNext: () => pagination.setPage(page + 1),\n };\n }\n const total = pagination.total;\n const ps = pageSize ?? 10;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: pagination.onPageChange,\n goPrev: () => pagination.onPageChange(page - 1),\n goNext: () => pagination.onPageChange(page + 1),\n };\n}\n\n/** Props for the DataTable headless component. */\nexport interface DataTableProps<T> extends AsyncStateProps {\n items: T[];\n columns: Column<T>[];\n keyOf?: (item: T) => string | number;\n pageSize?: number;\n\n // Controlled state — accepts object-literal OR helper instance\n sort?: readonly SortDescriptor[] | SortingHelper;\n onSort?: (key: string) => void;\n selection?: SelectionState | SelectionHelper;\n pagination?: PaginationState | PaginationHelper;\n paginationTotal?: number;\n\n // Render slots\n renderEmpty?: () => ReactNode;\n renderLoading?: () => ReactNode;\n renderError?: (error: string) => ReactNode;\n renderSortIndicator?: (props: SortHeaderProps) => ReactNode;\n renderRow?: (item: T, index: number, defaultCells: ReactNode) => ReactNode;\n renderPagination?: (info: PaginationInfo) => ReactNode;\n\n className?: string;\n 'aria-label'?: string;\n}\n\nconst defaultKeyOf = (item: any) => item.id;\n\nfunction getAriaSortValue(key: string, sorts: readonly SortDescriptor[] | undefined): 'ascending' | 'descending' | 'none' {\n if (!sorts) return 'none';\n const desc = sorts.find(s => s.key === key);\n if (!desc) return 'none';\n return desc.direction === 'asc' ? 'ascending' : 'descending';\n}\n\n/**\n * Headless data table with sort headers, selection checkboxes, and pagination slots.\n * Renders semantic HTML (`<table>`) with data attributes for styling.\n * Accepts Sorting/Selection/Pagination helpers directly via duck-typing.\n */\nexport function DataTable<T>({\n items,\n columns,\n keyOf = defaultKeyOf,\n pageSize,\n sort,\n onSort,\n selection,\n loading,\n error,\n pagination,\n paginationTotal,\n renderEmpty,\n renderLoading,\n renderError,\n renderSortIndicator,\n renderRow,\n renderPagination,\n className,\n 'aria-label': ariaLabel,\n}: DataTableProps<T>) {\n if (loading && renderLoading) return <>{renderLoading()}</>;\n if (error && renderError) return <>{renderError(error)}</>;\n if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;\n\n // ── Resolve props ──\n const resolvedSelection = selection ? resolveSelectionProp(selection) : undefined;\n\n let resolvedSorts: readonly SortDescriptor[] | undefined;\n let resolvedOnSort: ((key: string) => void) | undefined;\n if (sort) {\n const resolved = resolveSortProp(sort, onSort);\n resolvedSorts = resolved.sorts;\n resolvedOnSort = resolved.onSort;\n }\n\n let displayItems = items;\n let paginationInfo: PaginationInfo | undefined;\n if (pagination) {\n paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);\n } else if (pageSize && !pagination) {\n displayItems = items.slice(0, pageSize);\n }\n\n // Selection: check indeterminate state\n const allKeys = resolvedSelection ? displayItems.map(item => keyOf(item)) : [];\n const allSelected = resolvedSelection && allKeys.length > 0 && allKeys.every(k => resolvedSelection!.selected.has(k));\n const someSelected = resolvedSelection && allKeys.some(k => resolvedSelection!.selected.has(k));\n\n return (\n <div data-component=\"data-table\" className={className}>\n <table role=\"grid\" aria-label={ariaLabel}>\n <thead>\n <tr>\n {resolvedSelection && (\n <th data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!allSelected}\n ref={(el) => {\n if (el) el.indeterminate = !!someSelected && !allSelected;\n }}\n onChange={() => resolvedSelection!.onToggleAll(allKeys)}\n aria-label=\"Select all\"\n />\n </th>\n )}\n {columns.map((col) => {\n const isSortable = col.sortable && resolvedOnSort;\n const sortDesc = resolvedSorts?.find(s => s.key === col.key);\n const isActive = !!sortDesc;\n const sortIndex = resolvedSorts ? resolvedSorts.findIndex(s => s.key === col.key) : -1;\n\n return (\n <th\n key={col.key}\n data-sortable={isSortable ? '' : undefined}\n data-sorted={isActive ? '' : undefined}\n data-align={col.align}\n style={col.width ? { width: col.width } : undefined}\n aria-sort={isSortable ? getAriaSortValue(col.key, resolvedSorts) : undefined}\n >\n {isSortable ? (\n <button type=\"button\" onClick={() => resolvedOnSort!(col.key)}>\n {col.header}\n {renderSortIndicator?.({\n active: isActive,\n direction: sortDesc?.direction ?? 'asc',\n index: sortIndex,\n onToggle: () => resolvedOnSort!(col.key),\n })}\n </button>\n ) : (\n col.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {displayItems.map((item, index) => {\n const key = keyOf(item);\n const isSelected = resolvedSelection?.selected.has(key);\n\n const cells = (\n <>\n {resolvedSelection && (\n <td data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!isSelected}\n onChange={() => resolvedSelection!.onToggle(key)}\n aria-label={`Select row ${key}`}\n />\n </td>\n )}\n {columns.map((col) => (\n <td\n key={col.key}\n data-align={col.align}\n >\n {col.render(item, index)}\n </td>\n ))}\n </>\n );\n\n return (\n <tr key={key} data-selected={isSelected ? '' : undefined}>\n {renderRow ? renderRow(item, index, cells) : cells}\n </tr>\n );\n })}\n </tbody>\n </table>\n {paginationInfo && renderPagination?.(paginationInfo)}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable.cjs","names":[],"sources":["../../../src/react/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type {\n Column,\n SortHeaderProps,\n SelectionState,\n SelectionHelper,\n PaginationState,\n PaginationHelper,\n PaginationInfo,\n SortingHelper,\n AsyncStateProps,\n} from './types';\nimport { isSelectionHelper, isPaginationHelper, isSortingHelper } from './types';\nimport type { SortDescriptor } from '../../Sorting';\n\n// ── Prop resolution helpers ──\n\ninterface ResolvedSelection {\n selected: ReadonlySet<any>;\n onToggle: (key: any) => void;\n onToggleAll: (allKeys: any[]) => void;\n}\n\nfunction resolveSelectionProp(selection: SelectionState | SelectionHelper): ResolvedSelection {\n if (isSelectionHelper(selection)) {\n return {\n selected: selection.selected,\n onToggle: (key) => selection.toggle(key),\n onToggleAll: (allKeys) => selection.toggleAll(allKeys),\n };\n }\n return {\n selected: selection.selected,\n onToggle: selection.onToggle,\n onToggleAll: (allKeys) => selection.onToggleAll(allKeys),\n };\n}\n\nfunction resolveSortProp(\n sort: readonly SortDescriptor[] | SortingHelper,\n onSort?: (key: string) => void,\n): { sorts: readonly SortDescriptor[]; onSort: ((key: string) => void) | undefined } {\n if (isSortingHelper(sort)) {\n return { sorts: sort.sorts, onSort: (key) => sort.toggle(key) };\n }\n return { sorts: sort, onSort };\n}\n\nfunction resolvePaginationProp(\n pagination: PaginationState | PaginationHelper,\n pageSize?: number,\n paginationTotal?: number,\n): PaginationInfo {\n if (isPaginationHelper(pagination)) {\n const total = paginationTotal ?? 0;\n const ps = pagination.pageSize;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: (p) => pagination.setPage(p),\n goPrev: () => pagination.setPage(page - 1),\n goNext: () => pagination.setPage(page + 1),\n };\n }\n const total = pagination.total;\n const ps = pageSize ?? 10;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: pagination.onPageChange,\n goPrev: () => pagination.onPageChange(page - 1),\n goNext: () => pagination.onPageChange(page + 1),\n };\n}\n\n/** Props for the DataTable headless component. */\nexport interface DataTableProps<T> extends AsyncStateProps {\n items: T[];\n columns: Column<T>[];\n keyOf?: (item: T) => string | number;\n pageSize?: number;\n\n // Controlled state — accepts object-literal OR helper instance\n sort?: readonly SortDescriptor[] | SortingHelper;\n onSort?: (key: string) => void;\n selection?: SelectionState | SelectionHelper;\n pagination?: PaginationState | PaginationHelper;\n paginationTotal?: number;\n\n // Render slots\n renderEmpty?: () => ReactNode;\n renderLoading?: () => ReactNode;\n renderError?: (error: string) => ReactNode;\n renderSortIndicator?: (props: SortHeaderProps) => ReactNode;\n renderRow?: (item: T, index: number, defaultCells: ReactNode) => ReactNode;\n renderPagination?: (info: PaginationInfo) => ReactNode;\n\n className?: string;\n 'aria-label'?: string;\n}\n\nconst defaultKeyOf = (item: any) => item.id;\n\nfunction getAriaSortValue(key: string, sorts: readonly SortDescriptor[] | undefined): 'ascending' | 'descending' | 'none' {\n if (!sorts) return 'none';\n const desc = sorts.find(s => s.key === key);\n if (!desc) return 'none';\n return desc.direction === 'asc' ? 'ascending' : 'descending';\n}\n\n/**\n * Headless data table with sort headers, selection checkboxes, and pagination slots.\n * Renders semantic HTML (`<table>`) with data attributes for styling.\n * Accepts Sorting/Selection/Pagination helpers directly via duck-typing.\n */\nexport function DataTable<T>({\n items,\n columns,\n keyOf = defaultKeyOf,\n pageSize,\n sort,\n onSort,\n selection,\n loading,\n error,\n pagination,\n paginationTotal,\n renderEmpty,\n renderLoading,\n renderError,\n renderSortIndicator,\n renderRow,\n renderPagination,\n className,\n 'aria-label': ariaLabel,\n}: DataTableProps<T>) {\n if (loading && renderLoading) return <>{renderLoading()}</>;\n if (error && renderError) return <>{renderError(error)}</>;\n if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;\n\n // ── Resolve props ──\n const resolvedSelection = selection ? resolveSelectionProp(selection) : undefined;\n\n const resolvedSort = sort ? resolveSortProp(sort, onSort) : undefined;\n const resolvedSorts = resolvedSort?.sorts;\n const resolvedOnSort = resolvedSort?.onSort;\n\n let displayItems = items;\n let paginationInfo: PaginationInfo | undefined;\n if (pagination) {\n paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);\n } else if (pageSize && !pagination) {\n displayItems = items.slice(0, pageSize);\n }\n\n // Selection: check indeterminate state\n const allKeys = resolvedSelection ? displayItems.map(item => keyOf(item)) : [];\n const allSelected = !!resolvedSelection && allKeys.length > 0 && allKeys.every(k => resolvedSelection.selected.has(k));\n const someSelected = !!resolvedSelection && allKeys.some(k => resolvedSelection.selected.has(k));\n\n return (\n <div data-component=\"data-table\" className={className}>\n <table role=\"grid\" aria-label={ariaLabel}>\n <thead>\n <tr>\n {resolvedSelection && (\n <th data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!allSelected}\n ref={(el) => {\n if (el) el.indeterminate = !!someSelected && !allSelected;\n }}\n onChange={() => resolvedSelection.onToggleAll(allKeys)}\n aria-label=\"Select all\"\n />\n </th>\n )}\n {columns.map((col) => {\n const isSortable = col.sortable && resolvedOnSort;\n const sortDesc = resolvedSorts?.find(s => s.key === col.key);\n const isActive = !!sortDesc;\n const sortIndex = resolvedSorts ? resolvedSorts.findIndex(s => s.key === col.key) : -1;\n\n return (\n <th\n key={col.key}\n data-sortable={isSortable ? '' : undefined}\n data-sorted={isActive ? '' : undefined}\n data-align={col.align}\n style={col.width ? { width: col.width } : undefined}\n aria-sort={isSortable ? getAriaSortValue(col.key, resolvedSorts) : undefined}\n >\n {isSortable ? (\n <button type=\"button\" onClick={() => resolvedOnSort(col.key)}>\n {col.header}\n {renderSortIndicator?.({\n active: isActive,\n direction: sortDesc?.direction ?? 'asc',\n index: sortIndex,\n onToggle: () => resolvedOnSort(col.key),\n })}\n </button>\n ) : (\n col.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {displayItems.map((item, index) => {\n const key = keyOf(item);\n const isSelected = resolvedSelection?.selected.has(key);\n\n const cells = (\n <>\n {resolvedSelection && (\n <td data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!isSelected}\n onChange={() => resolvedSelection.onToggle(key)}\n aria-label={`Select row ${key}`}\n />\n </td>\n )}\n {columns.map((col) => (\n <td\n key={col.key}\n data-align={col.align}\n >\n {col.render(item, index)}\n </td>\n ))}\n </>\n );\n\n return (\n <tr key={key} data-selected={isSelected ? '' : undefined}>\n {renderRow ? renderRow(item, index, cells) : cells}\n </tr>\n );\n })}\n </tbody>\n </table>\n {paginationInfo && renderPagination?.(paginationInfo)}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/react/components/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA0EpD,kDAAkD;AAClD,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,eAAe;IACxD,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,IAAI,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,aAAa,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,GAAG,eAAe,CAAC;IAC7C,UAAU,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAAC;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,WAAW,CAAC,EAAE,MAAM,SAAS,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;IAC3C,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IAC5D,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,KAAK,SAAS,CAAC;IAC3E,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,SAAS,CAAC;IAEvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAC3B,KAAK,EACL,OAAO,EACP,KAAoB,EACpB,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,SAAS,EACT,OAAO,EACP,KAAK,EACL,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,gBAAgB,EAChB,SAAS,EACT,YAAY,EAAE,SAAS,GACxB,EAAE,cAAc,CAAC,CAAC,CAAC,2CAuHnB"}
1
+ {"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/react/components/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA0EpD,kDAAkD;AAClD,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,eAAe;IACxD,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,IAAI,CAAC,EAAE,SAAS,cAAc,EAAE,GAAG,aAAa,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,GAAG,eAAe,CAAC;IAC7C,UAAU,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAAC;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IAGzB,WAAW,CAAC,EAAE,MAAM,SAAS,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;IAC3C,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IAC5D,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,KAAK,SAAS,CAAC;IAC3E,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,SAAS,CAAC;IAEvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAC3B,KAAK,EACL,OAAO,EACP,KAAoB,EACpB,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,SAAS,EACT,OAAO,EACP,KAAK,EACL,UAAU,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,gBAAgB,EAChB,SAAS,EACT,YAAY,EAAE,SAAS,GACxB,EAAE,cAAc,CAAC,CAAC,CAAC,2CAmHnB"}
@@ -74,20 +74,16 @@ function DataTable({ items, columns, keyOf = defaultKeyOf, pageSize, sort, onSor
74
74
  if (error && renderError) return /* @__PURE__ */ jsx(Fragment, { children: renderError(error) });
75
75
  if (items.length === 0 && renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty() });
76
76
  const resolvedSelection = selection ? resolveSelectionProp(selection) : void 0;
77
- let resolvedSorts;
78
- let resolvedOnSort;
79
- if (sort) {
80
- const resolved = resolveSortProp(sort, onSort);
81
- resolvedSorts = resolved.sorts;
82
- resolvedOnSort = resolved.onSort;
83
- }
77
+ const resolvedSort = sort ? resolveSortProp(sort, onSort) : void 0;
78
+ const resolvedSorts = resolvedSort?.sorts;
79
+ const resolvedOnSort = resolvedSort?.onSort;
84
80
  let displayItems = items;
85
81
  let paginationInfo;
86
82
  if (pagination) paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);
87
83
  else if (pageSize && !pagination) displayItems = items.slice(0, pageSize);
88
84
  const allKeys = resolvedSelection ? displayItems.map((item) => keyOf(item)) : [];
89
- const allSelected = resolvedSelection && allKeys.length > 0 && allKeys.every((k) => resolvedSelection.selected.has(k));
90
- const someSelected = resolvedSelection && allKeys.some((k) => resolvedSelection.selected.has(k));
85
+ const allSelected = !!resolvedSelection && allKeys.length > 0 && allKeys.every((k) => resolvedSelection.selected.has(k));
86
+ const someSelected = !!resolvedSelection && allKeys.some((k) => resolvedSelection.selected.has(k));
91
87
  return /* @__PURE__ */ jsxs("div", {
92
88
  "data-component": "data-table",
93
89
  className,