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

5

5

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).

Risteau answered 6/2, 2023 at 6:52 Comment(0)
E
6

After loading a page, a browser automatically scrolls to the element identified by its id in the fragment part of the URL. It does the same when you click on an anchor with an href of the kind #element-id.

The page load behavior doesn't work for a Blazor Server because the element doesn't exist yet on page load.

The solution is to manually create a scroller using javascript and a razor component:

First of all, create a razor component like this

@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}

Then add this javascript code somewhere before the Blazor script renders. You can wrap it with script tags and place it in the head.

function BlazorScrollToId(id) {
            const element = document.getElementById(id);
            if (element instanceof HTMLElement) {
                element.scrollIntoView({
                    behavior: "smooth",
                    block: "start",
                    inline: "nearest"
                });
            }
        }

Finally implement it in your pages if needed. You can also place it inside your layouts, so it will work for every page you create.

@page "/"

<PageTitle>Index</PageTitle>

<a href="#my-id">
    <h1>Hello, world!</h1>
</a>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<div id="my-id">
    Hello!
</div>

<AnchorNavigation />

Source: link

Excel answered 6/2, 2023 at 8:14 Comment(1)
I tried this one it is working fine but getting unhandled error. My url looks like localhost:7137/brokerform?Rid=1448&Wid=12563#headerD where headerD is my id and my error was like Unhandled exception rendering component: Cannot parse the value '12563#headerD' as type 'System.Int32' for 'Wid'. How to catch this ErrorPyro
L
2

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>
Lakieshalakin answered 7/2, 2023 at 8:49 Comment(0)
S
2

In the Navigation links component or NavMenu inject NavigationManager, then call the NavigateTo method by replacing href url withhref="javascript:void(0)" to maintain the default functionality of the anchor tag, and use @onclick to navigate to section by Id and add the section Id to the BaseUri, and very important is to set forceload to true.

Example

In the NavMenu Component:

    @inject NavigationManager NavManager
    <nav>
        <NavLink 
@onclick="@(() => NavManager.NavigateTo(NavManager.BaseUri + "#about", forceLoad: true))"
href="javascript:void(0)"
           Match="@NavLinkMatch.Prefix">About
         </NavLink>
        <NavLink 
@onclick="@(() => NavManager.NavigateTo(NavManager.BaseUri + "#contact", forceLoad: true))"
href="javascript:void(0)"
           Match="@NavLinkMatch.Prefix">Contact
        </NavLink>
    </nav>

In the Index.razor:

 <section id="about">
     <h1>About</h1>
 </section>
 <section id="contact">
     <h1>Contact</h1>
 </section>
Sadonia answered 2/9, 2023 at 17:14 Comment(0)
I
2

Starting .NET 8, this is now possible with the help of Hashed routing to named elements as mentioned in their official docs

All of the below combinations will work.

@page "/hashed-routing"
@inject NavigationManager Navigation

<PageTitle>Hashed routing</PageTitle>

<h1>Hashed routing to named elements</h1>

<ul>
    <li>
        <a href="/hashed-routing#targetElement">
            Anchor in this component
        </a>
    </li>
    <li>
        <a href="/#targetElement">
            Anchor to the <code>Home</code> component
        </a>
    </li>
    <li>
        <a href="/counter#targetElement">
            Anchor to the <code>Counter</code> component
        </a>
    </li>
    <li>
        <NavLink href="/hashed-routing#targetElement">
            Use a `NavLink` component in this component
        </NavLink>
    </li>
    <li>
        <button @onclick="NavigateToElement">
            Navigate with <code>NavigationManager</code> to the 
            <code>Counter</code> component
        </button>
    </li>
</ul>

<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>

@code {
    private void NavigateToElement()
    {
        Navigation.NavigateTo("/counter#targetElement");
    }
}
Impeachment answered 25/3, 2024 at 10:30 Comment(0)
O
0

After seeing the other horrendous workarounds in this question, I decided to try a simple workaround:

Find href="#(.*?)" replace with onclick="window.location.hash = '$1'"

You may have to add some additional styling in your style.css to handle anchors without href's, but I think this is a simple/acceptable workaround outside of creating another component entirely.

Offen answered 7/8, 2023 at 19:3 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.