gp-grid-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +362 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# gp-grid-core
|
|
2
|
+
|
|
3
|
+
A framework-agnostic TypeScript library for building high-performance data grids with virtual scrolling, supporting 150,000+ rows with ease.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
**gp-grid-core** is built on three core principles:
|
|
8
|
+
|
|
9
|
+
### 1. Slot-Based Virtual Scrolling
|
|
10
|
+
|
|
11
|
+
Instead of rendering all rows, the grid maintains a pool of reusable "slots" (DOM containers) that are recycled as users scroll. This approach:
|
|
12
|
+
|
|
13
|
+
- Renders only visible rows plus a small overscan buffer
|
|
14
|
+
- Recycles DOM elements instead of creating/destroying them
|
|
15
|
+
- Maintains consistent performance regardless of dataset size
|
|
16
|
+
|
|
17
|
+
### 2. Instruction-Based Architecture
|
|
18
|
+
|
|
19
|
+
The core emits declarative **instructions** (commands) that describe what the UI should do, rather than manipulating the DOM directly. This pattern:
|
|
20
|
+
|
|
21
|
+
- Keeps the core framework-agnostic (works with React, Vue, Svelte, vanilla JS)
|
|
22
|
+
- Enables batched updates for optimal rendering performance
|
|
23
|
+
- Provides a clean separation between logic and presentation
|
|
24
|
+
|
|
25
|
+
### 3. DataSource Abstraction
|
|
26
|
+
|
|
27
|
+
Data fetching is abstracted through a `DataSource` interface, supporting both:
|
|
28
|
+
|
|
29
|
+
- **Client-side**: All data loaded in memory, with local sorting/filtering
|
|
30
|
+
- **Server-side**: Data fetched on-demand from an API with server-side operations
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
npm/pnpm/yarn
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add gp-grid-core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Architecture Overview
|
|
41
|
+
|
|
42
|
+
### GridCore
|
|
43
|
+
|
|
44
|
+
The main orchestrator class that manages:
|
|
45
|
+
|
|
46
|
+
- Viewport tracking and scroll synchronization
|
|
47
|
+
- Slot pool lifecycle (create, assign, move, destroy)
|
|
48
|
+
- Data fetching and caching
|
|
49
|
+
- Sort and filter state
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { GridCore, createClientDataSource } from "gp-grid-core";
|
|
53
|
+
|
|
54
|
+
const dataSource = createClientDataSource(myData);
|
|
55
|
+
|
|
56
|
+
const grid = new GridCore({
|
|
57
|
+
columns: [
|
|
58
|
+
{ field: "name", cellDataType: "text", width: 150 },
|
|
59
|
+
{ field: "age", cellDataType: "number", width: 80 },
|
|
60
|
+
],
|
|
61
|
+
dataSource,
|
|
62
|
+
rowHeight: 36,
|
|
63
|
+
headerHeight: 40,
|
|
64
|
+
overscan: 3,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Subscribe to instructions
|
|
68
|
+
grid.onBatchInstruction((instructions) => {
|
|
69
|
+
// Handle UI updates based on instructions
|
|
70
|
+
instructions.forEach((instruction) => {
|
|
71
|
+
switch (instruction.type) {
|
|
72
|
+
case "CREATE_SLOT":
|
|
73
|
+
// Create a new row container
|
|
74
|
+
break;
|
|
75
|
+
case "ASSIGN_SLOT":
|
|
76
|
+
// Assign row data to a slot
|
|
77
|
+
break;
|
|
78
|
+
case "MOVE_SLOT":
|
|
79
|
+
// Position slot via translateY
|
|
80
|
+
break;
|
|
81
|
+
// ... handle other instructions
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Initialize and start
|
|
87
|
+
await grid.initialize();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Managers
|
|
91
|
+
|
|
92
|
+
GridCore includes specialized managers for complex behaviors:
|
|
93
|
+
|
|
94
|
+
- **SelectionManager**: Handles cell selection, range selection, keyboard navigation
|
|
95
|
+
- **FillManager**: Implements Excel-like fill handle drag operations
|
|
96
|
+
|
|
97
|
+
### Instruction Types
|
|
98
|
+
|
|
99
|
+
The core emits these instruction types:
|
|
100
|
+
|
|
101
|
+
| Instruction | Description |
|
|
102
|
+
|-------------|-------------|
|
|
103
|
+
| `CREATE_SLOT` | Create a new slot in the DOM pool |
|
|
104
|
+
| `DESTROY_SLOT` | Remove a slot from the pool |
|
|
105
|
+
| `ASSIGN_SLOT` | Assign row data to a slot |
|
|
106
|
+
| `MOVE_SLOT` | Update slot position (translateY) |
|
|
107
|
+
| `SET_ACTIVE_CELL` | Update active cell highlight |
|
|
108
|
+
| `SET_SELECTION_RANGE` | Update selection range |
|
|
109
|
+
| `START_EDIT` / `STOP_EDIT` | Toggle edit mode |
|
|
110
|
+
| `COMMIT_EDIT` | Commit edited value |
|
|
111
|
+
| `UPDATE_HEADER` | Update header with sort state |
|
|
112
|
+
| `DATA_LOADING` / `DATA_LOADED` / `DATA_ERROR` | Data fetch lifecycle |
|
|
113
|
+
|
|
114
|
+
## Data Sources
|
|
115
|
+
|
|
116
|
+
### Client-Side Data Source
|
|
117
|
+
|
|
118
|
+
For datasets that can be loaded entirely in memory. Sorting and filtering are performed client-side.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { createClientDataSource } from "gp-grid-core";
|
|
122
|
+
|
|
123
|
+
interface Person {
|
|
124
|
+
id: number;
|
|
125
|
+
name: string;
|
|
126
|
+
age: number;
|
|
127
|
+
email: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data: Person[] = [
|
|
131
|
+
{ id: 1, name: "Alice", age: 30, email: "alice@example.com" },
|
|
132
|
+
{ id: 2, name: "Bob", age: 25, email: "bob@example.com" },
|
|
133
|
+
// ... more rows
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const dataSource = createClientDataSource(data);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**With custom field accessor** (for nested properties):
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const dataSource = createClientDataSource(data, {
|
|
143
|
+
getFieldValue: (row, field) => {
|
|
144
|
+
// Custom logic for accessing nested fields
|
|
145
|
+
if (field === "address.city") {
|
|
146
|
+
return row.address?.city;
|
|
147
|
+
}
|
|
148
|
+
return row[field];
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Server-Side Data Source
|
|
154
|
+
|
|
155
|
+
For large datasets that require server-side pagination, sorting, and filtering.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { createServerDataSource, DataSourceRequest, DataSourceResponse } from "gp-grid-core";
|
|
159
|
+
|
|
160
|
+
interface Person {
|
|
161
|
+
id: number;
|
|
162
|
+
name: string;
|
|
163
|
+
age: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const dataSource = createServerDataSource<Person>(async (request: DataSourceRequest) => {
|
|
167
|
+
// Build query parameters from request
|
|
168
|
+
const params = new URLSearchParams({
|
|
169
|
+
page: String(request.pagination.pageIndex),
|
|
170
|
+
pageSize: String(request.pagination.pageSize),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Add sort parameters
|
|
174
|
+
if (request.sort && request.sort.length > 0) {
|
|
175
|
+
params.set("sortBy", request.sort.map(s => `${s.colId}:${s.direction}`).join(","));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Add filter parameters
|
|
179
|
+
if (request.filter) {
|
|
180
|
+
Object.entries(request.filter).forEach(([field, value]) => {
|
|
181
|
+
params.set(`filter_${field}`, value);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fetch from your API
|
|
186
|
+
const response = await fetch(`/api/people?${params}`);
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
rows: data.items,
|
|
191
|
+
totalRows: data.totalCount,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### DataSource Interface
|
|
197
|
+
|
|
198
|
+
Both data source types implement this interface:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface DataSource<TData = Row> {
|
|
202
|
+
fetch(request: DataSourceRequest): Promise<DataSourceResponse<TData>>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface DataSourceRequest {
|
|
206
|
+
pagination: {
|
|
207
|
+
pageIndex: number;
|
|
208
|
+
pageSize: number;
|
|
209
|
+
};
|
|
210
|
+
sort?: SortModel[];
|
|
211
|
+
filter?: FilterModel;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface DataSourceResponse<TData> {
|
|
215
|
+
rows: TData[];
|
|
216
|
+
totalRows: number;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Types Reference
|
|
221
|
+
|
|
222
|
+
### ColumnDefinition
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
interface ColumnDefinition {
|
|
226
|
+
field: string; // Property path in row data
|
|
227
|
+
colId?: string; // Unique column ID (defaults to field)
|
|
228
|
+
cellDataType: CellDataType; // "text" | "number" | "boolean" | "date" | "object"
|
|
229
|
+
width: number; // Column width in pixels
|
|
230
|
+
headerName?: string; // Display name (defaults to field)
|
|
231
|
+
editable?: boolean; // Enable cell editing
|
|
232
|
+
cellRenderer?: string; // Custom renderer key
|
|
233
|
+
editRenderer?: string; // Custom edit renderer key
|
|
234
|
+
headerRenderer?: string; // Custom header renderer key
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Renderer Params
|
|
239
|
+
|
|
240
|
+
When building framework adapters, these params are passed to custom renderers:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
interface CellRendererParams {
|
|
244
|
+
value: CellValue;
|
|
245
|
+
rowData: Row;
|
|
246
|
+
column: ColumnDefinition;
|
|
247
|
+
rowIndex: number;
|
|
248
|
+
colIndex: number;
|
|
249
|
+
isActive: boolean;
|
|
250
|
+
isSelected: boolean;
|
|
251
|
+
isEditing: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface EditRendererParams extends CellRendererParams {
|
|
255
|
+
initialValue: CellValue;
|
|
256
|
+
onValueChange: (newValue: CellValue) => void;
|
|
257
|
+
onCommit: () => void;
|
|
258
|
+
onCancel: () => void;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface HeaderRendererParams {
|
|
262
|
+
column: ColumnDefinition;
|
|
263
|
+
colIndex: number;
|
|
264
|
+
sortDirection?: SortDirection;
|
|
265
|
+
sortIndex?: number;
|
|
266
|
+
onSort: (direction: SortDirection | null, addToExisting: boolean) => void;
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Creating a Framework Adapter
|
|
271
|
+
|
|
272
|
+
To integrate gp-grid-core with any UI framework:
|
|
273
|
+
|
|
274
|
+
1. **Subscribe to instructions** using `onBatchInstruction()`
|
|
275
|
+
2. **Maintain UI state** by processing instructions
|
|
276
|
+
3. **Render slots** based on the slot pool state
|
|
277
|
+
4. **Forward user interactions** back to GridCore
|
|
278
|
+
|
|
279
|
+
### Example: Minimal Adapter Pattern
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { GridCore, GridInstruction } from "gp-grid-core";
|
|
283
|
+
|
|
284
|
+
class MyGridAdapter {
|
|
285
|
+
private core: GridCore;
|
|
286
|
+
private slots: Map<string, SlotUIElement> = new Map();
|
|
287
|
+
|
|
288
|
+
constructor(options: GridCoreOptions) {
|
|
289
|
+
this.core = new GridCore(options);
|
|
290
|
+
|
|
291
|
+
// Process instructions to update UI
|
|
292
|
+
this.core.onBatchInstruction((instructions) => {
|
|
293
|
+
this.processInstructions(instructions);
|
|
294
|
+
this.render();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private processInstructions(instructions: GridInstruction[]) {
|
|
299
|
+
for (const instr of instructions) {
|
|
300
|
+
switch (instr.type) {
|
|
301
|
+
case "CREATE_SLOT":
|
|
302
|
+
this.slots.set(instr.slotId, this.createSlotElement());
|
|
303
|
+
break;
|
|
304
|
+
case "DESTROY_SLOT":
|
|
305
|
+
this.slots.delete(instr.slotId);
|
|
306
|
+
break;
|
|
307
|
+
case "ASSIGN_SLOT":
|
|
308
|
+
const slot = this.slots.get(instr.slotId);
|
|
309
|
+
if (slot) {
|
|
310
|
+
slot.rowIndex = instr.rowIndex;
|
|
311
|
+
slot.rowData = instr.rowData;
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
case "MOVE_SLOT":
|
|
315
|
+
const moveSlot = this.slots.get(instr.slotId);
|
|
316
|
+
if (moveSlot) {
|
|
317
|
+
moveSlot.translateY = instr.translateY;
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Handle scroll events
|
|
325
|
+
onScroll(scrollTop: number, scrollLeft: number, width: number, height: number) {
|
|
326
|
+
this.core.setViewport(scrollTop, scrollLeft, width, height);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Handle cell click
|
|
330
|
+
onCellClick(row: number, col: number, modifiers: { shift: boolean; ctrl: boolean }) {
|
|
331
|
+
this.core.selection.startSelection({ row, col }, modifiers);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async initialize() {
|
|
335
|
+
await this.core.initialize();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## API Reference
|
|
341
|
+
|
|
342
|
+
### GridCore Methods
|
|
343
|
+
|
|
344
|
+
| Method | Description |
|
|
345
|
+
|--------|-------------|
|
|
346
|
+
| `initialize()` | Initialize grid and load initial data |
|
|
347
|
+
| `setViewport(scrollTop, scrollLeft, width, height)` | Update viewport on scroll/resize |
|
|
348
|
+
| `setSort(colId, direction, addToExisting)` | Set column sort |
|
|
349
|
+
| `setFilter(colId, value)` | Set column filter |
|
|
350
|
+
| `startEdit(row, col)` | Start editing a cell |
|
|
351
|
+
| `commitEdit()` | Commit current edit |
|
|
352
|
+
| `cancelEdit()` | Cancel current edit |
|
|
353
|
+
| `refresh()` | Refetch data from source |
|
|
354
|
+
| `getRowCount()` | Get total row count |
|
|
355
|
+
| `getRowData(rowIndex)` | Get data for a specific row |
|
|
356
|
+
|
|
357
|
+
### GridCore Properties
|
|
358
|
+
|
|
359
|
+
| Property | Description |
|
|
360
|
+
|----------|-------------|
|
|
361
|
+
| `selection` | SelectionManager instance |
|
|
362
|
+
| `fill` | FillManager instance |
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "gp-grid-core",
|
|
3
3
|
"description": "A typescript library that enables grid creation",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"author": "Giovanni Patruno (giovanni.patruno@outlook.com)",
|