Skip to content

Commit 163cb95

Browse files
committed
url sync
1 parent 12e36ea commit 163cb95

22 files changed

Lines changed: 3533 additions & 1 deletion

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ bld/
1212
[Oo]bj/
1313
[Ll]og/
1414
[Ll]ogs/
15+
docs/
1516

1617
# Visual Studio cache/options
1718
.vs/

README.md

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ That's it. All components subscribed to `CounterState` update automatically.
100100
- [Query System](#query-system) | [Async Helpers](#async-helpers) | [Optimistic Updates](#optimistic-updates)
101101

102102
**Sync & Collaboration**
103-
- [Cross-Tab Sync](#cross-tab-sync) | [Server Sync](#server-sync-signalr) | [Persistence & DevTools](#state-persistence--redux-devtools-integration)
103+
- [URL Sync](#url-sync-experimental) | [Cross-Tab Sync](#cross-tab-sync) | [Server Sync](#server-sync-signalr) | [Persistence & DevTools](#state-persistence--redux-devtools-integration)
104104

105105
**History & Advanced**
106106
- [Undo/Redo](#undoredo-history) | [Immer-Style Updates](#immer-style-updates) | [Redux-Style Actions](#redux-style-actions) | [Selectors](#selectors--performance-optimization)
@@ -414,6 +414,129 @@ await store.UpdateOptimistic<AppState, ServerItem>(
414414

415415
---
416416

417+
## URL Sync (Experimental)
418+
419+
**Shareable URLs.** Sync component parameters with store state bidirectionally. Changes to URL update state, state changes update URL.
420+
421+
> **⚠️ Experimental Feature** - This API may change in future versions. `[Experimental("EASB001")]` attribute is applied. Phase 3 with attribute-based auto-sync is now available.
422+
423+
### Attribute-Based Auto-Sync (Zero Boilerplate)
424+
425+
The simplest way to sync URLs - just add attributes:
426+
427+
```csharp
428+
@page "/products"
429+
@inherits UrlSyncStoreComponent<ProductsState>
430+
431+
[SupplyParameterFromQuery]
432+
[AutoSyncWithQuery]
433+
public int Page { get; set; } = 1; // Auto-maps to state.Page or state.CurrentPage
434+
435+
[SupplyParameterFromQuery]
436+
[AutoSyncWithQuery("q")] // Custom query param name
437+
public string? Search { get; set; }
438+
439+
[SupplyParameterFromQuery]
440+
[AutoSyncWithQuery]
441+
public bool OnSaleOnly { get; set; }
442+
```
443+
444+
**Convention-based matching:**
445+
- Exact match: `Page``state.Page`
446+
- "Current" prefix: `Page``state.CurrentPage`
447+
- "Value" suffix: `Name``state.NameValue`
448+
- Case-insensitive: `page``state.Page`
449+
450+
### Manual Configuration (Advanced)
451+
452+
For more control, use manual configuration:
453+
454+
```csharp
455+
@page "/products"
456+
@inherits UrlSyncStoreComponent<ProductsState>
457+
458+
[SupplyParameterFromQuery] public int Page { get; set; } = 1;
459+
[SupplyParameterFromQuery] public string? Search { get; set; }
460+
[SupplyParameterFromQuery] public bool OnSaleOnly { get; set; }
461+
462+
protected override void ConfigureUrlSync(IUrlSyncBuilder<ProductsState> builder)
463+
{
464+
builder
465+
.SyncQueryParam(() => Page, s => s.CurrentPage)
466+
.SyncQueryParam(() => Search, s => s.SearchQuery)
467+
.SyncQueryParam(() => OnSaleOnly, s => s.FilterOnSale)
468+
.WithDebounce(TimeSpan.FromMilliseconds(500))
469+
.WithNavigationMode(UrlSyncNavigationMode.Replace);
470+
}
471+
```
472+
473+
### Hybrid Approach
474+
475+
Combine both for flexibility:
476+
477+
```csharp
478+
// Auto-sync simple properties
479+
[SupplyParameterFromQuery, AutoSyncWithQuery]
480+
public int Page { get; set; }
481+
482+
// Manual config for advanced options
483+
protected override void ConfigureUrlSync(IUrlSyncBuilder<ProductsState> builder)
484+
{
485+
builder
486+
.WithDebounce(TimeSpan.FromMilliseconds(750))
487+
.ExcludeActions("SERVER_SYNC");
488+
}
489+
```
490+
491+
**What happens:**
492+
1. User visits `/products?page=2&q=laptop&onSaleOnly=true`
493+
2. State updates: `state with { CurrentPage = 2, SearchQuery = "laptop", FilterOnSale = true }`
494+
3. User clicks filter → state changes → URL updates
495+
4. User shares URL → recipient sees same filtered view
496+
497+
### Supported Types
498+
499+
Only **value types** and **strings** are supported (prevents infinite loops):
500+
- Primitives: `int`, `long`, `short`, `byte`, `float`, `double`, `decimal`, `bool`
501+
- Special: `string`, `Guid`, `DateTime`, `DateTimeOffset`, `TimeSpan`
502+
- Enums: Any enum type
503+
- Nullable: All above types with `?`
504+
505+
**Not supported**: Lists, arrays, complex objects (causes infinite update loops)
506+
507+
### Options
508+
509+
```csharp
510+
builder
511+
.SyncQueryParam(() => Page, s => s.CurrentPage, "p") // Custom param name
512+
.WithDebounce(TimeSpan.FromMilliseconds(500)) // Debounce rapid changes
513+
.WithNavigationMode(UrlSyncNavigationMode.Replace) // Replace vs Push history
514+
.ExcludeActions("SERVER_SYNC", "CURSOR_UPDATE") // Don't sync these actions
515+
.OnConversionError((param, ex) => Logger.LogWarning("Invalid {Param}: {Error}", param, ex.Message))
516+
.OnError(ex => Logger.LogError(ex, "URL sync error"));
517+
```
518+
519+
### Navigation Modes
520+
521+
| Mode | Behavior | Use Case |
522+
|------|----------|----------|
523+
| **Replace** (default) | Replaces current history entry | High-frequency updates (sliders, filters) - prevents back button pollution |
524+
| **Push** | Adds new history entry | Intentional navigation (wizard steps, tabs) |
525+
526+
### Incompatibilities
527+
528+
⚠️ **Cannot use with:**
529+
- TabSync middleware (each tab needs independent URL)
530+
- Multiple `UrlSyncStoreComponent` per store (only one component can manage URL)
531+
532+
### Security Notes
533+
534+
- URL parameters are **user-controlled input** - always validate with `IStateValidator<T>`
535+
- Sensitive data should **never** be in URLs (use session storage or server state)
536+
- Maximum URL length: ~2000 chars (library warns at 1800)
537+
538+
---
539+
417540
## Undo/Redo History
418541

419542
**Ctrl+Z for your app state.** Full history stack with memory limits and action grouping.

samples/EasyAppDev.Blazor.Store.Sample/Layout/NavMenu.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
<span aria-hidden="true">=</span> Cross-Tab Sync
9595
</NavLink>
9696
</div>
97+
<div class="nav-item px-3">
98+
<NavLink class="nav-link" href="url-sync-demo">
99+
<span aria-hidden="true">~</span> URL Sync (Planned)
100+
</NavLink>
101+
</div>
97102
<div class="nav-item px-3">
98103
<NavLink class="nav-link" href="immer-demo">
99104
<span aria-hidden="true">%</span> Immer-Style Updates

0 commit comments

Comments
 (0)