simplesvelte 2.5.0 → 2.5.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 +15 -2
- package/dist/AG_GRID_SERVER_API.md +373 -373
- package/dist/Grid.svelte +168 -168
- package/dist/Select.svelte +4 -1
- package/dist/Select.svelte.d.ts +1 -0
- package/dist/TextArea.svelte +45 -45
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ Pop.toast('Updated!', 'success', 'top-end')
|
|
|
149
149
|
optionalPhone: fh.nString("phone")
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
}
|
|
154
154
|
</script>
|
|
155
155
|
|
|
@@ -200,11 +200,24 @@ The `src/lib` directory contains all library components, while `src/routes` prov
|
|
|
200
200
|
To build your library:
|
|
201
201
|
|
|
202
202
|
```bash
|
|
203
|
-
bun run
|
|
203
|
+
bun run build
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
This will generate the distribution files in the `dist` directory.
|
|
207
207
|
|
|
208
|
+
## Publishing
|
|
209
|
+
|
|
210
|
+
Publishing is automated through [.github/workflows/bun-publish.yml](.github/workflows/bun-publish.yml):
|
|
211
|
+
|
|
212
|
+
1. Create a GitHub release with a tag like `v2.5.2`
|
|
213
|
+
2. The workflow updates `package.json` to the release tag version
|
|
214
|
+
3. The workflow runs type checks and build packaging
|
|
215
|
+
4. The workflow publishes to npm
|
|
216
|
+
|
|
217
|
+
Required repository secret:
|
|
218
|
+
|
|
219
|
+
- `NPM_TOKEN` with publish access for the `simplesvelte` package
|
|
220
|
+
|
|
208
221
|
## License
|
|
209
222
|
|
|
210
223
|
MIT - see LICENSE file for details.
|
|
@@ -1,373 +1,373 @@
|
|
|
1
|
-
# AG Grid Server-Side Row Model (SSRM) - Server API
|
|
2
|
-
|
|
3
|
-
Clean, database-agnostic API for implementing AG Grid's Server-Side Row Model on the backend.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This library provides a simple, strongly-typed interface for handling AG Grid SSRM requests on the server side. It works with any backend data source: Prisma, raw SQL, MongoDB, REST APIs, etc.
|
|
8
|
-
|
|
9
|
-
## Key Features
|
|
10
|
-
|
|
11
|
-
- ✅ **Database Agnostic** - Works with any data source
|
|
12
|
-
- ✅ **Type Safe** - Full TypeScript support with generics
|
|
13
|
-
- ✅ **Simple API** - Just implement `fetch` and `count`
|
|
14
|
-
- ✅ **Computed Fields** - Easy handling of virtual/calculated fields
|
|
15
|
-
- ✅ **Grouping Support** - Built-in row grouping support
|
|
16
|
-
- ✅ **Filter Transforms** - Automatic filter/sort transformation
|
|
17
|
-
- ✅ **Zero Boilerplate** - Clean, declarative configuration
|
|
18
|
-
|
|
19
|
-
## Quick Start
|
|
20
|
-
|
|
21
|
-
### 1. Basic Implementation (Prisma)
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { createAGGridQuery } from '$lib/ag-grid'
|
|
25
|
-
import { query } from '$app/server'
|
|
26
|
-
import { DB } from '$lib/prisma.server'
|
|
27
|
-
|
|
28
|
-
export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
29
|
-
return await createAGGridQuery({
|
|
30
|
-
async fetch(params) {
|
|
31
|
-
return await DB.user.findMany({
|
|
32
|
-
where: params.where,
|
|
33
|
-
orderBy: params.orderBy,
|
|
34
|
-
skip: params.skip,
|
|
35
|
-
take: params.take,
|
|
36
|
-
})
|
|
37
|
-
},
|
|
38
|
-
async count(params) {
|
|
39
|
-
return await DB.user.count({ where: params.where })
|
|
40
|
-
},
|
|
41
|
-
defaultSort: { createdAt: 'desc' },
|
|
42
|
-
})(request)
|
|
43
|
-
})
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### 2. With Nested Relations (Auto-Handled!)
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
export const getInterventionsPaginated = query(agGridRequestSchema, async (request) => {
|
|
50
|
-
return await createAGGridQuery({
|
|
51
|
-
async fetch(params) {
|
|
52
|
-
return await DB.intervention.findMany({
|
|
53
|
-
where: params.where,
|
|
54
|
-
orderBy: params.orderBy,
|
|
55
|
-
skip: params.skip,
|
|
56
|
-
take: params.take,
|
|
57
|
-
include: { location: true }, // Include the relation
|
|
58
|
-
})
|
|
59
|
-
},
|
|
60
|
-
async count(params) {
|
|
61
|
-
return await DB.intervention.count({ where: params.where })
|
|
62
|
-
},
|
|
63
|
-
// No computedFields needed! Just use field: 'location.name' in column definitions
|
|
64
|
-
// The system automatically:
|
|
65
|
-
// - Handles text filters: where.location.name contains 'X'
|
|
66
|
-
// - Handles set filters: where.location.name in [...]
|
|
67
|
-
// - Handles sorting: orderBy: { location: { name: 'asc' } }
|
|
68
|
-
// - Handles grouping: where.location.name = 'groupKey'
|
|
69
|
-
// - AG Grid displays nested values natively with dot notation
|
|
70
|
-
})(request)
|
|
71
|
-
})
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Column Definition:**
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const columnDefs = [
|
|
78
|
-
{ field: 'id' },
|
|
79
|
-
{
|
|
80
|
-
field: 'location.name', // ✨ Use dot notation directly!
|
|
81
|
-
headerName: 'Location',
|
|
82
|
-
filter: 'agTextColumnFilter',
|
|
83
|
-
},
|
|
84
|
-
{ field: 'dateOfEngagement', filter: 'agDateColumnFilter' },
|
|
85
|
-
]
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### 3. With Complex Computed Fields
|
|
89
|
-
|
|
90
|
-
For truly computed/calculated values, provide custom handlers:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
computedFields: [
|
|
94
|
-
{
|
|
95
|
-
columnId: 'weekEnding',
|
|
96
|
-
valueGetter: (record) => {
|
|
97
|
-
// Calculate week ending date from dateOfEngagement
|
|
98
|
-
const date = new Date(record.dateOfEngagement)
|
|
99
|
-
const day = date.getDay()
|
|
100
|
-
date.setDate(date.getDate() + (7 - day))
|
|
101
|
-
return date.toISOString().split('T')[0]
|
|
102
|
-
},
|
|
103
|
-
filterHandler: (filterValue, where) => {
|
|
104
|
-
// Custom filter logic for week ending
|
|
105
|
-
const filter = filterValue as { dateFrom?: string }
|
|
106
|
-
if (filter.dateFrom) {
|
|
107
|
-
const targetDate = new Date(filter.dateFrom)
|
|
108
|
-
const weekStart = new Date(targetDate)
|
|
109
|
-
weekStart.setDate(targetDate.getDate() - 6)
|
|
110
|
-
where.dateOfEngagement = { gte: weekStart, lte: targetDate }
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Column Definition:**
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
const columnDefs = [
|
|
121
|
-
{
|
|
122
|
-
field: 'weekEnding', // Custom computed field
|
|
123
|
-
headerName: 'Week Ending',
|
|
124
|
-
filter: 'agDateColumnFilter',
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### 4. Client-Side Usage
|
|
130
|
-
|
|
131
|
-
```svelte
|
|
132
|
-
<script lang="ts">
|
|
133
|
-
import { getUsersPaginated } from './user.remote'
|
|
134
|
-
import { createAGGridDatasource, createRemoteFetcher } from '$lib/ag-grid'
|
|
135
|
-
import { AgGridSvelte } from 'ag-grid-svelte'
|
|
136
|
-
|
|
137
|
-
let gridApi: GridApi
|
|
138
|
-
|
|
139
|
-
const columnDefs = [
|
|
140
|
-
{ field: 'id' },
|
|
141
|
-
{ field: 'name', filter: 'agTextColumnFilter' },
|
|
142
|
-
{ field: 'email', filter: 'agTextColumnFilter' },
|
|
143
|
-
// For nested relations, use dot notation directly:
|
|
144
|
-
{ field: 'profile.avatar', headerName: 'Avatar' },
|
|
145
|
-
{ field: 'department.name', headerName: 'Department', filter: 'agTextColumnFilter' },
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
function onGridReady(params: any) {
|
|
149
|
-
gridApi = params.api
|
|
150
|
-
|
|
151
|
-
const datasource = createAGGridDatasource(createRemoteFetcher(getUsersPaginated))
|
|
152
|
-
|
|
153
|
-
gridApi.setGridOption('serverSideDatasource', datasource)
|
|
154
|
-
}
|
|
155
|
-
</script>
|
|
156
|
-
|
|
157
|
-
<div class="ag-theme-alpine" style="height: 600px;">
|
|
158
|
-
<AgGridSvelte {columnDefs} rowModelType="serverSide" pagination={true} paginationPageSize={100} {onGridReady} />
|
|
159
|
-
</div>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## API Reference
|
|
163
|
-
|
|
164
|
-
### `createAGGridQuery(config)`
|
|
165
|
-
|
|
166
|
-
Main function to create a server-side query handler.
|
|
167
|
-
|
|
168
|
-
**Parameters:**
|
|
169
|
-
|
|
170
|
-
- `config: AGGridQueryConfig<TRecord, TWhereInput>`
|
|
171
|
-
- `fetch: (params) => Promise<TRecord[]>` - Function to fetch data rows
|
|
172
|
-
- `count: (params) => Promise<number>` - Function to count total rows
|
|
173
|
-
- `computedFields?: ComputedField[]` - Optional computed field configurations
|
|
174
|
-
- `defaultSort?: Record<string, 'asc' | 'desc'>` - Optional default sort
|
|
175
|
-
- `transformWhere?: (where, request) => TWhereInput` - Optional WHERE clause transformer
|
|
176
|
-
|
|
177
|
-
**Returns:** `(request: AGGridRequest) => Promise<AGGridResponse<TRecord>>`
|
|
178
|
-
|
|
179
|
-
### `AGGridQueryParams`
|
|
180
|
-
|
|
181
|
-
The params object passed to `fetch` and `count` functions:
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
interface AGGridQueryParams {
|
|
185
|
-
where: TWhereInput // WHERE clause for filtering
|
|
186
|
-
orderBy: Record<string, unknown>[] // ORDER BY for sorting
|
|
187
|
-
skip: number // Pagination offset
|
|
188
|
-
take: number // Pagination limit
|
|
189
|
-
groupLevel: number // Current group level (for grouping)
|
|
190
|
-
groupKeys: string[] // Group drill-down keys
|
|
191
|
-
groupColumn?: AGGridColumn // Column being grouped
|
|
192
|
-
isGroupRequest: boolean // Whether this is a group or leaf request
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### `ComputedField`
|
|
197
|
-
|
|
198
|
-
Configuration for computed/virtual fields:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
interface ComputedField {
|
|
202
|
-
columnId: string // Column ID in AG Grid
|
|
203
|
-
dbField?: string // DB field path (e.g., 'location.name')
|
|
204
|
-
// 👇 Optional - only needed for complex computed logic
|
|
205
|
-
valueGetter?: (record) => unknown // Compute display value
|
|
206
|
-
filterHandler?: (filterValue, where) => void // Handle filtering
|
|
207
|
-
groupHandler?: (groupKey, where) => void // Handle grouping
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
**✨ Auto-Magic for Nested Fields:**
|
|
212
|
-
|
|
213
|
-
- If `dbField` contains a `.` (e.g., `'location.name'`), the system automatically:
|
|
214
|
-
- Extracts nested values for display
|
|
215
|
-
- Handles text and set filters
|
|
216
|
-
- Handles sorting and grouping
|
|
217
|
-
- You only need `valueGetter`, `filterHandler`, and `groupHandler` for **complex computed logic**
|
|
218
|
-
|
|
219
|
-
## Common Patterns
|
|
220
|
-
|
|
221
|
-
### Pattern 1: Simple Nested Relations (Use Dot Notation!)
|
|
222
|
-
|
|
223
|
-
**Recommended:** Use dot notation directly in column definitions - no `computedFields` needed!
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
// Server-side - just include the relation
|
|
227
|
-
return await createAGGridQuery({
|
|
228
|
-
async fetch(params) {
|
|
229
|
-
return await DB.model.findMany({
|
|
230
|
-
where: params.where,
|
|
231
|
-
orderBy: params.orderBy,
|
|
232
|
-
skip: params.skip,
|
|
233
|
-
take: params.take,
|
|
234
|
-
include: { location: true, category: true, user: true },
|
|
235
|
-
})
|
|
236
|
-
},
|
|
237
|
-
async count(params) {
|
|
238
|
-
return await DB.model.count({ where: params.where })
|
|
239
|
-
},
|
|
240
|
-
})(request)
|
|
241
|
-
|
|
242
|
-
// Client-side - use dot notation in field
|
|
243
|
-
const columnDefs = [
|
|
244
|
-
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' },
|
|
245
|
-
{ field: 'category.name', headerName: 'Category', filter: 'agTextColumnFilter' },
|
|
246
|
-
{ field: 'user.email', headerName: 'User', filter: 'agTextColumnFilter' },
|
|
247
|
-
]
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
The system automatically handles:
|
|
251
|
-
|
|
252
|
-
- **Display**: AG Grid extracts `record.location.name` natively
|
|
253
|
-
- **Text Filter**: `where.location.name = { contains: 'X', mode: 'insensitive' }`
|
|
254
|
-
- **Set Filter**: `where.location.name = { in: ['A', 'B'] }`
|
|
255
|
-
- **Sorting**: `orderBy: { location: { name: 'asc' } }`
|
|
256
|
-
- **Grouping**: `where.location.name = 'groupKey'`
|
|
257
|
-
|
|
258
|
-
### Pattern 2: Complex Calculated Fields (Custom Handlers Required)
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
computedFields: [
|
|
262
|
-
{
|
|
263
|
-
columnId: 'weekEnding',
|
|
264
|
-
valueGetter: (record) => {
|
|
265
|
-
const date = new Date(record.date)
|
|
266
|
-
const day = date.getDay()
|
|
267
|
-
date.setDate(date.getDate() + (7 - day))
|
|
268
|
-
return date.toISOString().split('T')[0]
|
|
269
|
-
},
|
|
270
|
-
filterHandler: (filterValue, where) => {
|
|
271
|
-
// Custom filter logic based on calculated value
|
|
272
|
-
const targetDate = new Date(filterValue.dateFrom)
|
|
273
|
-
const weekStart = new Date(targetDate)
|
|
274
|
-
weekStart.setDate(targetDate.getDate() - 6)
|
|
275
|
-
where.date = { gte: weekStart, lte: targetDate }
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
]
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Pattern 3: Custom WHERE Transform
|
|
282
|
-
|
|
283
|
-
```typescript
|
|
284
|
-
transformWhere: (where, request) => {
|
|
285
|
-
// Add tenant filtering
|
|
286
|
-
where.tenantId = getCurrentTenantId()
|
|
287
|
-
|
|
288
|
-
// Add soft delete filter
|
|
289
|
-
where.deletedAt = null
|
|
290
|
-
|
|
291
|
-
return where
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Database Examples
|
|
296
|
-
|
|
297
|
-
### Prisma
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
{
|
|
301
|
-
async fetch(params) {
|
|
302
|
-
return await DB.model.findMany({
|
|
303
|
-
where: params.where,
|
|
304
|
-
orderBy: params.orderBy,
|
|
305
|
-
skip: params.skip,
|
|
306
|
-
take: params.take,
|
|
307
|
-
})
|
|
308
|
-
},
|
|
309
|
-
async count(params) {
|
|
310
|
-
return await DB.model.count({ where: params.where })
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## Advanced Features
|
|
316
|
-
|
|
317
|
-
### Row Grouping
|
|
318
|
-
|
|
319
|
-
The API automatically handles row grouping. When `isGroupRequest` is `true`, it returns group rows with counts. When `false`, it returns leaf data.
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
if (params.isGroupRequest) {
|
|
323
|
-
// Return groups at current level
|
|
324
|
-
} else {
|
|
325
|
-
// Return actual data rows
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Type Safety
|
|
330
|
-
|
|
331
|
-
Use TypeScript generics for full type safety:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
type UserWhereInput = {
|
|
335
|
-
id?: number
|
|
336
|
-
name?: { contains: string; mode: 'insensitive' }
|
|
337
|
-
email?: { contains: string; mode: 'insensitive' }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
createAGGridQuery<User, UserWhereInput>({
|
|
341
|
-
// params.where is now typed as UserWhereInput
|
|
342
|
-
async fetch(params) { ... },
|
|
343
|
-
async count(params) { ... }
|
|
344
|
-
})
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
## Nested Field Magic Explained
|
|
348
|
-
|
|
349
|
-
When you use dot notation in field names (e.g., `'location.name'`), the system automatically handles everything:
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
// Column definition
|
|
353
|
-
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' }
|
|
354
|
-
|
|
355
|
-
// Server-side - just include the relation
|
|
356
|
-
include: { location: true }
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**What happens automatically:**
|
|
360
|
-
|
|
361
|
-
1. **Display**: AG Grid extracts `record.location.name` natively
|
|
362
|
-
2. **Text Filtering**: `where.location = { name: { contains: 'Office', mode: 'insensitive' } }`
|
|
363
|
-
3. **Set Filtering**: `where.location = { name: { in: ['Office A', 'Office B'] } }`
|
|
364
|
-
4. **Sorting**: `orderBy: [{ location: { name: 'asc' } }]`
|
|
365
|
-
5. **Grouping**: `where.location = { name: 'Office A' }`
|
|
366
|
-
|
|
367
|
-
**You write:** Column definition with dot notation
|
|
368
|
-
|
|
369
|
-
**You get:** All filtering, sorting, grouping, and display automatically! ✨
|
|
370
|
-
|
|
371
|
-
## See Also
|
|
372
|
-
|
|
373
|
-
- [AG Grid SSRM Documentation](https://www.ag-grid.com/javascript-data-grid/server-side-model/)
|
|
1
|
+
# AG Grid Server-Side Row Model (SSRM) - Server API
|
|
2
|
+
|
|
3
|
+
Clean, database-agnostic API for implementing AG Grid's Server-Side Row Model on the backend.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This library provides a simple, strongly-typed interface for handling AG Grid SSRM requests on the server side. It works with any backend data source: Prisma, raw SQL, MongoDB, REST APIs, etc.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- ✅ **Database Agnostic** - Works with any data source
|
|
12
|
+
- ✅ **Type Safe** - Full TypeScript support with generics
|
|
13
|
+
- ✅ **Simple API** - Just implement `fetch` and `count`
|
|
14
|
+
- ✅ **Computed Fields** - Easy handling of virtual/calculated fields
|
|
15
|
+
- ✅ **Grouping Support** - Built-in row grouping support
|
|
16
|
+
- ✅ **Filter Transforms** - Automatic filter/sort transformation
|
|
17
|
+
- ✅ **Zero Boilerplate** - Clean, declarative configuration
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Basic Implementation (Prisma)
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createAGGridQuery } from '$lib/ag-grid'
|
|
25
|
+
import { query } from '$app/server'
|
|
26
|
+
import { DB } from '$lib/prisma.server'
|
|
27
|
+
|
|
28
|
+
export const getUsersPaginated = query(agGridRequestSchema, async (request) => {
|
|
29
|
+
return await createAGGridQuery({
|
|
30
|
+
async fetch(params) {
|
|
31
|
+
return await DB.user.findMany({
|
|
32
|
+
where: params.where,
|
|
33
|
+
orderBy: params.orderBy,
|
|
34
|
+
skip: params.skip,
|
|
35
|
+
take: params.take,
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
async count(params) {
|
|
39
|
+
return await DB.user.count({ where: params.where })
|
|
40
|
+
},
|
|
41
|
+
defaultSort: { createdAt: 'desc' },
|
|
42
|
+
})(request)
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. With Nested Relations (Auto-Handled!)
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
export const getInterventionsPaginated = query(agGridRequestSchema, async (request) => {
|
|
50
|
+
return await createAGGridQuery({
|
|
51
|
+
async fetch(params) {
|
|
52
|
+
return await DB.intervention.findMany({
|
|
53
|
+
where: params.where,
|
|
54
|
+
orderBy: params.orderBy,
|
|
55
|
+
skip: params.skip,
|
|
56
|
+
take: params.take,
|
|
57
|
+
include: { location: true }, // Include the relation
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
async count(params) {
|
|
61
|
+
return await DB.intervention.count({ where: params.where })
|
|
62
|
+
},
|
|
63
|
+
// No computedFields needed! Just use field: 'location.name' in column definitions
|
|
64
|
+
// The system automatically:
|
|
65
|
+
// - Handles text filters: where.location.name contains 'X'
|
|
66
|
+
// - Handles set filters: where.location.name in [...]
|
|
67
|
+
// - Handles sorting: orderBy: { location: { name: 'asc' } }
|
|
68
|
+
// - Handles grouping: where.location.name = 'groupKey'
|
|
69
|
+
// - AG Grid displays nested values natively with dot notation
|
|
70
|
+
})(request)
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Column Definition:**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const columnDefs = [
|
|
78
|
+
{ field: 'id' },
|
|
79
|
+
{
|
|
80
|
+
field: 'location.name', // ✨ Use dot notation directly!
|
|
81
|
+
headerName: 'Location',
|
|
82
|
+
filter: 'agTextColumnFilter',
|
|
83
|
+
},
|
|
84
|
+
{ field: 'dateOfEngagement', filter: 'agDateColumnFilter' },
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. With Complex Computed Fields
|
|
89
|
+
|
|
90
|
+
For truly computed/calculated values, provide custom handlers:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
computedFields: [
|
|
94
|
+
{
|
|
95
|
+
columnId: 'weekEnding',
|
|
96
|
+
valueGetter: (record) => {
|
|
97
|
+
// Calculate week ending date from dateOfEngagement
|
|
98
|
+
const date = new Date(record.dateOfEngagement)
|
|
99
|
+
const day = date.getDay()
|
|
100
|
+
date.setDate(date.getDate() + (7 - day))
|
|
101
|
+
return date.toISOString().split('T')[0]
|
|
102
|
+
},
|
|
103
|
+
filterHandler: (filterValue, where) => {
|
|
104
|
+
// Custom filter logic for week ending
|
|
105
|
+
const filter = filterValue as { dateFrom?: string }
|
|
106
|
+
if (filter.dateFrom) {
|
|
107
|
+
const targetDate = new Date(filter.dateFrom)
|
|
108
|
+
const weekStart = new Date(targetDate)
|
|
109
|
+
weekStart.setDate(targetDate.getDate() - 6)
|
|
110
|
+
where.dateOfEngagement = { gte: weekStart, lte: targetDate }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Column Definition:**
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const columnDefs = [
|
|
121
|
+
{
|
|
122
|
+
field: 'weekEnding', // Custom computed field
|
|
123
|
+
headerName: 'Week Ending',
|
|
124
|
+
filter: 'agDateColumnFilter',
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Client-Side Usage
|
|
130
|
+
|
|
131
|
+
```svelte
|
|
132
|
+
<script lang="ts">
|
|
133
|
+
import { getUsersPaginated } from './user.remote'
|
|
134
|
+
import { createAGGridDatasource, createRemoteFetcher } from '$lib/ag-grid'
|
|
135
|
+
import { AgGridSvelte } from 'ag-grid-svelte'
|
|
136
|
+
|
|
137
|
+
let gridApi: GridApi
|
|
138
|
+
|
|
139
|
+
const columnDefs = [
|
|
140
|
+
{ field: 'id' },
|
|
141
|
+
{ field: 'name', filter: 'agTextColumnFilter' },
|
|
142
|
+
{ field: 'email', filter: 'agTextColumnFilter' },
|
|
143
|
+
// For nested relations, use dot notation directly:
|
|
144
|
+
{ field: 'profile.avatar', headerName: 'Avatar' },
|
|
145
|
+
{ field: 'department.name', headerName: 'Department', filter: 'agTextColumnFilter' },
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
function onGridReady(params: any) {
|
|
149
|
+
gridApi = params.api
|
|
150
|
+
|
|
151
|
+
const datasource = createAGGridDatasource(createRemoteFetcher(getUsersPaginated))
|
|
152
|
+
|
|
153
|
+
gridApi.setGridOption('serverSideDatasource', datasource)
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div class="ag-theme-alpine" style="height: 600px;">
|
|
158
|
+
<AgGridSvelte {columnDefs} rowModelType="serverSide" pagination={true} paginationPageSize={100} {onGridReady} />
|
|
159
|
+
</div>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API Reference
|
|
163
|
+
|
|
164
|
+
### `createAGGridQuery(config)`
|
|
165
|
+
|
|
166
|
+
Main function to create a server-side query handler.
|
|
167
|
+
|
|
168
|
+
**Parameters:**
|
|
169
|
+
|
|
170
|
+
- `config: AGGridQueryConfig<TRecord, TWhereInput>`
|
|
171
|
+
- `fetch: (params) => Promise<TRecord[]>` - Function to fetch data rows
|
|
172
|
+
- `count: (params) => Promise<number>` - Function to count total rows
|
|
173
|
+
- `computedFields?: ComputedField[]` - Optional computed field configurations
|
|
174
|
+
- `defaultSort?: Record<string, 'asc' | 'desc'>` - Optional default sort
|
|
175
|
+
- `transformWhere?: (where, request) => TWhereInput` - Optional WHERE clause transformer
|
|
176
|
+
|
|
177
|
+
**Returns:** `(request: AGGridRequest) => Promise<AGGridResponse<TRecord>>`
|
|
178
|
+
|
|
179
|
+
### `AGGridQueryParams`
|
|
180
|
+
|
|
181
|
+
The params object passed to `fetch` and `count` functions:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
interface AGGridQueryParams {
|
|
185
|
+
where: TWhereInput // WHERE clause for filtering
|
|
186
|
+
orderBy: Record<string, unknown>[] // ORDER BY for sorting
|
|
187
|
+
skip: number // Pagination offset
|
|
188
|
+
take: number // Pagination limit
|
|
189
|
+
groupLevel: number // Current group level (for grouping)
|
|
190
|
+
groupKeys: string[] // Group drill-down keys
|
|
191
|
+
groupColumn?: AGGridColumn // Column being grouped
|
|
192
|
+
isGroupRequest: boolean // Whether this is a group or leaf request
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `ComputedField`
|
|
197
|
+
|
|
198
|
+
Configuration for computed/virtual fields:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface ComputedField {
|
|
202
|
+
columnId: string // Column ID in AG Grid
|
|
203
|
+
dbField?: string // DB field path (e.g., 'location.name')
|
|
204
|
+
// 👇 Optional - only needed for complex computed logic
|
|
205
|
+
valueGetter?: (record) => unknown // Compute display value
|
|
206
|
+
filterHandler?: (filterValue, where) => void // Handle filtering
|
|
207
|
+
groupHandler?: (groupKey, where) => void // Handle grouping
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**✨ Auto-Magic for Nested Fields:**
|
|
212
|
+
|
|
213
|
+
- If `dbField` contains a `.` (e.g., `'location.name'`), the system automatically:
|
|
214
|
+
- Extracts nested values for display
|
|
215
|
+
- Handles text and set filters
|
|
216
|
+
- Handles sorting and grouping
|
|
217
|
+
- You only need `valueGetter`, `filterHandler`, and `groupHandler` for **complex computed logic**
|
|
218
|
+
|
|
219
|
+
## Common Patterns
|
|
220
|
+
|
|
221
|
+
### Pattern 1: Simple Nested Relations (Use Dot Notation!)
|
|
222
|
+
|
|
223
|
+
**Recommended:** Use dot notation directly in column definitions - no `computedFields` needed!
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Server-side - just include the relation
|
|
227
|
+
return await createAGGridQuery({
|
|
228
|
+
async fetch(params) {
|
|
229
|
+
return await DB.model.findMany({
|
|
230
|
+
where: params.where,
|
|
231
|
+
orderBy: params.orderBy,
|
|
232
|
+
skip: params.skip,
|
|
233
|
+
take: params.take,
|
|
234
|
+
include: { location: true, category: true, user: true },
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
async count(params) {
|
|
238
|
+
return await DB.model.count({ where: params.where })
|
|
239
|
+
},
|
|
240
|
+
})(request)
|
|
241
|
+
|
|
242
|
+
// Client-side - use dot notation in field
|
|
243
|
+
const columnDefs = [
|
|
244
|
+
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' },
|
|
245
|
+
{ field: 'category.name', headerName: 'Category', filter: 'agTextColumnFilter' },
|
|
246
|
+
{ field: 'user.email', headerName: 'User', filter: 'agTextColumnFilter' },
|
|
247
|
+
]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The system automatically handles:
|
|
251
|
+
|
|
252
|
+
- **Display**: AG Grid extracts `record.location.name` natively
|
|
253
|
+
- **Text Filter**: `where.location.name = { contains: 'X', mode: 'insensitive' }`
|
|
254
|
+
- **Set Filter**: `where.location.name = { in: ['A', 'B'] }`
|
|
255
|
+
- **Sorting**: `orderBy: { location: { name: 'asc' } }`
|
|
256
|
+
- **Grouping**: `where.location.name = 'groupKey'`
|
|
257
|
+
|
|
258
|
+
### Pattern 2: Complex Calculated Fields (Custom Handlers Required)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
computedFields: [
|
|
262
|
+
{
|
|
263
|
+
columnId: 'weekEnding',
|
|
264
|
+
valueGetter: (record) => {
|
|
265
|
+
const date = new Date(record.date)
|
|
266
|
+
const day = date.getDay()
|
|
267
|
+
date.setDate(date.getDate() + (7 - day))
|
|
268
|
+
return date.toISOString().split('T')[0]
|
|
269
|
+
},
|
|
270
|
+
filterHandler: (filterValue, where) => {
|
|
271
|
+
// Custom filter logic based on calculated value
|
|
272
|
+
const targetDate = new Date(filterValue.dateFrom)
|
|
273
|
+
const weekStart = new Date(targetDate)
|
|
274
|
+
weekStart.setDate(targetDate.getDate() - 6)
|
|
275
|
+
where.date = { gte: weekStart, lte: targetDate }
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Pattern 3: Custom WHERE Transform
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
transformWhere: (where, request) => {
|
|
285
|
+
// Add tenant filtering
|
|
286
|
+
where.tenantId = getCurrentTenantId()
|
|
287
|
+
|
|
288
|
+
// Add soft delete filter
|
|
289
|
+
where.deletedAt = null
|
|
290
|
+
|
|
291
|
+
return where
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Database Examples
|
|
296
|
+
|
|
297
|
+
### Prisma
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
{
|
|
301
|
+
async fetch(params) {
|
|
302
|
+
return await DB.model.findMany({
|
|
303
|
+
where: params.where,
|
|
304
|
+
orderBy: params.orderBy,
|
|
305
|
+
skip: params.skip,
|
|
306
|
+
take: params.take,
|
|
307
|
+
})
|
|
308
|
+
},
|
|
309
|
+
async count(params) {
|
|
310
|
+
return await DB.model.count({ where: params.where })
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Advanced Features
|
|
316
|
+
|
|
317
|
+
### Row Grouping
|
|
318
|
+
|
|
319
|
+
The API automatically handles row grouping. When `isGroupRequest` is `true`, it returns group rows with counts. When `false`, it returns leaf data.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
if (params.isGroupRequest) {
|
|
323
|
+
// Return groups at current level
|
|
324
|
+
} else {
|
|
325
|
+
// Return actual data rows
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Type Safety
|
|
330
|
+
|
|
331
|
+
Use TypeScript generics for full type safety:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
type UserWhereInput = {
|
|
335
|
+
id?: number
|
|
336
|
+
name?: { contains: string; mode: 'insensitive' }
|
|
337
|
+
email?: { contains: string; mode: 'insensitive' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
createAGGridQuery<User, UserWhereInput>({
|
|
341
|
+
// params.where is now typed as UserWhereInput
|
|
342
|
+
async fetch(params) { ... },
|
|
343
|
+
async count(params) { ... }
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Nested Field Magic Explained
|
|
348
|
+
|
|
349
|
+
When you use dot notation in field names (e.g., `'location.name'`), the system automatically handles everything:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Column definition
|
|
353
|
+
{ field: 'location.name', headerName: 'Location', filter: 'agTextColumnFilter' }
|
|
354
|
+
|
|
355
|
+
// Server-side - just include the relation
|
|
356
|
+
include: { location: true }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**What happens automatically:**
|
|
360
|
+
|
|
361
|
+
1. **Display**: AG Grid extracts `record.location.name` natively
|
|
362
|
+
2. **Text Filtering**: `where.location = { name: { contains: 'Office', mode: 'insensitive' } }`
|
|
363
|
+
3. **Set Filtering**: `where.location = { name: { in: ['Office A', 'Office B'] } }`
|
|
364
|
+
4. **Sorting**: `orderBy: [{ location: { name: 'asc' } }]`
|
|
365
|
+
5. **Grouping**: `where.location = { name: 'Office A' }`
|
|
366
|
+
|
|
367
|
+
**You write:** Column definition with dot notation
|
|
368
|
+
|
|
369
|
+
**You get:** All filtering, sorting, grouping, and display automatically! ✨
|
|
370
|
+
|
|
371
|
+
## See Also
|
|
372
|
+
|
|
373
|
+
- [AG Grid SSRM Documentation](https://www.ag-grid.com/javascript-data-grid/server-side-model/)
|
package/dist/Grid.svelte
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte'
|
|
3
|
-
import { env } from '$env/dynamic/public'
|
|
4
|
-
import {
|
|
5
|
-
AllEnterpriseModule,
|
|
6
|
-
ClientSideRowModelModule,
|
|
7
|
-
createGrid,
|
|
8
|
-
LicenseManager,
|
|
9
|
-
ModuleRegistry,
|
|
10
|
-
themeQuartz,
|
|
11
|
-
type GridApi,
|
|
12
|
-
type GridOptions,
|
|
13
|
-
} from 'ag-grid-enterprise'
|
|
14
|
-
|
|
15
|
-
type Props = {
|
|
16
|
-
gridEl?: HTMLDivElement
|
|
17
|
-
/** Bindable reference to the AG Grid API. Use `bind:gridApi` on the parent. */
|
|
18
|
-
gridApi?: GridApi
|
|
19
|
-
gridData?: any[] // Replace with your actual data type
|
|
20
|
-
gridOptions: GridOptions
|
|
21
|
-
/**
|
|
22
|
-
* localStorage key for automatic grid state persistence (filters, sorts, column widths).
|
|
23
|
-
* When set, state is saved on every change and restored on mount.
|
|
24
|
-
*/
|
|
25
|
-
stateKey?: string
|
|
26
|
-
class?: string
|
|
27
|
-
}
|
|
28
|
-
let {
|
|
29
|
-
gridEl = $bindable(),
|
|
30
|
-
gridApi = $bindable(),
|
|
31
|
-
gridData,
|
|
32
|
-
gridOptions,
|
|
33
|
-
stateKey,
|
|
34
|
-
class: gridClass = 'grow',
|
|
35
|
-
}: Props = $props()
|
|
36
|
-
let initCheckInterval: ReturnType<typeof setInterval> | undefined
|
|
37
|
-
let attemptCount = 0
|
|
38
|
-
|
|
39
|
-
function initializeGrid() {
|
|
40
|
-
if (!gridEl) {
|
|
41
|
-
console.log('⏳ Grid: Element not available yet')
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (gridApi) {
|
|
46
|
-
console.log('ℹ️ Grid: Already initialized, skipping')
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
attemptCount++
|
|
51
|
-
console.log(`📊 Grid: Initializing AG Grid (attempt ${attemptCount})...`)
|
|
52
|
-
console.log('📋 Grid: Registering modules...')
|
|
53
|
-
ModuleRegistry.registerModules([AllEnterpriseModule, ClientSideRowModelModule])
|
|
54
|
-
|
|
55
|
-
if (env.PUBLIC_AGGRID_KEY) {
|
|
56
|
-
LicenseManager.setLicenseKey(env.PUBLIC_AGGRID_KEY)
|
|
57
|
-
console.log('✅ Grid: License key applied successfully')
|
|
58
|
-
} else {
|
|
59
|
-
console.warn('⚠️ Grid: No license key found. Running in trial mode.')
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Wrap onGridReady to expose the gridApi bindable and handle stateKey persistence,
|
|
63
|
-
// while still calling the user's own onGridReady callback if provided.
|
|
64
|
-
const userOnGridReady = gridOptions.onGridReady
|
|
65
|
-
|
|
66
|
-
// Load saved state upfront — must be in gridConfig.initialState, not applied post-init.
|
|
67
|
-
let savedState: object | undefined
|
|
68
|
-
if (stateKey) {
|
|
69
|
-
try {
|
|
70
|
-
const raw = localStorage.getItem(stateKey)
|
|
71
|
-
if (raw) savedState = JSON.parse(raw)
|
|
72
|
-
} catch {
|
|
73
|
-
/* ignore parse errors */
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const gridConfig = {
|
|
78
|
-
...gridOptions,
|
|
79
|
-
theme: themeQuartz,
|
|
80
|
-
...(gridData !== undefined && { rowData: gridData }),
|
|
81
|
-
...(savedState !== undefined && { initialState: savedState }),
|
|
82
|
-
onGridReady: (params: Parameters<NonNullable<GridOptions['onGridReady']>>[0]) => {
|
|
83
|
-
userOnGridReady?.(params)
|
|
84
|
-
gridApi = params.api
|
|
85
|
-
if (stateKey) {
|
|
86
|
-
params.api.addEventListener('stateUpdated', (e: any) => {
|
|
87
|
-
try {
|
|
88
|
-
localStorage.setItem(stateKey, JSON.stringify(e.state))
|
|
89
|
-
} catch {
|
|
90
|
-
/* ignore storage errors */
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
console.log('🎨 Grid: Creating grid instance...')
|
|
98
|
-
gridApi = createGrid(gridEl, gridConfig)
|
|
99
|
-
|
|
100
|
-
if (gridData !== undefined) {
|
|
101
|
-
const rowCount = gridData.length
|
|
102
|
-
console.log(`✅ Grid: Initialized with ${rowCount} row(s) (client-side)`)
|
|
103
|
-
} else {
|
|
104
|
-
console.log('✅ Grid: Initialized with server-side data source')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Clear the interval once grid is created
|
|
108
|
-
if (initCheckInterval) {
|
|
109
|
-
console.log('⏹️ Grid: Stopping initialization checks')
|
|
110
|
-
clearInterval(initCheckInterval)
|
|
111
|
-
initCheckInterval = undefined
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
onMount(() => {
|
|
116
|
-
console.log('🚀 Grid: Component mounted')
|
|
117
|
-
|
|
118
|
-
// Try to initialize immediately
|
|
119
|
-
initializeGrid()
|
|
120
|
-
|
|
121
|
-
// If grid wasn't created, set up interval to keep checking
|
|
122
|
-
if (!gridApi) {
|
|
123
|
-
console.log('⏱️ Grid: Element not ready, checking every 100ms...')
|
|
124
|
-
initCheckInterval = setInterval(() => {
|
|
125
|
-
initializeGrid()
|
|
126
|
-
}, 100)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Cleanup function to destroy grid and clear interval when component unmounts
|
|
130
|
-
return () => {
|
|
131
|
-
console.log('💥 Grid: Component unmounting')
|
|
132
|
-
if (initCheckInterval) {
|
|
133
|
-
console.log('⏹️ Grid: Clearing initialization interval')
|
|
134
|
-
clearInterval(initCheckInterval)
|
|
135
|
-
initCheckInterval = undefined
|
|
136
|
-
}
|
|
137
|
-
if (gridApi) {
|
|
138
|
-
console.log('🧹 Grid: Destroying grid instance')
|
|
139
|
-
gridApi.destroy()
|
|
140
|
-
gridApi = undefined
|
|
141
|
-
console.log('✅ Grid: Cleanup complete')
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// Update grid when options or data change
|
|
147
|
-
$effect(() => {
|
|
148
|
-
if (gridApi && gridData !== undefined) {
|
|
149
|
-
const rowCount = gridData.length
|
|
150
|
-
console.log(`🔄 Grid: Data changed, updating grid with ${rowCount} row(s)`)
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
gridApi.updateGridOptions({
|
|
154
|
-
...gridOptions,
|
|
155
|
-
rowData: gridData,
|
|
156
|
-
})
|
|
157
|
-
gridApi.refreshCells()
|
|
158
|
-
console.log('✅ Grid: Data update complete')
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error('❌ Grid: Error updating data:', error)
|
|
161
|
-
}
|
|
162
|
-
} else if (!gridApi && gridData !== undefined) {
|
|
163
|
-
console.log('⚠️ Grid: Data available but grid not initialized yet')
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
</script>
|
|
167
|
-
|
|
168
|
-
<div bind:this={gridEl} class={gridClass}></div>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { env } from '$env/dynamic/public'
|
|
4
|
+
import {
|
|
5
|
+
AllEnterpriseModule,
|
|
6
|
+
ClientSideRowModelModule,
|
|
7
|
+
createGrid,
|
|
8
|
+
LicenseManager,
|
|
9
|
+
ModuleRegistry,
|
|
10
|
+
themeQuartz,
|
|
11
|
+
type GridApi,
|
|
12
|
+
type GridOptions,
|
|
13
|
+
} from 'ag-grid-enterprise'
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
gridEl?: HTMLDivElement
|
|
17
|
+
/** Bindable reference to the AG Grid API. Use `bind:gridApi` on the parent. */
|
|
18
|
+
gridApi?: GridApi
|
|
19
|
+
gridData?: any[] // Replace with your actual data type
|
|
20
|
+
gridOptions: GridOptions
|
|
21
|
+
/**
|
|
22
|
+
* localStorage key for automatic grid state persistence (filters, sorts, column widths).
|
|
23
|
+
* When set, state is saved on every change and restored on mount.
|
|
24
|
+
*/
|
|
25
|
+
stateKey?: string
|
|
26
|
+
class?: string
|
|
27
|
+
}
|
|
28
|
+
let {
|
|
29
|
+
gridEl = $bindable(),
|
|
30
|
+
gridApi = $bindable(),
|
|
31
|
+
gridData,
|
|
32
|
+
gridOptions,
|
|
33
|
+
stateKey,
|
|
34
|
+
class: gridClass = 'grow',
|
|
35
|
+
}: Props = $props()
|
|
36
|
+
let initCheckInterval: ReturnType<typeof setInterval> | undefined
|
|
37
|
+
let attemptCount = 0
|
|
38
|
+
|
|
39
|
+
function initializeGrid() {
|
|
40
|
+
if (!gridEl) {
|
|
41
|
+
console.log('⏳ Grid: Element not available yet')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (gridApi) {
|
|
46
|
+
console.log('ℹ️ Grid: Already initialized, skipping')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attemptCount++
|
|
51
|
+
console.log(`📊 Grid: Initializing AG Grid (attempt ${attemptCount})...`)
|
|
52
|
+
console.log('📋 Grid: Registering modules...')
|
|
53
|
+
ModuleRegistry.registerModules([AllEnterpriseModule, ClientSideRowModelModule])
|
|
54
|
+
|
|
55
|
+
if (env.PUBLIC_AGGRID_KEY) {
|
|
56
|
+
LicenseManager.setLicenseKey(env.PUBLIC_AGGRID_KEY)
|
|
57
|
+
console.log('✅ Grid: License key applied successfully')
|
|
58
|
+
} else {
|
|
59
|
+
console.warn('⚠️ Grid: No license key found. Running in trial mode.')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wrap onGridReady to expose the gridApi bindable and handle stateKey persistence,
|
|
63
|
+
// while still calling the user's own onGridReady callback if provided.
|
|
64
|
+
const userOnGridReady = gridOptions.onGridReady
|
|
65
|
+
|
|
66
|
+
// Load saved state upfront — must be in gridConfig.initialState, not applied post-init.
|
|
67
|
+
let savedState: object | undefined
|
|
68
|
+
if (stateKey) {
|
|
69
|
+
try {
|
|
70
|
+
const raw = localStorage.getItem(stateKey)
|
|
71
|
+
if (raw) savedState = JSON.parse(raw)
|
|
72
|
+
} catch {
|
|
73
|
+
/* ignore parse errors */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gridConfig = {
|
|
78
|
+
...gridOptions,
|
|
79
|
+
theme: themeQuartz,
|
|
80
|
+
...(gridData !== undefined && { rowData: gridData }),
|
|
81
|
+
...(savedState !== undefined && { initialState: savedState }),
|
|
82
|
+
onGridReady: (params: Parameters<NonNullable<GridOptions['onGridReady']>>[0]) => {
|
|
83
|
+
userOnGridReady?.(params)
|
|
84
|
+
gridApi = params.api
|
|
85
|
+
if (stateKey) {
|
|
86
|
+
params.api.addEventListener('stateUpdated', (e: any) => {
|
|
87
|
+
try {
|
|
88
|
+
localStorage.setItem(stateKey, JSON.stringify(e.state))
|
|
89
|
+
} catch {
|
|
90
|
+
/* ignore storage errors */
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('🎨 Grid: Creating grid instance...')
|
|
98
|
+
gridApi = createGrid(gridEl, gridConfig)
|
|
99
|
+
|
|
100
|
+
if (gridData !== undefined) {
|
|
101
|
+
const rowCount = gridData.length
|
|
102
|
+
console.log(`✅ Grid: Initialized with ${rowCount} row(s) (client-side)`)
|
|
103
|
+
} else {
|
|
104
|
+
console.log('✅ Grid: Initialized with server-side data source')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Clear the interval once grid is created
|
|
108
|
+
if (initCheckInterval) {
|
|
109
|
+
console.log('⏹️ Grid: Stopping initialization checks')
|
|
110
|
+
clearInterval(initCheckInterval)
|
|
111
|
+
initCheckInterval = undefined
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onMount(() => {
|
|
116
|
+
console.log('🚀 Grid: Component mounted')
|
|
117
|
+
|
|
118
|
+
// Try to initialize immediately
|
|
119
|
+
initializeGrid()
|
|
120
|
+
|
|
121
|
+
// If grid wasn't created, set up interval to keep checking
|
|
122
|
+
if (!gridApi) {
|
|
123
|
+
console.log('⏱️ Grid: Element not ready, checking every 100ms...')
|
|
124
|
+
initCheckInterval = setInterval(() => {
|
|
125
|
+
initializeGrid()
|
|
126
|
+
}, 100)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Cleanup function to destroy grid and clear interval when component unmounts
|
|
130
|
+
return () => {
|
|
131
|
+
console.log('💥 Grid: Component unmounting')
|
|
132
|
+
if (initCheckInterval) {
|
|
133
|
+
console.log('⏹️ Grid: Clearing initialization interval')
|
|
134
|
+
clearInterval(initCheckInterval)
|
|
135
|
+
initCheckInterval = undefined
|
|
136
|
+
}
|
|
137
|
+
if (gridApi) {
|
|
138
|
+
console.log('🧹 Grid: Destroying grid instance')
|
|
139
|
+
gridApi.destroy()
|
|
140
|
+
gridApi = undefined
|
|
141
|
+
console.log('✅ Grid: Cleanup complete')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Update grid when options or data change
|
|
147
|
+
$effect(() => {
|
|
148
|
+
if (gridApi && gridData !== undefined) {
|
|
149
|
+
const rowCount = gridData.length
|
|
150
|
+
console.log(`🔄 Grid: Data changed, updating grid with ${rowCount} row(s)`)
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
gridApi.updateGridOptions({
|
|
154
|
+
...gridOptions,
|
|
155
|
+
rowData: gridData,
|
|
156
|
+
})
|
|
157
|
+
gridApi.refreshCells()
|
|
158
|
+
console.log('✅ Grid: Data update complete')
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('❌ Grid: Error updating data:', error)
|
|
161
|
+
}
|
|
162
|
+
} else if (!gridApi && gridData !== undefined) {
|
|
163
|
+
console.log('⚠️ Grid: Data available but grid not initialized yet')
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<div bind:this={gridEl} class={gridClass}></div>
|
package/dist/Select.svelte
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
multiple?: boolean
|
|
28
28
|
error?: string
|
|
29
29
|
hideOptional?: boolean
|
|
30
|
+
dropdownMinWidth?: string
|
|
30
31
|
zodErrors?: {
|
|
31
32
|
expected: string
|
|
32
33
|
code: string
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
placeholder = 'Select an item...',
|
|
51
52
|
error,
|
|
52
53
|
hideOptional,
|
|
54
|
+
dropdownMinWidth,
|
|
53
55
|
zodErrors,
|
|
54
56
|
onchange,
|
|
55
57
|
}: Props = $props()
|
|
@@ -485,6 +487,7 @@
|
|
|
485
487
|
<button
|
|
486
488
|
type="button"
|
|
487
489
|
popovertarget={popoverId}
|
|
490
|
+
popovertargetaction="show"
|
|
488
491
|
role="combobox"
|
|
489
492
|
aria-expanded={dropdownOpen}
|
|
490
493
|
aria-haspopup="listbox"
|
|
@@ -576,7 +579,7 @@
|
|
|
576
579
|
role="listbox"
|
|
577
580
|
inert={!dropdownOpen}
|
|
578
581
|
class="dropdown menu bg-base-100 rounded-box z-50 m-0 flex flex-col flex-nowrap gap-1 p-2 shadow outline"
|
|
579
|
-
style="position-anchor: {anchorName}; position: fixed; top: anchor(bottom); left: anchor(left); width: anchor-size(width); margin-block: 0.5rem; position-try-fallbacks: flip-block;"
|
|
582
|
+
style="position-anchor: {anchorName}; position: fixed; top: anchor(bottom); left: anchor(left); width: anchor-size(width);{dropdownMinWidth ? ` min-width: ${dropdownMinWidth};` : ''} margin-block: 0.5rem; position-try-fallbacks: flip-block;"
|
|
580
583
|
ontoggle={handlePopoverToggle}>
|
|
581
584
|
{#if multiple && filteredItems.length > 1}
|
|
582
585
|
<!-- Select All / Clear All options for multi-select -->
|
package/dist/Select.svelte.d.ts
CHANGED
package/dist/TextArea.svelte
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import Label from './Label.svelte'
|
|
3
|
-
|
|
4
|
-
type Props = {
|
|
5
|
-
value?: any
|
|
6
|
-
name?: string
|
|
7
|
-
label?: string
|
|
8
|
-
class?: string
|
|
9
|
-
required?: boolean
|
|
10
|
-
disabled?: boolean
|
|
11
|
-
element?: HTMLElement
|
|
12
|
-
error?: string
|
|
13
|
-
hideOptional?: boolean
|
|
14
|
-
zodErrors?: {
|
|
15
|
-
expected: string
|
|
16
|
-
code: string
|
|
17
|
-
path: string[]
|
|
18
|
-
message: string
|
|
19
|
-
}[]
|
|
20
|
-
[x: string]: any
|
|
21
|
-
}
|
|
22
|
-
let {
|
|
23
|
-
value = $bindable(),
|
|
24
|
-
element = $bindable(),
|
|
25
|
-
label,
|
|
26
|
-
name,
|
|
27
|
-
required,
|
|
28
|
-
disabled,
|
|
29
|
-
class: myClass,
|
|
30
|
-
error,
|
|
31
|
-
hideOptional,
|
|
32
|
-
zodErrors,
|
|
33
|
-
...rest
|
|
34
|
-
}: Props = $props()
|
|
35
|
-
const errorText = $derived.by(() => {
|
|
36
|
-
if (error) return error
|
|
37
|
-
if (!name) return undefined
|
|
38
|
-
if (zodErrors) return zodErrors.find((e) => e.path.includes(name))?.message
|
|
39
|
-
return undefined
|
|
40
|
-
})
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<Label class={myClass} {label} {name} optional={!required && !hideOptional} {disabled} error={errorText}>
|
|
44
|
-
<textarea bind:this={element} {disabled} {name} {required} class="textarea w-full" {...rest} bind:value></textarea>
|
|
45
|
-
</Label>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Label from './Label.svelte'
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value?: any
|
|
6
|
+
name?: string
|
|
7
|
+
label?: string
|
|
8
|
+
class?: string
|
|
9
|
+
required?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
element?: HTMLElement
|
|
12
|
+
error?: string
|
|
13
|
+
hideOptional?: boolean
|
|
14
|
+
zodErrors?: {
|
|
15
|
+
expected: string
|
|
16
|
+
code: string
|
|
17
|
+
path: string[]
|
|
18
|
+
message: string
|
|
19
|
+
}[]
|
|
20
|
+
[x: string]: any
|
|
21
|
+
}
|
|
22
|
+
let {
|
|
23
|
+
value = $bindable(),
|
|
24
|
+
element = $bindable(),
|
|
25
|
+
label,
|
|
26
|
+
name,
|
|
27
|
+
required,
|
|
28
|
+
disabled,
|
|
29
|
+
class: myClass,
|
|
30
|
+
error,
|
|
31
|
+
hideOptional,
|
|
32
|
+
zodErrors,
|
|
33
|
+
...rest
|
|
34
|
+
}: Props = $props()
|
|
35
|
+
const errorText = $derived.by(() => {
|
|
36
|
+
if (error) return error
|
|
37
|
+
if (!name) return undefined
|
|
38
|
+
if (zodErrors) return zodErrors.find((e) => e.path.includes(name))?.message
|
|
39
|
+
return undefined
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<Label class={myClass} {label} {name} optional={!required && !hideOptional} {disabled} error={errorText}>
|
|
44
|
+
<textarea bind:this={element} {disabled} {name} {required} class="textarea w-full" {...rest} bind:value></textarea>
|
|
45
|
+
</Label>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simplesvelte",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "bun vite dev",
|
|
6
6
|
"build": "bun vite build && bun run prepack",
|
|
@@ -65,5 +65,9 @@
|
|
|
65
65
|
},
|
|
66
66
|
"keywords": [
|
|
67
67
|
"svelte"
|
|
68
|
-
]
|
|
68
|
+
],
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/derekhearst/SimpleSvelte"
|
|
72
|
+
}
|
|
69
73
|
}
|