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
|
||||
<c:LayoutDisplay Page=@typeof(StandaloneApp.Pages.Home) />
|
||||
@using Microsoft.AspNetCore.Blazor.Browser.Routing
|
||||
<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>())
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
|
|
@ -3,13 +3,14 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Blazor standalone</title>
|
||||
<link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="/css/site.css" rel="stylesheet" />
|
||||
<base href="/" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/site.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { platform } from './Environment';
|
||||
import { getAssemblyNameFromUrl } from './Platform/DotNet';
|
||||
import './Rendering/Renderer';
|
||||
import './Routing/UriHelper';
|
||||
import './GlobalExports';
|
||||
|
||||
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