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 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 package
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>
@@ -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 -->
@@ -21,6 +21,7 @@ type Props = {
21
21
  multiple?: boolean;
22
22
  error?: string;
23
23
  hideOptional?: boolean;
24
+ dropdownMinWidth?: string;
24
25
  zodErrors?: {
25
26
  expected: string;
26
27
  code: string;
@@ -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.0",
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
  }