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