Search code examples
htmlnavigationblazoranchorblazor-server-side

Routing to named element in Blazor (use anchor to navigate to specific element)


I cannot use an HTML anchor to navigate to a specific HTML element of a page in the Blazor Server. For example:

@page "/test"

<nav>
    <!-- One version I've tried -->
    <a href="#section2">Section2</a>

    <!-- Another version I've tried -->
    <NavLink href="#section2">Section2</NavLink>    
</nav>

@* ... *@


<h2 id="section2">It's Section2.</h2>
@* ... *@

When I click the link to Section2, I get redirected to the route http://localhost:5000/test#section2, however, will be at the top of the page. In my opinion, the browser should scroll down to the proper element, as specified by the Element Selector, but it can't.

Does it have to be done in a special way in Blazor?

I use Blazor 6 in .Net6 with Visual Studio 2022 (ver:17.0.2).


Solution

  • You can also use an ElementReference and FocusAsync which uses the built in Blazor JS. To use it you need to use a small hack to make the component "Focusable" which is to set a tabindex. I've used a span but you can use what you like. I've used @alessandromanzini's code to get the element from the NavigationManager.

    Here's a component:

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Rendering;
    using Microsoft.AspNetCore.Components.Routing;
    using System.Diagnostics.CodeAnalysis;
    
    namespace SO75358165;
    
    public class Bookmark : ComponentBase, IDisposable
    {
        private bool _setFocus;
    
        [Inject] private NavigationManager NavManager { get; set; } = default!;
        [Parameter] public RenderFragment? ChildContent { get; set; }
        [Parameter] public string? BookmarkName { get; set; }
        [DisallowNull] public ElementReference? Element { get; private set; }
    
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.OpenElement(0, "span");
            builder.AddAttribute(2, "tabindex", "-1");
            builder.AddContent(3, this.ChildContent);
            builder.AddElementReferenceCapture(4, this.SetReference);
            builder.CloseElement();
        }
    
        protected override void OnInitialized()
            => NavManager.LocationChanged += this.OnLocationChanged;
    
        protected override void OnParametersSet()
            => _setFocus = this.IsMe();
    
        private void SetReference(ElementReference reference)
            => this.Element = reference;
    
        private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
        {
            if (this.IsMe())
            {
                _setFocus = true;
                this.StateHasChanged();
            }
        }
    
        protected async override Task OnAfterRenderAsync(bool firstRender)
        {
            if (_setFocus)
                await this.Element!.Value.FocusAsync(false);
    
            _setFocus = false;
        }
    
        private bool IsMe()
        {
            string? elementId = null;
    
            var uri = new Uri(this.NavManager.Uri, UriKind.Absolute);
            if (uri.Fragment.StartsWith('#'))
            {
                elementId = uri.Fragment.Substring(1);
                return elementId == BookmarkName;
            }
            return false;
        }
    
        public void Dispose()
            => NavManager.LocationChanged -= this.OnLocationChanged;
    }
    

    Here's my test page:

    @page "/"
    <PageTitle>Index</PageTitle>
    <NavLink href="#me">To me</NavLink>
    <h1>Hello, world!</h1>
    <h1>Hello, world!</h1>
    <h1>Hello, world!</h1>
    //.....
    <h1>Hello, world!</h1>
    <Bookmark BookmarkName="me" >
        <h1 id="me">Focus on Me</h1>
    </Bookmark>