Low-level NavLink implementation

This commit is contained in:
Steve Sanderson 2018-02-22 12:00:49 +00:00
parent 25b76bc6dc
commit 3e30655ea4
5 changed files with 184 additions and 9 deletions

View File

@ -1,4 +1,5 @@
<div class='main-nav'>
@using Microsoft.AspNetCore.Blazor.Browser.Routing
<div class='main-nav'>
<div class='navbar navbar-inverse'>
<div class='navbar-header'>
<button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'>
@ -13,14 +14,14 @@
<div class='navbar-collapse collapse'>
<ul class='nav navbar-nav'>
<li>
<a href='/'>
<c:NavLink href=@("/")>
<span class='glyphicon glyphicon-home'></span> Home
</a>
</c:NavLink>
</li>
<li>
<a href='/counter'>
<c:NavLink href=@("/counter")>
<span class='glyphicon glyphicon-education'></span> Counter
</a>
</c:NavLink>
</li>
</ul>
</div>

View File

@ -64,3 +64,22 @@
text-overflow: ellipsis;
}
}
/*
Temporary workaround due to the <blazor-component> wrapper element. The wrapper element
will be eliminated in a future update, making this workaround unnecessary.
*/
.navbar-nav > li a {
color: #9d9d9d;
line-height: 20px;
position: relative;
display: block;
}
.navbar-nav > li a:hover {
color: #fff;
background-color: transparent;
text-decoration: none;
}
.navbar-nav > li a:focus {
text-decoration: none;
}

View File

@ -0,0 +1,131 @@
// 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.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Browser.Routing
{
// TODO: Move this into Microsoft.AspNetCore.Blazor, and use DI to break the
// coupling on Microsoft.AspNetCore.Blazor.Browser.Routing.UriHelper.
// That's because you'd use NavLink in non-browser scenarios too (e.g., prerendering).
// Can't do this until DI is implemented.
public class NavLink : IComponent, IDisposable
{
const string CssClassAttributeName = "class";
const string HrefAttributeName = "href";
private RenderHandle _renderHandle;
private bool _isActive;
private RenderFragment _childContent;
private string _cssClass;
private string _href;
private string _hrefAbsolute;
private IDictionary<string, string> _otherAttributes;
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
UriHelper.OnLocationChanged += OnLocationChanged;
}
public void SetParameters(ParameterCollection parameters)
{
_childContent = null;
_href = null;
_hrefAbsolute = null;
_cssClass = null;
_otherAttributes?.Clear();
foreach (var kvp in parameters)
{
switch (kvp.Name)
{
case RenderTreeBuilder.ChildContent:
_childContent = kvp.Value as RenderFragment;
break;
case CssClassAttributeName:
_cssClass = kvp.Value as string;
break;
case HrefAttributeName:
_href = kvp.Value as string;
_hrefAbsolute = UriHelper.ToAbsoluteUri(_href).AbsoluteUri;
break;
default:
if (kvp.Value != null)
{
if (_otherAttributes == null)
{
_otherAttributes = new Dictionary<string, string>();
}
_otherAttributes.Add(kvp.Name, kvp.Value.ToString());
}
break;
}
}
_isActive = UriHelper.GetAbsoluteUri().Equals(_hrefAbsolute, StringComparison.Ordinal);
_renderHandle.Render(Render);
}
public void Dispose()
{
UriHelper.OnLocationChanged -= OnLocationChanged;
}
private void OnLocationChanged(object sender, string e)
{
var shouldBeActiveNow = UriHelper.GetAbsoluteUri().Equals(
_hrefAbsolute,
StringComparison.Ordinal);
if (shouldBeActiveNow != _isActive)
{
_isActive = shouldBeActiveNow;
if (_renderHandle.IsInitialized)
{
_renderHandle.Render(Render);
}
}
}
private void Render(RenderTreeBuilder builder)
{
builder.OpenElement(0, "a");
if (!string.IsNullOrEmpty(_href))
{
builder.AddAttribute(0, HrefAttributeName, _href);
}
var combinedClassValue = CombineWithSpace(_cssClass, _isActive ? "active" : null);
if (combinedClassValue != null)
{
builder.AddAttribute(0, CssClassAttributeName, combinedClassValue);
}
if (_otherAttributes != null)
{
foreach (var kvp in _otherAttributes)
{
builder.AddAttribute(0, kvp.Key, kvp.Value);
}
}
if (_childContent != null)
{
builder.AddContent(1, _childContent);
}
builder.CloseElement();
}
private string CombineWithSpace(string str1, string str2)
=> str1 == null ? str2
: (str2 == null ? str1 : $"{str1} {str2}");
}
}

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing
{
static readonly string _functionPrefix = typeof(UriHelper).FullName;
static string _currentAbsoluteUri;
static string _baseUriString;
static Uri _baseUri;
/// <summary>
/// An event that fires when the navigation location has changed.
@ -38,9 +40,20 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing
/// <returns>The URI prefix.</returns>
public static string GetBaseUriPrefix()
{
var baseUri = RegisteredFunction.InvokeUnmarshalled<string>(
$"{_functionPrefix}.getBaseURI");
return ToBaseUriPrefix(baseUri);
EnsureBaseUriPopulated();
return _baseUriString;
}
private static void EnsureBaseUriPopulated()
{
// The <base href> is fixed for the lifetime of the page, so just cache it
if (_baseUriString == null)
{
var baseUri = RegisteredFunction.InvokeUnmarshalled<string>(
$"{_functionPrefix}.getBaseURI");
_baseUriString = ToBaseUriPrefix(baseUri);
_baseUri = new Uri(_baseUriString);
}
}
/// <summary>
@ -58,6 +71,17 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Routing
return _currentAbsoluteUri;
}
/// <summary>
/// Converts a relative URI into an absolute one.
/// </summary>
/// <param name="relativeUri">The relative URI.</param>
/// <returns>The absolute URI.</returns>
public static Uri ToAbsoluteUri(string relativeUri)
{
EnsureBaseUriPopulated();
return new Uri(_baseUri, relativeUri);
}
/// <summary>
/// Given a base URI prefix (e.g., one previously returned by <see cref="GetBaseUriPrefix"/>),
/// converts an absolute URI into one relative to the base URI prefix.

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// The reserved parameter name used for supplying child content.
/// </summary>
public static string ChildContent = nameof(ChildContent);
public const string ChildContent = nameof(ChildContent);
/// <summary>
/// Constructs an instance of <see cref="RenderTreeBuilder"/>.