DEV Community

Chris Sainty
Chris Sainty

Posted on • Originally published at chrissainty.com on

Fragment Routing with Blazor

Fragment Routing with Blazor

A common question I've been asked and I've seen asked many times is, how can I route to a fragment in my Blazor app? If you're not aware what I mean by a "fragment", let me explain.

Fragment routing, or linking, is the term given when linking to a specific element on a page, say a header for example. This technique is often used in FAQ pages or technical documentation and links using this technique look like this, www.mysite.com/faq#some-header. In this example, if an element was present on the page with an id of some-header the page would automatically scroll to that element when it loads.

In this post, I'm going to show you how you can achieve this in Blazor as it's not something which we can do out of the box.

I've added a sample project on my GitHub account showing this solution in action.

The Problems

Blazor doesn't include anything out of the box which allows us to handle fragment routing. In fact, Blazor's router will actively ignore any fragments, or query strings for that matter, attached to a URL.

The next problem we face is that there is no feature in Blazor which enables us to scroll to a certain point on a page. Scrolling to specific place in web page is something that can only be achieved by JavaScript, currently.

The final issue we need to overcome is how to get hold of the fragment from the URL in the first place. We could use the NavigationManagers URI property, and then use some string manipulation to find a fragment and pull it out. But that sounds like a lot of hard work — surely there must be a better way.

The Solution

Now we've understood the problems, what's the solution?

The first thing we're going to do is write a small piece of JavaScript, as we identified above, it's the only option right now.

window.blazorHelpers = {
    scrollToFragment: (elementId) => {
        var element = document.getElementById(elementId);

        if (element) {
            element.scrollIntoView({
                behavior: 'smooth'
            });
        }
    }
};

The code above takes an element ID. We then try to find an element matching that ID on the page using the getElementById function. If we find an element, then we invoke the scrollIntoView function on that element. As part of doing that we're passing in a configuration object which sets the behaviour of the scroll to smooth. This will give us a nice smooth scrolling effect to the target element.

Now we have the JavaScript piece in place, we're going to create an extension method for the NavigationManager class.

public static class Extensions
{
    public static ValueTask NavigateToFragmentAsync(this NavigationManager navigationManager, IJSRuntime jSRuntime)
    {
        var uri = navigationManager.ToAbsoluteUri(navigationManager.Uri);

        if (uri.Fragment.Length == 0)
          return default;

        return jSRuntime.InvokeVoidAsync("blazorHelpers.scrollToFragment", uri.Fragment.Substring(1));
    }
}

We start by getting the current URI using the NavigationManagers ToAbsoluteUri method. This returns us the URI as a URI object. This makes our life a lot easier as the Uri class allows us to easily check for a fragment in the URI using the Fragment property.

If no URI is present then we will return and do nothing. However, if there is a fragment we call our JavaScript function passing in the fragment. You may have noticed that we're actually cutting off the first character of the fragment when we do this. This is because the Fragment property on the Uri class returns the fragment with the # symbol included. So if we had a URI which looked like this, https://mysite.com/faq#contact, the Fragment property would return #contact.

That's it, we should now be able to navigate to fragments by doing the following.

@inject IJSRuntime _jsRuntime
@inject NavigationManager _navManager

...

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await _navManager.NavigateToFragmentAsync(_jsRuntime);
        }
    }
}

That's better, but we're not quite there yet

At first glance it looks like we've solved our issues but there is another use case we haven't covered. Say we're on the home page and navigate to a fragment on a FAQ page using our fragment helper above. All works as expected, but, if we try to navigate to another fragment on the same page, nothing happens.

This because Blazor doesn't care about URI fragments, clicking the link updates the fragment in the URI but doesn't trigger Blazor to re-render the page. And even if the page did re-render we're only doing fragment navigation on the first render. This isn't very good at all, so how can we fix it?

In order to get this scenario working we need to hook into the NavigationManagers LocationChanged event. By providing a handler for this event we can call our fragment navigation helper whenever the URI changes. Our updated implementation code now looks like this.

protected override void OnInitialized()
{
    _navManager.LocationChanged += TryFragmentNavigation;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await _navManager.NavigateToFragmentAsync(_jsRuntime);
    }
}

private async void TryFragmentNavigation(object sender, LocationChangedEventArgs args)
{
    await _navManager.NavigateToFragmentAsync(_jsRuntime);
}

void IDisposable.Dispose()
{
    _navManager.LocationChanged -= TryFragmentNavigation;
}

Now we are using event handlers our component must implement IDisposable, which has added a lot of extra code. Having to add all this code to every page that we want to enable fragment navigation on would be a real pain. So what can we do about it?

Using a base class to create a nice reusable solution

I think the best option at this point would be to put all this code into a base class, that way any pages we want to enable fragment navigation on can just implement our base class and they're away!

public class FragmentNavigationBase : ComponentBase, IDisposable
{
    [Inject] NavigationManager NavManager { get; set; }
    [Inject] IJSRuntime JsRuntime { get; set; }

    protected override void OnInitialized()
    {
        NavManager.LocationChanged += TryFragmentNavigation;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await NavManager.NavigateToFragmentAsync(JsRuntime);
    }

    private async void TryFragmentNavigation(object sender, LocationChangedEventArgs args)
    {
        await NavManager.NavigateToFragmentAsync(JsRuntime);
    }

    public void Dispose()
    {
        NavManager.LocationChanged -= TryFragmentNavigation;
    }
}

Now in order to have a page use fragment navigation we can simply have it inherit from FragmentNavigationBase and everything will just work.

Fragment Routing with Blazor

Summary

In this post, we have created a solution for fragment navigation in Blazor. We started off by identifying the problems:

  • Blazor's router doesn't deal with fragments
  • There is no mechanism in Blazor to scroll to a certain position on a page
  • Getting the fragment from the URL without having to do a load of string manipulation

We then created a simple solution using a small amount of JavaScript and an extension method on the NavigationManager to allow navigation to a fragment. We finished by wrapping that all up in a reusable base class which our page components can inherit from.

All of the code from this post can be found on GitHub.

Top comments (0)