Blazor WebAssembly App - Warn user on navigation - warn user before leaving web page with unsaved changes

6.7k views Asked by At

I have implemented the following code in wwwroot - index.html:

"use strict";
(() => {
const modified_inputs = new Set;
const defaultValue = "defaultValue";
// store default values
addEventListener("beforeinput", (evt) => {
    const target = evt.target;
    if (!(defaultValue in target || defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ("" + (target.value || target.textContent)).trim();
    }
});
// detect input modifications
addEventListener("input", (evt) => {
    const target = evt.target;
    let original;
    if (defaultValue in target) {
        original = target[defaultValue];
    } else {
        original = target.dataset[defaultValue];
    }
    if (original !== ("" + (target.value || target.textContent)).trim()) {
        if (!modified_inputs.has(target)) {
            modified_inputs.add(target);
        }
    } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
    }
});
// clear modified inputs upon form submission
addEventListener("submit", (evt) => {
    modified_inputs.clear();
    // to prevent the warning from happening, it is advisable
    // that you clear your form controls back to their default
    // state with evt.target.reset() or form.reset() after submission
});
// warn before closing if any inputs are modified
addEventListener("beforeunload", (evt) => {
    if (modified_inputs.size) {
        const unsaved_changes_warning = "Changes you made may not be saved.";
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
    }
});
})();

Source:

https://stackoverflow.com/a/48238659/3850405

This works really well if I try to close the tab, reload or leave the site without using Blazor navigation.

enter image description here

What I would like to do is be able to catch events from these types of navigation events as well:

Navigation.NavigateTo("/");

<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
    <span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>

<MatButtonLink Href="/" Raised="true">Test navigation</MatButtonLink>

My example test code:

@page "/editor"
@using Markdig;
@inject NavigationManager Navigation


<div class="row">
    <div class="col-6">
        <textarea class="form-control" @bind-value="Body" @bind-value:event="oninput"></textarea>
        <MatButtonLink Href="/" Raised="true">Test navigation</MatButtonLink>
        <button @onclick="Navigate">
            Test navigation 2
        </button>
    </div>
    <div class="col-6">
        @if (!string.IsNullOrWhiteSpace(Body))
        {
            @((MarkupString)Preview)
        }
    </div>
</div>

@code {
    public string Body { get; set; }

    public string Preview => Markdown.ToHtml(Body);

    private void Navigate()
    {
        Navigation.NavigateTo("/");

    }
}
5

There are 5 answers

0
Ogglas On BEST ANSWER

Found a similar question that had tried window.onbeforeunload:

Blazor Navigationmanager cancel navigation on locationchanged

There does not seem to be a fix for this at the moment but a location changing event for NavigationManger is committed for .NET 6.0. I hope it will be possible to catch this event on a global level in the application.

https://github.com/dotnet/aspnetcore/issues/14962

1
Connor Low On

Summary
There is no convenient way to do this with Blazor as of .NET 5: either catch click/mouse events or use JS.


You can subscribe to these events with NavigationManager.LocationChanged:

[Inject]
NavigationManager Navigation { get; set; }

protected override OnInitialized() {
    Navigation.LocationChanged += Handler; // make sure you unsubscribe in a Dispose method
}

private void Handler(LocationChangedEventArgs args) {
    // Handle navigation event here
}

Note, you cannot intercept/prevent these navigation events (currently): you would need to intercept the click/mouse events instead to implement this in c#. It's not ideal, but you would need to do something like this:

<NavLink class="nav-link" 
         href="" 
         Match="NavLinkMatch.All"
         @onclick:preventDefault="shouldPreventDefault">
    <span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>

Where shouldPreventDefault is a boolean, perhaps provided by some scoped state or service.

In JS, it might be as simple as listening for click events, for example:

document.addEventListener('click', (e) => {
    // shouldPreventDefault set when this behavior is desirable, 
    // e.g. when changes are unsaved.
    if (shouldPreventDefault && e.target instanceof HTMLAnchorElement) {
        e.preventDefault();
        // etc....
    }
}
0
Todd On

If you are using .NET 7 they have finally added an event for this making it very easy. My sample code below is in my ".cs" code-behind file not my ".razor" file (so that you understand the syntax). Also it's from a client-side Blazor WASM project, but I think something similar should be possible server-side.

First, inject the NavigationManager:

[Inject] public NavigationManager navigationManager { get; set; }

Then register your handler from somewhere (in my case from one of my lifecycle event handlers):

navigationManager.RegisterLocationChangingHandler(LocationChangingHandler);

My event handler method looks like this:

private ValueTask LocationChangingHandler(LocationChangingContext arg) {
    Console.WriteLine($"AUTH: NAV: About to navigate to {arg.TargetLocation} by {Environment.StackTrace}");
    return ValueTask.CompletedTask;
}

I haven't tested using the LocationChangingContext.PreventNavigation(); method yet, but it sure SOUNDS good.

0
tomRedox On

As of .NET 7 Blazor now has native support for handling/preventing location changes. The official documentation is here.

The original discussion around the requirements for this functionality can be found in aspnetcore repo issue 42877.

0
Radinovic Rastko On

You can do that without JavaScript by deleting href attribute on links and then adding onclick event

<NavLink class="custom-a" @onclick="() => ToggleNavLink("link")">

to the element and handle navigation inside ToggleNavLink function,

where ToggleNavLink looks something like this:

    public async void ToggleNavLink(string link)
{
    bool confirmExit = true;
    if (NavigationManager.Uri.Contains("uri of the page you are leaving"))
        confirmExit = await DialogService.ConfirmAsync("Are you sure u wanna leave?", "Leave ?");
    if (confirmExit)    
        NavigationManager.NavigateTo(link);
}

And inside it you can also add whatever you want to check or save.

P.S. This is done in .Net 6 Blazor