ezfw-core 1.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 (154) hide show
  1. package/components/EzBaseComponent.ts +648 -0
  2. package/components/EzComponent.ts +89 -0
  3. package/components/EzInput.module.scss +183 -0
  4. package/components/EzInput.ts +104 -0
  5. package/components/EzLabel.ts +22 -0
  6. package/components/EzOutlet.ts +181 -0
  7. package/components/HtmlWrapper.ts +305 -0
  8. package/components/avatar/EzAvatar.module.scss +200 -0
  9. package/components/avatar/EzAvatar.ts +130 -0
  10. package/components/badge/EzBadge.module.scss +202 -0
  11. package/components/badge/EzBadge.ts +77 -0
  12. package/components/button/EzButton.module.scss +402 -0
  13. package/components/button/EzButton.ts +175 -0
  14. package/components/button/EzButtonGroup.ts +48 -0
  15. package/components/card/EzCard.module.scss +71 -0
  16. package/components/card/EzCard.ts +120 -0
  17. package/components/chart/EzBarChart.ts +47 -0
  18. package/components/chart/EzChart.module.scss +14 -0
  19. package/components/chart/EzChart.ts +279 -0
  20. package/components/chart/EzDoughnutChart.ts +47 -0
  21. package/components/chart/EzLineChart.ts +53 -0
  22. package/components/checkbox/EzCheckbox.module.scss +145 -0
  23. package/components/checkbox/EzCheckbox.ts +115 -0
  24. package/components/dataview/EzDataView.module.scss +115 -0
  25. package/components/dataview/EzDataView.ts +355 -0
  26. package/components/dataview/modes/EzDataViewCards.ts +322 -0
  27. package/components/dataview/modes/EzDataViewGrid.ts +76 -0
  28. package/components/datepicker/EzDatePicker.module.scss +348 -0
  29. package/components/datepicker/EzDatePicker.ts +519 -0
  30. package/components/dialog/EzDialog.module.scss +180 -0
  31. package/components/dropdown/EzDropdown.module.scss +107 -0
  32. package/components/dropdown/EzDropdown.ts +235 -0
  33. package/components/feed/EzActivityFeed.module.scss +90 -0
  34. package/components/feed/EzActivityFeed.ts +78 -0
  35. package/components/form/EzForm.ts +364 -0
  36. package/components/form/EzValidators.test.js +421 -0
  37. package/components/form/EzValidators.ts +202 -0
  38. package/components/grid/EzGrid.scss +88 -0
  39. package/components/grid/EzGrid.ts +1085 -0
  40. package/components/grid/EzGridContainer.ts +104 -0
  41. package/components/grid/body/EzGridBody.scss +283 -0
  42. package/components/grid/body/EzGridBody.ts +549 -0
  43. package/components/grid/body/EzGridCell.ts +211 -0
  44. package/components/grid/body/EzGridRow.ts +196 -0
  45. package/components/grid/filter/EzGridFilters.scss +78 -0
  46. package/components/grid/filter/EzGridFilters.ts +285 -0
  47. package/components/grid/footer/EzGridFooter.scss +136 -0
  48. package/components/grid/footer/EzGridFooter.ts +448 -0
  49. package/components/grid/header/EzGridHeader.scss +199 -0
  50. package/components/grid/header/EzGridHeader.ts +430 -0
  51. package/components/grid/query/EzGridQuery.ts +81 -0
  52. package/components/grid/state/EzGridColumns.ts +155 -0
  53. package/components/grid/state/EzGridController.ts +470 -0
  54. package/components/grid/state/EzGridLifecycle.ts +136 -0
  55. package/components/grid/state/EzGridNormalizers.test.js +273 -0
  56. package/components/grid/state/EzGridNormalizers.ts +162 -0
  57. package/components/grid/state/EzGridParts.ts +233 -0
  58. package/components/grid/state/EzGridPersistence.ts +140 -0
  59. package/components/grid/state/EzGridRemote.test.js +573 -0
  60. package/components/grid/state/EzGridRemote.ts +335 -0
  61. package/components/grid/state/EzGridSelection.ts +231 -0
  62. package/components/grid/state/EzGridSort.ts +286 -0
  63. package/components/grid/title/EzGridActionBar.ts +98 -0
  64. package/components/grid/title/EzGridTitle.ts +114 -0
  65. package/components/grid/title/EzGridTitleBar.scss +65 -0
  66. package/components/grid/title/EzGridTitleBar.ts +87 -0
  67. package/components/grid/types.ts +607 -0
  68. package/components/panel/EzPanel.module.scss +133 -0
  69. package/components/panel/EzPanel.ts +147 -0
  70. package/components/radio/EzRadio.module.scss +190 -0
  71. package/components/radio/EzRadio.ts +149 -0
  72. package/components/select/EzSelect.module.scss +153 -0
  73. package/components/select/EzSelect.ts +238 -0
  74. package/components/skeleton/EzSkeleton.module.scss +95 -0
  75. package/components/skeleton/EzSkeleton.ts +70 -0
  76. package/components/store/EzStore.ts +344 -0
  77. package/components/switch/EzSwitch.module.scss +164 -0
  78. package/components/switch/EzSwitch.ts +117 -0
  79. package/components/tabs/EzTabPanel.module.scss +181 -0
  80. package/components/tabs/EzTabPanel.ts +402 -0
  81. package/components/textarea/EzTextarea.module.scss +131 -0
  82. package/components/textarea/EzTextarea.ts +161 -0
  83. package/components/timepicker/EzTimePicker.module.scss +282 -0
  84. package/components/timepicker/EzTimePicker.ts +540 -0
  85. package/components/toast/EzToast.module.scss +291 -0
  86. package/components/tooltip/EzTooltip.module.scss +124 -0
  87. package/components/tooltip/EzTooltip.ts +153 -0
  88. package/core/EzComponentTypes.ts +693 -0
  89. package/core/EzError.ts +63 -0
  90. package/core/EzModel.ts +268 -0
  91. package/core/EzTypes.ts +328 -0
  92. package/core/eventBus.ts +284 -0
  93. package/core/ez.ts +617 -0
  94. package/core/loader.ts +725 -0
  95. package/core/renderer.ts +1010 -0
  96. package/core/router.ts +490 -0
  97. package/core/services.ts +124 -0
  98. package/core/state.ts +142 -0
  99. package/core/utils.ts +81 -0
  100. package/package.json +51 -0
  101. package/services/RouteUI.js +17 -0
  102. package/services/crypto.js +64 -0
  103. package/services/dialog.js +222 -0
  104. package/services/fetchApi.js +63 -0
  105. package/services/firebase.js +30 -0
  106. package/services/toast.js +214 -0
  107. package/template/doc/EzDocs.js +15 -0
  108. package/template/doc/EzDocs.module.scss +627 -0
  109. package/template/doc/EzDocsController.js +164 -0
  110. package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
  111. package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
  112. package/template/doc/data/badge/EzBadgeDoc.js +92 -0
  113. package/template/doc/data/button/EzButtonDoc.js +77 -0
  114. package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
  115. package/template/doc/data/card/EzCardDoc.js +39 -0
  116. package/template/doc/data/chart/EzChartDoc.js +60 -0
  117. package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
  118. package/template/doc/data/component/EzComponentDoc.js +34 -0
  119. package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
  120. package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
  121. package/template/doc/data/dialog/EzDialogDoc.js +217 -0
  122. package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
  123. package/template/doc/data/form/EzFormDoc.js +90 -0
  124. package/template/doc/data/grid/EzGridDoc.js +99 -0
  125. package/template/doc/data/input/EzInputDoc.js +92 -0
  126. package/template/doc/data/label/EzLabelDoc.js +40 -0
  127. package/template/doc/data/model/EzModelDoc.js +53 -0
  128. package/template/doc/data/outlet/EzOutletDoc.js +63 -0
  129. package/template/doc/data/panel/EzPanelDoc.js +214 -0
  130. package/template/doc/data/radio/EzRadioDoc.js +174 -0
  131. package/template/doc/data/router/EzRouterDoc.js +75 -0
  132. package/template/doc/data/select/EzSelectDoc.js +37 -0
  133. package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
  134. package/template/doc/data/switch/EzSwitchDoc.js +82 -0
  135. package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
  136. package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
  137. package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
  138. package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
  139. package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
  140. package/template/doc/sidebar/EzDocsSidebar.js +32 -0
  141. package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
  142. package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
  143. package/template/doc/viewer/EzDocsViewer.js +18 -0
  144. package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
  145. package/template/doc/viewer/content/EzDocsContent.js +315 -0
  146. package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
  147. package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
  148. package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
  149. package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
  150. package/template/doc/welcome/EzDocsWelcome.js +48 -0
  151. package/themes/ez-theme.scss +179 -0
  152. package/themes/nature-fresh.scss +169 -0
  153. package/types/global.d.ts +21 -0
  154. package/utils/cssModules.js +81 -0
@@ -0,0 +1,470 @@
1
+ // EzGrid/state/EzGridController.ts
2
+
3
+ // WARNING: EzGridController is a state owner, NOT a lifecycle or rendering component.
4
+ // It must NEVER:
5
+ // - trigger grid refreshes
6
+ // - access EzGridLifecycle
7
+ // - mutate selection, columns, or sort
8
+ // - perform rendering or persistence
9
+ //
10
+ // Allowed responsibilities:
11
+ // - own controller.state
12
+ // - emit events (datachange)
13
+ // - notify loading / error state via grid callbacks
14
+
15
+ import type {
16
+ DataMode,
17
+ RowData,
18
+ SortSnapshot,
19
+ FilterSnapshot,
20
+ TransportParams,
21
+ TransportResult,
22
+ TransportFn,
23
+ RemoteConfig,
24
+ EzGridModel,
25
+ EzGridController,
26
+ EzGridControllerState,
27
+ EventCallback
28
+ } from '../types.js';
29
+
30
+ // NOTE: Remote transport contract:
31
+ // transport(params) => Promise<{ data: RowData[], total?: number }>
32
+ //
33
+ // - params is a plain object (paging, filters, sort, etc.)
34
+ // - controller does NOT know how data is fetched
35
+ // - transport must return an object with "data"
36
+ // - EzGridRemote provides the transport implementation
37
+
38
+ export interface EzGridControllerRef {
39
+ _lifecycle?: {
40
+ onControllerDataChange?: () => void;
41
+ };
42
+ _onLoadingChanged?: (loading: boolean) => void;
43
+ _onError?: (error: Error | null) => void;
44
+ }
45
+
46
+ type EventListeners = Record<string, EventCallback[]>;
47
+
48
+ export function createEzGridController(grid: EzGridControllerRef): EzGridController {
49
+ const listeners: EventListeners = Object.create(null);
50
+
51
+ function devWarn(message: string): void {
52
+ // NOTE: Dev-only warnings. Never executed in production builds.
53
+ if (import.meta?.env?.DEV) {
54
+ console.warn(`[EzGridController] ${message}`);
55
+ }
56
+ }
57
+
58
+ function on(event: string, fn: EventCallback): void {
59
+ (listeners[event] ??= []).push(fn);
60
+ }
61
+
62
+ function off(event: string, fn: EventCallback): void {
63
+ if (!listeners[event]) return;
64
+ listeners[event] = listeners[event].filter(l => l !== fn);
65
+ }
66
+
67
+ function emit(event: string, payload?: unknown): void {
68
+ // WARNING: 'datachange' is the ONLY event that triggers grid refresh.
69
+ // Do NOT emit custom events that affect rendering.
70
+ if (event !== 'datachange') {
71
+ devWarn(
72
+ `Emitting non-contract event "${event}". ` +
73
+ `Only "datachange" is allowed to affect grid rendering.`
74
+ );
75
+ }
76
+
77
+ if (!listeners[event]) return;
78
+ for (const fn of listeners[event]) {
79
+ fn(payload);
80
+ }
81
+ }
82
+
83
+ // ======================================================
84
+ // PUBLIC CONTROLLER API
85
+ // ------------------------------------------------------
86
+ // The following methods are intended to be used by:
87
+ // - EzGrid
88
+ // - Grid behaviors
89
+ // - Application code (advanced use)
90
+ //
91
+ // Public methods:
92
+ // - load()
93
+ // - reload()
94
+ // - requestReload(reason)
95
+ // - setPage(page)
96
+ // - setPageSize(size)
97
+ // - setSortSnapshot(snapshot)
98
+ // - setFilterSnapshot(snapshot)
99
+ //
100
+ // WARNING:
101
+ // - Methods not listed above are considered INTERNAL.
102
+ // - Internal methods may change without notice.
103
+ // - Do NOT call transport directly from outside.
104
+ // ======================================================
105
+
106
+ const controller: EzGridController = {
107
+ grid: grid as any,
108
+
109
+ on,
110
+ off,
111
+ emit,
112
+
113
+ // NOTE: Prevents concurrent remote loads.
114
+ _isLoadingRemote: false,
115
+
116
+ // NOTE: Ensures page/pageSize changes during an active load are not lost.
117
+ _pendingRemoteReload: false,
118
+
119
+ // NOTE: Latest sort snapshot provided by the grid.
120
+ _lastSortSnapshot: null,
121
+
122
+ // NOTE: Latest filters snapshot provided by the grid.
123
+ _lastFilterSnapshot: null,
124
+
125
+ // NOTE: Controller state is the single source of truth for grid data.
126
+ // Grid rendering reacts to changes emitted via events.
127
+ state: {
128
+ data: [],
129
+ loading: false,
130
+ error: null,
131
+
132
+ page: 1,
133
+ pageSize: 25,
134
+ total: 0,
135
+
136
+ // NOTE: Data mode.
137
+ // 'local' is the default and only active mode for now.
138
+ // 'remote' is a contract placeholder.
139
+ mode: 'local' as DataMode
140
+
141
+ },
142
+
143
+ // ---- lifecycle ----
144
+ async load(params: Partial<TransportParams> = {}) {
145
+ // NOTE: Local mode does nothing.
146
+ if (this.state.mode !== 'remote') {
147
+ if (import.meta?.env?.DEV) {
148
+ console.warn(
149
+ '[EzGridController] load() called in local mode. No action taken.'
150
+ );
151
+ }
152
+ return;
153
+ }
154
+
155
+ const transport = this._remoteConfig?.transport;
156
+
157
+ if (typeof transport !== 'function') {
158
+ if (import.meta?.env?.DEV) {
159
+ console.warn(
160
+ '[EzGridController] Remote load called without a valid transport.'
161
+ );
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (this._isLoadingRemote === true) {
167
+ // NOTE: Do not drop reload requests triggered during an active load.
168
+ this._pendingRemoteReload = true;
169
+
170
+ if (import.meta?.env?.DEV) {
171
+ console.warn(
172
+ '[EzGridController] Remote load deferred: already loading.'
173
+ );
174
+ }
175
+ return;
176
+ }
177
+
178
+ this.setLoading(true);
179
+ this.setError(null);
180
+
181
+ // Clear current data before loading new data
182
+ this.state.data = [];
183
+ emit('datachange', this.state.data);
184
+
185
+ this._isLoadingRemote = true;
186
+
187
+ try {
188
+ // WARNING: transport execution is centralized here.
189
+ // Controller is the only layer allowed to call it.
190
+ const finalParams: TransportParams = {
191
+ page: this.state.page,
192
+ pageSize: this.state.pageSize,
193
+ sort: this._lastSortSnapshot,
194
+ filters: this._lastFilterSnapshot,
195
+ ...params
196
+ };
197
+
198
+ const result = await transport(finalParams);
199
+
200
+ const data: RowData[] = Array.isArray(result?.data)
201
+ ? result.data
202
+ : [];
203
+
204
+ const total =
205
+ typeof result?.total === 'number'
206
+ ? result.total
207
+ : data.length;
208
+
209
+ this.setData(data, { total });
210
+ } catch (err) {
211
+ this.setError(err as Error);
212
+
213
+ if (import.meta?.env?.DEV) {
214
+ console.error(
215
+ '[EzGridController] Remote load failed:',
216
+ err
217
+ );
218
+ }
219
+ } finally {
220
+ this._isLoadingRemote = false;
221
+ this.setLoading(false);
222
+
223
+ if (this._pendingRemoteReload === true) {
224
+ this._pendingRemoteReload = false;
225
+ // NOTE: Replay the most recent requested reload.
226
+ this.load();
227
+ }
228
+ }
229
+ },
230
+
231
+ // ======================================================
232
+ // STATE SETTERS
233
+ // ------------------------------------------------------
234
+ // WARNING:
235
+ // - These methods mutate controller state.
236
+ // - Some of them MAY request a reload in remote mode.
237
+ // - They must NEVER perform transport logic directly.
238
+ // ======================================================
239
+
240
+ // ---- paging ----
241
+ setPage(page: number) {
242
+ const next = Number(page);
243
+ if (!Number.isInteger(next) || next < 1) return;
244
+
245
+ if (this.state.page === next) return;
246
+
247
+ this.state.page = next;
248
+
249
+ if (this.state.mode === 'remote') {
250
+ this.requestReload('page-change');
251
+ } else {
252
+ // Local mode: trigger body refresh
253
+ this.grid?._lifecycle?.onControllerDataChange?.();
254
+ }
255
+ },
256
+
257
+ setPageSize(size: number) {
258
+ const next = Number(size);
259
+ if (!Number.isInteger(next) || next < 1) return;
260
+
261
+ if (this.state.pageSize === next) return;
262
+
263
+ this.state.pageSize = next;
264
+ this.state.page = 1;
265
+
266
+ if (this.state.mode === 'remote') {
267
+ this.requestReload('pageSize-change');
268
+ } else {
269
+ // Local mode: trigger body refresh
270
+ this.grid?._lifecycle?.onControllerDataChange?.();
271
+ }
272
+ },
273
+
274
+ // ---- sort bridge (remote) ----
275
+ setSortSnapshot(snapshot: SortSnapshot | null) {
276
+ // NOTE: Snapshot is owned by EzGridSort.
277
+ // Controller only stores and forwards it to transport.
278
+ this._lastSortSnapshot = snapshot;
279
+
280
+ if (this.state.mode === 'remote') {
281
+ this.requestReload('sort-change');
282
+ }
283
+ },
284
+
285
+ // ---- filters bridge (remote contract) ----
286
+ setFilterSnapshot(snapshot: FilterSnapshot[] | null) {
287
+ // NOTE: Snapshot is owned by the grid/filter system.
288
+ // Controller only stores and forwards it to transport.
289
+ this._lastFilterSnapshot = snapshot;
290
+
291
+ if (this.state.mode === 'remote') {
292
+ this.requestReload('filter-change');
293
+ }
294
+ },
295
+
296
+ setData(data: RowData[], meta: { total?: number } = {}) {
297
+ let processedData = Array.isArray(data) ? data : [];
298
+
299
+ // Process through model if defined
300
+ if (this._model && processedData.length > 0) {
301
+ processedData = this._model.processAll(processedData);
302
+ }
303
+
304
+ this.state.data = processedData;
305
+ this.state.total = meta.total ?? this.state.data.length;
306
+
307
+ emit('datachange', this.state.data);
308
+ },
309
+
310
+ setLoading(value: boolean) {
311
+ // NOTE: Loading state notification is delegated to the grid lifecycle.
312
+ // Controller must not assume how loading is rendered.
313
+ this.state.loading = !!value;
314
+ grid._onLoadingChanged?.(this.state.loading);
315
+ },
316
+
317
+ setError(error: Error | null) {
318
+ // NOTE: Error propagation is delegated to the grid lifecycle.
319
+ // Controller must not perform UI-side error handling.
320
+ this.state.error = error;
321
+ grid._onError?.(error);
322
+ },
323
+
324
+ // ======================================================
325
+ // ACTIONS (DATA LOADING)
326
+ // ------------------------------------------------------
327
+ // WARNING:
328
+ // - These methods may trigger remote IO.
329
+ // - load() executes the transport.
330
+ // - reload() is an alias for load().
331
+ // - requestReload() is the preferred entry point.
332
+ // ======================================================
333
+
334
+ reload() {
335
+ return this.load();
336
+ },
337
+
338
+ // ---- reload orchestration (DX) ----
339
+ requestReload(reason: string = 'unspecified') {
340
+ // NOTE: Centralized reload request entry point.
341
+ // This method does NOT change behavior.
342
+ // It exists to make reload intent explicit and traceable.
343
+
344
+ if (import.meta?.env?.DEV) {
345
+ console.debug(
346
+ `[EzGridController] Reload requested (${reason})`
347
+ );
348
+ }
349
+
350
+ return this.reload();
351
+ },
352
+
353
+ setRemoteConfig(config: Partial<RemoteConfig & { transport?: TransportFn }> = {}) {
354
+ this.state.mode = 'remote';
355
+
356
+ this._remoteConfig = {
357
+ transport: typeof config.transport === 'function'
358
+ ? config.transport
359
+ : null,
360
+
361
+ autoLoad: config.autoLoad === true,
362
+ pageSize:
363
+ typeof config.pageSize === 'number'
364
+ ? config.pageSize
365
+ : this.state.pageSize
366
+ };
367
+
368
+ // Apply pageSize from config
369
+ if (typeof config.pageSize === 'number') {
370
+ this.state.pageSize = config.pageSize;
371
+ }
372
+ },
373
+
374
+ appendData(data: RowData[]) {
375
+ if (Array.isArray(data)) {
376
+ // Process through model if defined
377
+ const processed = this._model
378
+ ? this._model.processAll(data)
379
+ : data;
380
+
381
+ this.state.data = this.state.data.concat(processed);
382
+ emit('datachange', this.state.data);
383
+ }
384
+ },
385
+
386
+ clear() {
387
+ this.state.data = [];
388
+ this.state.total = 0;
389
+ emit('datachange', this.state.data);
390
+ },
391
+
392
+ // ======================================================
393
+ // MODEL-AWARE RECORD OPERATIONS
394
+ // ------------------------------------------------------
395
+ // These methods leverage the model (if defined) to process
396
+ // records before adding/updating them in the grid.
397
+ // ======================================================
398
+
399
+ addRecord(record: RowData) {
400
+ const processed = this._model
401
+ ? this._model.process(record)
402
+ : record;
403
+
404
+ this.state.data = [...this.state.data, processed];
405
+ this.state.total = this.state.data.length;
406
+ emit('datachange', this.state.data);
407
+ },
408
+
409
+ updateRecord(key: unknown, updates: Partial<RowData>): boolean {
410
+ const primaryKey = this._model?.primaryKey || 'id';
411
+ const idx = this.state.data.findIndex(r => r[primaryKey] === key);
412
+
413
+ if (idx < 0) {
414
+ if (import.meta?.env?.DEV) {
415
+ console.warn(
416
+ `[EzGridController] updateRecord: No record found with ${primaryKey}="${key}"`
417
+ );
418
+ }
419
+ return false;
420
+ }
421
+
422
+ const merged = { ...this.state.data[idx], ...updates };
423
+ const processed = this._model
424
+ ? this._model.process(merged)
425
+ : merged;
426
+
427
+ this.state.data[idx] = processed;
428
+ this.state.data = [...this.state.data]; // Trigger reactivity
429
+ emit('datachange', this.state.data);
430
+ return true;
431
+ },
432
+
433
+ removeRecord(key: unknown): boolean {
434
+ const primaryKey = this._model?.primaryKey || 'id';
435
+ const idx = this.state.data.findIndex(r => r[primaryKey] === key);
436
+
437
+ if (idx < 0) {
438
+ if (import.meta?.env?.DEV) {
439
+ console.warn(
440
+ `[EzGridController] removeRecord: No record found with ${primaryKey}="${key}"`
441
+ );
442
+ }
443
+ return false;
444
+ }
445
+
446
+ this.state.data.splice(idx, 1);
447
+ this.state.data = [...this.state.data]; // Trigger reactivity
448
+ this.state.total = this.state.data.length;
449
+ emit('datachange', this.state.data);
450
+ return true;
451
+ }
452
+
453
+ };
454
+
455
+ return controller;
456
+ }
457
+
458
+ // Re-export types for backwards compatibility
459
+ export type {
460
+ DataMode,
461
+ SortSnapshot,
462
+ FilterSnapshot,
463
+ TransportParams,
464
+ TransportResult,
465
+ TransportFn,
466
+ RemoteConfig,
467
+ EzGridModel,
468
+ EzGridController,
469
+ EzGridControllerState
470
+ } from '../types.js';
@@ -0,0 +1,136 @@
1
+ // EzGrid/state/EzGridLifecycle.ts
2
+
3
+ import type { EzGridBodyRef } from '../types.js';
4
+
5
+ export interface EzGridLifecycleRef {
6
+ el?: HTMLElement | null;
7
+ _suppressSelectionPersistence?: boolean;
8
+ _bodyInstance?: EzGridBodyRef;
9
+ _refreshBodyInternal: () => Promise<void>;
10
+ }
11
+
12
+ export class EzGridLifecycle {
13
+ grid: EzGridLifecycleRef;
14
+ private _isRefreshing: boolean;
15
+ private _pendingRefresh: boolean;
16
+ private _refreshSuspended: boolean;
17
+ private _hydrated: boolean;
18
+ private _loading: boolean;
19
+ private _error: Error | null;
20
+
21
+ constructor(grid: EzGridLifecycleRef) {
22
+ this.grid = grid;
23
+
24
+ // WARNING: Lifecycle owns refresh orchestration ONLY.
25
+ // Rendering and side effects remain in EzGrid executor methods.
26
+ this._isRefreshing = false;
27
+ this._pendingRefresh = false;
28
+ this._refreshSuspended = false;
29
+
30
+ // NOTE: Tracks whether the grid has completed its first successful hydration.
31
+ // This controls when selection persistence is allowed.
32
+ this._hydrated = false;
33
+
34
+ this._loading = false;
35
+ this._error = null;
36
+ }
37
+
38
+ onControllerDataChange(): void {
39
+ // WARNING: If the grid is not rendered yet, we must NOT attempt a refresh.
40
+ // The request must be deferred, otherwise the first refresh can be lost.
41
+ if (!this.grid?.el) {
42
+ this._pendingRefresh = true;
43
+ return;
44
+ }
45
+
46
+ if (this._refreshSuspended) {
47
+ this._pendingRefresh = true;
48
+ return;
49
+ }
50
+
51
+ if (this._isRefreshing) {
52
+ this._pendingRefresh = true;
53
+ return;
54
+ }
55
+
56
+ void this._runRefresh();
57
+ }
58
+
59
+ markHydrated(): void {
60
+ // WARNING: This must be called exactly once,
61
+ // after the first successful body refresh.
62
+ if (this._hydrated === true) return;
63
+
64
+ this._hydrated = true;
65
+
66
+ // NOTE: Selection persistence is enabled only after hydration.
67
+ this.grid._suppressSelectionPersistence = false;
68
+ }
69
+
70
+
71
+ async _runRefresh(): Promise<void> {
72
+ this._isRefreshing = true;
73
+
74
+ try {
75
+ await this.grid._refreshBodyInternal();
76
+ } finally {
77
+ this._isRefreshing = false;
78
+
79
+ if (this._pendingRefresh) {
80
+ // WARNING: Must clear flag BEFORE rerun to avoid endless loops.
81
+ this._pendingRefresh = false;
82
+ await this._runRefresh();
83
+ }
84
+ }
85
+ }
86
+
87
+ async suspend<T>(fn: () => Promise<T>): Promise<void> {
88
+ this._refreshSuspended = true;
89
+
90
+ try {
91
+ await fn();
92
+ } finally {
93
+ this._refreshSuspended = false;
94
+
95
+ if (this._pendingRefresh) {
96
+ this._pendingRefresh = false;
97
+ await this._runRefresh();
98
+ }
99
+ }
100
+ }
101
+
102
+ destroy(): void {
103
+ // WARNING: Cleanup must be idempotent.
104
+ this._pendingRefresh = false;
105
+ this._refreshSuspended = false;
106
+ this._isRefreshing = false;
107
+ }
108
+
109
+ onLoadingChanged(loading: boolean): void {
110
+ // NOTE: Lifecycle receives loading state changes.
111
+ // It must NOT re-dispatch them back to the grid,
112
+ // otherwise a recursion loop will occur.
113
+
114
+ this._loading = loading;
115
+
116
+ // Notify body instance of loading state
117
+ const body = this.grid._bodyInstance;
118
+ if (body?.setLoading) {
119
+ body.setLoading(loading);
120
+ }
121
+ }
122
+
123
+ onError(error: Error | null): void {
124
+ // NOTE: Lifecycle receives error state.
125
+ // It must NOT re-dispatch back to the grid.
126
+
127
+ this._error = error;
128
+
129
+ // Notify body instance of error state
130
+ const body = this.grid._bodyInstance;
131
+ if (body?.setError) {
132
+ body.setError(error);
133
+ }
134
+ }
135
+
136
+ }