ag-grid-url-sync 0.5.0 โ†’ 0.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.
Files changed (83) hide show
  1. package/README.md +339 -66
  2. package/dist/core/ag-grid-url-sync.d.ts +16 -9
  3. package/dist/core/ag-grid-url-sync.js +23 -19
  4. package/dist/core/ag-grid-url-sync.js.map +1 -1
  5. package/dist/core/codecs.d.ts +28 -0
  6. package/dist/core/codecs.js +39 -0
  7. package/dist/core/codecs.js.map +1 -0
  8. package/dist/core/data-issue-codes.d.ts +3 -0
  9. package/dist/core/data-issue-codes.js +10 -0
  10. package/dist/core/data-issue-codes.js.map +1 -0
  11. package/dist/core/grid-integration.d.ts +38 -2
  12. package/dist/core/grid-integration.js +169 -128
  13. package/dist/core/grid-integration.js.map +1 -1
  14. package/dist/core/grid-url-sync.d.ts +62 -0
  15. package/dist/core/grid-url-sync.js +738 -0
  16. package/dist/core/grid-url-sync.js.map +1 -0
  17. package/dist/core/index.d.ts +4 -1
  18. package/dist/core/index.js +4 -1
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/individual-params.d.ts +12 -5
  21. package/dist/core/individual-params.js +38 -62
  22. package/dist/core/individual-params.js.map +1 -1
  23. package/dist/core/issue-columns.d.ts +2 -0
  24. package/dist/core/issue-columns.js +11 -0
  25. package/dist/core/issue-columns.js.map +1 -0
  26. package/dist/core/issues.d.ts +34 -0
  27. package/dist/core/issues.js +51 -0
  28. package/dist/core/issues.js.map +1 -0
  29. package/dist/core/operators.d.ts +189 -0
  30. package/dist/core/operators.js +60 -0
  31. package/dist/core/operators.js.map +1 -0
  32. package/dist/core/options.d.ts +21 -0
  33. package/dist/core/options.js +21 -0
  34. package/dist/core/options.js.map +1 -0
  35. package/dist/core/own-write.d.ts +138 -0
  36. package/dist/core/own-write.js +133 -0
  37. package/dist/core/own-write.js.map +1 -0
  38. package/dist/core/schemas.d.ts +51 -0
  39. package/dist/core/schemas.js +59 -0
  40. package/dist/core/schemas.js.map +1 -0
  41. package/dist/core/serialization/formats.d.ts +7 -7
  42. package/dist/core/serialization/formats.js +34 -28
  43. package/dist/core/serialization/formats.js.map +1 -1
  44. package/dist/core/serialization/grouped.d.ts +9 -12
  45. package/dist/core/serialization/grouped.js +24 -17
  46. package/dist/core/serialization/grouped.js.map +1 -1
  47. package/dist/core/to-error.d.ts +5 -0
  48. package/dist/core/to-error.js +4 -0
  49. package/dist/core/to-error.js.map +1 -0
  50. package/dist/core/types.d.ts +296 -43
  51. package/dist/core/types.js +39 -108
  52. package/dist/core/types.js.map +1 -1
  53. package/dist/core/url-generator.d.ts +4 -4
  54. package/dist/core/url-generator.js +9 -7
  55. package/dist/core/url-generator.js.map +1 -1
  56. package/dist/core/url-parser.d.ts +4 -4
  57. package/dist/core/url-parser.js +6 -4
  58. package/dist/core/url-parser.js.map +1 -1
  59. package/dist/core/validation.d.ts +21 -14
  60. package/dist/core/validation.js +58 -54
  61. package/dist/core/validation.js.map +1 -1
  62. package/dist/core/view-storage.js +21 -19
  63. package/dist/core/view-storage.js.map +1 -1
  64. package/dist/index.d.ts +4 -1
  65. package/dist/index.js +4 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/react/index.d.ts +3 -2
  68. package/dist/react/index.js +1 -0
  69. package/dist/react/index.js.map +1 -1
  70. package/dist/react/sync-holder.d.ts +22 -0
  71. package/dist/react/sync-holder.js +68 -0
  72. package/dist/react/sync-holder.js.map +1 -0
  73. package/dist/react/sync-options.d.ts +17 -0
  74. package/dist/react/sync-options.js +22 -0
  75. package/dist/react/sync-options.js.map +1 -0
  76. package/dist/react/types.d.ts +101 -14
  77. package/dist/react/use-ag-grid-url-sync.d.ts +3 -0
  78. package/dist/react/use-ag-grid-url-sync.js +344 -499
  79. package/dist/react/use-ag-grid-url-sync.js.map +1 -1
  80. package/dist/react/use-grid-url-sync.d.ts +14 -0
  81. package/dist/react/use-grid-url-sync.js +206 -0
  82. package/dist/react/use-grid-url-sync.js.map +1 -0
  83. package/package.json +35 -15
package/README.md CHANGED
@@ -145,7 +145,7 @@ The library provides two main entry points:
145
145
 
146
146
  ```typescript
147
147
  // Core functionality - works with any framework
148
- import { createUrlSync } from 'ag-grid-url-sync'
148
+ import { createGridUrlSync } from 'ag-grid-url-sync'
149
149
 
150
150
  // React hook - for React applications
151
151
  import { useAGGridUrlSync } from 'ag-grid-url-sync/react'
@@ -195,16 +195,13 @@ import type {
195
195
 
196
196
  ```typescript
197
197
  import { parseUrlFilters } from 'ag-grid-url-sync/url-parser'
198
- import { DEFAULT_CONFIG } from 'ag-grid-url-sync/validation'
199
- import type { FilterState, ParseConfig } from 'ag-grid-url-sync/types'
198
+ import type { FilterState } from 'ag-grid-url-sync/types'
200
199
 
201
200
  function getFiltersFromUrl(url: string): FilterState {
202
- const config: ParseConfig = {
203
- ...DEFAULT_CONFIG, // gridApi is not needed for parsing
201
+ // Options that you leave out get their defaults. No grid is needed.
202
+ return parseUrlFilters(url, {
204
203
  onParseError: error => console.error('Parse error:', error)
205
- }
206
-
207
- return parseUrlFilters(url, config)
204
+ })
208
205
  }
209
206
  ```
210
207
 
@@ -227,22 +224,78 @@ function getFiltersFromUrl(url: string): FilterState {
227
224
 
228
225
  ```typescript
229
226
  import { generateUrl } from 'ag-grid-url-sync/url-generator'
230
- import { DEFAULT_CONFIG } from 'ag-grid-url-sync/validation'
231
- import type { FilterState, ParseConfig } from 'ag-grid-url-sync/types'
227
+ import type { FilterState } from 'ag-grid-url-sync/types'
232
228
 
233
229
  function createShareableUrl(filters: FilterState, baseUrl: string): string {
234
- const config: ParseConfig = {
235
- ...DEFAULT_CONFIG, // gridApi is not needed for generation
236
- maxValueLength: 500
237
- }
238
-
239
- return generateUrl(baseUrl, filters, config)
230
+ return generateUrl(baseUrl, filters, { maxValueLength: 500 })
240
231
  }
241
232
  ```
242
233
 
234
+ #### Example: Read and Write the Grid Filters
235
+
236
+ ```typescript
237
+ import {
238
+ getFilterModel,
239
+ applyFilterModel
240
+ } from 'ag-grid-url-sync/grid-integration'
241
+
242
+ const filters = getFilterModel(gridApi, { onParseError: console.warn })
243
+ applyFilterModel(filters, otherGridApi)
244
+ ```
245
+
246
+ The config form (`getFilterModel({ ...DEFAULT_CONFIG, gridApi })`) still works, but it is deprecated.
247
+
243
248
  **๐Ÿ’ก Pro Tip**: Use granular imports in libraries and applications where bundle size matters. All existing import patterns remain fully supported for backward compatibility.
244
249
 
245
- ### Basic Example
250
+ ### Vanilla JS: `createGridUrlSync`
251
+
252
+ One call gives full sync. `createGridUrlSync` applies the address bar's filters to the grid, follows the grid's filter changes, can write them to the address bar, and removes its listeners when the grid is destroyed.
253
+
254
+ ```typescript
255
+ import { createGridUrlSync, individual, grouped } from 'ag-grid-url-sync'
256
+
257
+ const sync = createGridUrlSync({
258
+ api: gridApi,
259
+ codec: individual({ prefix: 'f_' }), // or grouped({ param: 'gf', format: 'json' })
260
+ history: 'replace', // 'none' (default) | 'replace' | 'push' | (url) => router.replace(url)
261
+ onIssue: ({ issue, source }) => {
262
+ if (issue.severity === 'warning') showToast(issue.message)
263
+ }
264
+ })
265
+
266
+ // The current state: { status, url, hasFilters, issues, views, activeViewId }
267
+ const { url, hasFilters } = sync.getSnapshot()
268
+
269
+ // Called after each change of the snapshot. Returns an unsubscribe function.
270
+ const unsubscribe = sync.subscribe(() => render(sync.getSnapshot()))
271
+
272
+ sync.applyUrl('https://app.com/data?f_name_contains=jo') // { issues }
273
+ sync.getUrl('https://app.com/share') // a share link for the grid's filters
274
+ sync.applyFilters({
275
+ name: { filterType: 'text', type: 'contains', filter: 'jo' }
276
+ }) // { issues }
277
+ sync.destroy()
278
+ ```
279
+
280
+ `applyFilters` writes filters to the grid as the app's own write. When they land, the sync writes the URL in the `history` mode, as it does for a user edit. `applyUrl` writes no history, because the URL already holds those filters.
281
+
282
+ | Option | Default | Meaning |
283
+ | ------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
284
+ | `api` | (required) | The AG Grid API |
285
+ | `codec` | `individual()` | How the filters are written in the URL. `individual({ prefix })` writes one param for each filter; `grouped({ param, format })` writes all filters in one param |
286
+ | `maxValueLength` | `200` | The longest text value that is kept |
287
+ | `history` | `'none'` | How a filter change goes to the address bar. `'replace'` and `'push'` use `window.history`; a function gets the URL, for a router. The page's other params, the `#hash` and `history.state` stay |
288
+ | `popstate` | `true` only with `'push'` | Apply the URL when the back or forward button changes it |
289
+ | `applyUrlOnCreate` | `true` | Apply the address bar's filters on create. A URL with no filter params keeps the grid's filters (for example from `initialState`), and with `history` they are written to the URL with replace |
290
+ | `views` | none | The saved views. Give `createViewStore(key)` from `ag-grid-url-sync/view-storage`. See [Saved Views](#saved-views) |
291
+ | `throwOnError` | `false` | Throw an issue instead of reporting it, in the methods that the app calls (`applyUrl`, `getUrl`, `applyFilters`, `saveView`, `loadView`, `deleteView`). `createGridUrlSync` and its listeners never throw |
292
+ | `onIssue` | none | Gets each issue one time, as `{ issue, source }`. `source` is `'url'` (the sync read the address bar), `'grid'` (it read the grid) or `'api'` (a method that the app called) |
293
+
294
+ The snapshot `status` is `'active'` from create, and `'destroyed'` after `destroy()` or after the grid is destroyed. After that, `applyUrl`, `applyFilters`, `saveView` and `loadView` report `NOT_READY` to `onIssue`. `deleteView` needs only storage, so it still works. `IDLE_SNAPSHOT` (`status: 'idle'`) is the snapshot for a time when there is no sync yet. `getSnapshot()` returns the same object until something changes, so it works with React's `useSyncExternalStore`. The sync writes no history for an `applyUrl` apply, because the URL holds those filters already. `applyFilters` and `loadView` write the URL as a user edit does. Nothing reads `window` when the module loads, and a sync made with no `window` does nothing to the address bar.
295
+
296
+ ### Basic Example (deprecated `createUrlSync`)
297
+
298
+ `createUrlSync` and `AGGridUrlSync` will be removed in 0.6.0. Use `createGridUrlSync`.
246
299
 
247
300
  ```typescript
248
301
  import { createUrlSync } from 'ag-grid-url-sync'
@@ -435,38 +488,83 @@ import {
435
488
 
436
489
  ---
437
490
 
438
- ### Error Handling for Grouped Serialization
491
+ ### Error Handling
492
+
493
+ The library never writes to the console. Each problem goes to one callback, and each error that the library makes has a `code` that you can switch on:
494
+
495
+ | Code | Meaning | Callback |
496
+ | ------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
497
+ | `INVALID_PARAM` | A param name that is not valid, or an unknown operation | `onParseError` |
498
+ | `INVALID_NUMBER` | A value that is not a number, or that would be rounded | `onParseError` |
499
+ | `INVALID_DATE` | A date that does not exist, or a date range in the wrong order | `onParseError` |
500
+ | `VALUE_TOO_LONG` | A text value over `maxValueLength` | `onParseError` |
501
+ | `INVALID_GROUPED` | A grouped value that does not decode (bad JSON or base64) | `onParseError` |
502
+ | `TYPE_MISMATCH` | A number or date filter on a column of another type | `onParseError` |
503
+ | `INVALID_URL` | A URL that the parser cannot read | Thrown by `parseUrlFilters` and `applyFromUrl`. The React hook catches it and sends it to `onParseError` |
504
+ | `GRID_ERROR` | The AG Grid API threw | `onParseError` in the core, `onError` in the React hook |
505
+ | `NOT_READY` | A hook operation called before the grid is ready | `onError` (React) |
506
+ | `VIEW_NOT_FOUND`, `VIEW_NAME_REQUIRED` | A saved view operation that cannot run | `onError` (React) |
507
+ | `STORAGE_UNAVAILABLE`, `STORAGE_FULL`, `STORAGE_WRITE_FAILED` | Browser storage for saved views failed | `onError` (React) |
508
+ | `HISTORY_WRITE_FAILED` | The sync could not write the address bar | `onIssue` in the core, `onError` in the React hook |
439
509
 
440
- You can provide a custom error handler to gracefully handle malformed grouped parameters or serialization errors. In grouped mode, `onParseError` receives an `InvalidSerializationError` for invalid formats:
510
+ Each error has a `severity`: `'warning'` for the first seven rows, `'error'` for the others. `createGridUrlSync` sends every issue to `onIssue`; the callbacks in the table are for the deprecated API and the React hook.
511
+
512
+ The codes in the first seven rows are data issues: a filter that is dropped or is not valid. The library skips that filter and keeps the others. When the library knows the column, it is in `error.column`. `onParseError` is typed with `Error`, so check `instanceof URLSyncError` before you read `code` or `column`.
513
+
514
+ Every error is a `URLSyncError`. The classes from earlier versions (`InvalidFilterError`, `InvalidDateError`, `InvalidURLError`, `InvalidSerializationError`) are subclasses of it, so an `instanceof` check still works.
441
515
 
442
516
  ```typescript
443
- import { InvalidSerializationError } from 'ag-grid-url-sync/types'
517
+ import { createUrlSync, URLSyncError } from 'ag-grid-url-sync'
444
518
 
445
519
  const urlSync = createUrlSync(gridApi, {
446
- serialization: 'grouped',
447
- format: 'json',
448
520
  onParseError: error => {
449
- if (error instanceof InvalidSerializationError) {
450
- // Handle grouped serialization error
451
- alert('Invalid filter data in URL!')
452
- } else {
453
- // Handle other errors
454
- console.error(error)
521
+ if (error instanceof URLSyncError && error.code === 'VALUE_TOO_LONG') {
522
+ showToast(`The link leaves out the filter on ${error.column}`)
455
523
  }
456
524
  }
457
525
  })
458
526
  ```
459
527
 
528
+ In grouped mode, an error that comes from the grouped URL value is an `InvalidSerializationError`, and the original error is in `error.cause`. When the original error is an issue with one filter (for example a value over `maxValueLength`), the wrapper has the same `code` and `column`. So a check on `error.code` works the same in both modes. `INVALID_GROUPED` means that the grouped value itself does not decode.
529
+
460
530
  In every grouped format (`querystring`, `json`, `base64`), a text value over `maxValueLength` (default 200) is dropped and reported through `onParseError`, both when the URL is generated and when it is parsed.
461
531
 
462
- In grouped mode, an error that comes from the grouped URL value is an `InvalidSerializationError`. This includes an error for one bad parameter inside a grouped querystring value, such as a value over `maxValueLength`. The original error is in `error.cause`. In individual mode, the same failure arrives as the original error, with no wrapper. To handle a specific error in both modes, check the error and its cause:
532
+ #### Dropped Filters in the Result
533
+
534
+ A caller does not need a callback to see dropped filters. `getUrlState()` and the apply methods return the issues of each call. They are the same objects that `onParseError` gets.
463
535
 
464
536
  ```typescript
465
- import { InvalidFilterError } from 'ag-grid-url-sync/types'
537
+ const { url, hasFilters, issues } = urlSync.getUrlState()
466
538
 
467
- const isFilterError = (error: Error) =>
468
- error instanceof InvalidFilterError ||
469
- error.cause instanceof InvalidFilterError
539
+ if (issues.length > 0) {
540
+ // The link leaves out these filters, so the recipient sees other rows
541
+ showToast(`The link leaves out ${issues.length} filter(s)`)
542
+ }
543
+
544
+ const { issues: urlIssues } = urlSync.applyFromUrl()
545
+ ```
546
+
547
+ In React, the hook returns `issues`: the issues of the latest URL apply (`applyUrlFilters`, `applyFilters`, auto-apply), followed by the issues of the latest URL write. The apply issues stay until the next apply, `clearFilters` or `loadView`. Operation failures (`onError`) are not in the list:
548
+
549
+ ```tsx
550
+ const { shareUrl, issues } = useAGGridUrlSync(gridApi)
551
+
552
+ return issues.length > 0 ? (
553
+ <Warning>The link leaves out a filter</Warning>
554
+ ) : null
555
+ ```
556
+
557
+ #### `throwOnError`
558
+
559
+ Like TanStack Query, the library can throw an issue instead of reporting it. Set `throwOnError: true` to throw every issue, or give a function that picks issues. In the core, a thrown issue does not go to `onParseError`, and the first one stops the call. In React, the hook keeps reporting each issue to `onParseError` one time, and then throws the picked issue during render, so an error boundary catches it. The default is `false`.
560
+
561
+ Only `createGridUrlSync` or the hook with `history` changes the address bar. Before an error boundary resets, remove the bad param from the URL (or the filter from the grid), or the same issue is thrown again.
562
+
563
+ ```tsx
564
+ useAGGridUrlSync(gridApi, {
565
+ // Throw only when a link would leave out a filter
566
+ throwOnError: issue => issue.code === 'VALUE_TOO_LONG'
567
+ })
470
568
  ```
471
569
 
472
570
  ### Exports
@@ -486,7 +584,7 @@ Refer to the [package.json](./package.json) for the full export map.
486
584
 
487
585
  ## React Integration
488
586
 
489
- For React applications, use the dedicated `useAGGridUrlSync` hook that provides a complete solution with React-specific features and state management.
587
+ For React applications, use the `useGridUrlSync` hook. It is a thin adapter on [`createGridUrlSync`](#vanilla-js-creategridurlsync): it takes the same options, and the grid behaves as with the vanilla sync. The older `useAGGridUrlSync` hook is deprecated and will be removed in 0.6.0 (see [Migrating from `useAGGridUrlSync`](#migrating-from-useaggridurlsync)).
490
588
 
491
589
  ### Installation for React
492
590
 
@@ -494,7 +592,90 @@ For React applications, use the dedicated `useAGGridUrlSync` hook that provides
494
592
  npm install ag-grid-url-sync
495
593
  ```
496
594
 
497
- ### Basic React Usage
595
+ The hook works with React 16.8 and later. It uses the `use-sync-external-store` package, which npm installs with the library. On React 18 and later, that package uses React's own `useSyncExternalStore`.
596
+
597
+ ### `useGridUrlSync({ api, ...options })`
598
+
599
+ ```tsx
600
+ import { useState } from 'react'
601
+ import { AgGridReact } from 'ag-grid-react'
602
+ import type { GridApi } from 'ag-grid-community'
603
+ import { useGridUrlSync } from 'ag-grid-url-sync/react'
604
+ import { individual } from 'ag-grid-url-sync'
605
+
606
+ function GridComponent() {
607
+ const [api, setApi] = useState<GridApi | null>(null)
608
+
609
+ const { status, hasFilters, issues, getUrl, applyFilters } = useGridUrlSync({
610
+ api,
611
+ codec: individual({ prefix: 'filter_' }),
612
+ history: 'push', // each filter change adds a history entry; back and forward apply the URL
613
+ onIssue: ({ issue, source }) => console.warn(source, issue.code)
614
+ })
615
+
616
+ return (
617
+ <div>
618
+ <button
619
+ disabled={status !== 'active'}
620
+ onClick={() => navigator.clipboard.writeText(getUrl())}
621
+ >
622
+ Share filters
623
+ </button>
624
+ <button disabled={!hasFilters} onClick={() => applyFilters({})}>
625
+ Clear filters
626
+ </button>
627
+ {issues.length > 0 && (
628
+ <p>The link leaves out {issues.length} filter(s)</p>
629
+ )}
630
+ <AgGridReact onGridReady={params => setApi(params.api)} {...gridProps} />
631
+ </div>
632
+ )
633
+ }
634
+ ```
635
+
636
+ The options are the [`createGridUrlSync` options](#vanilla-js-creategridurlsync) (`codec`, `maxValueLength`, `history`, `popstate`, `applyUrlOnCreate`, `throwOnError`, `onIssue`), with these changes:
637
+
638
+ | Option | Default | Description |
639
+ | -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
640
+ | `api` | none | The AG Grid API, or `null` while the grid starts |
641
+ | `storageKey` | none | Enables saved views, kept in `localStorage` under this key (in place of the core's `views` store). With no key, the view methods do nothing |
642
+ | `enabled` | `true` | With `false`, the hook makes no sync: `status` is `'idle'`, and a method reports `NOT_READY`. `views` still shows what is in storage. Back to `true` starts again, as a new sync does: the URL is applied again |
643
+ | `throwOnError` | `false` | As in TanStack Query: the hook throws a picked issue during render, so an error boundary catches it. The methods do not throw |
644
+
645
+ The hook returns the snapshot and the methods of the sync:
646
+
647
+ | Property | Description |
648
+ | ------------------------------------ | -------------------------------------------------------------------------------- |
649
+ | `status` | `'idle'` (no grid, no sync yet, or a server render), `'active'` or `'destroyed'` |
650
+ | `url`, `hasFilters`, `issues` | The URL with the grid's filters, whether there are filters, the latest issues |
651
+ | `views`, `activeViewId` | The saved views and the active one |
652
+ | `applyUrl(url?)`, `applyFilters(f)` | Apply a URL or filters. They return `{ issues }` |
653
+ | `getUrl(baseUrl?)` | The URL with the grid's filters, for example to share |
654
+ | `saveView`, `loadView`, `deleteView` | The saved view methods of the sync |
655
+
656
+ - **The core's rules hold.** On mount, a URL with filters wins over the stored view, a URL with no filters keeps the grid's filters, and a URL apply writes no history.
657
+ - **Options can change after mount.** A new codec, `maxValueLength`, history mode, `popstate` or `storageKey` makes a new sync on the same grid. It does not apply the URL again, and it keeps the active view. A new `storageKey` restores that store's view. A new codec object with the same settings, a new `onIssue` or a new `history` function makes no new sync.
658
+ - **The result is stable.** It is the same object until a snapshot value changes, and the methods never change.
659
+ - **A method with no sync** (no grid yet, `enabled: false`): `applyUrl` and `applyFilters` report `NOT_READY` to `onIssue`, and so do `saveView` and `loadView` with a `storageKey`. `getUrl` returns the base URL. `deleteView` still works with storage only.
660
+ - **Issues stay across a new sync.** After an option change or a `StrictMode` remount, `issues` still shows the latest apply's issues until the next apply. `onIssue` gets each issue one time.
661
+ - `StrictMode`, React's `<Activity>` and server rendering are supported.
662
+
663
+ ### Migrating from `useAGGridUrlSync`
664
+
665
+ | `useAGGridUrlSync` | `useGridUrlSync` |
666
+ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
667
+ | `useAGGridUrlSync(gridApi, options)` | `useGridUrlSync({ api: gridApi, ...options })` |
668
+ | `paramPrefix`, `serialization`, `groupedParam`, `format` | `codec: individual({ prefix })` or `grouped({ param, format })` |
669
+ | `autoApplyOnMount` (default `false`) | `applyUrlOnCreate` (default `true`). A URL with no filters keeps the grid's filters |
670
+ | `enabledWhenReady` | `enabled` |
671
+ | `onParseError`, `onError(error, context)` | `onIssue({ issue, source })` |
672
+ | `isReady` | `status === 'active'` |
673
+ | `currentUrl`, `shareUrl(baseUrl?)` | `url`, `getUrl(baseUrl?)` |
674
+ | `applyUrlFilters(url?)`, `clearFilters()` | `applyUrl(url?)`, `applyFilters({})` |
675
+ | `getQueryParams()` | `new URL(getUrl()).search`, while `status` is `'active'` |
676
+ | `parseUrlFilters`, `getFiltersAsFormat`, `getSerializationMode` | The module functions `parseUrlFilters` and `serializeGrouped` from `ag-grid-url-sync` |
677
+
678
+ ### Basic React Usage (deprecated `useAGGridUrlSync`)
498
679
 
499
680
  ```tsx
500
681
  import React, { useState } from 'react'
@@ -572,7 +753,7 @@ function GridComponent() {
572
753
  }
573
754
  ```
574
755
 
575
- ### React Hook API
756
+ ### React Hook API (deprecated `useAGGridUrlSync`)
576
757
 
577
758
  #### `useAGGridUrlSync(gridApi, options?)`
578
759
 
@@ -587,13 +768,39 @@ const {
587
768
  isReady,
588
769
  currentUrl,
589
770
  hasFilters,
771
+ issues,
590
772
  parseUrlFilters,
591
773
  applyFilters,
592
774
  getFiltersAsFormat,
593
- getCurrentFormat
775
+ getSerializationMode
594
776
  } = useAGGridUrlSync(gridApi, options)
595
777
  ```
596
778
 
779
+ The hook runs on [`createGridUrlSync`](#vanilla-js-creategridurlsync) and reads its state with `useSyncExternalStore`:
780
+
781
+ - **Options can change after mount.** A change to `paramPrefix`, `maxValueLength`, `serialization`, `groupedParam`, `format`, `history` (the mode) or `popstate` makes a new sync. The grid keeps its filters, and `currentUrl` uses the new format. The URL is not applied again. A new options object with the same values, or a new callback, makes no new sync.
782
+ - **The returned object is stable.** It is the same object across renders until a value in it changes.
783
+ - **A grid-read issue goes to `onParseError` one time** while it stays. It is reported again only after it goes away and comes back.
784
+ - **After AG Grid destroys the grid**, `isReady` is `false` and `currentUrl` is `''`, as with no grid. `applyUrlFilters`, `clearFilters`, `applyFilters`, `saveView` and `loadView` then report `NOT_READY` to `onError`. `shareUrl`, `getQueryParams` and `getFiltersAsFormat` return their fallback values.
785
+ - **The hook writes to the address bar only with `history`.** With the default `'none'`, use `currentUrl` or `shareUrl()` to update it.
786
+
787
+ #### Address bar (`history` and `popstate`)
788
+
789
+ The hook takes the `history` and `popstate` options of [`createGridUrlSync`](#vanilla-js-creategridurlsync), so a React app gets the same address-bar sync with one option:
790
+
791
+ ```tsx
792
+ // Each filter change adds a history entry, and back and forward apply the URL
793
+ useAGGridUrlSync(gridApi, { autoApplyOnMount: true, history: 'push' })
794
+
795
+ // Or give the URL to a router. A new function on each render is fine.
796
+ useAGGridUrlSync(gridApi, { history: url => router.replace(url) })
797
+ ```
798
+
799
+ - A user edit, `applyFilters`, `clearFilters` and `loadView` write the URL. `'push'` adds one entry for each of them.
800
+ - A URL apply writes nothing, because the URL holds those filters already: auto-apply on mount, `applyUrlFilters` and the back button.
801
+ - The restore of a stored view on mount replaces the entry. A new sync for an option change, or a `StrictMode` remount, adds no entry.
802
+ - `popstate` is `true` by default with `'push'`. On back or forward, the hook applies the new URL. The view is then no longer active, and the issues of that apply are in `issues`. A grid error goes to `onError` with the context `'popstate'`. A failed address-bar write is `HISTORY_WRITE_FAILED`, to `onError` with the context `'filter-change'`.
803
+
597
804
  #### Hook Options
598
805
 
599
806
  ```tsx
@@ -602,11 +809,16 @@ interface UseAGGridUrlSyncOptions {
602
809
  paramPrefix?: string // Default: 'f_'
603
810
  maxValueLength?: number // Default: 200
604
811
  onParseError?: (error: Error) => void
812
+ throwOnError?: boolean | ((issue: URLSyncError) => boolean) // Default: false
813
+
814
+ // Address bar (see Address bar above)
815
+ history?: 'none' | 'replace' | 'push' | ((url: string) => void) // Default: 'none'
816
+ popstate?: boolean // Default: true with 'push', else false
605
817
 
606
818
  // React-specific options
607
819
  autoApplyOnMount?: boolean // Default: false
608
820
  enabledWhenReady?: boolean // Default: true
609
- onError?: (error: Error, context: string) => void // Advanced error handling
821
+ onError?: (error: Error, context: string) => void // Operation failures (see Error Handling)
610
822
 
611
823
  // Saved views: setting a key enables the feature and namespaces its storage
612
824
  storageKey?: string // Default: undefined (saved views disabled)
@@ -615,19 +827,21 @@ interface UseAGGridUrlSyncOptions {
615
827
 
616
828
  #### Hook Return Values
617
829
 
618
- | Property | Type | Description |
619
- | -------------------- | -------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------- |
620
- | `shareUrl` | `(baseUrl?: string) => string` | Generate shareable URL with current filters |
621
- | `getQueryParams` | `() => string` | Get filter state as query parameters |
622
- | `applyUrlFilters` | `(url?: string) => void` | Apply filters from URL to grid |
623
- | `clearFilters` | `() => void` | Clear all text filters |
624
- | `isReady` | `boolean` | Whether grid API is available and hook is ready |
625
- | `currentUrl` | `string` | Current URL with filters applied |
626
- | `hasFilters` | `boolean` | Whether the grid has supported filters |
627
- | `parseUrlFilters` | `(url: string) => FilterState` | Parse filters from URL without applying |
628
- | `applyFilters` | `(filters: FilterState) => void` | Apply filter state object to grid |
629
- | `getFiltersAsFormat` | `(format: 'querystring' | 'json' | 'base64') => string` | Serialize filters to any supported format (for sharing/export) |
630
- | `getCurrentFormat` | `() => 'individual' | 'grouped'` | Get the current serialization mode (individual or grouped) |
830
+ | Property | Type | Description |
831
+ | ---------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
832
+ | `shareUrl` | `(baseUrl?: string) => string` | Generate shareable URL with current filters |
833
+ | `getQueryParams` | `() => string` | Get filter state as query parameters |
834
+ | `applyUrlFilters` | `(url?: string) => void` | Apply filters from URL to grid |
835
+ | `clearFilters` | `() => void` | Clear all text filters |
836
+ | `isReady` | `boolean` | Whether the grid API is available and the hook is ready. `false` after AG Grid destroys the grid. |
837
+ | `currentUrl` | `string` | Current URL with filters applied |
838
+ | `hasFilters` | `boolean` | Whether the grid has supported filters |
839
+ | `issues` | `readonly URLSyncError[]` | Filters that the latest URL apply or URL write dropped (see Error Handling) |
840
+ | `parseUrlFilters` | `(url: string) => FilterState` | Parse filters from URL without applying |
841
+ | `applyFilters` | `(filters: FilterState) => void` | Apply filter state object to grid |
842
+ | `getFiltersAsFormat` | `(format: 'querystring' \| 'json' \| 'base64') => string` | Serialize filters to any supported format (for sharing/export) |
843
+ | `getSerializationMode` | `() => 'individual' \| 'grouped'` | Get the current serialization mode (individual or grouped) |
844
+ | `getCurrentFormat` | `() => 'individual' \| 'grouped'` | **Deprecated** (removed in 0.6.0): use `getSerializationMode` |
631
845
 
632
846
  Saved views (inert unless `storageKey` is set):
633
847
 
@@ -639,10 +853,58 @@ Saved views (inert unless `storageKey` is set):
639
853
  | `loadView` | `(id: string \| null) => void` | Apply a saved view, or pass `null` to reset to unfiltered |
640
854
  | `deleteView` | `(id: string) => void` | Delete a saved view |
641
855
 
642
- > **Note:** `getFiltersAsFormat` and `getCurrentFormat` are especially useful for grouped serialization, format conversion, and advanced sharing scenarios.
856
+ > **Note:** `getFiltersAsFormat` and `getSerializationMode` are especially useful for grouped serialization, format conversion, and advanced sharing scenarios.
643
857
 
644
858
  ### Saved Views
645
859
 
860
+ The views are part of `createGridUrlSync`. The React hook uses the same code, so a vanilla app and a React app get the same rules.
861
+
862
+ #### Saved views in vanilla JS
863
+
864
+ Give the `views` option a view store. `createViewStore` is in its own entry, so an app with no saved views does not get the storage code.
865
+
866
+ ```ts
867
+ import { createGridUrlSync } from 'ag-grid-url-sync'
868
+ import { createViewStore } from 'ag-grid-url-sync/view-storage'
869
+
870
+ const sync = createGridUrlSync({
871
+ api: gridApi,
872
+ history: 'push',
873
+ views: createViewStore('orders')
874
+ })
875
+
876
+ const { views, activeViewId } = sync.getSnapshot()
877
+
878
+ const view = sync.saveView('Open orders') // GridView, or null if the save failed
879
+ sync.loadView(view.id)
880
+ sync.loadView(null) // clear the filters
881
+ sync.deleteView(view.id)
882
+ ```
883
+
884
+ You can also give your own `ViewStore`, for example one that talks to a server. With no `views` option, `views` is `[]`, `activeViewId` is `null`, and the three methods do nothing.
885
+
886
+ Rules for the vanilla API:
887
+
888
+ - **`activeViewId` is tracked.** `saveView`, `loadView` and the restore of the stored view set it. A load sets it before the grid write, so a snapshot never shows `null` while the write is still pending.
889
+ - **Any other filter model clears it.** A user edit, `applyUrl`, `applyFilters` and a back-button apply all set `activeViewId` to `null` and clear the stored pointer. A view that AG Grid changes when it applies it stays active.
890
+ - **A hand edit does not make a view active.** If the user types the filters of a view, `activeViewId` stays `null`. When two views have the same model, the active one is the one the user loaded.
891
+ - **A view load writes the URL as a user edit does.** With `history: 'push'`, the back button goes back to the URL from before the load. If the URL does not change, the sync writes no entry. Set and multi filters are kept in a view, but the URL cannot hold them. A view that differs only in those filters adds no entry.
892
+ - **A URL apply writes no history.** This holds for `applyUrl`, the apply on create and the back button.
893
+ - **A failed grid write restores `activeViewId`.** The failure goes to `onIssue` as `GRID_ERROR`.
894
+ - **Deleting is conservative.** If the deleted view is active and the grid still shows it, the grid is cleared. If the user edited the filters, they stay.
895
+ - **Failures go to `onIssue`**, not to `snapshot.issues`. They are `NOT_READY`, `GRID_ERROR`, `VIEW_NOT_FOUND`, `VIEW_NAME_REQUIRED`, `STORAGE_FULL`, `STORAGE_UNAVAILABLE` and `STORAGE_WRITE_FAILED`. `loadView` with an unknown id reads the store again, then reports `VIEW_NOT_FOUND`. After `destroy`, `saveView` and `loadView` report `NOT_READY`, and `deleteView` still deletes from the store.
896
+
897
+ On create (when `applyUrlOnCreate` is not `false`), the sync picks the starting filters in this order:
898
+
899
+ 1. The URL holds the filters of the stored view (compared in URL form). The sync restores the view with its full model, and keeps `activeViewId` and the pointer. This is how a reload keeps a view that was loaded with `history` on.
900
+ 2. The URL has at least one filter. The URL wins. `activeViewId` is `null` and the pointer is cleared. A URL with only bad params does not win.
901
+ 3. A stored view exists. The sync restores it. With `history` on, the URL is written with replace, so page load adds no back-button step.
902
+ 4. Else, the grid keeps its filters.
903
+
904
+ A stored pointer that names a missing view is ignored.
905
+
906
+ #### Saved views in React
907
+
646
908
  Pass a `storageKey` to let users name and revisit filter sets. The key both enables
647
909
  the feature and namespaces its storage, so two grids on the same origin must use
648
910
  different keys:
@@ -707,7 +969,7 @@ Behaviour worth knowing:
707
969
  `onError` with context `'save-view'`. Reads degrade to an empty list instead of
708
970
  throwing, so server rendering is safe.
709
971
 
710
- For non-React consumers the same store is available directly:
972
+ The store is also available directly:
711
973
 
712
974
  ```ts
713
975
  import { createViewStore } from 'ag-grid-url-sync/view-storage'
@@ -721,8 +983,9 @@ store.saveView('Engineering', gridApi.getFilterModel())
721
983
  - **๐Ÿ”„ Automatic State Management**: Tracks grid state and filter changes automatically
722
984
  - **โšก Ready State**: `isReady` indicates when the grid API is available
723
985
  - **๐ŸŽฏ Auto-Apply**: `autoApplyOnMount` applies URL filters when component mounts.
724
- It is attempted once per grid: a failure is reported through `onError` and
725
- `onParseError` and is not retried on later renders
986
+ It is attempted once per grid: a failure is reported one time (a bad filter
987
+ through `onParseError`, any other failure through `onError`) and is not
988
+ retried on later renders
726
989
  - **๐Ÿงน Cleanup**: Automatically cleans up resources when component unmounts
727
990
  - **๐Ÿ›ก๏ธ Error Boundaries**: Graceful error handling with configurable callbacks
728
991
  - **๐Ÿ“Š Filter Status**: `hasFilters` tracks whether any filters are active
@@ -885,9 +1148,13 @@ Parameter structure:
885
1148
 
886
1149
  ## API Reference
887
1150
 
888
- ### `createUrlSync(gridApi, config?)`
1151
+ ### `createGridUrlSync(options)`
1152
+
1153
+ See [Vanilla JS: `createGridUrlSync`](#vanilla-js-creategridurlsync) for the options. It returns `{ getSnapshot, subscribe, applyUrl, applyFilters, getUrl, saveView, loadView, deleteView, destroy }`. The snapshot is `{ status, url, hasFilters, issues, views, activeViewId }`. See [Saved Views](#saved-views) for the view methods.
889
1154
 
890
- Factory function to create a new AGGridUrlSync instance.
1155
+ ### `createUrlSync(gridApi, config?)` (deprecated)
1156
+
1157
+ Factory function to create a new AGGridUrlSync instance. Deprecated: use `createGridUrlSync`. It will be removed in 0.6.0.
891
1158
 
892
1159
  ```typescript
893
1160
  import { createUrlSync } from 'ag-grid-url-sync'
@@ -899,7 +1166,9 @@ const urlSync = createUrlSync(gridApi, {
899
1166
  })
900
1167
  ```
901
1168
 
902
- ### `AGGridUrlSync` Class
1169
+ ### `AGGridUrlSync` Class (deprecated)
1170
+
1171
+ Deprecated: use `createGridUrlSync`. It will be removed in 0.6.0.
903
1172
 
904
1173
  #### Constructor
905
1174
 
@@ -918,7 +1187,7 @@ const url = urlSync.generateUrl('https://app.com/page')
918
1187
  // https://app.com/page?f_name_contains=john
919
1188
  ```
920
1189
 
921
- ##### `getUrlState(baseUrl?: string): { url: string; hasFilters: boolean }`
1190
+ ##### `getUrlState(baseUrl?: string): { url: string; hasFilters: boolean; issues: readonly URLSyncError[] }`
922
1191
 
923
1192
  Generates the same URL as `generateUrl`, and tells if the grid has filters that the library supports. It reads the grid one time for both values. `hasFilters` comes from the filter state, not from the URL, because the URL also keeps the page's own params.
924
1193
 
@@ -927,6 +1196,8 @@ const { url, hasFilters } = urlSync.getUrlState('https://app.com/page?tab=1')
927
1196
  // url: https://app.com/page?tab=1 (no filters), hasFilters: false
928
1197
  ```
929
1198
 
1199
+ `issues` holds the filters that this call dropped, for example a text value over `maxValueLength`.
1200
+
930
1201
  ##### `getQueryParams(): string`
931
1202
 
932
1203
  Gets the current filter state as URL query parameters.
@@ -936,17 +1207,17 @@ const params = urlSync.getQueryParams()
936
1207
  // ?f_name_contains=john
937
1208
  ```
938
1209
 
939
- ##### `applyFromUrl(url?: string): void`
1210
+ ##### `applyFromUrl(url?: string): { issues: readonly URLSyncError[] }`
940
1211
 
941
- Applies filters from a URL to the grid.
1212
+ Applies filters from a URL to the grid, and returns the filters in the URL that were dropped.
942
1213
 
943
1214
  ```typescript
944
1215
  urlSync.applyFromUrl('https://app.com/page?f_name_contains=john')
945
1216
  ```
946
1217
 
947
- ##### `applyFilters(filterState: FilterState): void`
1218
+ ##### `applyFilters(filterState: FilterState): { issues: readonly URLSyncError[] }`
948
1219
 
949
- Applies a filter state object to the grid.
1220
+ Applies a filter state object to the grid, and returns the filters that were dropped (for example a number filter on a text column).
950
1221
 
951
1222
  ```typescript
952
1223
  urlSync.applyFilters({
@@ -958,7 +1229,7 @@ urlSync.applyFilters({
958
1229
  })
959
1230
  ```
960
1231
 
961
- ##### `clearFilters(): void`
1232
+ ##### `clearFilters(): { issues: readonly URLSyncError[] }`
962
1233
 
963
1234
  Clears all text filters from the grid.
964
1235
 
@@ -992,8 +1263,10 @@ interface AGGridUrlSyncConfig {
992
1263
  // Parameter name for grouped mode (default: 'grid_filters')
993
1264
  groupedParam?: string
994
1265
 
995
- // Prefix for URL parameters (default: 'f_')
996
- // In grouped mode, this affects individual parameter names within the group
1266
+ // Prefix for URL parameters in individual mode (default: 'f_')
1267
+ // A prefix without a trailing '_' gets one ('filter' -> 'filter_name_contains').
1268
+ // An empty prefix means the default. Grouped mode always uses 'f_' inside the
1269
+ // grouped value, so grouped URLs do not change with this option.
997
1270
  paramPrefix?: string
998
1271
 
999
1272
  // Maximum length for filter values (default: 200)