Skip to content

Defer link interception until Router is initialized #10062

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 7 commits into from
May 17, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ internal WebAssemblyUriHelper() { }
protected override void EnsureInitialized() { }
protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string newAbsoluteUri) { }
public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink) { }
}
}
namespace Microsoft.AspNetCore.Components.Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;

Expand Down Expand Up @@ -90,6 +91,7 @@ private void CreateServiceProvider()
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Routing;
using Interop = Microsoft.AspNetCore.Components.Browser.BrowserUriHelperInterop;

namespace Microsoft.AspNetCore.Blazor.Services
{
internal sealed class WebAssemblyNavigationInterception : INavigationInterception
{
public static readonly WebAssemblyNavigationInterception Instance = new WebAssemblyNavigationInterception();

public Task EnableNavigationInterceptionAsync()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(Interop.EnableNavigationInterception);
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal WebAssemblyUriHelper()
protected override void EnsureInitialized()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(
Interop.EnableNavigationInterception,
Interop.ListenForNavigationEvents,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, this is so much clearer! 👍

typeof(WebAssemblyUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));

Expand All @@ -54,10 +54,10 @@ protected override void NavigateToCore(string uri, bool forceLoad)
/// For framework use only.
/// </summary>
[JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string newAbsoluteUri)
public static void NotifyLocationChanged(string newAbsoluteUri, bool isInterceptedLink)
{
Instance.SetAbsoluteUri(newAbsoluteUri);
Instance.TriggerOnLocationChanged();
Instance.TriggerOnLocationChanged(isInterceptedLink);
}

/// <summary>
Expand Down
46 changes: 30 additions & 16 deletions src/Components/Browser.JS/dist/Debug/blazor.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14885,62 +14885,76 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
};
Object.defineProperty(exports, "__esModule", { value: true });
__webpack_require__(/*! @dotnet/jsinterop */ "../node_modules/@dotnet/jsinterop/dist/Microsoft.JSInterop.js");
var hasRegisteredEventListeners = false;
var hasRegisteredNavigationInterception = false;
var hasRegisteredNavigationEventListeners = false;
// Will be initialized once someone registers
var notifyLocationChangedCallback = null;
// These are the functions we're making available for invocation from .NET
exports.internalFunctions = {
listenForNavigationEvents: listenForNavigationEvents,
enableNavigationInterception: enableNavigationInterception,
navigateTo: navigateTo,
getBaseURI: function () { return document.baseURI; },
getLocationHref: function () { return location.href; },
};
function enableNavigationInterception(assemblyName, functionName) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) {
function listenForNavigationEvents(assemblyName, functionName) {
if (hasRegisteredNavigationEventListeners) {
return;
}
notifyLocationChangedCallback = { assemblyName: assemblyName, functionName: functionName };
hasRegisteredEventListeners = true;
hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', function () { return notifyLocationChanged(false); });
}
function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}
hasRegisteredNavigationInterception = true;
document.addEventListener('click', function (event) {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
var anchorTarget = findClosestAncestor(event.target, 'A');
var hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) {
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
var targetAttributeValue = anchorTarget.getAttribute('target');
var opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) {
if (!opensInSameFrame) {
return;
}
var href = anchorTarget.getAttribute(hrefAttributeName);
var absoluteHref = toAbsoluteUri(href);
if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref);
performInternalNavigation(absoluteHref, true);
}
}
});
window.addEventListener('popstate', handleInternalNavigation);
}
function navigateTo(uri, forceLoad) {
var absoluteUri = toAbsoluteUri(uri);
if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri);
performInternalNavigation(absoluteUri, false);
}
else {
location.href = uri;
}
}
exports.navigateTo = navigateTo;
function performInternalNavigation(absoluteInternalHref) {
function performInternalNavigation(absoluteInternalHref, interceptedLink) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation();
notifyLocationChanged(interceptedLink);
}
function handleInternalNavigation() {
function notifyLocationChanged(interceptedLink) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!notifyLocationChangedCallback) return [3 /*break*/, 2];
return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href)];
return [4 /*yield*/, DotNet.invokeMethodAsync(notifyLocationChangedCallback.assemblyName, notifyLocationChangedCallback.functionName, location.href, interceptedLink)];
case 1:
_a.sent();
_a.label = 2;
Expand Down
46 changes: 30 additions & 16 deletions src/Components/Browser.JS/dist/Debug/blazor.webassembly.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Components/Browser.JS/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "yarn run build:debug && yarn run build:production",
"build:debug": "cd src && webpack --mode development --config ./webpack.config.js",
"build:production": "cd src && webpack --mode production --config ./webpack.config.js",
"test": "jest"
Expand Down
53 changes: 36 additions & 17 deletions src/Components/Browser.JS/src/Services/UriHelper.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,88 @@
import '@dotnet/jsinterop';

let hasRegisteredEventListeners = false;
let hasRegisteredNavigationInterception = false;
let hasRegisteredNavigationEventListeners = false;

// Will be initialized once someone registers
let notifyLocationChangedCallback: { assemblyName: string; functionName: string } | null = null;

// These are the functions we're making available for invocation from .NET
export const internalFunctions = {
listenForNavigationEvents,
enableNavigationInterception,
navigateTo,
getBaseURI: () => document.baseURI,
getLocationHref: () => location.href,
};

function enableNavigationInterception(assemblyName: string, functionName: string) {
if (hasRegisteredEventListeners || assemblyName === undefined || functionName === undefined) {
function listenForNavigationEvents(assemblyName: string, functionName: string) {
if (hasRegisteredNavigationEventListeners) {
return;
}

notifyLocationChangedCallback = { assemblyName, functionName };
hasRegisteredEventListeners = true;

hasRegisteredNavigationEventListeners = true;
window.addEventListener('popstate', () => notifyLocationChanged(false));
}

function enableNavigationInterception() {
if (hasRegisteredNavigationInterception) {
return;
}

hasRegisteredNavigationInterception = true;

document.addEventListener('click', event => {
if (event.button !== 0 || eventHasSpecialKey(event)) {
// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
return;
}

// Intercept clicks on all <a> elements where the href is within the <base href> URI space
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement;
const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName) && event.button === 0) {
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorTarget.getAttribute('target');
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
if (!opensInSameFrame) {
return;
}

// Don't stop ctrl/meta-click (etc) from opening links in new tabs/windows
if (isWithinBaseUriSpace(absoluteHref) && !eventHasSpecialKey(event) && opensInSameFrame) {
const href = anchorTarget.getAttribute(hrefAttributeName)!;
const absoluteHref = toAbsoluteUri(href);

if (isWithinBaseUriSpace(absoluteHref)) {
event.preventDefault();
performInternalNavigation(absoluteHref);
performInternalNavigation(absoluteHref, true);
}
}
});

window.addEventListener('popstate', handleInternalNavigation);
}

export function navigateTo(uri: string, forceLoad: boolean) {
const absoluteUri = toAbsoluteUri(uri);

if (!forceLoad && isWithinBaseUriSpace(absoluteUri)) {
performInternalNavigation(absoluteUri);
performInternalNavigation(absoluteUri, false);
} else {
location.href = uri;
}
}

function performInternalNavigation(absoluteInternalHref: string) {
function performInternalNavigation(absoluteInternalHref: string, interceptedLink: boolean) {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
handleInternalNavigation();
notifyLocationChanged(interceptedLink);
}

async function handleInternalNavigation() {
async function notifyLocationChanged(interceptedLink: boolean) {
if (notifyLocationChangedCallback) {
await DotNet.invokeMethodAsync(
notifyLocationChangedCallback.assemblyName,
notifyLocationChangedCallback.functionName,
location.href
location.href,
interceptedLink
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Browser/src/BrowserUriHelperInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ internal static class BrowserUriHelperInterop
{
private static readonly string Prefix = "Blazor._internal.uriHelper.";

public static readonly string ListenForNavigationEvents = Prefix + "listenForNavigationEvents";

public static readonly string EnableNavigationInterception = Prefix + "enableNavigationInterception";

public static readonly string GetLocationHref = Prefix + "getLocationHref";
Expand Down
Loading