@gp-grid/vue 0.7.2

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 ADDED
@@ -0,0 +1,624 @@
1
+ # gp-grid-vue 🏁 🏎️
2
+
3
+ <div align="center">
4
+ <a href="https://www.gp-grid.io">
5
+ <picture>
6
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/GioPat/gp-grid-docs/refs/heads/master/public/logo-light.svg"/>
7
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/GioPat/gp-grid-docs/refs/heads/master/public/logo-dark.svg"/>
8
+ <img width="50%" alt="AG Grid Logo" src="https://raw.githubusercontent.com/GioPat/gp-grid-docs/refs/heads/master/public/logo-dark.svg"/>
9
+ </picture>
10
+ </a>
11
+ <div align="center">
12
+ Logo by <a href="https://github.com/camillo18tre">camillo18tre ❤️</a>
13
+ <h4><a href="https://www.gp-grid.io/">🎮 Demo</a> • <a href="https://www.gp-grid.io/docs/vue">📖 Documentation</a>
14
+ </div>
15
+ </div>
16
+
17
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GioPat/gp-grid)
18
+
19
+ A high-performance, feature lean Vue 3 data grid component built to manage grids with huge amount (millions) of rows. It's based on its core dependency: `gp-grid-core`, featuring virtual scrolling, cell selection, sorting, filtering, editing, and Excel-like fill handle.
20
+
21
+ ## Table of Contents
22
+
23
+ - [Features](#features)
24
+ - [Installation](#installation)
25
+ - [Quick Start](#quick-start)
26
+ - [Examples](#examples)
27
+ - [API Reference](#api-reference)
28
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
29
+ - [Styling](#styling)
30
+ - [Donations](#donations)
31
+
32
+ ## Features
33
+
34
+ - **Virtual Scrolling**: Efficiently handles 150,000+ rows through slot-based recycling
35
+ - **Cell Selection**: Single cell, range selection, Shift+click extend, Ctrl+click toggle
36
+ - **Multi-Column Sorting**: Click to sort, Shift+click for multi-column sort
37
+ - **Column Filtering**: Built-in filter row with debounced input
38
+ - **Cell Editing**: Double-click or press Enter to edit, with custom editor support
39
+ - **Fill Handle**: Excel-like drag-to-fill for editable cells
40
+ - **Keyboard Navigation**: Arrow keys, Tab, Enter, Escape, Ctrl+A, Ctrl+C
41
+ - **Custom Renderers**: Registry-based cell, edit, and header renderers
42
+ - **Dark Mode**: Built-in dark theme support
43
+ - **TypeScript**: Full type safety with exported types
44
+
45
+ ## Installation
46
+
47
+ Use `npm`, `yarn` or `pnpm`
48
+
49
+ ```bash
50
+ pnpm add gp-grid-vue
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```vue
56
+ <script setup lang="ts">
57
+ import { GpGrid, type ColumnDefinition } from "gp-grid-vue";
58
+
59
+ interface Person {
60
+ id: number;
61
+ name: string;
62
+ age: number;
63
+ email: string;
64
+ }
65
+
66
+ const columns: ColumnDefinition[] = [
67
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
68
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
69
+ { field: "age", cellDataType: "number", width: 80, headerName: "Age" },
70
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
71
+ ];
72
+
73
+ const data: Person[] = [
74
+ { id: 1, name: "Alice", age: 30, email: "alice@example.com" },
75
+ { id: 2, name: "Bob", age: 25, email: "bob@example.com" },
76
+ { id: 3, name: "Charlie", age: 35, email: "charlie@example.com" },
77
+ ];
78
+ </script>
79
+
80
+ <template>
81
+ <div style="width: 800px; height: 400px">
82
+ <GpGrid :columns="columns" :row-data="data" :row-height="36" />
83
+ </div>
84
+ </template>
85
+ ```
86
+
87
+ ## Examples
88
+
89
+ ### Client-Side Data Source with Sorting and Filtering
90
+
91
+ For larger datasets with client-side sort/filter operations:
92
+
93
+ ```vue
94
+ <script setup lang="ts">
95
+ import { computed } from "vue";
96
+ import {
97
+ GpGrid,
98
+ createClientDataSource,
99
+ type ColumnDefinition,
100
+ } from "gp-grid-vue";
101
+
102
+ interface Product {
103
+ id: number;
104
+ name: string;
105
+ price: number;
106
+ category: string;
107
+ }
108
+
109
+ const columns: ColumnDefinition[] = [
110
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
111
+ { field: "name", cellDataType: "text", width: 200, headerName: "Product" },
112
+ { field: "price", cellDataType: "number", width: 100, headerName: "Price" },
113
+ {
114
+ field: "category",
115
+ cellDataType: "text",
116
+ width: 150,
117
+ headerName: "Category",
118
+ },
119
+ ];
120
+
121
+ const products = computed<Product[]>(() =>
122
+ Array.from({ length: 10000 }, (_, i) => ({
123
+ id: i + 1,
124
+ name: `Product ${i + 1}`,
125
+ price: Math.round(Math.random() * 1000) / 10,
126
+ category: ["Electronics", "Clothing", "Food", "Books"][i % 4],
127
+ })),
128
+ );
129
+
130
+ const dataSource = computed(() => createClientDataSource(products.value));
131
+ </script>
132
+
133
+ <template>
134
+ <div style="width: 100%; height: 500px">
135
+ <GpGrid
136
+ :columns="columns"
137
+ :data-source="dataSource"
138
+ :row-height="36"
139
+ :header-height="40"
140
+ />
141
+ </div>
142
+ </template>
143
+ ```
144
+
145
+ ### Server-Side Data Source
146
+
147
+ For datasets too large to load entirely in memory, use a server-side data source:
148
+
149
+ ```vue
150
+ <script setup lang="ts">
151
+ import { computed } from "vue";
152
+ import {
153
+ GpGrid,
154
+ createServerDataSource,
155
+ type ColumnDefinition,
156
+ type DataSourceRequest,
157
+ type DataSourceResponse,
158
+ } from "gp-grid-vue";
159
+
160
+ interface User {
161
+ id: number;
162
+ name: string;
163
+ email: string;
164
+ role: string;
165
+ createdAt: string;
166
+ }
167
+
168
+ const columns: ColumnDefinition[] = [
169
+ { field: "id", cellDataType: "number", width: 80, headerName: "ID" },
170
+ { field: "name", cellDataType: "text", width: 150, headerName: "Name" },
171
+ { field: "email", cellDataType: "text", width: 250, headerName: "Email" },
172
+ { field: "role", cellDataType: "text", width: 120, headerName: "Role" },
173
+ {
174
+ field: "createdAt",
175
+ cellDataType: "dateString",
176
+ width: 150,
177
+ headerName: "Created",
178
+ },
179
+ ];
180
+
181
+ // API fetch function that handles pagination, sorting, and filtering
182
+ async function fetchUsers(
183
+ request: DataSourceRequest,
184
+ ): Promise<DataSourceResponse<User>> {
185
+ const { pagination, sort, filter } = request;
186
+
187
+ // Build query parameters
188
+ const params = new URLSearchParams({
189
+ page: String(pagination.pageIndex),
190
+ limit: String(pagination.pageSize),
191
+ });
192
+
193
+ // Add sorting parameters
194
+ if (sort && sort.length > 0) {
195
+ // Format: sortBy=name:asc,email:desc
196
+ const sortString = sort.map((s) => `${s.colId}:${s.direction}`).join(",");
197
+ params.set("sortBy", sortString);
198
+ }
199
+
200
+ // Add filter parameters
201
+ if (filter) {
202
+ Object.entries(filter).forEach(([field, value]) => {
203
+ if (value) {
204
+ params.set(`filter[${field}]`, String(value));
205
+ }
206
+ });
207
+ }
208
+
209
+ // Make API request
210
+ const response = await fetch(`https://api.example.com/users?${params}`);
211
+
212
+ if (!response.ok) {
213
+ throw new Error(`API error: ${response.status}`);
214
+ }
215
+
216
+ const data = await response.json();
217
+
218
+ // Return in DataSourceResponse format
219
+ return {
220
+ rows: data.users, // Array of User objects
221
+ totalRows: data.total, // Total count for virtual scrolling
222
+ };
223
+ }
224
+
225
+ // Create server data source - computed to prevent recreation
226
+ const dataSource = computed(() => createServerDataSource<User>(fetchUsers));
227
+ </script>
228
+
229
+ <template>
230
+ <div style="width: 100%; height: 600px">
231
+ <GpGrid
232
+ :columns="columns"
233
+ :data-source="dataSource"
234
+ :row-height="36"
235
+ :header-height="40"
236
+ :dark-mode="true"
237
+ />
238
+ </div>
239
+ </template>
240
+ ```
241
+
242
+ ### Custom Cell Renderers
243
+
244
+ Use the registry pattern to define reusable renderers:
245
+
246
+ ```vue
247
+ <script setup lang="ts">
248
+ import { h } from "vue";
249
+ import {
250
+ GpGrid,
251
+ type ColumnDefinition,
252
+ type CellRendererParams,
253
+ type VueCellRenderer,
254
+ } from "gp-grid-vue";
255
+
256
+ interface Order {
257
+ id: number;
258
+ customer: string;
259
+ total: number;
260
+ status: "pending" | "shipped" | "delivered" | "cancelled";
261
+ }
262
+
263
+ // Define reusable renderers
264
+ const cellRenderers: Record<string, VueCellRenderer> = {
265
+ // Currency formatter
266
+ currency: (params: CellRendererParams) => {
267
+ const value = params.value as number;
268
+ return h(
269
+ "span",
270
+ { style: { color: "#047857", fontWeight: 600 } },
271
+ `$${value.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
272
+ );
273
+ },
274
+
275
+ // Status badge
276
+ statusBadge: (params: CellRendererParams) => {
277
+ const status = params.value as Order["status"];
278
+ const colors: Record<string, { bg: string; text: string }> = {
279
+ pending: { bg: "#fef3c7", text: "#92400e" },
280
+ shipped: { bg: "#dbeafe", text: "#1e40af" },
281
+ delivered: { bg: "#dcfce7", text: "#166534" },
282
+ cancelled: { bg: "#fee2e2", text: "#991b1b" },
283
+ };
284
+ const color = colors[status] ?? { bg: "#f3f4f6", text: "#374151" };
285
+
286
+ return h(
287
+ "span",
288
+ {
289
+ style: {
290
+ backgroundColor: color.bg,
291
+ color: color.text,
292
+ padding: "2px 8px",
293
+ borderRadius: "12px",
294
+ fontSize: "12px",
295
+ fontWeight: 600,
296
+ },
297
+ },
298
+ status.toUpperCase(),
299
+ );
300
+ },
301
+
302
+ // Bold text
303
+ bold: (params: CellRendererParams) => h("strong", String(params.value ?? "")),
304
+ };
305
+
306
+ const columns: ColumnDefinition[] = [
307
+ {
308
+ field: "id",
309
+ cellDataType: "number",
310
+ width: 80,
311
+ headerName: "ID",
312
+ cellRenderer: "bold",
313
+ },
314
+ {
315
+ field: "customer",
316
+ cellDataType: "text",
317
+ width: 200,
318
+ headerName: "Customer",
319
+ },
320
+ {
321
+ field: "total",
322
+ cellDataType: "number",
323
+ width: 120,
324
+ headerName: "Total",
325
+ cellRenderer: "currency",
326
+ },
327
+ {
328
+ field: "status",
329
+ cellDataType: "text",
330
+ width: 120,
331
+ headerName: "Status",
332
+ cellRenderer: "statusBadge",
333
+ },
334
+ ];
335
+
336
+ const orders: Order[] = [
337
+ { id: 1, customer: "Acme Corp", total: 1250.0, status: "shipped" },
338
+ { id: 2, customer: "Globex Inc", total: 890.5, status: "pending" },
339
+ { id: 3, customer: "Initech", total: 2100.75, status: "delivered" },
340
+ ];
341
+ </script>
342
+
343
+ <template>
344
+ <div style="width: 100%; height: 400px">
345
+ <GpGrid
346
+ :columns="columns"
347
+ :row-data="orders"
348
+ :row-height="40"
349
+ :cell-renderers="cellRenderers"
350
+ />
351
+ </div>
352
+ </template>
353
+ ```
354
+
355
+ ### Editable Cells with Custom Editors
356
+
357
+ ```vue
358
+ <script setup lang="ts">
359
+ import { h, ref as vueRef } from "vue";
360
+ import {
361
+ GpGrid,
362
+ createClientDataSource,
363
+ type ColumnDefinition,
364
+ type EditRendererParams,
365
+ type VueEditRenderer,
366
+ } from "gp-grid-vue";
367
+
368
+ interface Task {
369
+ id: number;
370
+ title: string;
371
+ priority: "low" | "medium" | "high";
372
+ completed: boolean;
373
+ }
374
+
375
+ // Custom select editor for priority field
376
+ const editRenderers: Record<string, VueEditRenderer> = {
377
+ prioritySelect: (params: EditRendererParams) => {
378
+ return h(
379
+ "select",
380
+ {
381
+ autofocus: true,
382
+ value: params.initialValue as string,
383
+ onChange: (e: Event) => {
384
+ const target = e.target as HTMLSelectElement;
385
+ params.onValueChange(target.value);
386
+ },
387
+ onBlur: () => params.onCommit(),
388
+ onKeydown: (e: KeyboardEvent) => {
389
+ if (e.key === "Enter") params.onCommit();
390
+ if (e.key === "Escape") params.onCancel();
391
+ },
392
+ style: {
393
+ width: "100%",
394
+ height: "100%",
395
+ border: "none",
396
+ outline: "none",
397
+ padding: "0 8px",
398
+ },
399
+ },
400
+ [
401
+ h("option", { value: "low" }, "Low"),
402
+ h("option", { value: "medium" }, "Medium"),
403
+ h("option", { value: "high" }, "High"),
404
+ ],
405
+ );
406
+ },
407
+
408
+ checkbox: (params: EditRendererParams) =>
409
+ h("input", {
410
+ type: "checkbox",
411
+ autofocus: true,
412
+ checked: params.initialValue as boolean,
413
+ onChange: (e: Event) => {
414
+ const target = e.target as HTMLInputElement;
415
+ params.onValueChange(target.checked);
416
+ params.onCommit();
417
+ },
418
+ style: { width: "20px", height: "20px" },
419
+ }),
420
+ };
421
+
422
+ const columns: ColumnDefinition[] = [
423
+ { field: "id", cellDataType: "number", width: 60, headerName: "ID" },
424
+ {
425
+ field: "title",
426
+ cellDataType: "text",
427
+ width: 300,
428
+ headerName: "Title",
429
+ editable: true, // Uses default text input
430
+ },
431
+ {
432
+ field: "priority",
433
+ cellDataType: "text",
434
+ width: 120,
435
+ headerName: "Priority",
436
+ editable: true,
437
+ editRenderer: "prioritySelect", // Custom editor
438
+ },
439
+ {
440
+ field: "completed",
441
+ cellDataType: "boolean",
442
+ width: 100,
443
+ headerName: "Done",
444
+ editable: true,
445
+ editRenderer: "checkbox", // Custom editor
446
+ },
447
+ ];
448
+
449
+ const tasks: Task[] = [
450
+ { id: 1, title: "Write documentation", priority: "high", completed: false },
451
+ { id: 2, title: "Fix bugs", priority: "medium", completed: true },
452
+ { id: 3, title: "Add tests", priority: "low", completed: false },
453
+ ];
454
+
455
+ const dataSource = createClientDataSource(tasks);
456
+ </script>
457
+
458
+ <template>
459
+ <div style="width: 600px; height: 300px">
460
+ <GpGrid
461
+ :columns="columns"
462
+ :data-source="dataSource"
463
+ :row-height="40"
464
+ :edit-renderers="editRenderers"
465
+ />
466
+ </div>
467
+ </template>
468
+ ```
469
+
470
+ ### Dark Mode
471
+
472
+ ```vue
473
+ <template>
474
+ <GpGrid
475
+ :columns="columns"
476
+ :row-data="data"
477
+ :row-height="36"
478
+ :dark-mode="true"
479
+ />
480
+ </template>
481
+ ```
482
+
483
+ ## API Reference
484
+
485
+ ### GpGridProps
486
+
487
+ | Prop | Type | Default | Description |
488
+ | ----------------- | ----------------------------------- | ----------- | ----------------------------------------------------------- |
489
+ | `columns` | `ColumnDefinition[]` | required | Column definitions |
490
+ | `dataSource` | `DataSource<TData>` | - | Data source for fetching data |
491
+ | `rowData` | `TData[]` | - | Alternative: raw data array (wrapped in client data source) |
492
+ | `rowHeight` | `number` | required | Height of each row in pixels |
493
+ | `headerHeight` | `number` | `rowHeight` | Height of header row |
494
+ | `overscan` | `number` | `3` | Number of rows to render outside viewport |
495
+ | `sortingEnabled` | `boolean` | `true` | Enable column sorting |
496
+ | `darkMode` | `boolean` | `false` | Enable dark theme |
497
+ | `wheelDampening` | `number` | `0.1` | Scroll wheel sensitivity (0-1) |
498
+ | `cellRenderers` | `Record<string, VueCellRenderer>` | `{}` | Cell renderer registry |
499
+ | `editRenderers` | `Record<string, VueEditRenderer>` | `{}` | Edit renderer registry |
500
+ | `headerRenderers` | `Record<string, VueHeaderRenderer>` | `{}` | Header renderer registry |
501
+ | `cellRenderer` | `VueCellRenderer` | - | Global fallback cell renderer |
502
+ | `editRenderer` | `VueEditRenderer` | - | Global fallback edit renderer |
503
+ | `headerRenderer` | `VueHeaderRenderer` | - | Global fallback header renderer |
504
+
505
+ ### ColumnDefinition
506
+
507
+ | Property | Type | Description |
508
+ | ---------------- | -------------- | ------------------------------------------------------------------- |
509
+ | `field` | `string` | Property path in row data (supports dot notation: `"address.city"`) |
510
+ | `colId` | `string` | Unique column ID (defaults to `field`) |
511
+ | `cellDataType` | `CellDataType` | `"text"` \| `"number"` \| `"boolean"` \| `"date"` \| `"object"` |
512
+ | `width` | `number` | Column width in pixels |
513
+ | `headerName` | `string` | Display name in header (defaults to `field`) |
514
+ | `editable` | `boolean` | Enable cell editing |
515
+ | `cellRenderer` | `string` | Key in `cellRenderers` registry |
516
+ | `editRenderer` | `string` | Key in `editRenderers` registry |
517
+ | `headerRenderer` | `string` | Key in `headerRenderers` registry |
518
+
519
+ ### Renderer Types
520
+
521
+ ```typescript
522
+ import type { VNode } from "vue";
523
+
524
+ // Cell renderer receives these params
525
+ interface CellRendererParams {
526
+ value: CellValue; // Current cell value
527
+ rowData: Row; // Full row data
528
+ column: ColumnDefinition; // Column definition
529
+ rowIndex: number; // Row index
530
+ colIndex: number; // Column index
531
+ isActive: boolean; // Is this the active cell?
532
+ isSelected: boolean; // Is this cell in selection?
533
+ isEditing: boolean; // Is this cell being edited?
534
+ }
535
+
536
+ // Vue cell renderer - can return VNode or string
537
+ type VueCellRenderer = (params: CellRendererParams) => VNode | string | null;
538
+
539
+ // Edit renderer receives additional callbacks
540
+ interface EditRendererParams extends CellRendererParams {
541
+ initialValue: CellValue;
542
+ onValueChange: (newValue: CellValue) => void;
543
+ onCommit: () => void;
544
+ onCancel: () => void;
545
+ }
546
+
547
+ // Vue edit renderer - returns VNode for edit input
548
+ type VueEditRenderer = (params: EditRendererParams) => VNode | null;
549
+
550
+ // Header renderer params
551
+ interface HeaderRendererParams {
552
+ column: ColumnDefinition;
553
+ colIndex: number;
554
+ sortDirection?: "asc" | "desc";
555
+ sortIndex?: number; // For multi-column sort
556
+ onSort: (direction: "asc" | "desc" | null, addToExisting: boolean) => void;
557
+ }
558
+
559
+ // Vue header renderer
560
+ type VueHeaderRenderer = (
561
+ params: HeaderRendererParams,
562
+ ) => VNode | string | null;
563
+ ```
564
+
565
+ ## Keyboard Shortcuts
566
+
567
+ | Key | Action |
568
+ | ------------------ | --------------------------------- |
569
+ | Arrow keys | Navigate between cells |
570
+ | Shift + Arrow | Extend selection |
571
+ | Enter | Start editing / Commit edit |
572
+ | Escape | Cancel edit / Clear selection |
573
+ | Tab | Commit and move right |
574
+ | Shift + Tab | Commit and move left |
575
+ | F2 | Start editing |
576
+ | Delete / Backspace | Start editing with empty value |
577
+ | Ctrl + A | Select all |
578
+ | Ctrl + C | Copy selection to clipboard |
579
+ | Any character | Start editing with that character |
580
+
581
+ ## Styling
582
+
583
+ The grid injects its own styles automatically. The main container uses these CSS classes:
584
+
585
+ - `.gp-grid-container` - Main container
586
+ - `.gp-grid-container--dark` - Dark mode modifier
587
+ - `.gp-grid-header` - Header row container
588
+ - `.gp-grid-header-cell` - Individual header cell
589
+ - `.gp-grid-row` - Row container
590
+ - `.gp-grid-cell` - Cell container
591
+ - `.gp-grid-cell--active` - Active cell
592
+ - `.gp-grid-cell--selected` - Selected cell
593
+ - `.gp-grid-cell--editing` - Cell in edit mode
594
+ - `.gp-grid-filter-row` - Filter row container
595
+ - `.gp-grid-filter-input` - Filter input field
596
+ - `.gp-grid-fill-handle` - Fill handle element
597
+
598
+ ## Donations
599
+
600
+ Keeping this library requires effort and passion, I'm a full time engineer employed on other project and I'm trying my best to keep this work free! For all the features.
601
+
602
+ If you think this project helped you achieve your goals, it's hopefully worth a beer! 🍻
603
+
604
+ <div align="center">
605
+
606
+ ### Paypal
607
+
608
+ [![Paypal QR Code](../../public/images/donazione_paypal.png "Paypal QR Code donation")](https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY)
609
+
610
+ [https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY](https://www.paypal.com/donate/?hosted_button_id=XCNMG6BR4ZMLY)
611
+
612
+ ### Bitcoin
613
+
614
+ [![Bitcoin QR Donation](../../public/images/bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m.png "Bitcoin QR Donation")](bitcoin:bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m)
615
+
616
+ bitcoin:bc1qcukwmzver59eyqq442xyzscmxavqjt568kkc9m
617
+
618
+ ### Lightning Network
619
+
620
+ [![Lightning Network QR Donation](../../public/images/lightning.png "Lightning Network QR Donation")](lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhx6rpvanhjetdvfjhyvf4xs0xu5p7)
621
+
622
+ lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhhx6rpvanhjetdvfjhyvf4xs0xu5p7
623
+
624
+ </div>