Skip to content

[release/8.0] [Blazor] Dynamically toggle navigation interception #50564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, Syste
Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
Microsoft.AspNetCore.Components.RouteData.Template.get -> string?
Microsoft.AspNetCore.Components.RouteData.Template.set -> void
Microsoft.AspNetCore.Components.Routing.INavigationInterception.DisableNavigationInterceptionAsync() -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider
Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider.RouteData.get -> Microsoft.AspNetCore.Components.RouteData?
Microsoft.AspNetCore.Components.RenderModeAttribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@ public interface INavigationInterception
/// </summary>
/// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
Task EnableNavigationInterceptionAsync();

/// <summary>
/// Disables navigation interception on the client.
/// </summary>
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
Task DisableNavigationInterceptionAsync()
=> Task.CompletedTask;
}
13 changes: 12 additions & 1 deletion src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Routing;
/// <summary>
/// A component that supplies route data corresponding to the current navigation state.
/// </summary>
public partial class Router : IComponent, IHandleAfterRender, IDisposable
public partial class Router : IComponent, IHandleAfterRender, IDisposable, IAsyncDisposable
{
// Dictionary is intentionally used instead of ReadOnlyDictionary to reduce Blazor size
static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
Expand Down Expand Up @@ -143,12 +143,23 @@ public async Task SetParametersAsync(ParameterView parameters)

/// <inheritdoc />
public void Dispose()
{
_ = ((IAsyncDisposable)this).DisposeAsync().Preserve();
}

/// <inheritdoc />
async ValueTask IAsyncDisposable.DisposeAsync()
{
NavigationManager.LocationChanged -= OnLocationChanged;
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied -= ClearRouteCaches;
}

if (_navigationInterceptionEnabled)
{
await NavigationInterception.DisableNavigationInterceptionAsync();
}
}

private static ReadOnlySpan<char> TrimQueryOrHash(ReadOnlySpan<char> str)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits;

internal sealed class RemoteNavigationInterception : INavigationInterception
{
private readonly NavigationManager _navigationManager;

private IJSRuntime _jsRuntime;

public RemoteNavigationInterception(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}

public void AttachJSRuntime(IJSRuntime jsRuntime)
{
if (HasAttachedJSRuntime)
Expand All @@ -35,6 +42,22 @@ public async Task EnableNavigationInterceptionAsync()
"attempted during prerendering or while the client is disconnected.");
}

await _jsRuntime.InvokeAsync<object>(Interop.EnableNavigationInterception);
await _jsRuntime.InvokeAsync<object>(Interop.EnableNavigationInterception, _navigationManager.Uri);
}

public async Task DisableNavigationInterceptionAsync()
{
if (!HasAttachedJSRuntime)
{
return;
}

try
{
await _jsRuntime.InvokeAsync<object>(Interop.DisableNavigationInterception);
}
catch (JSDisconnectedException)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal static class BrowserNavigationManagerInterop

public const string EnableNavigationInterception = Prefix + "enableNavigationInterception";

public const string DisableNavigationInterception = Prefix + "disableNavigationInterception";

public const string GetLocationHref = Prefix + "getLocationHref";

public const string GetBaseUri = Prefix + "getBaseURI";
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.web.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ let resolveCurrentNavigation: ((shouldContinueNavigation: boolean) => void) | nu
// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
listenForNavigationEvents,
enableNavigationInterception: setHasInteractiveRouter,
enableNavigationInterception,
disableNavigationInterception,
setHasLocationChangingListeners,
endLocationChanging,
navigateTo: navigateToFromDotNet,
Expand All @@ -47,6 +48,20 @@ function listenForNavigationEvents(
currentHistoryIndex = history.state?._index ?? 0;
}

async function enableNavigationInterception(uriInDotNet?: string) {
setHasInteractiveRouter(true);
if (uriInDotNet && location.href !== uriInDotNet) {
// The location known by .NET is out of sync with the actual browser location.
// Therefore, we should notify .NET that the location has changed so that any
// interactive router can react accordingly.
await notifyLocationChanged(false);
}
}

function disableNavigationInterception() {
setHasInteractiveRouter(false);
}

function setHasLocationChangingListeners(hasListeners: boolean) {
hasLocationChangingEventListeners = hasListeners;
}
Expand Down
12 changes: 6 additions & 6 deletions src/Components/Web.JS/src/Services/NavigationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,16 @@ function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | null {

function findClosestAnchorAncestorLegacy(element: Element | null, tagName: string) {
return !element
? null
: element.tagName === tagName
? element
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
? null
: element.tagName === tagName
? element
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
}

export function hasInteractiveRouter(): boolean {
return hasInteractiveRouterValue;
}

export function setHasInteractiveRouter() {
hasInteractiveRouterValue = true;
export function setHasInteractiveRouter(hasInteractiveRouter: boolean) {
hasInteractiveRouterValue = hasInteractiveRouter;
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ internal void InitializeDefaultServices()
{
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<INavigationInterception, WebAssemblyNavigationInterception>();
Services.AddSingleton<IScrollToLocationHash>(WebAssemblyScrollToLocationHash.Instance);
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
Services.AddSingleton<RootComponentTypeCache>(_ => _rootComponentCache ?? new());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ internal interface IInternalJSImportMethods

string GetApplicationEnvironment();

void NavigationManager_EnableNavigationInterception();
void NavigationManager_EnableNavigationInterception(string uri);

void NavigationManager_DisableNavigationInterception();

void NavigationManager_ScrollToElement(string id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ public string GetPersistedState()
public string GetApplicationEnvironment()
=> GetApplicationEnvironmentCore();

public void NavigationManager_EnableNavigationInterception()
=> NavigationManager_EnableNavigationInterceptionCore();
public void NavigationManager_EnableNavigationInterception(string uri)
=> NavigationManager_EnableNavigationInterceptionCore(uri);

public void NavigationManager_DisableNavigationInterception()
=> NavigationManager_DisableNavigationInterceptionCore();

public void NavigationManager_ScrollToElement(string id)
=> NavigationManager_ScrollToElementCore(id);
Expand Down Expand Up @@ -56,7 +59,10 @@ public string RegisteredComponents_GetParameterValues(int id)
private static partial string GetApplicationEnvironmentCore();

[JSImport(BrowserNavigationManagerInterop.EnableNavigationInterception, "blazor-internal")]
private static partial void NavigationManager_EnableNavigationInterceptionCore();
private static partial void NavigationManager_EnableNavigationInterceptionCore(string uri);

[JSImport(BrowserNavigationManagerInterop.DisableNavigationInterception, "blazor-internal")]
private static partial void NavigationManager_DisableNavigationInterceptionCore();

[JSImport(BrowserNavigationManagerInterop.ScrollToElement, "blazor-internal")]
private static partial void NavigationManager_ScrollToElementCore(string id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services;

internal sealed class WebAssemblyNavigationInterception : INavigationInterception
{
public static readonly WebAssemblyNavigationInterception Instance = new WebAssemblyNavigationInterception();
private readonly NavigationManager _navigationManager;

public WebAssemblyNavigationInterception(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}

public Task EnableNavigationInterceptionAsync()
{
InternalJSImportMethods.Instance.NavigationManager_EnableNavigationInterception();
InternalJSImportMethods.Instance.NavigationManager_EnableNavigationInterception(_navigationManager.Uri);
return Task.CompletedTask;
}

public Task DisableNavigationInterceptionAsync()
{
InternalJSImportMethods.Instance.NavigationManager_DisableNavigationInterception();
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public string GetApplicationEnvironment()
public string GetPersistedState()
=> null;

public void NavigationManager_EnableNavigationInterception() { }
public void NavigationManager_EnableNavigationInterception(string uri) { }

public void NavigationManager_DisableNavigationInterception() { }

public void NavigationManager_ScrollToElement(string id) { }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@page "/routing/interactive"
@using TestContentPackage

<nav>
<NavLink href="routing/interactive">Home</NavLink>
<NavLink href="routing/interactive/navigation">Navigation</NavLink>
</nav>
<hr />

<InteractiveRouter @rendermode="@(new ServerRenderMode(false))" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page "/routing/interactive/{pageName}"

<h1>Fallback page for <span id="page-name">@PageName</span>!</h1>

@code {
[Parameter]
public string PageName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@using Microsoft.AspNetCore.Components.Routing

<Router AppAssembly="@typeof(InteractiveRouter).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>Not found using interactive router</NotFound>
</Router>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@page "/routing/interactive"

<h1>Hello from index!</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@page "/routing/interactive/navigation"

<h3>Hello from navigation page!</h3>

<InteractiveNavigationComponent />