Initial, minimal BrowserRouter implementation. No config besides subclassing yet.
This commit is contained in:
parent
0b5294a8f5
commit
7370d748c6
|
|
@ -1,2 +1,2 @@
|
||||||
@using Microsoft.AspNetCore.Blazor.Layouts
|
@using Microsoft.AspNetCore.Blazor.Browser.Routing
|
||||||
<c:LayoutDisplay Page=@typeof(StandaloneApp.Pages.Home) />
|
<c:BrowserRouter AppAssembly=@(typeof(StandaloneApp.Program).Assembly) PagesNamespace=@("StandaloneApp.Pages") />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1>About</h1>
|
||||||
|
|
||||||
|
Some content goes here.
|
||||||
|
|
||||||
|
<a href="/">Home</a>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
|
Welcome to your new app.
|
||||||
|
|
||||||
|
<a href="/about">About</a>
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
@(Layout<StandaloneApp.Shared.MainLayout>())
|
@(Layout<StandaloneApp.Shared.MainLayout>())
|
||||||
<h1>Hello, world!</h1>
|
|
||||||
|
|
||||||
Welcome to your new app.
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Blazor standalone</title>
|
<title>Blazor standalone</title>
|
||||||
<link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<base href="/" />
|
||||||
<link href="/css/site.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="css/site.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app>Loading...</app>
|
<app>Loading...</app>
|
||||||
|
|
||||||
<script src="/css/bootstrap/bootstrap-native.min.js"></script>
|
<script src="css/bootstrap/bootstrap-native.min.js"></script>
|
||||||
<script type="blazor-boot"></script>
|
<script type="blazor-boot"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { platform } from './Environment';
|
import { platform } from './Environment';
|
||||||
import { getAssemblyNameFromUrl } from './Platform/DotNet';
|
import { getAssemblyNameFromUrl } from './Platform/DotNet';
|
||||||
import './Rendering/Renderer';
|
import './Rendering/Renderer';
|
||||||
|
import './Routing/UriHelper';
|
||||||
import './GlobalExports';
|
import './GlobalExports';
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||||
|
import { platform } from '../Environment';
|
||||||
|
import { MethodHandle } from '../Platform/Platform';
|
||||||
|
const registeredFunctionPrefix = 'Microsoft.AspNetCore.Blazor.Browser.Routing.UriHelper';
|
||||||
|
let notifyLocationChangedMethod: MethodHandle;
|
||||||
|
let hasRegisteredEventListeners = false;
|
||||||
|
|
||||||
|
registerFunction(`${registeredFunctionPrefix}.getLocationHref`,
|
||||||
|
() => platform.toDotNetString(location.href));
|
||||||
|
|
||||||
|
registerFunction(`${registeredFunctionPrefix}.getBaseURI`,
|
||||||
|
() => document.baseURI ? platform.toDotNetString(document.baseURI) : null);
|
||||||
|
|
||||||
|
registerFunction(`${registeredFunctionPrefix}.enableNavigationInteception`, () => {
|
||||||
|
if (hasRegisteredEventListeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasRegisteredEventListeners = true;
|
||||||
|
|
||||||
|
document.addEventListener('click', event => {
|
||||||
|
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
|
||||||
|
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A');
|
||||||
|
if (anchorTarget) {
|
||||||
|
const href = anchorTarget.getAttribute('href');
|
||||||
|
if (isWithinBaseUriSpace(toAbsoluteUri(href))) {
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState(null, /* ignored title */ '', href);
|
||||||
|
handleInternalNavigation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handleInternalNavigation);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleInternalNavigation() {
|
||||||
|
if (!notifyLocationChangedMethod) {
|
||||||
|
notifyLocationChangedMethod = platform.findMethod(
|
||||||
|
'Microsoft.AspNetCore.Blazor.Browser',
|
||||||
|
'Microsoft.AspNetCore.Blazor.Browser.Routing',
|
||||||
|
'UriHelper',
|
||||||
|
'NotifyLocationChanged'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
platform.callMethod(notifyLocationChangedMethod, null, [
|
||||||
|
platform.toDotNetString(location.href)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let testAnchor: HTMLAnchorElement;
|
||||||
|
function toAbsoluteUri(relativeUri: string) {
|
||||||
|
testAnchor = testAnchor || document.createElement('a');
|
||||||
|
testAnchor.href = relativeUri;
|
||||||
|
return testAnchor.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosestAncestor(element: Element | null, tagName: string) {
|
||||||
|
return !element
|
||||||
|
? null
|
||||||
|
: element.tagName === tagName
|
||||||
|
? element
|
||||||
|
: findClosestAncestor(element.parentElement, tagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWithinBaseUriSpace(href: string) {
|
||||||
|
const baseUriPrefixWithTrailingSlash = toBaseUriPrefixWithTrailingSlash(document.baseURI!); // TODO: Might baseURI really be null?
|
||||||
|
return href.startsWith(baseUriPrefixWithTrailingSlash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBaseUriPrefixWithTrailingSlash(baseUri: string) {
|
||||||
|
return baseUri.substr(0, baseUri.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
// 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;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||||
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Browser.Routing
|
||||||
|
{
|
||||||
|
public class BrowserRouter : IComponent, IDisposable
|
||||||
|
{
|
||||||
|
RenderHandle _renderHandle;
|
||||||
|
string _baseUriPrefix;
|
||||||
|
string _locationAbsolute;
|
||||||
|
|
||||||
|
public Assembly AppAssembly { get; set; }
|
||||||
|
|
||||||
|
public string PagesNamespace { get; set; }
|
||||||
|
|
||||||
|
public string DefaultComponentName { get; set; } = "Index";
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
{
|
||||||
|
_renderHandle = renderHandle;
|
||||||
|
|
||||||
|
UriHelper.EnableNavigationInteception();
|
||||||
|
UriHelper.OnLocationChanged += OnLocationChanged;
|
||||||
|
_baseUriPrefix = UriHelper.GetBaseUriPrefix();
|
||||||
|
_locationAbsolute = UriHelper.GetAbsoluteUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
{
|
||||||
|
parameters.AssignToProperties(this);
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
UriHelper.OnLocationChanged -= OnLocationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Type GetComponentTypeForPath(string locationPath)
|
||||||
|
{
|
||||||
|
if (AppAssembly == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"No value was specified for {nameof(AppAssembly)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(PagesNamespace))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"No value was specified for {nameof(PagesNamespace)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentTypeName = $"{PagesNamespace}{locationPath.Replace('/', '.')}";
|
||||||
|
if (componentTypeName[componentTypeName.Length - 1] == '.')
|
||||||
|
{
|
||||||
|
componentTypeName += DefaultComponentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FindComponentTypeInAssemblyOrReferences(AppAssembly, componentTypeName)
|
||||||
|
?? throw new InvalidOperationException($"{nameof(BrowserRouter)} cannot find any component type with name {componentTypeName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Type FindComponentTypeInAssemblyOrReferences(Assembly assembly, string typeName)
|
||||||
|
=> assembly.GetType(typeName, throwOnError: false, ignoreCase: true)
|
||||||
|
?? assembly.GetReferencedAssemblies()
|
||||||
|
.Select(Assembly.Load)
|
||||||
|
.Select(referencedAssembly => FindComponentTypeInAssemblyOrReferences(referencedAssembly, typeName))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
protected virtual void Render(RenderTreeBuilder builder, Type matchedComponentType)
|
||||||
|
{
|
||||||
|
builder.OpenComponent(0, typeof(LayoutDisplay));
|
||||||
|
builder.AddAttribute(1, nameof(LayoutDisplay.Page), matchedComponentType);
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
var locationPath = UriHelper.ToBaseRelativePath(_baseUriPrefix, _locationAbsolute);
|
||||||
|
var matchedComponentType = GetComponentTypeForPath(locationPath);
|
||||||
|
if (!typeof(IComponent).IsAssignableFrom(matchedComponentType))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The type {matchedComponentType.FullName} " +
|
||||||
|
$"does not implement {typeof(IComponent).FullName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderHandle.Render(builder => Render(builder, matchedComponentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocationChanged(object sender, string newAbsoluteUri)
|
||||||
|
{
|
||||||
|
_locationAbsolute = newAbsoluteUri;
|
||||||
|
if (_renderHandle.IsInitialized)
|
||||||
|
{
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Blazor.Browser.Interop;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Browser.Routing
|
||||||
|
{
|
||||||
|
public static class UriHelper
|
||||||
|
{
|
||||||
|
static readonly string _functionPrefix = typeof(UriHelper).FullName;
|
||||||
|
|
||||||
|
public static event EventHandler<string> OnLocationChanged;
|
||||||
|
|
||||||
|
public static void EnableNavigationInteception()
|
||||||
|
=> RegisteredFunction.InvokeUnmarshalled<object>(
|
||||||
|
$"{_functionPrefix}.enableNavigationInteception");
|
||||||
|
|
||||||
|
public static string GetBaseUriPrefix()
|
||||||
|
{
|
||||||
|
var baseUri = RegisteredFunction.InvokeUnmarshalled<string>(
|
||||||
|
$"{_functionPrefix}.getBaseURI");
|
||||||
|
return ToBaseURIPrefix(baseUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetAbsoluteUri()
|
||||||
|
{
|
||||||
|
return RegisteredFunction.InvokeUnmarshalled<string>(
|
||||||
|
$"{_functionPrefix}.getLocationHref");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToBaseRelativePath(string baseUriPrefix, string absoluteUri)
|
||||||
|
{
|
||||||
|
// The absolute URI must be of the form "{baseUriPrefix}/something",
|
||||||
|
// and from that we return "/something" (also stripping any querystring
|
||||||
|
// and/or hash value)
|
||||||
|
if (absoluteUri.StartsWith(baseUriPrefix, StringComparison.Ordinal)
|
||||||
|
&& absoluteUri.Length > baseUriPrefix.Length
|
||||||
|
&& absoluteUri[baseUriPrefix.Length] == '/')
|
||||||
|
{
|
||||||
|
// TODO: Remove querystring and/or hash
|
||||||
|
return absoluteUri.Substring(baseUriPrefix.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"The URI '{absoluteUri}' is not contained by the base URI '{baseUriPrefix}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NotifyLocationChanged(string newAbsoluteUri)
|
||||||
|
=> OnLocationChanged?.Invoke(null, newAbsoluteUri);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given the href value from the document's <base> element, returns the URI
|
||||||
|
/// prefix that can be prepended to URI paths to produce an absolute URI.
|
||||||
|
/// This is computed by removing the final slash and any following characters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseUri">The href value from a document's <base> element.</param>
|
||||||
|
/// <returns>The URI prefix</returns>
|
||||||
|
private static string ToBaseURIPrefix(string baseUri)
|
||||||
|
{
|
||||||
|
if (baseUri != null)
|
||||||
|
{
|
||||||
|
var lastSlashIndex = baseUri.LastIndexOf('/');
|
||||||
|
if (lastSlashIndex >= 0)
|
||||||
|
{
|
||||||
|
return baseUri.Substring(0, lastSlashIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue