Skip to content

[release/8.0] [Blazor] Avoid fetching new page content when only the hash changes #53542

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

Merged
merged 2 commits into from
Feb 7, 2024
Merged
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
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.

10 changes: 8 additions & 2 deletions src/Components/Web.JS/src/Services/NavigationEnhancement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

import { synchronizeDomContent } from '../Rendering/DomMerging/DomSync';
import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, notifyEnhancedNavigationListners } from './NavigationUtils';
import { attachProgrammaticEnhancedNavigationHandler, handleClickForNavigationInterception, hasInteractiveRouter, isSamePageWithHash, notifyEnhancedNavigationListners, performScrollToElementOnTheSamePage } from './NavigationUtils';

/*
In effect, we have two separate client-side navigation mechanisms:
Expand Down Expand Up @@ -89,8 +89,14 @@ function onDocumentClick(event: MouseEvent) {
}

handleClickForNavigationInterception(event, absoluteInternalHref => {
const shouldScrollToHash = isSamePageWithHash(absoluteInternalHref);
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ true);

if (shouldScrollToHash) {
performScrollToElementOnTheSamePage(absoluteInternalHref);
} else {
performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ true);
}
});
}

Expand Down
31 changes: 3 additions & 28 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import '@microsoft/dotnet-js-interop';
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
import { EventDelegator } from '../Rendering/Events/EventDelegator';
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
import { WebRendererId } from '../Rendering/WebRendererId';
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
import { IBlazor } from '../GlobalExports';
Expand Down Expand Up @@ -71,16 +71,6 @@ function setHasLocationChangingListeners(rendererId: WebRendererId, hasListeners
callbacks.hasLocationChangingEventListeners = hasListeners;
}

export function scrollToElement(identifier: string): boolean {
const element = document.getElementById(identifier);

if (element) {
element.scrollIntoView();
return true;
}

return false;
}

export function attachToEventDelegator(eventDelegator: EventDelegator): void {
// We need to respond to clicks on <a> elements *after* the EventDelegator has finished
Expand All @@ -97,22 +87,6 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
});
}

function isSamePageWithHash(absoluteHref: string): boolean {
const hashIndex = absoluteHref.indexOf('#');
return hashIndex > -1 && location.href.replace(location.hash, '') === absoluteHref.substring(0, hashIndex);
}

function performScrollToElementOnTheSamePage(absoluteHref : string, replace: boolean, state: string | undefined = undefined): void {
saveToBrowserHistory(absoluteHref, replace, state);

const hashIndex = absoluteHref.indexOf('#');
if (hashIndex === absoluteHref.length - 1) {
return;
}

const identifier = absoluteHref.substring(hashIndex + 1);
scrollToElement(identifier);
}

function refresh(forceReload: boolean): void {
if (!forceReload && hasProgrammaticEnhancedNavigationHandler()) {
Expand Down Expand Up @@ -181,7 +155,8 @@ async function performInternalNavigation(absoluteInternalHref: string, intercept
ignorePendingNavigation();

if (isSamePageWithHash(absoluteInternalHref)) {
performScrollToElementOnTheSamePage(absoluteInternalHref, replace, state);
saveToBrowserHistory(absoluteInternalHref, replace, state);
performScrollToElementOnTheSamePage(absoluteInternalHref);
return;
}

Expand Down
19 changes: 19 additions & 0 deletions src/Components/Web.JS/src/Services/NavigationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ export function isWithinBaseUriSpace(href: string) {
&& (nextChar === '' || nextChar === '/' || nextChar === '?' || nextChar === '#');
}

export function isSamePageWithHash(absoluteHref: string): boolean {
const url = new URL(absoluteHref);
return url.hash !== '' && location.origin === url.origin && location.pathname === url.pathname && location.search === url.search;
}

export function performScrollToElementOnTheSamePage(absoluteHref : string): void {
const hashIndex = absoluteHref.indexOf('#');
if (hashIndex === absoluteHref.length - 1) {
return;
}

const identifier = absoluteHref.substring(hashIndex + 1);
scrollToElement(identifier);
}

export function scrollToElement(identifier: string): void {
document.getElementById(identifier)?.scrollIntoView();
}

export function attachEnhancedNavigationListener(listener: typeof enhancedNavigationListener) {
enhancedNavigationListener = listener;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public void CanNavigateToAnotherPageWhilePreservingCommonDOMElements()

var h1Elem = Browser.Exists(By.TagName("h1"));
Browser.Equal("Hello", () => h1Elem.Text);

Browser.Exists(By.TagName("nav")).FindElement(By.LinkText("Streaming")).Click();

// Important: we're checking the *same* <h1> element as earlier, showing that we got to the
Expand Down Expand Up @@ -172,6 +172,20 @@ public void ScrollsToHashWithContentAddedAsynchronously()
Browser.True(() => Browser.GetScrollY() > 500);
}

[Fact]
public void CanScrollToHashWithoutPerformingFullNavigation()
{
Navigate($"{ServerPathBase}/nav/scroll-to-hash");
Browser.Equal("Scroll to hash", () => Browser.Exists(By.TagName("h1")).Text);

Browser.Exists(By.Id("scroll-anchor")).Click();
Browser.True(() => Browser.GetScrollY() > 500);
Browser.True(() => Browser
.Exists(By.Id("uri-on-page-load"))
.GetAttribute("data-value")
.EndsWith("scroll-to-hash", StringComparison.Ordinal));
}

[Theory]
[InlineData("server")]
[InlineData("webassembly")]
Expand Down Expand Up @@ -327,7 +341,7 @@ public void RefreshWithForceReloadDoesFullPageReload(string renderMode)

Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);

// Normally, you shouldn't store references to elements because they could become stale references
// after the page re-renders. However, we want to explicitly test that the element becomes stale
// across renders to ensure that a full page reload occurs.
Expand Down Expand Up @@ -602,7 +616,7 @@ public void LocationChangingEventGetsInvokedOnEnhancedNavigationOnlyForRuntimeTh
[InlineData("wasm")]
public void CanReceiveNullParameterValueOnEnhancedNavigation(string renderMode)
{
// See: https://github.com/dotnet/aspnetcore/issues/52434
// See: https://github.com/dotnet/aspnetcore/issues/52434
Navigate($"{ServerPathBase}/nav");
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
@page "/nav/scroll-to-hash"
@attribute [StreamRendering]
@inject NavigationManager NavigationManager

<PageTitle>Page for scrolling to hash</PageTitle>

<h1>Scroll to hash</h1>

<p>If you scroll down a long way, you'll find more content. We add it asynchronously via streaming rendering.</p>

<p>
<a id="scroll-anchor" href="nav/scroll-to-hash#some-content">Scroll via anchor</a>
<div id="uri-on-page-load" style="display: none" data-value="@uriOnPageLoad"></div>
</p>

<div style="height: 2000px; border: 2px dashed red;">spacer</div>

@if (showContent)
Expand All @@ -18,9 +24,11 @@

@code {
bool showContent;
string uriOnPageLoad;

protected override async Task OnInitializedAsync()
{
uriOnPageLoad = NavigationManager.Uri;
await Task.Delay(1000);
showContent = true;
}
Expand Down