aspnetcore/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs

160 lines
6.4 KiB
C#

// 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 Microsoft.AspNetCore.Blazor.Services;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Routing
{
// NOTE: This could be implemented in a more performant way by iterating through
// the ParameterCollection only once (instead of multiple TryGetValue calls), and
// avoiding allocating a dictionary in the case where there are no additional params.
// However the intention here is to get a sense of what more high-level coding patterns
// will exist and what APIs are needed to support them. Later in the project when we
// have more examples of components implemented in pure C# (not Razor) we could change
// this one to the more low-level perf-sensitive implementation.
/// <summary>
/// A component that renders an anchor tag, automatically toggling its 'active'
/// class based on whether its 'href' matches the current URI.
/// </summary>
public class NavLink : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private bool _isActive;
private RenderFragment _childContent;
private string _cssClass;
private string _hrefAbsolute;
private IReadOnlyDictionary<string, object> _allAttributes;
/// <summary>
/// Gets or sets the CSS class name applied to the NavLink when the
/// current route matches the NavLink href.
/// </summary>
public string ActiveClass { get; set; }
/// <summary>
/// Gets or sets a value representing the URL matching behavior.
/// </summary>
public NavLinkMatch Match { get; set; }
[Inject] private IUriHelper UriHelper { get; set; }
/// <inheritdoc />
public void Init(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
// We'll consider re-rendering on each location change
UriHelper.OnLocationChanged += OnLocationChanged;
}
/// <inheritdoc />
public void SetParameters(ParameterCollection parameters)
{
// Capture the parameters we want to do special things with, plus all as a dictionary
parameters.TryGetValue(RenderTreeBuilder.ChildContent, out _childContent);
parameters.TryGetValue("class", out _cssClass);
parameters.TryGetValue("href", out string href);
ActiveClass = parameters.GetValueOrDefault(nameof(ActiveClass), "active");
Match = parameters.GetValueOrDefault(nameof(Match), NavLinkMatch.Prefix);
_allAttributes = parameters.ToDictionary();
// Update computed state and render
_hrefAbsolute = href == null ? null : UriHelper.ToAbsoluteUri(href).AbsoluteUri;
_isActive = ShouldMatch(UriHelper.GetAbsoluteUri());
_renderHandle.Render(Render);
}
public void Dispose()
{
// To avoid leaking memory, it's important to detach any event handlers in Dispose()
UriHelper.OnLocationChanged -= OnLocationChanged;
}
private void OnLocationChanged(object sender, string newUriAbsolute)
{
// We could just re-render always, but for this component we know the
// only relevant state change is to the _isActive property.
var shouldBeActiveNow = ShouldMatch(newUriAbsolute);
if (shouldBeActiveNow != _isActive)
{
_isActive = shouldBeActiveNow;
_renderHandle.Render(Render);
}
}
private bool ShouldMatch(string currentUriAbsolute)
{
if (Match == NavLinkMatch.Prefix)
{
return StartsWithAndHasSeparator(currentUriAbsolute, _hrefAbsolute);
}
else if (Match == NavLinkMatch.All)
{
return string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.Ordinal);
}
else
{
throw new InvalidOperationException($"Unsupported {nameof(NavLinkMatch)} value: {Match}");
}
}
private void Render(RenderTreeBuilder builder)
{
builder.OpenElement(0, "a");
// Set class attribute
string classAttrValue = CombineWithSpace(_cssClass, _isActive ? ActiveClass : null);
builder.AddAttribute(0, "class", classAttrValue);
// Pass through all other attributes unchanged
foreach (var kvp in _allAttributes.Where(kvp => kvp.Key != "class" && kvp.Key != nameof(RenderTreeBuilder.ChildContent)))
{
builder.AddAttribute(0, kvp.Key, kvp.Value);
}
// Pass through any child content unchanged
builder.AddContent(1, _childContent);
builder.CloseElement();
}
private string CombineWithSpace(string str1, string str2)
=> str1 == null ? (str2 ?? string.Empty) // Return an empty string if both str1 and str2 are null
: (str2 == null ? str1 : $"{str1} {str2}");
private static bool StartsWithAndHasSeparator(string value, string prefix)
{
var valueLength = value.Length;
var prefixLength = prefix.Length;
if (prefixLength == valueLength)
{
return string.Equals(value, prefix, StringComparison.Ordinal);
}
else if (valueLength > prefixLength)
{
return value.StartsWith(prefix, StringComparison.Ordinal)
&& (
// Only match when there's a separator character either at the end of the
// prefix or right after it.
// Example: "/abc" is treated as a prefix of "/abc/def" but not "/abcdef"
// Example: "/abc/" is treated as a prefix of "/abc/def" but not "/abcdef"
prefixLength == 0
|| !char.IsLetterOrDigit(prefix[prefixLength - 1])
|| !char.IsLetterOrDigit(value[prefixLength])
);
}
else
{
return false;
}
}
}
}