@gridstorm/vue 0.1.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,209 @@
1
+ # @gridstorm/vue
2
+
3
+ Vue 3 adapter for [GridStorm](https://grid-data-analytics-explorer.vercel.app/) -- a high-performance, headless data grid engine.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @gridstorm/vue @gridstorm/core @gridstorm/dom-renderer
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```vue
14
+ <script setup lang="ts">
15
+ import { ref } from 'vue';
16
+ import { GridStorm } from '@gridstorm/vue';
17
+ import type { ColumnDef, GridApi } from '@gridstorm/vue';
18
+ import { sortingPlugin } from '@gridstorm/plugin-sorting';
19
+ import { filterPlugin } from '@gridstorm/plugin-filtering';
20
+
21
+ interface Employee {
22
+ id: number;
23
+ name: string;
24
+ department: string;
25
+ salary: number;
26
+ }
27
+
28
+ const columns: ColumnDef<Employee>[] = [
29
+ { field: 'name', headerName: 'Name', sortable: true },
30
+ { field: 'department', headerName: 'Department', filter: true },
31
+ { field: 'salary', headerName: 'Salary', width: 120 },
32
+ ];
33
+
34
+ const rowData = ref<Employee[]>([
35
+ { id: 1, name: 'Alice', department: 'Engineering', salary: 95000 },
36
+ { id: 2, name: 'Bob', department: 'Marketing', salary: 72000 },
37
+ { id: 3, name: 'Charlie', department: 'Engineering', salary: 88000 },
38
+ ]);
39
+
40
+ const gridRef = ref<InstanceType<typeof GridStorm>>();
41
+
42
+ function onGridReady(api: GridApi<Employee>) {
43
+ console.log('Grid ready!', api.getDisplayedRowCount(), 'rows');
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <GridStorm
49
+ ref="gridRef"
50
+ :columns="columns"
51
+ :row-data="rowData"
52
+ :plugins="[sortingPlugin(), filterPlugin()]"
53
+ :get-row-id="(params) => String(params.data.id)"
54
+ :row-height="40"
55
+ theme="light"
56
+ height="400px"
57
+ @grid-ready="onGridReady"
58
+ @sort-changed="(e) => console.log('Sort:', e)"
59
+ />
60
+ </template>
61
+ ```
62
+
63
+ ## Composables
64
+
65
+ All composables must be used within a child component of `<GridStorm>`.
66
+
67
+ ### useGridApi
68
+
69
+ Access the GridApi instance.
70
+
71
+ ```vue
72
+ <script setup lang="ts">
73
+ import { useGridApi } from '@gridstorm/vue';
74
+
75
+ const api = useGridApi();
76
+
77
+ function refresh() {
78
+ api.value?.refreshCells();
79
+ }
80
+ </script>
81
+ ```
82
+
83
+ ### useGridSort
84
+
85
+ Reactive sort model and sort actions.
86
+
87
+ ```vue
88
+ <script setup lang="ts">
89
+ import { useGridSort } from '@gridstorm/vue';
90
+
91
+ const { sortModel, isSorted, toggleSort, clearSort } = useGridSort();
92
+ </script>
93
+
94
+ <template>
95
+ <button @click="toggleSort('name')">Sort by Name</button>
96
+ <button v-if="isSorted" @click="clearSort()">Clear Sort</button>
97
+ </template>
98
+ ```
99
+
100
+ ### useGridFilter
101
+
102
+ Reactive filter model and filter actions.
103
+
104
+ ```vue
105
+ <script setup lang="ts">
106
+ import { useGridFilter } from '@gridstorm/vue';
107
+
108
+ const { isFiltered, setQuickFilter, clearFilters } = useGridFilter();
109
+ </script>
110
+
111
+ <template>
112
+ <input
113
+ placeholder="Search..."
114
+ @input="(e) => setQuickFilter((e.target as HTMLInputElement).value)"
115
+ />
116
+ <button v-if="isFiltered" @click="clearFilters()">Clear</button>
117
+ </template>
118
+ ```
119
+
120
+ ### useGridSelection
121
+
122
+ Reactive selection state and selection actions.
123
+
124
+ ```vue
125
+ <script setup lang="ts">
126
+ import { useGridSelection } from '@gridstorm/vue';
127
+
128
+ const { selectedCount, selectAll, deselectAll } = useGridSelection();
129
+ </script>
130
+
131
+ <template>
132
+ <p>{{ selectedCount }} rows selected</p>
133
+ <button @click="selectAll()">Select All</button>
134
+ <button @click="deselectAll()">Deselect All</button>
135
+ </template>
136
+ ```
137
+
138
+ ### useGridPagination
139
+
140
+ Reactive pagination state and navigation.
141
+
142
+ ```vue
143
+ <script setup lang="ts">
144
+ import { useGridPagination } from '@gridstorm/vue';
145
+
146
+ const {
147
+ currentPage,
148
+ totalPages,
149
+ hasNextPage,
150
+ hasPreviousPage,
151
+ nextPage,
152
+ previousPage,
153
+ } = useGridPagination();
154
+ </script>
155
+
156
+ <template>
157
+ <div class="pagination">
158
+ <button :disabled="!hasPreviousPage" @click="previousPage()">Prev</button>
159
+ <span>Page {{ currentPage + 1 }} of {{ totalPages }}</span>
160
+ <button :disabled="!hasNextPage" @click="nextPage()">Next</button>
161
+ </div>
162
+ </template>
163
+ ```
164
+
165
+ ### useGridEvent
166
+
167
+ Subscribe to grid events with automatic cleanup.
168
+
169
+ ```vue
170
+ <script setup lang="ts">
171
+ import { useGridEvent } from '@gridstorm/vue';
172
+
173
+ useGridEvent('selection:changed', (e) => {
174
+ console.log('Selection changed:', e.selectedNodes);
175
+ });
176
+
177
+ useGridEvent('cell:clicked', (e) => {
178
+ console.log('Cell clicked:', e.colId, e.value);
179
+ });
180
+ </script>
181
+ ```
182
+
183
+ ## Template Ref API
184
+
185
+ Access the grid API via a template ref:
186
+
187
+ ```vue
188
+ <script setup lang="ts">
189
+ import { ref } from 'vue';
190
+ import { GridStorm } from '@gridstorm/vue';
191
+
192
+ const gridRef = ref<InstanceType<typeof GridStorm>>();
193
+
194
+ function exportSelected() {
195
+ const api = gridRef.value?.getApi();
196
+ const rows = api?.getSelectedRows() ?? [];
197
+ console.log('Selected rows:', rows);
198
+ }
199
+ </script>
200
+
201
+ <template>
202
+ <GridStorm ref="gridRef" :columns="columns" :row-data="rowData" />
203
+ <button @click="exportSelected">Export Selected</button>
204
+ </template>
205
+ ```
206
+
207
+ ## License
208
+
209
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,468 @@
1
+ 'use strict';
2
+
3
+ var vue = require('vue');
4
+ var core = require('@gridstorm/core');
5
+ var domRenderer = require('@gridstorm/dom-renderer');
6
+
7
+ // src/GridStorm.ts
8
+
9
+ // src/types.ts
10
+ var gridStormPropDefs = {
11
+ columns: {
12
+ type: Array,
13
+ required: true
14
+ },
15
+ rowData: {
16
+ type: Array,
17
+ required: true
18
+ },
19
+ plugins: {
20
+ type: Array,
21
+ default: () => []
22
+ },
23
+ getRowId: {
24
+ type: Function,
25
+ default: void 0
26
+ },
27
+ rowHeight: {
28
+ type: Number,
29
+ default: 40
30
+ },
31
+ headerHeight: {
32
+ type: Number,
33
+ default: void 0
34
+ },
35
+ theme: {
36
+ type: String,
37
+ default: "light"
38
+ },
39
+ density: {
40
+ type: String,
41
+ default: "normal"
42
+ },
43
+ defaultColDef: {
44
+ type: Object,
45
+ default: void 0
46
+ },
47
+ paginationPageSize: {
48
+ type: Number,
49
+ default: void 0
50
+ },
51
+ pagination: {
52
+ type: Boolean,
53
+ default: void 0
54
+ },
55
+ rowSelection: {
56
+ type: [String, Boolean],
57
+ default: void 0
58
+ },
59
+ editType: {
60
+ type: String,
61
+ default: void 0
62
+ },
63
+ ariaLabel: {
64
+ type: String,
65
+ default: void 0
66
+ },
67
+ height: {
68
+ type: [Number, String],
69
+ default: "100%"
70
+ },
71
+ width: {
72
+ type: [Number, String],
73
+ default: "100%"
74
+ },
75
+ containerClass: {
76
+ type: String,
77
+ default: void 0
78
+ }
79
+ };
80
+ var GRID_CONTEXT_KEY = /* @__PURE__ */ Symbol("gridstorm-context");
81
+ function useGridContext() {
82
+ const context = vue.inject(GRID_CONTEXT_KEY, null);
83
+ if (!context) {
84
+ throw new Error(
85
+ "[GridStorm] Composable must be used within a <GridStorm> component."
86
+ );
87
+ }
88
+ return context;
89
+ }
90
+ function useStoreRef(context, selector) {
91
+ const value = vue.ref();
92
+ let unsubscribe = null;
93
+ function subscribe(ctx) {
94
+ if (unsubscribe) {
95
+ unsubscribe();
96
+ unsubscribe = null;
97
+ }
98
+ if (!ctx) {
99
+ value.value = void 0;
100
+ return;
101
+ }
102
+ value.value = selector(ctx.engine);
103
+ unsubscribe = ctx.engine.store.subscribe(() => {
104
+ value.value = selector(ctx.engine);
105
+ });
106
+ }
107
+ vue.watch(context, (newCtx) => subscribe(newCtx), { immediate: true });
108
+ vue.onUnmounted(() => {
109
+ if (unsubscribe) {
110
+ unsubscribe();
111
+ unsubscribe = null;
112
+ }
113
+ });
114
+ return value;
115
+ }
116
+ function useGridApi() {
117
+ const context = useGridContext();
118
+ return vue.computed(() => context.value?.api);
119
+ }
120
+ function useGridEngine() {
121
+ const context = useGridContext();
122
+ return vue.computed(() => context.value?.engine);
123
+ }
124
+ function useGridSort() {
125
+ const context = useGridContext();
126
+ const sortModel = useStoreRef(
127
+ context,
128
+ (engine) => engine.store.getState().sortModel
129
+ );
130
+ const isSorted = vue.computed(() => (sortModel.value?.length ?? 0) > 0);
131
+ function setSortModel(model) {
132
+ context.value?.api.setSortModel(model);
133
+ }
134
+ function toggleSort(colId, multiSort = false) {
135
+ context.value?.engine.commandBus.dispatch("sort:toggle", {
136
+ colId,
137
+ multiSort
138
+ });
139
+ }
140
+ function clearSort() {
141
+ context.value?.api.setSortModel([]);
142
+ }
143
+ return {
144
+ sortModel,
145
+ isSorted,
146
+ setSortModel,
147
+ toggleSort,
148
+ clearSort
149
+ };
150
+ }
151
+ function useGridFilter() {
152
+ const context = useGridContext();
153
+ const filterModel = useStoreRef(
154
+ context,
155
+ (engine) => engine.store.getState().filterModel
156
+ );
157
+ const quickFilterText = useStoreRef(
158
+ context,
159
+ (engine) => engine.store.getState().quickFilterText
160
+ );
161
+ const isFiltered = vue.computed(() => {
162
+ const hasColumnFilters = Object.keys(filterModel.value ?? {}).length > 0;
163
+ const hasQuickFilter = (quickFilterText.value ?? "").length > 0;
164
+ return hasColumnFilters || hasQuickFilter;
165
+ });
166
+ function setFilterModel(model) {
167
+ context.value?.api.setFilterModel(model);
168
+ }
169
+ function setQuickFilter(text) {
170
+ context.value?.api.setQuickFilter(text);
171
+ }
172
+ function clearFilters() {
173
+ context.value?.api.setFilterModel({});
174
+ context.value?.api.setQuickFilter("");
175
+ }
176
+ return {
177
+ filterModel,
178
+ quickFilterText,
179
+ isFiltered,
180
+ setFilterModel,
181
+ setQuickFilter,
182
+ clearFilters
183
+ };
184
+ }
185
+ function useGridSelection() {
186
+ const context = useGridContext();
187
+ const selectedRowIds = useStoreRef(
188
+ context,
189
+ (engine) => engine.store.getState().selection.selectedRowIds
190
+ );
191
+ const selectedCount = vue.computed(() => selectedRowIds.value?.size ?? 0);
192
+ function getSelectedRows() {
193
+ return context.value?.api.getSelectedRows() ?? [];
194
+ }
195
+ function getSelectedNodes() {
196
+ return context.value?.api.getSelectedNodes() ?? [];
197
+ }
198
+ function isRowSelected(rowId) {
199
+ return selectedRowIds.value?.has(rowId) ?? false;
200
+ }
201
+ function selectAll() {
202
+ context.value?.api.selectAll();
203
+ }
204
+ function deselectAll() {
205
+ context.value?.api.deselectAll();
206
+ }
207
+ return {
208
+ selectedRowIds,
209
+ selectedCount,
210
+ getSelectedRows,
211
+ getSelectedNodes,
212
+ isRowSelected,
213
+ selectAll,
214
+ deselectAll
215
+ };
216
+ }
217
+ function useGridPagination() {
218
+ const context = useGridContext();
219
+ const paginationState = useStoreRef(
220
+ context,
221
+ (engine) => engine.store.getState().pagination
222
+ );
223
+ const currentPage = vue.computed(() => paginationState.value?.currentPage ?? 0);
224
+ const pageSize = vue.computed(() => paginationState.value?.pageSize ?? 100);
225
+ const totalRows = vue.computed(() => paginationState.value?.totalRows ?? 0);
226
+ const totalPages = vue.computed(
227
+ () => Math.max(1, Math.ceil(totalRows.value / pageSize.value))
228
+ );
229
+ const hasNextPage = vue.computed(() => currentPage.value < totalPages.value - 1);
230
+ const hasPreviousPage = vue.computed(() => currentPage.value > 0);
231
+ function goToPage(page) {
232
+ context.value?.api.paginationGoToPage(page);
233
+ }
234
+ function nextPage() {
235
+ if (hasNextPage.value) {
236
+ context.value?.api.paginationGoToPage(currentPage.value + 1);
237
+ }
238
+ }
239
+ function previousPage() {
240
+ if (hasPreviousPage.value) {
241
+ context.value?.api.paginationGoToPage(currentPage.value - 1);
242
+ }
243
+ }
244
+ function firstPage() {
245
+ context.value?.api.paginationGoToPage(0);
246
+ }
247
+ function lastPage() {
248
+ context.value?.api.paginationGoToPage(totalPages.value - 1);
249
+ }
250
+ return {
251
+ currentPage,
252
+ totalPages,
253
+ pageSize,
254
+ totalRows,
255
+ hasNextPage,
256
+ hasPreviousPage,
257
+ goToPage,
258
+ nextPage,
259
+ previousPage,
260
+ firstPage,
261
+ lastPage
262
+ };
263
+ }
264
+ function useGridEvent(event, handler) {
265
+ const context = useGridContext();
266
+ let unsubscribe = null;
267
+ vue.watch(
268
+ context,
269
+ (ctx) => {
270
+ if (unsubscribe) {
271
+ unsubscribe();
272
+ unsubscribe = null;
273
+ }
274
+ if (ctx) {
275
+ unsubscribe = ctx.engine.eventBus.on(event, handler);
276
+ }
277
+ },
278
+ { immediate: true }
279
+ );
280
+ vue.onUnmounted(() => {
281
+ if (unsubscribe) {
282
+ unsubscribe();
283
+ unsubscribe = null;
284
+ }
285
+ });
286
+ }
287
+
288
+ // src/GridStorm.ts
289
+ var GridStorm = vue.defineComponent({
290
+ name: "GridStorm",
291
+ props: gridStormPropDefs,
292
+ emits: [
293
+ "gridReady",
294
+ "rowDataChanged",
295
+ "selectionChanged",
296
+ "sortChanged",
297
+ "filterChanged",
298
+ "cellValueChanged",
299
+ "cellClicked",
300
+ "cellDoubleClicked",
301
+ "rowClicked",
302
+ "paginationChanged",
303
+ "columnResized"
304
+ ],
305
+ setup(props, { emit, expose }) {
306
+ const containerRef = vue.ref(null);
307
+ let engine = null;
308
+ let renderer = null;
309
+ const eventUnsubscribers = [];
310
+ function buildConfig() {
311
+ return {
312
+ columns: props.columns,
313
+ rowData: props.rowData,
314
+ plugins: props.plugins,
315
+ getRowId: props.getRowId,
316
+ rowHeight: props.rowHeight,
317
+ headerHeight: props.headerHeight,
318
+ defaultColDef: props.defaultColDef,
319
+ paginationPageSize: props.paginationPageSize,
320
+ pagination: props.pagination,
321
+ rowSelection: props.rowSelection,
322
+ editType: props.editType,
323
+ ariaLabel: props.ariaLabel
324
+ };
325
+ }
326
+ function subscribeToEvents() {
327
+ if (!engine) return;
328
+ const eb = engine.eventBus;
329
+ eventUnsubscribers.push(
330
+ eb.on("rowData:changed", (e) => emit("rowDataChanged", e))
331
+ );
332
+ eventUnsubscribers.push(
333
+ eb.on("selection:changed", (e) => emit("selectionChanged", e))
334
+ );
335
+ eventUnsubscribers.push(
336
+ eb.on("column:sort:changed", (e) => emit("sortChanged", e))
337
+ );
338
+ eventUnsubscribers.push(
339
+ eb.on("filter:changed", (e) => emit("filterChanged", e))
340
+ );
341
+ eventUnsubscribers.push(
342
+ eb.on("cell:valueChanged", (e) => emit("cellValueChanged", e))
343
+ );
344
+ eventUnsubscribers.push(
345
+ eb.on("cell:clicked", (e) => emit("cellClicked", e))
346
+ );
347
+ eventUnsubscribers.push(
348
+ eb.on("cell:doubleClicked", (e) => emit("cellDoubleClicked", e))
349
+ );
350
+ eventUnsubscribers.push(
351
+ eb.on("row:clicked", (e) => emit("rowClicked", e))
352
+ );
353
+ eventUnsubscribers.push(
354
+ eb.on("pagination:changed", (e) => emit("paginationChanged", e))
355
+ );
356
+ eventUnsubscribers.push(
357
+ eb.on("column:resized", (e) => emit("columnResized", e))
358
+ );
359
+ }
360
+ const gridContext = vue.shallowRef(null);
361
+ vue.provide(GRID_CONTEXT_KEY, gridContext);
362
+ function initGrid() {
363
+ if (!containerRef.value) return;
364
+ const config = buildConfig();
365
+ engine = core.createGrid(config);
366
+ gridContext.value = {
367
+ engine,
368
+ api: engine.api
369
+ };
370
+ renderer = new domRenderer.DomRenderer({
371
+ container: containerRef.value,
372
+ engine
373
+ });
374
+ renderer.mount();
375
+ subscribeToEvents();
376
+ emit("gridReady", engine.api);
377
+ }
378
+ function destroyGrid() {
379
+ for (const unsub of eventUnsubscribers) {
380
+ unsub();
381
+ }
382
+ eventUnsubscribers.length = 0;
383
+ renderer?.destroy();
384
+ renderer = null;
385
+ engine?.destroy();
386
+ engine = null;
387
+ gridContext.value = null;
388
+ }
389
+ vue.onMounted(() => {
390
+ initGrid();
391
+ });
392
+ vue.onBeforeUnmount(() => {
393
+ destroyGrid();
394
+ });
395
+ vue.watch(
396
+ () => props.rowData,
397
+ (newData) => {
398
+ if (engine && newData) {
399
+ engine.api.setRowData(newData);
400
+ }
401
+ },
402
+ { deep: false }
403
+ );
404
+ vue.watch(
405
+ () => props.columns,
406
+ (newCols) => {
407
+ if (engine && newCols) {
408
+ engine.api.setColumnDefs(newCols);
409
+ }
410
+ },
411
+ { deep: false }
412
+ );
413
+ vue.watch(
414
+ () => props.theme,
415
+ (newTheme) => {
416
+ if (containerRef.value && newTheme) {
417
+ containerRef.value.setAttribute("data-theme", newTheme);
418
+ }
419
+ }
420
+ );
421
+ vue.watch(
422
+ () => props.density,
423
+ (newDensity) => {
424
+ if (containerRef.value && newDensity) {
425
+ containerRef.value.setAttribute("data-density", newDensity);
426
+ }
427
+ }
428
+ );
429
+ expose({
430
+ /**
431
+ * Get the GridApi instance.
432
+ * Returns undefined if the grid has not been initialized yet.
433
+ */
434
+ getApi: () => engine?.api,
435
+ /**
436
+ * Get the GridEngine instance.
437
+ * Returns undefined if the grid has not been initialized yet.
438
+ */
439
+ getEngine: () => engine
440
+ });
441
+ return () => {
442
+ const heightStyle = typeof props.height === "number" ? `${props.height}px` : props.height;
443
+ const widthStyle = typeof props.width === "number" ? `${props.width}px` : props.width;
444
+ return vue.h("div", {
445
+ ref: containerRef,
446
+ class: ["gridstorm-wrapper", props.containerClass].filter(Boolean).join(" "),
447
+ "data-theme": props.theme,
448
+ "data-density": props.density,
449
+ style: {
450
+ width: widthStyle,
451
+ height: heightStyle
452
+ }
453
+ });
454
+ };
455
+ }
456
+ });
457
+
458
+ exports.GRID_CONTEXT_KEY = GRID_CONTEXT_KEY;
459
+ exports.GridStorm = GridStorm;
460
+ exports.useGridApi = useGridApi;
461
+ exports.useGridEngine = useGridEngine;
462
+ exports.useGridEvent = useGridEvent;
463
+ exports.useGridFilter = useGridFilter;
464
+ exports.useGridPagination = useGridPagination;
465
+ exports.useGridSelection = useGridSelection;
466
+ exports.useGridSort = useGridSort;
467
+ //# sourceMappingURL=index.cjs.map
468
+ //# sourceMappingURL=index.cjs.map