Initial, minimal BrowserRouter implementation. No config besides subclassing yet.

This commit is contained in:
Steve Sanderson 2018-02-20 14:46:50 +00:00
parent 0b5294a8f5
commit 7370d748c6
9 changed files with 266 additions and 8 deletions

View File

@ -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") />

View File

@ -0,0 +1,5 @@
<h1>About</h1>
Some content goes here.
<a href="/">Home</a>

View File

@ -0,0 +1,5 @@
<h1>Hello, world!</h1>
Welcome to your new app.
<a href="/about">About</a>

View File

@ -1,4 +1 @@
@(Layout<StandaloneApp.Shared.MainLayout>())
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -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>

View File

@ -1,6 +1,7 @@
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
import './Rendering/Renderer';
import './Routing/UriHelper';
import './GlobalExports';
async function boot() {

View File

@ -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);
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}
}