Skip to content

Commit 74f7006

Browse files
Fix Nav Drawer interaction and persistence in Static SSR (#64)
* Fix MudStaticNavDrawerToggle and MudDrawer interaction in Static SSR - Implemented state persistence for drawers using localStorage to preserve open/closed state across navigations. - Added support for Mini drawer variant in the JavaScript logic. - Improved resize monitoring to correctly handle both Responsive and Mini drawers. - Updated breakpoint values to match MudBlazor defaults. - Refactored layout class replacement to be more robust. - Fixed an issue where the drawer state would reset on every navigation during enhanced loading. Co-authored-by: Anu6is <[email protected]> * fix(drawer): implement state persistence and sync for MudStaticNavDrawerToggle - Implemented localStorage persistence in JS for drawer state. - Refactored JS to correctly handle MudBlazor responsive/mini layout classes. - Added Open/OpenChanged parameters to MudStaticNavDrawerToggle for state binding. - Added OnAfterRenderAsync sync logic in MudStaticNavDrawerToggle to restore state during WASM transition. - Updated demo project to utilize @bind-Open on the toggle component. - Removed debug logs and cleaned up verification artifacts. Co-authored-by: Anu6is <[email protected]> * fix(drawer): implement state persistence and sync for all drawer variants - Implemented localStorage persistence in JS for drawer state. - Refactored JS layout class parsing to correctly handle Responsive, Mini, and Persistent variants with or without breakpoints. - Added Open/OpenChanged parameters to MudStaticNavDrawerToggle for state binding. - Implemented state sync in MudStaticNavDrawerToggle.OnAfterRenderAsync to restore state during transitions to interactive mode. - Updated demo project to support @bind-Open on the toggle component. - Improved JS resilience by handling varying layout class section counts. Co-authored-by: Anu6is <[email protected]> * fix(drawer): improve SSR persistence and WASM transition sync - Added Cookie-based state persistence in JS to support server-side state awareness. - Implemented synchronous state sync in WASM OnInitialized using IJSInProcessRuntime to eliminate flicker. - Implemented HttpContext cookie sync in SSR OnInitialized for correct initial rendering. - Enhanced JS layout class parsing for Persistent, Mini, and Responsive variants. - Ensured stable storage keys ('default') when no DrawerId is provided. - Updated demo project to use @bind-Open on the toggle component. Co-authored-by: Anu6is <[email protected]> * fix(drawer): improve mobile support and state persistence for MudStaticNavDrawerToggle - Implemented state persistence via Cookies and localStorage to support Static SSR and hydration. - Added PersistentComponentState to bridge state from SSR to WASM, preventing flickering. - Refactored JS layout logic to be anchor-aware, supporting multiple drawers. - Added comprehensive unit tests for drawer state persistence and multi-drawer scenarios. - Improved resize monitoring for responsive and mini variants on mobile. Co-authored-by: Anu6is <[email protected]> * fix(drawer): finalize MudStaticNavDrawerToggle improvements and address feedback - Refactored MudStaticNavDrawerToggle to avoid direct [Parameter] mutation. - Added PersistMode parameter to support InteractiveServer and other render modes. - Updated cookie expiration to session-only for better privacy and consistency. - Refined multi-drawer layout logic in JS to be anchor-aware. - Verified all scenarios with comprehensive unit and Playwright tests. Co-authored-by: Anu6is <[email protected]> * fix(drawer): finalize MudStaticNavDrawerToggle improvements and address all feedback - Refactored MudStaticNavDrawerToggle to avoid direct [Parameter] mutation. - Added PersistMode parameter to support InteractiveServer and other render modes. - Updated cookie expiration to session-only for better privacy and consistency. - Refined multi-drawer layout logic in JS to be anchor-aware. - Implemented `syncDrawerState` to capture automatic state changes in interactive modes (resizing, mini-variant) for persistence in static pages. - Verified all scenarios with comprehensive unit and Playwright tests. Co-authored-by: Anu6is <[email protected]> * nits --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Anu6is <[email protected]>
1 parent c6d5181 commit 74f7006

9 files changed

Lines changed: 545 additions & 54 deletions

File tree

demo/StaticSample/StaticSample.Client/Layout/MainLayout.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
<MudLayout>
88
<MudAppBar Elevation="0">
9-
<MudStaticNavDrawerToggle Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" />
9+
<MudStaticNavDrawerToggle @bind-Open="_drawerOpen" Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" />
1010
<MudText Typo="Typo.h6" Style="white-space:nowrap;">Static Input</MudText>
1111
<MudSpacer />
1212
<RenderStateViewer Parent="this" Class="px-4" />
1313
<MudIconButton Icon="@Icons.Custom.Brands.MudBlazor" Color="Color.Inherit" Href="https://mudblazor.com/" Target="_blank" />
1414
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/0phois/MudBlazor.StaticInput" Target="_blank" />
1515
</MudAppBar>
1616

17-
<MudDrawer @bind-Open="_drawerOpen" Elevation="1" Variant="@DrawerVariant.Responsive" Breakpoint="@Breakpoint.Md" ClipMode="DrawerClipMode.Always">
17+
<MudDrawer @bind-Open="_drawerOpen" Elevation="1" Variant="@DrawerVariant.Mini" Breakpoint="@Breakpoint.Md" ClipMode="DrawerClipMode.Always">
1818
<NavMenu />
1919
</MudDrawer>
2020

package-lock.json

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"playwright": "^1.58.2"
4+
}
5+
}

src/Components/NavMenu/MudStaticNavDrawerToggle.razor

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
@namespace MudBlazor.StaticInput
33

44
@inherits MudIconButton
5-
@inject IJSRuntime JsRuntime
65

76
@{
87
base.BuildRenderTree(__builder);
@@ -20,6 +19,22 @@
2019

2120
protected override void OnParametersSet()
2221
{
22+
if (_lastOpen.HasValue && _lastOpen.Value != Open)
23+
{
24+
_open = Open;
25+
_lastOpen = Open;
26+
27+
if (!IsStatic())
28+
{
29+
_ = JsRuntime.InvokeVoidAsync("MudDrawerInterop.syncDrawerState", DrawerId, _open);
30+
}
31+
}
32+
else if (!_lastOpen.HasValue)
33+
{
34+
_open = Open;
35+
_lastOpen = Open;
36+
}
37+
2338
if (IsStatic())
2439
{
2540
UserAttributes["data-mud-static-type"] = "drawer-toggle";
@@ -32,7 +47,12 @@
3247
UserAttributes.Remove("data-mud-static-initialized");
3348

3449
base.OnClick = EventCallback.Factory.Create<MouseEventArgs>(this, async () =>
35-
await JsRuntime.InvokeVoidAsync("MudDrawerInterop.toggleDrawer", DrawerId));
50+
{
51+
_open = !_open;
52+
_lastOpen = _open;
53+
await OpenChanged.InvokeAsync(_open);
54+
await JsRuntime.InvokeVoidAsync("MudDrawerInterop.setDrawerState", DrawerId, _open);
55+
});
3656
}
3757

3858
base.OnParametersSet();

src/Components/NavMenu/MudStaticNavDrawerToggle.razor.cs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Components;
22
using Microsoft.AspNetCore.Components.Web;
33
using Microsoft.AspNetCore.Http;
4+
using Microsoft.JSInterop;
45

56
namespace MudBlazor.StaticInput;
67

@@ -17,12 +18,42 @@ namespace MudBlazor.StaticInput;
1718
/// </item>
1819
/// </list>
1920
/// </remarks>
20-
public partial class MudStaticNavDrawerToggle : MudIconButton
21+
public partial class MudStaticNavDrawerToggle : MudIconButton, IDisposable
2122

2223
{
2324
[CascadingParameter]
2425
private HttpContext HttpContext { get; set; } = default!;
2526

27+
/// <summary>
28+
/// The open state of the drawer. Bind this to the same variable as your MudDrawer.
29+
/// </summary>
30+
[Parameter]
31+
public bool Open { get; set; }
32+
33+
/// <summary>
34+
/// Event callback for when the open state changes.
35+
/// </summary>
36+
[Parameter]
37+
public EventCallback<bool> OpenChanged { get; set; }
38+
39+
/// <summary>
40+
/// The render mode to persist the state for./>.
41+
/// Defaults to <c>null</c> to register all render modes.
42+
/// </summary>
43+
[Parameter]
44+
public IComponentRenderMode? PersistMode { get; set; } = RenderMode.InteractiveAuto;
45+
46+
private bool _open;
47+
private bool? _lastOpen;
48+
49+
[Inject]
50+
private IJSRuntime JsRuntime { get; set; } = default!;
51+
52+
[Inject]
53+
private PersistentComponentState PersistentState { get; set; } = default!;
54+
55+
private PersistingComponentStateSubscription? _subscription;
56+
2657
private bool IsStatic()
2758
{
2859
#if NET9_0_OR_GREATER
@@ -38,4 +69,139 @@ private bool IsStatic()
3869
protected new string? Href { get; set; }
3970
protected new string? Target { get; set; }
4071
protected new bool ClickPropagation { get; set; }
72+
73+
protected override void OnInitialized()
74+
{
75+
_open = Open;
76+
_lastOpen = Open;
77+
78+
var storageKey = !string.IsNullOrEmpty(DrawerId) && DrawerId != "_no_id_provided_"
79+
? $"mud-static-drawer-open-{DrawerId}"
80+
: "mud-static-drawer-open-default";
81+
82+
if (IsStatic())
83+
{
84+
if (PersistMode != null)
85+
{
86+
_subscription = PersistentState.RegisterOnPersisting(() =>
87+
{
88+
PersistentState.PersistAsJson(storageKey, _open);
89+
return Task.CompletedTask;
90+
}, PersistMode);
91+
}
92+
else
93+
{
94+
_subscription = PersistentState.RegisterOnPersisting(() =>
95+
{
96+
PersistentState.PersistAsJson(storageKey, _open);
97+
return Task.CompletedTask;
98+
}, RenderMode.InteractiveAuto);
99+
}
100+
101+
if (HttpContext?.Request.Cookies.TryGetValue(storageKey, out var value) == true)
102+
{
103+
bool storedOpen = value == "true";
104+
if (storedOpen != _open)
105+
{
106+
_open = storedOpen;
107+
_lastOpen = _open;
108+
_ = OpenChanged.InvokeAsync(_open);
109+
}
110+
}
111+
}
112+
else
113+
{
114+
if (PersistentState.TryTakeFromJson<bool>(storageKey, out var restored))
115+
{
116+
if (restored != _open)
117+
{
118+
_open = restored;
119+
_lastOpen = _open;
120+
_ = OpenChanged.InvokeAsync(_open);
121+
}
122+
}
123+
else if (JsRuntime is IJSInProcessRuntime inProcess)
124+
{
125+
try
126+
{
127+
var stored = inProcess.Invoke<bool?>("MudDrawerInterop.getDrawerState", DrawerId);
128+
if (stored.HasValue && stored.Value != _open)
129+
{
130+
_open = stored.Value;
131+
_lastOpen = _open;
132+
_ = OpenChanged.InvokeAsync(_open);
133+
}
134+
}
135+
catch
136+
{
137+
// Ignore sync JS failures
138+
}
139+
}
140+
}
141+
142+
base.OnInitialized();
143+
}
144+
145+
protected override async Task OnInitializedAsync()
146+
{
147+
if (!IsStatic())
148+
{
149+
try
150+
{
151+
// In WASM, JS Interop is available in OnInitializedAsync if not pre-rendering.
152+
// This helps avoid the flicker by setting the state before the first render.
153+
var stored = await JsRuntime.InvokeAsync<bool?>("MudDrawerInterop.getDrawerState", DrawerId);
154+
if (stored.HasValue && stored.Value != _open)
155+
{
156+
_open = stored.Value;
157+
_lastOpen = _open;
158+
await OpenChanged.InvokeAsync(_open);
159+
}
160+
}
161+
catch
162+
{
163+
// Ignore JS interop failures during pre-rendering or if not yet available
164+
}
165+
}
166+
167+
await base.OnInitializedAsync();
168+
}
169+
170+
public void Dispose()
171+
{
172+
_subscription?.Dispose();
173+
}
174+
175+
protected override async Task OnAfterRenderAsync(bool firstRender)
176+
{
177+
if (firstRender && !IsStatic())
178+
{
179+
// Fallback sync in case OnInitializedAsync was pre-rendering
180+
var storageKey = !string.IsNullOrEmpty(DrawerId) && DrawerId != "_no_id_provided_"
181+
? $"mud-static-drawer-open-{DrawerId}"
182+
: "mud-static-drawer-open-default";
183+
184+
try
185+
{
186+
var stored = await JsRuntime.InvokeAsync<string>("localStorage.getItem", storageKey);
187+
if (stored != null)
188+
{
189+
bool storedOpen = stored == "true";
190+
if (storedOpen != _open)
191+
{
192+
_open = storedOpen;
193+
_lastOpen = _open;
194+
await OpenChanged.InvokeAsync(_open);
195+
StateHasChanged();
196+
}
197+
}
198+
}
199+
catch (Exception ex)
200+
{
201+
try { await JsRuntime.InvokeVoidAsync("console.error", $"Error syncing drawer state: {ex.Message}"); } catch { }
202+
}
203+
}
204+
205+
await base.OnAfterRenderAsync(firstRender);
206+
}
41207
}

0 commit comments

Comments
 (0)