Skip to content
Open
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/Web.JS/src/Boot.Web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function boot(options?: Partial<WebStartOptions>) : Promise<void> {
started = true;
options = options || {};
options.logLevel ??= LogLevel.Error;
Blazor._internal.isBlazorWeb = true;

// Defined here to avoid inadvertently imported enhanced navigation
// related APIs in WebAssembly or Blazor Server contexts.
Expand Down
1 change: 1 addition & 0 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface IBlazor {
receiveWebAssemblyDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
receiveWebViewDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
attachWebRendererInterop?: typeof attachWebRendererInterop;
isBlazorWeb?: boolean;

// JSExport APIs
dotNetExports?: {
Expand Down
38 changes: 28 additions & 10 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EventDelegator } from '../Rendering/Events/EventDelegator';
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isForSamePath, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
import { WebRendererId } from '../Rendering/WebRendererId';
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
import { IBlazor } from '../GlobalExports';

let hasRegisteredNavigationEventListeners = false;
let currentHistoryIndex = 0;
Expand Down Expand Up @@ -116,18 +117,21 @@ function navigateToFromDotNet(uri: string, options: NavigationOptions): void {

function navigateToCore(uri: string, options: NavigationOptions, skipLocationChangingCallback = false): void {
const absoluteUri = toAbsoluteUri(uri);
const pageLoadMechanism = currentPageLoadMechanism();

if (!options.forceLoad && isWithinBaseUriSpace(absoluteUri)) {
if (shouldUseClientSideRouting()) {
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
} else {
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
}
} else {
if (options.forceLoad || !isWithinBaseUriSpace(absoluteUri) || pageLoadMechanism === 'serverside-fullpageload') {
// For external navigation, we work in terms of the originally-supplied uri string,
// not the computed absoluteUri. This is in case there are some special URI formats
// we're unable to translate into absolute URIs.
performExternalNavigation(uri, options.replaceHistoryEntry);
} else if (pageLoadMechanism === 'clientside-router') {
performInternalNavigation(absoluteUri, false, options.replaceHistoryEntry, options.historyEntryState, skipLocationChangingCallback);
} else if (pageLoadMechanism === 'serverside-enhanced') {
performProgrammaticEnhancedNavigation(absoluteUri, options.replaceHistoryEntry);
} else {
// Force a compile-time error if some other case needs to be handled in the future
const unreachable: never = pageLoadMechanism;
throw new Error(`Unsupported page load mechanism: ${unreachable}`);
}
}

Expand Down Expand Up @@ -266,7 +270,7 @@ async function notifyLocationChanged(interceptedLink: boolean, internalDestinati
}

async function onPopState(state: PopStateEvent) {
if (popStateCallback && shouldUseClientSideRouting()) {
if (popStateCallback && currentPageLoadMechanism() !== 'serverside-enhanced') {
await popStateCallback(state);
}

Expand All @@ -282,10 +286,24 @@ function getInteractiveRouterNavigationCallbacks(): NavigationCallbacks | undefi
return navigationCallbacks.get(interactiveRouterRendererId);
}

function shouldUseClientSideRouting() {
return hasInteractiveRouter() || !hasProgrammaticEnhancedNavigationHandler();
function currentPageLoadMechanism(): PageLoadMechanism {
if (hasInteractiveRouter()) {
return 'clientside-router';
} else if (hasProgrammaticEnhancedNavigationHandler()) {
return 'serverside-enhanced';
} else {
// For back-compat, in blazor.server.js or blazor.webassembly.js, we always behave as if there's an interactive
// router even if there isn't one attached. This preserves a niche case where people may call Blazor.navigateTo
// without a router and expect to receive a notification on the .NET side but no page load occurs.
// In blazor.web.js, we explicitly recognize the case where you have neither an interactive nor enhanced SSR router
// attached, and then handle Blazor.navigateTo by doing a full page load because that's more useful (issue #51636).
const isBlazorWeb = (window['Blazor'] as IBlazor)._internal.isBlazorWeb;
return isBlazorWeb ? 'serverside-fullpageload' : 'clientside-router';
}
}

type PageLoadMechanism = 'clientside-router' | 'serverside-enhanced' | 'serverside-fullpageload';

// Keep in sync with Components/src/NavigationOptions.cs
export interface NavigationOptions {
forceLoad: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,30 @@ void SetUpPageWithOneInteractiveServerComponent()
}
}

[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void CanPerformNavigateToFromInteractiveEventHandler(bool suppressEnhancedNavigation, bool forceLoad)
{
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, suppressEnhancedNavigation);

// Get to the test page
Navigate($"{ServerPathBase}/interactivity/navigateto");
Browser.Equal("Interactive NavigateTo", () => Browser.FindElement(By.TagName("h1")).Text);
var originalNavElem = Browser.FindElement(By.TagName("nav"));

// Perform the navigation
Browser.Click(By.Id(forceLoad ? "perform-navigateto-force" : "perform-navigateto"));
Browser.True(() => Browser.Url.EndsWith("/nav", StringComparison.Ordinal));
Browser.Equal("Hello", () => Browser.FindElement(By.Id("nav-home")).Text);

// Verify the elements were preserved if and only if they should be
var shouldPreserveElements = !suppressEnhancedNavigation && !forceLoad;
Assert.Equal(shouldPreserveElements, !originalNavElem.IsStale());
}

private void BlockWebAssemblyResourceLoad()
{
// Force a WebAssembly resource cache miss so that we can fall back to using server interactivity
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/interactivity/navigateto"
@layout Components.TestServer.RazorComponents.Shared.EnhancedNavLayout
@inject NavigationManager Nav
@rendermode RenderMode.InteractiveServer

<h1>Interactive NavigateTo</h1>

<p>Shows that NavigateTo from an interactive event handler works as expected, with or without enhanced navigation.</p>

<button id="perform-navigateto" @onclick="@(() => PerformNavigateTo(false))">Navigate</button>
<button id="perform-navigateto-force" @onclick="@(() => PerformNavigateTo(true))">Navigate (force load)</button>

@code {
void PerformNavigateTo(bool forceLoad)
{
Nav.NavigateTo("nav", forceLoad);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@
<br />
</nav>
<hr />
@Body
<main>
@Body
</main>
Loading