Skip to content

Commit 3b5a29c

Browse files
Clean up notes about interaction
1 parent 4ad7537 commit 3b5a29c

File tree

3 files changed

+40
-33
lines changed

3 files changed

+40
-33
lines changed

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import { synchronizeDomContent } from '../Rendering/DomMerging/DomSync';
2-
import { isNavigationInterceptionEnabled } from './NavigationManager';
3-
import { handleClickForNavigationInterception } from './NavigationUtils';
2+
import { handleClickForNavigationInterception, hasInteractiveRouter } from './NavigationUtils';
43

54
/*
6-
We want this to work independently from all the existing logic for NavigationManager and EventDelegator, since
7-
this feature should be available in minimal .js bundles that don't include support for any interactive mode.
8-
So, this file should not import those modules. Likewise, we don't want NavigationManager.ts to import this module,
9-
since then it would have to bundle all the DOM-diffing logic in blazor.server|webassembly|webview.js.
10-
11-
However, when NavigationManager/EventDelegator are being used, we do have to defer to them since SPA-style
12-
interactive navigation needs to take precedence. The approach is:
13-
- When blazor.web.js starts up, it will enable progressively-enhanced (PE) nav
14-
- If an interactive <Router/> is added, it will call enableNavigationInterception and that will disable PE nav's event listener.
15-
- When NavigationManager.ts sees a navigation occur, it goes through a complex flow (respecting @preventDefault,
16-
triggering OnLocationChanging, evaluating the URL against the .NET route table, etc). Normally this will conclude
17-
by picking a component page and rendering it without notifying the PE nav logic at all.
18-
- But if no component page is matched, it then explicitly calls back into the PE nav logic to fall back on the logic
19-
that would have happened if there was no interactive <Router/>. As such, PE nav isn't *really* disabled; it just only
20-
runs as a fallback if <Router/> nav doesn't match the URL.
21-
- If an interactive <Router/> is removed, we don't currently handle that. No notification goes back from .NET
22-
to JS about that. But if the scenario becomes important, we could add some disableNavigationInterception and resume PE nav.
5+
In effect, we have two separate client-side navigation mechanisms:
6+
7+
[1] Interactive client-side routing. This is the traditional Blazor Server/WebAssembly navigation mechanism for SPAs.
8+
It is enabled whenever you have a <Router/> rendering as interactive. This intercepts all navigation within the
9+
base href URI space and tries to display a corresponding [Route] component or the NotFoundContent.
10+
[2] Progressively-enhanced navigation. This is a new mechanism in .NET 8 and is only relevant for multi-page apps.
11+
It is enabled when you load blazor.web.js and don't have an interactive <Router/>. This intercepts navigation within
12+
the base href URI space and tries to load it via a `fetch` request and DOM syncing.
13+
14+
Only one of these can be enabled at a time, otherwise both would be trying to intercept click/popstate and act on them.
15+
In fact even if we made the event handlers able to coexist, the two together would still not produce useful behaviors because
16+
[1] implies you have a <Router/>, and that will try to supply UI content for all pages or NotFoundContent if the URL doesn't
17+
match a [Route] component, so there would be nothing left for [2] to handle.
18+
19+
So, whenever [1] is enabled, we automatically disable [2].
20+
21+
However, a single site can use both [1] and [2] on different URLs.
22+
- You can navigate from [1] to [2] by setting up the interactive <Router/> not to know about any [Route] components in your MPA,
23+
and so it will fall back on a full-page load to get from the SPA URLs to the MPA URLs.
24+
- You can navigate from [2] to [1] in that it just works by default. A <Router/> can be added dynamically and will then take
25+
over and disable [2].
26+
27+
Note that we don't reference NavigationManager.ts from NavigationEnhancement.ts or vice-versa. This is to ensure we could produce
28+
different bundles that only contain minimal content.
2329
*/
2430

2531
let currentEnhancedNavigationAbortController: AbortController | null;
@@ -30,7 +36,7 @@ export function attachProgressivelyEnhancedNavigationListener() {
3036
}
3137

3238
function onBodyClicked(event: MouseEvent) {
33-
if (isNavigationInterceptionEnabled()) {
39+
if (hasInteractiveRouter()) {
3440
return;
3541
}
3642

@@ -41,7 +47,7 @@ function onBodyClicked(event: MouseEvent) {
4147
}
4248

4349
function onPopState(state: PopStateEvent) {
44-
if (isNavigationInterceptionEnabled()) {
50+
if (hasInteractiveRouter()) {
4551
return;
4652
}
4753

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
import '@microsoft/dotnet-js-interop';
55
import { resetScrollAfterNextBatch } from '../Rendering/Renderer';
66
import { EventDelegator } from '../Rendering/Events/EventDelegator';
7-
import { handleClickForNavigationInterception, isWithinBaseUriSpace, toAbsoluteUri } from './NavigationUtils';
7+
import { handleClickForNavigationInterception, hasInteractiveRouter, isWithinBaseUriSpace, setHasInteractiveRouter, toAbsoluteUri } from './NavigationUtils';
88

9-
let hasEnabledNavigationInterception = false;
109
let hasRegisteredNavigationEventListeners = false;
1110
let hasLocationChangingEventListeners = false;
1211
let currentHistoryIndex = 0;
@@ -22,7 +21,7 @@ let resolveCurrentNavigation: ((shouldContinueNavigation: boolean) => void) | nu
2221
// These are the functions we're making available for invocation from .NET
2322
export const internalFunctions = {
2423
listenForNavigationEvents,
25-
enableNavigationInterception,
24+
enableNavigationInterception: setHasInteractiveRouter,
2625
setHasLocationChangingListeners,
2726
endLocationChanging,
2827
navigateTo: navigateToFromDotNet,
@@ -47,14 +46,6 @@ function listenForNavigationEvents(
4746
currentHistoryIndex = history.state?._index ?? 0;
4847
}
4948

50-
function enableNavigationInterception(): void {
51-
hasEnabledNavigationInterception = true;
52-
}
53-
54-
export function isNavigationInterceptionEnabled(): boolean {
55-
return hasEnabledNavigationInterception;
56-
}
57-
5849
function setHasLocationChangingListeners(hasListeners: boolean) {
5950
hasLocationChangingEventListeners = hasListeners;
6051
}
@@ -75,7 +66,7 @@ export function attachToEventDelegator(eventDelegator: EventDelegator): void {
7566
// running its simulated bubbling process so that we can respect any preventDefault requests.
7667
// So instead of registering our own native event, register using the EventDelegator.
7768
eventDelegator.notifyAfterClick(event => {
78-
if (!hasEnabledNavigationInterception) {
69+
if (!hasInteractiveRouter()) {
7970
return;
8071
}
8172

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
let hasInteractiveRouterValue = false;
2+
13
/**
24
* Checks if a click event corresponds to an <a> tag referencing a URL within the base href, and that interception
35
* isn't bypassed (e.g., by a 'download' attribute or the user holding a meta key while clicking).
@@ -90,3 +92,11 @@ function findClosestAnchorAncestorLegacy(element: Element | null, tagName: strin
9092
? element
9193
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
9294
}
95+
96+
export function hasInteractiveRouter(): boolean {
97+
return hasInteractiveRouterValue;
98+
}
99+
100+
export function setHasInteractiveRouter() {
101+
hasInteractiveRouterValue = true;
102+
}

0 commit comments

Comments
 (0)