Skip to content

Commit 1aef4aa

Browse files
[Blazor] Avoid fetching new page content when only the hash changes (#53341)
1 parent 6a6e21d commit 1aef4aa

File tree

5 files changed

+57
-35
lines changed

5 files changed

+57
-35
lines changed

src/Components/Web.JS/src/Services/NavigationEnhancement.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

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

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

9191
handleClickForNavigationInterception(event, absoluteInternalHref => {
92+
const shouldScrollToHash = isSamePageWithHash(absoluteInternalHref);
9293
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
93-
performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ true);
94+
95+
if (shouldScrollToHash) {
96+
performScrollToElementOnTheSamePage(absoluteInternalHref);
97+
} else {
98+
performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ true);
99+
}
94100
});
95101
}
96102

src/Components/Web.JS/src/Services/NavigationManager.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import '@microsoft/dotnet-js-interop';
55
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
66
import { EventDelegator } from '../Rendering/Events/EventDelegator';
7-
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
7+
import { attachEnhancedNavigationListener, getInteractiveRouterRendererId, handleClickForNavigationInterception, hasInteractiveRouter, hasProgrammaticEnhancedNavigationHandler, isSamePageWithHash, isWithinBaseUriSpace, performProgrammaticEnhancedNavigation, performScrollToElementOnTheSamePage, scrollToElement, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
88
import { WebRendererId } from '../Rendering/WebRendererId';
99
import { isRendererAttached } from '../Rendering/WebRendererInteropMethods';
1010

@@ -70,16 +70,6 @@ function setHasLocationChangingListeners(rendererId: WebRendererId, hasListeners
7070
callbacks.hasLocationChangingEventListeners = hasListeners;
7171
}
7272

73-
export function scrollToElement(identifier: string): boolean {
74-
const element = document.getElementById(identifier);
75-
76-
if (element) {
77-
element.scrollIntoView();
78-
return true;
79-
}
80-
81-
return false;
82-
}
8373

8474
export function attachToEventDelegator(eventDelegator: EventDelegator): void {
8575
// We need to respond to clicks on <a> elements *after* the EventDelegator has finished
@@ -96,22 +86,6 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
9686
});
9787
}
9888

99-
function isSamePageWithHash(absoluteHref: string): boolean {
100-
const hashIndex = absoluteHref.indexOf('#');
101-
return hashIndex > -1 && location.href.replace(location.hash, '') === absoluteHref.substring(0, hashIndex);
102-
}
103-
104-
function performScrollToElementOnTheSamePage(absoluteHref : string, replace: boolean, state: string | undefined = undefined): void {
105-
saveToBrowserHistory(absoluteHref, replace, state);
106-
107-
const hashIndex = absoluteHref.indexOf('#');
108-
if (hashIndex === absoluteHref.length - 1) {
109-
return;
110-
}
111-
112-
const identifier = absoluteHref.substring(hashIndex + 1);
113-
scrollToElement(identifier);
114-
}
11589

11690
function refresh(forceReload: boolean): void {
11791
if (!forceReload && hasProgrammaticEnhancedNavigationHandler()) {
@@ -177,7 +151,8 @@ async function performInternalNavigation(absoluteInternalHref: string, intercept
177151
ignorePendingNavigation();
178152

179153
if (isSamePageWithHash(absoluteInternalHref)) {
180-
performScrollToElementOnTheSamePage(absoluteInternalHref, replace, state);
154+
saveToBrowserHistory(absoluteInternalHref, replace, state);
155+
performScrollToElementOnTheSamePage(absoluteInternalHref);
181156
return;
182157
}
183158

src/Components/Web.JS/src/Services/NavigationUtils.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ export function isWithinBaseUriSpace(href: string) {
4747
&& (nextChar === '' || nextChar === '/' || nextChar === '?' || nextChar === '#');
4848
}
4949

50+
export function isSamePageWithHash(absoluteHref: string): boolean {
51+
const url = new URL(absoluteHref);
52+
return url.hash !== '' && location.origin === url.origin && location.pathname === url.pathname && location.search === url.search;
53+
}
54+
55+
export function performScrollToElementOnTheSamePage(absoluteHref : string): void {
56+
const hashIndex = absoluteHref.indexOf('#');
57+
if (hashIndex === absoluteHref.length - 1) {
58+
return;
59+
}
60+
61+
const identifier = absoluteHref.substring(hashIndex + 1);
62+
scrollToElement(identifier);
63+
}
64+
65+
export function scrollToElement(identifier: string): void {
66+
document.getElementById(identifier)?.scrollIntoView();
67+
}
68+
5069
export function attachEnhancedNavigationListener(listener: typeof enhancedNavigationListener) {
5170
enhancedNavigationListener = listener;
5271
}
@@ -103,8 +122,8 @@ function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | SVGAElement |
103122
if (candidate instanceof HTMLAnchorElement || candidate instanceof SVGAElement) {
104123
return candidate;
105124
}
106-
}
107-
}
125+
}
126+
}
108127
return null;
109128
}
110129

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void CanNavigateToAnotherPageWhilePreservingCommonDOMElements()
3535

3636
var h1Elem = Browser.Exists(By.TagName("h1"));
3737
Browser.Equal("Hello", () => h1Elem.Text);
38-
38+
3939
Browser.Exists(By.TagName("nav")).FindElement(By.LinkText("Streaming")).Click();
4040

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

175+
[Fact]
176+
public void CanScrollToHashWithoutPerformingFullNavigation()
177+
{
178+
Navigate($"{ServerPathBase}/nav/scroll-to-hash");
179+
Browser.Equal("Scroll to hash", () => Browser.Exists(By.TagName("h1")).Text);
180+
181+
Browser.Exists(By.Id("scroll-anchor")).Click();
182+
Browser.True(() => Browser.GetScrollY() > 500);
183+
Browser.True(() => Browser
184+
.Exists(By.Id("uri-on-page-load"))
185+
.GetAttribute("data-value")
186+
.EndsWith("scroll-to-hash", StringComparison.Ordinal));
187+
}
188+
175189
[Theory]
176190
[InlineData("server")]
177191
[InlineData("webassembly")]
@@ -327,7 +341,7 @@ public void RefreshWithForceReloadDoesFullPageReload(string renderMode)
327341

328342
Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
329343
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);
330-
344+
331345
// Normally, you shouldn't store references to elements because they could become stale references
332346
// after the page re-renders. However, we want to explicitly test that the element becomes stale
333347
// across renders to ensure that a full page reload occurs.
@@ -602,7 +616,7 @@ public void LocationChangingEventGetsInvokedOnEnhancedNavigationOnlyForRuntimeTh
602616
[InlineData("wasm")]
603617
public void CanReceiveNullParameterValueOnEnhancedNavigation(string renderMode)
604618
{
605-
// See: https://github.com/dotnet/aspnetcore/issues/52434
619+
// See: https://github.com/dotnet/aspnetcore/issues/52434
606620
Navigate($"{ServerPathBase}/nav");
607621
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
608622

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageForScrollingToHash.razor

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
@page "/nav/scroll-to-hash"
22
@attribute [StreamRendering]
3+
@inject NavigationManager NavigationManager
34

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

67
<h1>Scroll to hash</h1>
78

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

11+
<p>
12+
<a id="scroll-anchor" href="nav/scroll-to-hash#some-content">Scroll via anchor</a>
13+
<div id="uri-on-page-load" style="display: none" data-value="@uriOnPageLoad"></div>
14+
</p>
15+
1016
<div style="height: 2000px; border: 2px dashed red;">spacer</div>
1117

1218
@if (showContent)
@@ -18,9 +24,11 @@
1824

1925
@code {
2026
bool showContent;
27+
string uriOnPageLoad;
2128

2229
protected override async Task OnInitializedAsync()
2330
{
31+
uriOnPageLoad = NavigationManager.Uri;
2432
await Task.Delay(1000);
2533
showContent = true;
2634
}

0 commit comments

Comments
 (0)