@alaarab/ogrid-angular 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -4,6 +4,8 @@ export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './types
4
4
  // Services
5
5
  export { OGridService } from './services/ogrid.service';
6
6
  export { DataGridStateService } from './services/datagrid-state.service';
7
+ export { ColumnReorderService } from './services/column-reorder.service';
8
+ export { VirtualScrollService } from './services/virtual-scroll.service';
7
9
  // Components
8
10
  export { OGridLayoutComponent } from './components/ogrid-layout.component';
9
11
  export { StatusBarComponent } from './components/status-bar.component';
@@ -11,3 +13,4 @@ export { GridContextMenuComponent } from './components/grid-context-menu.compone
11
13
  export { SideBarComponent } from './components/sidebar.component';
12
14
  export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
13
15
  export { EmptyStateComponent } from './components/empty-state.component';
16
+ export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
@@ -0,0 +1,180 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Injectable, signal, DestroyRef, inject } from '@angular/core';
8
+ import { reorderColumnArray } from '@alaarab/ogrid-core';
9
+ /** Width of the resize handle zone on the right edge of each header cell. */
10
+ const RESIZE_HANDLE_ZONE = 8;
11
+ /**
12
+ * Manages column reorder drag interactions with RAF-throttled updates.
13
+ * Angular signals-based port of React's useColumnReorder hook.
14
+ */
15
+ let ColumnReorderService = class ColumnReorderService {
16
+ constructor() {
17
+ this.destroyRef = inject(DestroyRef);
18
+ // --- Input signals (set by consuming component) ---
19
+ this.columns = signal([]);
20
+ this.columnOrder = signal(undefined);
21
+ this.onColumnOrderChange = signal(undefined);
22
+ this.enabled = signal(true);
23
+ this.wrapperEl = signal(null);
24
+ // --- Internal state ---
25
+ this.isDragging = signal(false);
26
+ this.dropIndicatorX = signal(null);
27
+ // Imperative drag tracking (not reactive)
28
+ this.rafId = 0;
29
+ this.cleanupFn = null;
30
+ // Refs for latest values (captured in closure)
31
+ this.latestDropTargetIndex = null;
32
+ this.destroyRef.onDestroy(() => {
33
+ if (this.cleanupFn) {
34
+ this.cleanupFn();
35
+ this.cleanupFn = null;
36
+ }
37
+ if (this.rafId) {
38
+ cancelAnimationFrame(this.rafId);
39
+ this.rafId = 0;
40
+ }
41
+ });
42
+ }
43
+ /**
44
+ * Call this from the header cell's mousedown handler.
45
+ * @param columnId - The column being dragged
46
+ * @param event - The native MouseEvent
47
+ */
48
+ handleHeaderMouseDown(columnId, event) {
49
+ if (!this.enabled())
50
+ return;
51
+ if (!this.onColumnOrderChange())
52
+ return;
53
+ // Gate on left-click only
54
+ if (event.button !== 0)
55
+ return;
56
+ // Skip if in resize handle zone (right 8px of the header cell)
57
+ const target = event.currentTarget;
58
+ const rect = target.getBoundingClientRect();
59
+ if (event.clientX > rect.right - RESIZE_HANDLE_ZONE)
60
+ return;
61
+ // Skip column groups — only reorder leaf columns
62
+ const cols = this.columns();
63
+ const colIndex = cols.findIndex((c) => c.columnId === columnId);
64
+ if (colIndex === -1)
65
+ return;
66
+ event.preventDefault();
67
+ const startX = event.clientX;
68
+ let hasMoved = false;
69
+ this.latestDropTargetIndex = null;
70
+ // Lock text selection during drag
71
+ const prevUserSelect = document.body.style.userSelect;
72
+ document.body.style.userSelect = 'none';
73
+ const onMove = (moveEvent) => {
74
+ // Require a small minimum drag distance before activating
75
+ if (!hasMoved && Math.abs(moveEvent.clientX - startX) < 5)
76
+ return;
77
+ if (!hasMoved) {
78
+ hasMoved = true;
79
+ this.isDragging.set(true);
80
+ }
81
+ if (this.rafId)
82
+ cancelAnimationFrame(this.rafId);
83
+ this.rafId = requestAnimationFrame(() => {
84
+ this.rafId = 0;
85
+ const wrapper = this.wrapperEl();
86
+ if (!wrapper)
87
+ return;
88
+ const headerCells = wrapper.querySelectorAll('th[data-column-id]');
89
+ const rects = [];
90
+ for (let i = 0; i < headerCells.length; i++) {
91
+ const th = headerCells[i];
92
+ const id = th.getAttribute('data-column-id');
93
+ if (!id)
94
+ continue;
95
+ const thRect = th.getBoundingClientRect();
96
+ rects.push({
97
+ columnId: id,
98
+ left: thRect.left,
99
+ right: thRect.right,
100
+ centerX: thRect.left + thRect.width / 2,
101
+ });
102
+ }
103
+ const result = this.calculateDrop(columnId, moveEvent.clientX, rects);
104
+ this.latestDropTargetIndex = result.dropIndex;
105
+ this.dropIndicatorX.set(result.indicatorX);
106
+ });
107
+ };
108
+ const cleanup = () => {
109
+ window.removeEventListener('mousemove', onMove, true);
110
+ window.removeEventListener('mouseup', onUp, true);
111
+ this.cleanupFn = null;
112
+ // Restore user-select
113
+ document.body.style.userSelect = prevUserSelect;
114
+ // Cancel pending RAF
115
+ if (this.rafId) {
116
+ cancelAnimationFrame(this.rafId);
117
+ this.rafId = 0;
118
+ }
119
+ };
120
+ const onUp = () => {
121
+ cleanup();
122
+ if (hasMoved && this.latestDropTargetIndex != null) {
123
+ const currentOrder = this.columnOrder() ?? this.columns().map((c) => c.columnId);
124
+ const newOrder = reorderColumnArray(currentOrder, columnId, this.latestDropTargetIndex);
125
+ this.onColumnOrderChange()?.(newOrder);
126
+ }
127
+ this.isDragging.set(false);
128
+ this.dropIndicatorX.set(null);
129
+ };
130
+ window.addEventListener('mousemove', onMove, true);
131
+ window.addEventListener('mouseup', onUp, true);
132
+ this.cleanupFn = cleanup;
133
+ }
134
+ /**
135
+ * Calculate drop target from mouse position and header cell rects.
136
+ * Same logic as React's useColumnReorder inline calculation.
137
+ */
138
+ calculateDrop(draggedColumnId, mouseX, rects) {
139
+ if (rects.length === 0) {
140
+ return { dropIndex: null, indicatorX: null };
141
+ }
142
+ const order = this.columnOrder() ?? this.columns().map((c) => c.columnId);
143
+ const currentIndex = order.indexOf(draggedColumnId);
144
+ // Find which column the mouse is closest to
145
+ let bestIndex = 0;
146
+ let indicatorX = null;
147
+ if (mouseX <= rects[0].centerX) {
148
+ // Before the first column
149
+ bestIndex = 0;
150
+ indicatorX = rects[0].left;
151
+ }
152
+ else if (mouseX >= rects[rects.length - 1].centerX) {
153
+ // After the last column
154
+ bestIndex = rects.length;
155
+ indicatorX = rects[rects.length - 1].right;
156
+ }
157
+ else {
158
+ for (let i = 0; i < rects.length - 1; i++) {
159
+ if (mouseX >= rects[i].centerX && mouseX < rects[i + 1].centerX) {
160
+ bestIndex = i + 1;
161
+ indicatorX = rects[i].right;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ // Map visual index back to order array index
167
+ const targetOrderIndex = bestIndex < rects.length
168
+ ? order.indexOf(rects[bestIndex]?.columnId ?? '')
169
+ : order.length;
170
+ // Check if this is a no-op (dropping at same position)
171
+ if (currentIndex === targetOrderIndex || currentIndex + 1 === targetOrderIndex) {
172
+ return { dropIndex: targetOrderIndex, indicatorX: null };
173
+ }
174
+ return { dropIndex: targetOrderIndex, indicatorX };
175
+ }
176
+ };
177
+ ColumnReorderService = __decorate([
178
+ Injectable()
179
+ ], ColumnReorderService);
180
+ export { ColumnReorderService };
@@ -51,8 +51,6 @@ let DataGridStateService = class DataGridStateService {
51
51
  this.rafId = 0;
52
52
  this.lastMousePos = null;
53
53
  this.autoScrollInterval = null;
54
- // Window event listeners cleanup
55
- this.windowCleanups = [];
56
54
  // ResizeObserver
57
55
  this.resizeObserver = null;
58
56
  // --- Derived computed ---
@@ -199,13 +197,16 @@ let DataGridStateService = class DataGridStateService {
199
197
  return p.items.length === 0 && !!p.emptyState && !p.isLoading;
200
198
  });
201
199
  // Setup window event listeners for cell selection drag
202
- const onMove = (e) => this.onWindowMouseMove(e);
203
- const onUp = () => this.onWindowMouseUp();
204
- window.addEventListener('mousemove', onMove, true);
205
- window.addEventListener('mouseup', onUp, true);
206
- this.windowCleanups.push(() => {
207
- window.removeEventListener('mousemove', onMove, true);
208
- window.removeEventListener('mouseup', onUp, true);
200
+ // Using effect with cleanup return to ensure proper removal on destroy
201
+ effect((onCleanup) => {
202
+ const onMove = (e) => this.onWindowMouseMove(e);
203
+ const onUp = () => this.onWindowMouseUp();
204
+ window.addEventListener('mousemove', onMove, true);
205
+ window.addEventListener('mouseup', onUp, true);
206
+ onCleanup(() => {
207
+ window.removeEventListener('mousemove', onMove, true);
208
+ window.removeEventListener('mouseup', onUp, true);
209
+ });
209
210
  });
210
211
  // Initialize column sizing overrides from initial widths
211
212
  effect(() => {
@@ -240,7 +241,6 @@ let DataGridStateService = class DataGridStateService {
240
241
  });
241
242
  // Cleanup on destroy
242
243
  this.destroyRef.onDestroy(() => {
243
- this.windowCleanups.forEach((fn) => fn());
244
244
  if (this.rafId)
245
245
  cancelAnimationFrame(this.rafId);
246
246
  if (this.autoScrollInterval)
@@ -1257,6 +1257,9 @@ let DataGridStateService = class DataGridStateService {
1257
1257
  this.endBatch();
1258
1258
  }
1259
1259
  this.fillDragStart = null;
1260
+ // Remove event listeners after mouseup completes
1261
+ window.removeEventListener('mousemove', onMove, true);
1262
+ window.removeEventListener('mouseup', onUp, true);
1260
1263
  };
1261
1264
  window.addEventListener('mousemove', onMove, true);
1262
1265
  window.addEventListener('mouseup', onUp, true);
@@ -72,10 +72,10 @@ let OGridService = class OGridService {
72
72
  this.internalData = signal([]);
73
73
  this.internalLoading = signal(false);
74
74
  this.internalPage = signal(1);
75
- this.internalPageSize = signal(DEFAULT_PAGE_SIZE);
76
- this.internalSort = signal({ field: '', direction: 'asc' });
75
+ this.internalPageSizeOverride = signal(null);
76
+ this.internalSortOverride = signal(null);
77
77
  this.internalFilters = signal({});
78
- this.internalVisibleColumns = signal(new Set());
78
+ this.internalVisibleColumnsOverride = signal(null);
79
79
  this.internalSelectedRows = signal(new Set());
80
80
  this.columnWidthOverrides = signal({});
81
81
  this.pinnedOverrides = signal({});
@@ -91,10 +91,6 @@ let OGridService = class OGridService {
91
91
  // Filter options state
92
92
  this.serverFilterOptions = signal({});
93
93
  this.loadingFilterOptions = signal({});
94
- // --- Initialization ---
95
- this.sortInitialized = false;
96
- this.visibleColumnsInitialized = false;
97
- this.pageSizeInitialized = false;
98
94
  // --- Derived computed signals ---
99
95
  this.columns = computed(() => flattenColumns(this.columnsProp()));
100
96
  this.isServerSide = computed(() => this.dataSource() != null);
@@ -103,10 +99,23 @@ let OGridService = class OGridService {
103
99
  this.displayLoading = computed(() => this.controlledLoading() ?? this.internalLoading());
104
100
  this.defaultSortField = computed(() => this.defaultSortBy() ?? this.columns()[0]?.columnId ?? '');
105
101
  this.page = computed(() => this.controlledPage() ?? this.internalPage());
106
- this.pageSize = computed(() => this.controlledPageSize() ?? this.internalPageSize());
107
- this.sort = computed(() => this.controlledSort() ?? this.internalSort());
102
+ this.pageSize = computed(() => this.controlledPageSize() ?? this.internalPageSizeOverride() ?? this.defaultPageSize());
103
+ this.sort = computed(() => this.controlledSort() ?? this.internalSortOverride() ?? {
104
+ field: this.defaultSortField(),
105
+ direction: this.defaultSortDirection(),
106
+ });
108
107
  this.filters = computed(() => this.controlledFilters() ?? this.internalFilters());
109
- this.visibleColumns = computed(() => this.controlledVisibleColumns() ?? this.internalVisibleColumns());
108
+ this.visibleColumns = computed(() => {
109
+ if (this.controlledVisibleColumns())
110
+ return this.controlledVisibleColumns();
111
+ if (this.internalVisibleColumnsOverride())
112
+ return this.internalVisibleColumnsOverride();
113
+ const cols = this.columns();
114
+ if (cols.length === 0)
115
+ return new Set();
116
+ const visible = cols.filter((c) => c.defaultVisible !== false).map((c) => c.columnId);
117
+ return new Set(visible.length > 0 ? visible : cols.map((c) => c.columnId));
118
+ });
110
119
  this.effectiveSelectedRows = computed(() => this.selectedRows() ?? this.internalSelectedRows());
111
120
  this.columnChooserPlacement = computed(() => {
112
121
  const prop = this.columnChooserProp();
@@ -281,30 +290,6 @@ let OGridService = class OGridService {
281
290
  filterOptions: this.clientFilterOptions(),
282
291
  };
283
292
  });
284
- // Initialize internal default values based on config
285
- effect(() => {
286
- if (!this.sortInitialized) {
287
- this.sortInitialized = true;
288
- this.internalSort.set({
289
- field: this.defaultSortField(),
290
- direction: this.defaultSortDirection(),
291
- });
292
- }
293
- });
294
- effect(() => {
295
- if (!this.pageSizeInitialized) {
296
- this.pageSizeInitialized = true;
297
- this.internalPageSize.set(this.defaultPageSize());
298
- }
299
- });
300
- effect(() => {
301
- if (!this.visibleColumnsInitialized && this.columns().length > 0) {
302
- this.visibleColumnsInitialized = true;
303
- const cols = this.columns();
304
- const visible = cols.filter((c) => c.defaultVisible !== false).map((c) => c.columnId);
305
- this.internalVisibleColumns.set(new Set(visible.length > 0 ? visible : cols.map((c) => c.columnId)));
306
- }
307
- });
308
293
  // Server-side data fetching effect
309
294
  effect(() => {
310
295
  const ds = this.dataSource();
@@ -391,13 +376,13 @@ let OGridService = class OGridService {
391
376
  }
392
377
  setPageSize(size) {
393
378
  if (this.controlledPageSize() === undefined)
394
- this.internalPageSize.set(size);
379
+ this.internalPageSizeOverride.set(size);
395
380
  this.onPageSizeChange()?.(size);
396
381
  this.setPage(1);
397
382
  }
398
383
  setSort(s) {
399
384
  if (this.controlledSort() === undefined)
400
- this.internalSort.set(s);
385
+ this.internalSortOverride.set(s);
401
386
  this.onSortChange()?.(s);
402
387
  this.setPage(1);
403
388
  }
@@ -409,7 +394,7 @@ let OGridService = class OGridService {
409
394
  }
410
395
  setVisibleColumns(cols) {
411
396
  if (this.controlledVisibleColumns() === undefined)
412
- this.internalVisibleColumns.set(cols);
397
+ this.internalVisibleColumnsOverride.set(cols);
413
398
  this.onVisibleColumnsChange()?.(cols);
414
399
  }
415
400
  handleSort(columnKey) {
@@ -610,6 +595,14 @@ let OGridService = class OGridService {
610
595
  this.refreshCounter.update((c) => c + 1);
611
596
  }
612
597
  },
598
+ getColumnOrder: () => this.columnOrder() ?? this.columns().map((c) => c.columnId),
599
+ setColumnOrder: (order) => {
600
+ this.onColumnOrderChange()?.(order);
601
+ },
602
+ scrollToRow: (_index, _options) => {
603
+ // Scrolling is handled by VirtualScrollService at the UI layer.
604
+ // The UI component should wire this to VirtualScrollService.scrollToRow().
605
+ },
613
606
  };
614
607
  }
615
608
  };
@@ -0,0 +1,91 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Injectable, signal, computed, DestroyRef, inject } from '@angular/core';
8
+ import { computeVisibleRange, computeTotalHeight, getScrollTopForRow, } from '@alaarab/ogrid-core';
9
+ /** Threshold below which virtual scrolling is a no-op (all rows rendered). */
10
+ const PASSTHROUGH_THRESHOLD = 100;
11
+ /**
12
+ * Manages virtual scrolling state using Angular signals.
13
+ * Port of React's useVirtualScroll hook.
14
+ *
15
+ * Uses core's pure-TS `computeVisibleRange` and `getScrollTopForRow` utilities.
16
+ * The UI layer (Angular Material / PrimeNG) provides the scrollable container
17
+ * and calls `onScroll()` / sets `containerHeight`.
18
+ */
19
+ let VirtualScrollService = class VirtualScrollService {
20
+ constructor() {
21
+ this.destroyRef = inject(DestroyRef);
22
+ // --- Input signals (set by consuming component) ---
23
+ this.totalRows = signal(0);
24
+ this.config = signal({ rowHeight: 36 });
25
+ this.containerHeight = signal(0);
26
+ // --- Internal state ---
27
+ this.scrollTop = signal(0);
28
+ // Scrollable container reference for programmatic scrolling
29
+ this.containerEl = null;
30
+ // --- Derived computed signals ---
31
+ this.rowHeight = computed(() => this.config().rowHeight);
32
+ this.overscan = computed(() => this.config().overscan ?? 5);
33
+ this.enabled = computed(() => this.config().enabled !== false);
34
+ /** Whether virtual scrolling is actually active (enabled + enough rows). */
35
+ this.isActive = computed(() => this.enabled() && this.totalRows() >= PASSTHROUGH_THRESHOLD);
36
+ /** The visible range of rows with spacer offsets. */
37
+ this.visibleRange = computed(() => {
38
+ if (!this.isActive()) {
39
+ // Passthrough: render all rows
40
+ return {
41
+ startIndex: 0,
42
+ endIndex: Math.max(0, this.totalRows() - 1),
43
+ offsetTop: 0,
44
+ offsetBottom: 0,
45
+ };
46
+ }
47
+ return computeVisibleRange(this.scrollTop(), this.rowHeight(), this.containerHeight(), this.totalRows(), this.overscan());
48
+ });
49
+ /** Total scrollable height in pixels. */
50
+ this.totalHeight = computed(() => computeTotalHeight(this.totalRows(), this.rowHeight()));
51
+ this.destroyRef.onDestroy(() => {
52
+ this.containerEl = null;
53
+ });
54
+ }
55
+ /**
56
+ * Set the scrollable container element.
57
+ * Used for programmatic scrolling (scrollToRow).
58
+ */
59
+ setContainer(el) {
60
+ this.containerEl = el;
61
+ }
62
+ /**
63
+ * Call this from the container's scroll event handler.
64
+ */
65
+ onScroll(event) {
66
+ const target = event.target;
67
+ this.scrollTop.set(target.scrollTop);
68
+ }
69
+ /**
70
+ * Scroll to a specific row index.
71
+ * @param index - The row index to scroll to.
72
+ * @param align - Where to position the row: 'start' (top), 'center', or 'end' (bottom). Default: 'start'.
73
+ */
74
+ scrollToRow(index, align = 'start') {
75
+ const container = this.containerEl;
76
+ if (!container)
77
+ return;
78
+ const targetScrollTop = getScrollTopForRow(index, this.rowHeight(), this.containerHeight(), align);
79
+ container.scrollTo({ top: targetScrollTop, behavior: 'auto' });
80
+ }
81
+ /**
82
+ * Update the virtual scroll configuration.
83
+ */
84
+ updateConfig(updates) {
85
+ this.config.update((prev) => ({ ...prev, ...updates }));
86
+ }
87
+ };
88
+ VirtualScrollService = __decorate([
89
+ Injectable()
90
+ ], VirtualScrollService);
91
+ export { VirtualScrollService };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * View model helpers for Angular DataGridTable. Core owns the logic; UI packages only render.
3
+ * Ported from React's dataGridViewModel.ts to eliminate duplication in Angular Material and PrimeNG packages.
4
+ */
5
+ import { getCellValue, isInSelectionRange } from '@alaarab/ogrid-core';
6
+ /**
7
+ * Returns ColumnHeaderFilter props from column def and grid filter/sort state.
8
+ * Use in Angular Material and PrimeNG DataGridTableComponent instead of inline logic.
9
+ */
10
+ export function getHeaderFilterConfig(col, input) {
11
+ const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
12
+ const filterType = (filterable?.type ?? 'none');
13
+ const filterField = filterable?.filterField ?? col.columnId;
14
+ const sortable = col.sortable !== false;
15
+ const filterValue = input.filters[filterField];
16
+ const base = {
17
+ columnKey: col.columnId,
18
+ columnName: col.name,
19
+ filterType,
20
+ isSorted: input.sortBy === col.columnId,
21
+ isSortedDescending: input.sortBy === col.columnId && input.sortDirection === 'desc',
22
+ onSort: sortable ? () => input.onColumnSort(col.columnId) : undefined,
23
+ };
24
+ if (filterType === 'text') {
25
+ return {
26
+ ...base,
27
+ textValue: filterValue?.type === 'text' ? filterValue.value : '',
28
+ onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
29
+ };
30
+ }
31
+ if (filterType === 'people') {
32
+ return {
33
+ ...base,
34
+ selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
35
+ onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
36
+ peopleSearch: input.peopleSearch,
37
+ };
38
+ }
39
+ if (filterType === 'multiSelect') {
40
+ return {
41
+ ...base,
42
+ options: input.filterOptions[filterField] ?? [],
43
+ isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
44
+ selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
45
+ onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
46
+ };
47
+ }
48
+ if (filterType === 'date') {
49
+ return {
50
+ ...base,
51
+ dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
52
+ onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
53
+ };
54
+ }
55
+ return base;
56
+ }
57
+ /**
58
+ * Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
59
+ * and to apply isActive, isInRange, etc. without duplicating the boolean logic.
60
+ */
61
+ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
62
+ const rowId = input.getRowId(item);
63
+ const globalColIndex = colIdx + input.colOffset;
64
+ const colEditable = col.editable === true ||
65
+ (typeof col.editable === 'function' && col.editable(item));
66
+ const canEditInline = input.editable !== false &&
67
+ !!colEditable &&
68
+ !!input.onCellValueChanged &&
69
+ typeof col.cellEditor !== 'function';
70
+ const canEditPopup = input.editable !== false &&
71
+ !!colEditable &&
72
+ !!input.onCellValueChanged &&
73
+ typeof col.cellEditor === 'function';
74
+ const canEditAny = canEditInline || canEditPopup;
75
+ const isEditing = input.editingCell?.rowId === rowId &&
76
+ input.editingCell?.columnId === col.columnId;
77
+ const isActive = input.activeCell?.rowIndex === rowIndex &&
78
+ input.activeCell?.columnIndex === globalColIndex;
79
+ const isInRange = input.selectionRange != null &&
80
+ isInSelectionRange(input.selectionRange, rowIndex, colIdx);
81
+ const isInCutRange = input.cutRange != null &&
82
+ isInSelectionRange(input.cutRange, rowIndex, colIdx);
83
+ const isInCopyRange = input.copyRange != null &&
84
+ isInSelectionRange(input.copyRange, rowIndex, colIdx);
85
+ const isSelectionEndCell = !input.isDragging &&
86
+ input.copyRange == null &&
87
+ input.cutRange == null &&
88
+ input.selectionRange != null &&
89
+ rowIndex === input.selectionRange.endRow &&
90
+ colIdx === input.selectionRange.endCol;
91
+ let mode = 'display';
92
+ let editorType;
93
+ const value = getCellValue(item, col);
94
+ if (isEditing && canEditInline) {
95
+ mode = 'editing-inline';
96
+ if (col.cellEditor === 'text' ||
97
+ col.cellEditor === 'select' ||
98
+ col.cellEditor === 'checkbox' ||
99
+ col.cellEditor === 'richSelect' ||
100
+ col.cellEditor === 'date') {
101
+ editorType = col.cellEditor;
102
+ }
103
+ else if (col.type === 'date') {
104
+ editorType = 'date';
105
+ }
106
+ else if (col.type === 'boolean') {
107
+ editorType = 'checkbox';
108
+ }
109
+ else {
110
+ editorType = 'text';
111
+ }
112
+ }
113
+ else if (isEditing && canEditPopup) {
114
+ mode = 'editing-popover';
115
+ }
116
+ return {
117
+ mode,
118
+ editorType,
119
+ value,
120
+ isActive,
121
+ isInRange,
122
+ isInCutRange,
123
+ isInCopyRange,
124
+ isSelectionEndCell,
125
+ canEditAny,
126
+ globalColIndex,
127
+ rowId,
128
+ rowIndex,
129
+ displayValue: value,
130
+ };
131
+ }
132
+ // --- Cell rendering helpers (reduce DataGridTable view-layer duplication) ---
133
+ /**
134
+ * Resolves display content for a cell in display mode.
135
+ * Handles the renderCell → valueFormatter → String() fallback chain.
136
+ */
137
+ export function resolveCellDisplayContent(col, item, displayValue) {
138
+ if (col.renderCell && typeof col.renderCell === 'function') {
139
+ const result = col.renderCell(item);
140
+ return result != null ? String(result) : '';
141
+ }
142
+ if (col.valueFormatter)
143
+ return String(col.valueFormatter(displayValue, item) ?? '');
144
+ if (displayValue == null)
145
+ return '';
146
+ if (col.type === 'date') {
147
+ const d = new Date(String(displayValue));
148
+ if (!Number.isNaN(d.getTime()))
149
+ return d.toLocaleDateString();
150
+ }
151
+ if (col.type === 'boolean') {
152
+ return displayValue ? 'True' : 'False';
153
+ }
154
+ return String(displayValue);
155
+ }
156
+ /**
157
+ * Resolves the cellStyle from a column def, handling both function and static values.
158
+ */
159
+ export function resolveCellStyle(col, item) {
160
+ if (!col.cellStyle)
161
+ return undefined;
162
+ return typeof col.cellStyle === 'function' ? col.cellStyle(item) : col.cellStyle;
163
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Debounce utilities for Angular using signals.
3
+ * Provides functional parity with React's useDebounce and useDebouncedCallback.
4
+ */
5
+ import { signal, effect } from '@angular/core';
6
+ /**
7
+ * Creates a debounced signal that updates after the specified delay when the source value changes.
8
+ *
9
+ * @param source - The signal to debounce
10
+ * @param delayMs - Delay in milliseconds
11
+ * @returns A signal containing the debounced value
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const searchQuery = signal('');
16
+ * const debouncedQuery = createDebouncedSignal(searchQuery, 300);
17
+ *
18
+ * effect(() => {
19
+ * console.log('Debounced search:', debouncedQuery());
20
+ * });
21
+ * ```
22
+ */
23
+ export function createDebouncedSignal(source, delayMs) {
24
+ const debouncedValue = signal(source());
25
+ effect((onCleanup) => {
26
+ const currentValue = source();
27
+ const timeoutId = setTimeout(() => {
28
+ debouncedValue.set(currentValue);
29
+ }, delayMs);
30
+ onCleanup(() => clearTimeout(timeoutId));
31
+ });
32
+ return debouncedValue;
33
+ }
34
+ /**
35
+ * Creates a debounced function that delays invoking the provided function
36
+ * until after `delayMs` milliseconds have elapsed since the last time it was invoked.
37
+ *
38
+ * @param fn - The function to debounce
39
+ * @param delayMs - Delay in milliseconds
40
+ * @returns A debounced version of the function
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const saveData = (value: string) => {
45
+ * console.log('Saving:', value);
46
+ * };
47
+ *
48
+ * const debouncedSave = createDebouncedCallback(saveData, 500);
49
+ *
50
+ * // Multiple rapid calls will only trigger once after 500ms
51
+ * debouncedSave('hello');
52
+ * debouncedSave('world'); // Only this will execute after 500ms
53
+ * ```
54
+ */
55
+ export function createDebouncedCallback(fn, delayMs) {
56
+ let timeoutId = null;
57
+ return ((...args) => {
58
+ if (timeoutId !== null) {
59
+ clearTimeout(timeoutId);
60
+ }
61
+ timeoutId = setTimeout(() => {
62
+ fn(...args);
63
+ timeoutId = null;
64
+ }, delayMs);
65
+ });
66
+ }
67
+ /**
68
+ * Simple debounce function (non-Angular-specific, can be used anywhere).
69
+ * Returns a debounced version of the provided function.
70
+ *
71
+ * @param fn - The function to debounce
72
+ * @param delayMs - Delay in milliseconds
73
+ * @returns A debounced version of the function with a `cancel()` method
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const handleResize = debounce(() => {
78
+ * console.log('Window resized');
79
+ * }, 200);
80
+ *
81
+ * window.addEventListener('resize', handleResize);
82
+ *
83
+ * // Later, cancel pending execution
84
+ * handleResize.cancel();
85
+ * ```
86
+ */
87
+ export function debounce(fn, delayMs) {
88
+ let timeoutId = null;
89
+ const debounced = ((...args) => {
90
+ if (timeoutId !== null) {
91
+ clearTimeout(timeoutId);
92
+ }
93
+ timeoutId = setTimeout(() => {
94
+ fn(...args);
95
+ timeoutId = null;
96
+ }, delayMs);
97
+ });
98
+ debounced.cancel = () => {
99
+ if (timeoutId !== null) {
100
+ clearTimeout(timeoutId);
101
+ timeoutId = null;
102
+ }
103
+ };
104
+ return debounced;
105
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared utilities for Angular DataGridTable view layer.
3
+ */
4
+ export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from './dataGridViewModel';
5
+ // Debounce utilities
6
+ export { createDebouncedSignal, createDebouncedCallback, debounce, } from './debounce';
7
+ // Latest ref utilities
8
+ export { createLatestRef, createLatestCallback, } from './latestRef';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Latest ref utility for Angular using signals.
3
+ * Provides functional parity with React's useLatestRef.
4
+ */
5
+ /**
6
+ * Creates a stable wrapper function that always calls the latest version of the provided function.
7
+ * Useful for event handlers and callbacks where you want a stable reference but need to call
8
+ * the latest implementation.
9
+ *
10
+ * @param fn - Signal containing the function to wrap
11
+ * @returns A stable function that always invokes the latest version
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * class MyService {
16
+ * readonly onSave = signal<(value: string) => void>((val) => console.log('Default:', val));
17
+ * readonly stableOnSave = createLatestCallback(this.onSave);
18
+ *
19
+ * constructor() {
20
+ * // Setup event listener with stable reference
21
+ * effect((onCleanup) => {
22
+ * // stableOnSave never changes, so this effect only runs once
23
+ * const callback = () => this.stableOnSave('data');
24
+ * window.addEventListener('click', callback);
25
+ * onCleanup(() => window.removeEventListener('click', callback));
26
+ * });
27
+ * }
28
+ *
29
+ * updateHandler(newFn: (value: string) => void) {
30
+ * // Even though we change the function, the callback reference stays stable
31
+ * this.onSave.set(newFn);
32
+ * }
33
+ * }
34
+ * ```
35
+ */
36
+ export function createLatestCallback(fn) {
37
+ // Return a stable function that always calls the current value of the signal
38
+ return ((...args) => {
39
+ return fn()(...args);
40
+ });
41
+ }
42
+ /**
43
+ * Alias for createLatestCallback for consistency with React/Vue naming.
44
+ * @deprecated Use createLatestCallback instead
45
+ */
46
+ export const createLatestRef = createLatestCallback;
@@ -7,6 +7,9 @@ export { OGridService } from './services/ogrid.service';
7
7
  export type { ColumnChooserPlacement, OGridPagination, OGridColumnChooser, OGridFilters, OGridSideBarState, } from './services/ogrid.service';
8
8
  export { DataGridStateService } from './services/datagrid-state.service';
9
9
  export type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingState, DataGridCellInteractionState, DataGridContextMenuState, DataGridViewModelState, DataGridStateResult, } from './services/datagrid-state.service';
10
+ export { ColumnReorderService } from './services/column-reorder.service';
11
+ export { VirtualScrollService } from './services/virtual-scroll.service';
12
+ export type { IVirtualScrollConfig } from './services/virtual-scroll.service';
10
13
  export { OGridLayoutComponent } from './components/ogrid-layout.component';
11
14
  export { StatusBarComponent } from './components/status-bar.component';
12
15
  export { GridContextMenuComponent } from './components/grid-context-menu.component';
@@ -14,3 +17,5 @@ export { SideBarComponent } from './components/sidebar.component';
14
17
  export type { SideBarProps, SideBarFilterColumn } from './components/sidebar.component';
15
18
  export { MarchingAntsOverlayComponent } from './components/marching-ants-overlay.component';
16
19
  export { EmptyStateComponent } from './components/empty-state.component';
20
+ export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
21
+ export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, createDebouncedSignal, createDebouncedCallback, debounce, createLatestRef, createLatestCallback, } from './utils';
@@ -0,0 +1,30 @@
1
+ import type { IColumnDef } from '../types';
2
+ /**
3
+ * Manages column reorder drag interactions with RAF-throttled updates.
4
+ * Angular signals-based port of React's useColumnReorder hook.
5
+ */
6
+ export declare class ColumnReorderService<T> {
7
+ private destroyRef;
8
+ readonly columns: import("@angular/core").WritableSignal<IColumnDef<T>[]>;
9
+ readonly columnOrder: import("@angular/core").WritableSignal<string[] | undefined>;
10
+ readonly onColumnOrderChange: import("@angular/core").WritableSignal<((order: string[]) => void) | undefined>;
11
+ readonly enabled: import("@angular/core").WritableSignal<boolean>;
12
+ readonly wrapperEl: import("@angular/core").WritableSignal<HTMLElement | null>;
13
+ readonly isDragging: import("@angular/core").WritableSignal<boolean>;
14
+ readonly dropIndicatorX: import("@angular/core").WritableSignal<number | null>;
15
+ private rafId;
16
+ private cleanupFn;
17
+ private latestDropTargetIndex;
18
+ constructor();
19
+ /**
20
+ * Call this from the header cell's mousedown handler.
21
+ * @param columnId - The column being dragged
22
+ * @param event - The native MouseEvent
23
+ */
24
+ handleHeaderMouseDown(columnId: string, event: MouseEvent): void;
25
+ /**
26
+ * Calculate drop target from mouse position and header cell rects.
27
+ * Same logic as React's useColumnReorder inline calculation.
28
+ */
29
+ private calculateDrop;
30
+ }
@@ -159,7 +159,6 @@ export declare class DataGridStateService<T> {
159
159
  private rafId;
160
160
  private lastMousePos;
161
161
  private autoScrollInterval;
162
- private windowCleanups;
163
162
  private resizeObserver;
164
163
  private readonly propsResolved;
165
164
  readonly cellSelection: import("@angular/core").Signal<boolean>;
@@ -105,10 +105,10 @@ export declare class OGridService<T> {
105
105
  private readonly internalData;
106
106
  private readonly internalLoading;
107
107
  private readonly internalPage;
108
- private readonly internalPageSize;
109
- private readonly internalSort;
108
+ private readonly internalPageSizeOverride;
109
+ private readonly internalSortOverride;
110
110
  private readonly internalFilters;
111
- private readonly internalVisibleColumns;
111
+ private readonly internalVisibleColumnsOverride;
112
112
  private readonly internalSelectedRows;
113
113
  private readonly columnWidthOverrides;
114
114
  private readonly pinnedOverrides;
@@ -121,9 +121,6 @@ export declare class OGridService<T> {
121
121
  private readonly sideBarActivePanel;
122
122
  private readonly serverFilterOptions;
123
123
  private readonly loadingFilterOptions;
124
- private sortInitialized;
125
- private visibleColumnsInitialized;
126
- private pageSizeInitialized;
127
124
  readonly columns: import("@angular/core").Signal<IColumnDef<T>[]>;
128
125
  readonly isServerSide: import("@angular/core").Signal<boolean>;
129
126
  readonly isClientSide: import("@angular/core").Signal<boolean>;
@@ -0,0 +1,54 @@
1
+ import type { IVisibleRange } from '@alaarab/ogrid-core';
2
+ export interface IVirtualScrollConfig {
3
+ /** Enable virtual scrolling. Default: true when provided. */
4
+ enabled?: boolean;
5
+ /** Row height in pixels (required for virtualization). */
6
+ rowHeight: number;
7
+ /** Number of rows to render outside the visible area. Default: 5. */
8
+ overscan?: number;
9
+ }
10
+ /**
11
+ * Manages virtual scrolling state using Angular signals.
12
+ * Port of React's useVirtualScroll hook.
13
+ *
14
+ * Uses core's pure-TS `computeVisibleRange` and `getScrollTopForRow` utilities.
15
+ * The UI layer (Angular Material / PrimeNG) provides the scrollable container
16
+ * and calls `onScroll()` / sets `containerHeight`.
17
+ */
18
+ export declare class VirtualScrollService {
19
+ private destroyRef;
20
+ readonly totalRows: import("@angular/core").WritableSignal<number>;
21
+ readonly config: import("@angular/core").WritableSignal<IVirtualScrollConfig>;
22
+ readonly containerHeight: import("@angular/core").WritableSignal<number>;
23
+ readonly scrollTop: import("@angular/core").WritableSignal<number>;
24
+ private containerEl;
25
+ readonly rowHeight: import("@angular/core").Signal<number>;
26
+ readonly overscan: import("@angular/core").Signal<number>;
27
+ readonly enabled: import("@angular/core").Signal<boolean>;
28
+ /** Whether virtual scrolling is actually active (enabled + enough rows). */
29
+ readonly isActive: import("@angular/core").Signal<boolean>;
30
+ /** The visible range of rows with spacer offsets. */
31
+ readonly visibleRange: import("@angular/core").Signal<IVisibleRange>;
32
+ /** Total scrollable height in pixels. */
33
+ readonly totalHeight: import("@angular/core").Signal<number>;
34
+ constructor();
35
+ /**
36
+ * Set the scrollable container element.
37
+ * Used for programmatic scrolling (scrollToRow).
38
+ */
39
+ setContainer(el: HTMLElement | null): void;
40
+ /**
41
+ * Call this from the container's scroll event handler.
42
+ */
43
+ onScroll(event: Event): void;
44
+ /**
45
+ * Scroll to a specific row index.
46
+ * @param index - The row index to scroll to.
47
+ * @param align - Where to position the row: 'start' (top), 'center', or 'end' (bottom). Default: 'start'.
48
+ */
49
+ scrollToRow(index: number, align?: 'start' | 'center' | 'end'): void;
50
+ /**
51
+ * Update the virtual scroll configuration.
52
+ */
53
+ updateConfig(updates: Partial<IVirtualScrollConfig>): void;
54
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * View model helpers for Angular DataGridTable. Core owns the logic; UI packages only render.
3
+ * Ported from React's dataGridViewModel.ts to eliminate duplication in Angular Material and PrimeNG packages.
4
+ */
5
+ import type { ColumnFilterType, IDateFilterValue } from '../types/columnTypes';
6
+ import type { IColumnDef } from '../types/columnTypes';
7
+ import type { RowId, UserLike, IFilters, FilterValue } from '../types/dataGridTypes';
8
+ export interface HeaderFilterConfigInput {
9
+ sortBy?: string;
10
+ sortDirection: 'asc' | 'desc';
11
+ onColumnSort: (columnKey: string) => void;
12
+ filters: IFilters;
13
+ onFilterChange: (key: string, value: FilterValue | undefined) => void;
14
+ filterOptions: Record<string, string[]>;
15
+ loadingFilterOptions: Record<string, boolean>;
16
+ peopleSearch?: (query: string) => Promise<UserLike[]>;
17
+ }
18
+ /** Props to pass to ColumnHeaderFilter. */
19
+ export interface HeaderFilterConfig {
20
+ columnKey: string;
21
+ columnName: string;
22
+ filterType: ColumnFilterType;
23
+ isSorted?: boolean;
24
+ isSortedDescending?: boolean;
25
+ onSort?: () => void;
26
+ selectedValues?: string[];
27
+ onFilterChange?: (values: string[]) => void;
28
+ options?: string[];
29
+ isLoadingOptions?: boolean;
30
+ textValue?: string;
31
+ onTextChange?: (value: string) => void;
32
+ selectedUser?: UserLike;
33
+ onUserChange?: (user: UserLike | undefined) => void;
34
+ peopleSearch?: (query: string) => Promise<UserLike[]>;
35
+ dateValue?: IDateFilterValue;
36
+ onDateChange?: (value: IDateFilterValue | undefined) => void;
37
+ }
38
+ /**
39
+ * Returns ColumnHeaderFilter props from column def and grid filter/sort state.
40
+ * Use in Angular Material and PrimeNG DataGridTableComponent instead of inline logic.
41
+ */
42
+ export declare function getHeaderFilterConfig<T>(col: IColumnDef<T>, input: HeaderFilterConfigInput): HeaderFilterConfig;
43
+ export type CellRenderMode = 'editing-inline' | 'editing-popover' | 'display';
44
+ export interface CellRenderDescriptorInput<T> {
45
+ editingCell: {
46
+ rowId: RowId;
47
+ columnId: string;
48
+ } | null;
49
+ activeCell: {
50
+ rowIndex: number;
51
+ columnIndex: number;
52
+ } | null;
53
+ selectionRange: {
54
+ startRow: number;
55
+ startCol: number;
56
+ endRow: number;
57
+ endCol: number;
58
+ } | null;
59
+ cutRange: {
60
+ startRow: number;
61
+ startCol: number;
62
+ endRow: number;
63
+ endCol: number;
64
+ } | null;
65
+ copyRange: {
66
+ startRow: number;
67
+ startCol: number;
68
+ endRow: number;
69
+ endCol: number;
70
+ } | null;
71
+ colOffset: number;
72
+ getRowId: (item: T) => RowId;
73
+ editable?: boolean;
74
+ onCellValueChanged?: unknown;
75
+ /** True while user is drag-selecting cells — hides fill handle during drag. */
76
+ isDragging?: boolean;
77
+ }
78
+ export interface CellRenderDescriptor {
79
+ mode: CellRenderMode;
80
+ editorType?: 'text' | 'select' | 'checkbox' | 'richSelect' | 'date';
81
+ value?: unknown;
82
+ isActive: boolean;
83
+ isInRange: boolean;
84
+ isInCutRange: boolean;
85
+ isInCopyRange: boolean;
86
+ isSelectionEndCell: boolean;
87
+ canEditAny: boolean;
88
+ globalColIndex: number;
89
+ rowId: RowId;
90
+ rowIndex: number;
91
+ /** Raw value for display (when mode === 'display'). */
92
+ displayValue?: unknown;
93
+ }
94
+ /**
95
+ * Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
96
+ * and to apply isActive, isInRange, etc. without duplicating the boolean logic.
97
+ */
98
+ export declare function getCellRenderDescriptor<T>(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number, input: CellRenderDescriptorInput<T>): CellRenderDescriptor;
99
+ /**
100
+ * Resolves display content for a cell in display mode.
101
+ * Handles the renderCell → valueFormatter → String() fallback chain.
102
+ */
103
+ export declare function resolveCellDisplayContent<T>(col: IColumnDef<T>, item: T, displayValue: unknown): string;
104
+ /**
105
+ * Resolves the cellStyle from a column def, handling both function and static values.
106
+ */
107
+ export declare function resolveCellStyle<T>(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Debounce utilities for Angular using signals.
3
+ * Provides functional parity with React's useDebounce and useDebouncedCallback.
4
+ */
5
+ import { type Signal } from '@angular/core';
6
+ /**
7
+ * Creates a debounced signal that updates after the specified delay when the source value changes.
8
+ *
9
+ * @param source - The signal to debounce
10
+ * @param delayMs - Delay in milliseconds
11
+ * @returns A signal containing the debounced value
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const searchQuery = signal('');
16
+ * const debouncedQuery = createDebouncedSignal(searchQuery, 300);
17
+ *
18
+ * effect(() => {
19
+ * console.log('Debounced search:', debouncedQuery());
20
+ * });
21
+ * ```
22
+ */
23
+ export declare function createDebouncedSignal<T>(source: Signal<T>, delayMs: number): Signal<T>;
24
+ /**
25
+ * Creates a debounced function that delays invoking the provided function
26
+ * until after `delayMs` milliseconds have elapsed since the last time it was invoked.
27
+ *
28
+ * @param fn - The function to debounce
29
+ * @param delayMs - Delay in milliseconds
30
+ * @returns A debounced version of the function
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const saveData = (value: string) => {
35
+ * console.log('Saving:', value);
36
+ * };
37
+ *
38
+ * const debouncedSave = createDebouncedCallback(saveData, 500);
39
+ *
40
+ * // Multiple rapid calls will only trigger once after 500ms
41
+ * debouncedSave('hello');
42
+ * debouncedSave('world'); // Only this will execute after 500ms
43
+ * ```
44
+ */
45
+ export declare function createDebouncedCallback<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): T;
46
+ /**
47
+ * Simple debounce function (non-Angular-specific, can be used anywhere).
48
+ * Returns a debounced version of the provided function.
49
+ *
50
+ * @param fn - The function to debounce
51
+ * @param delayMs - Delay in milliseconds
52
+ * @returns A debounced version of the function with a `cancel()` method
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const handleResize = debounce(() => {
57
+ * console.log('Window resized');
58
+ * }, 200);
59
+ *
60
+ * window.addEventListener('resize', handleResize);
61
+ *
62
+ * // Later, cancel pending execution
63
+ * handleResize.cancel();
64
+ * ```
65
+ */
66
+ export declare function debounce<T extends (...args: unknown[]) => void>(fn: T, delayMs: number): T & {
67
+ cancel: () => void;
68
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared utilities for Angular DataGridTable view layer.
3
+ */
4
+ export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './dataGridViewModel';
5
+ export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, } from './dataGridViewModel';
6
+ export { createDebouncedSignal, createDebouncedCallback, debounce, } from './debounce';
7
+ export { createLatestRef, createLatestCallback, } from './latestRef';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Latest ref utility for Angular using signals.
3
+ * Provides functional parity with React's useLatestRef.
4
+ */
5
+ import { type Signal } from '@angular/core';
6
+ /**
7
+ * Creates a stable wrapper function that always calls the latest version of the provided function.
8
+ * Useful for event handlers and callbacks where you want a stable reference but need to call
9
+ * the latest implementation.
10
+ *
11
+ * @param fn - Signal containing the function to wrap
12
+ * @returns A stable function that always invokes the latest version
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * class MyService {
17
+ * readonly onSave = signal<(value: string) => void>((val) => console.log('Default:', val));
18
+ * readonly stableOnSave = createLatestCallback(this.onSave);
19
+ *
20
+ * constructor() {
21
+ * // Setup event listener with stable reference
22
+ * effect((onCleanup) => {
23
+ * // stableOnSave never changes, so this effect only runs once
24
+ * const callback = () => this.stableOnSave('data');
25
+ * window.addEventListener('click', callback);
26
+ * onCleanup(() => window.removeEventListener('click', callback));
27
+ * });
28
+ * }
29
+ *
30
+ * updateHandler(newFn: (value: string) => void) {
31
+ * // Even though we change the function, the callback reference stays stable
32
+ * this.onSave.set(newFn);
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ export declare function createLatestCallback<T extends (...args: unknown[]) => unknown>(fn: Signal<T>): T;
38
+ /**
39
+ * Alias for createLatestCallback for consistency with React/Vue naming.
40
+ * @deprecated Use createLatestCallback instead
41
+ */
42
+ export declare const createLatestRef: typeof createLatestCallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -22,7 +22,7 @@
22
22
  "files": ["dist", "README.md", "LICENSE"],
23
23
  "engines": { "node": ">=18" },
24
24
  "dependencies": {
25
- "@alaarab/ogrid-core": "2.0.2"
25
+ "@alaarab/ogrid-core": "2.0.3"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@angular/core": "^21.0.0",