Creating a Modal component in Blazor

 
 
  • Gérald Barré

Blazor is a framework for building web applications. In this post, I'll show you how to create a component to display a modal dialog. A component is a self-contained chunk of user interface (UI). You can compare a component to a user control in WebForms, WinForms, or WPF. Components enable reusability and sharing across projects.

The component we'll build is a modal dialog. There are many ways to implement one using CSS and JS, but in this post we'll use the standard dialog HTML element (reference). This element provides exactly what we need: the ability to display a modal and retrieve a return value of type string. It was still experimental at the time, but browser support was reasonable. In my case, it's for an internal website where all users use Chromium-based browsers, so support is good.

Source: Can I use… Support tables for HTML5, CSS3, etc

Here's the final result:

#First attempt

Here's the code of my initial attempt:

Razor
<dialog open=@Open @onclose="OnClose"></dialog>

@code {
    private bool Open { get; set; }

    public void OnClose()
    {
        // TODO
    }
}

However, there are 2 issues:

  • The open attribute doesn't show the dialog as modal, so you can still use the page in the background
  • The @onclose doesn't bind to the C# method and raises an error

So, I need to find another solution: using the interop between Blazor and JavaScript. Let's first explore how the JS interop works before creating the component.

#JavaScript interop

Blazor supports interoperability with JavaScript. You can call JavaScript functions from C# and call C# methods from JavaScript. This enables integration with existing JavaScript code.

##Opening the dialog using showModal

To open the dialog as a modal, you need to call the showModal function (documentation) from C# code. You can do this using the IJSRuntime interface.

First, you need to create the JavaScript function to open the modal:

JavaScript
function blazorOpenModal(dialog) {
    if (!dialog.open) {
        dialog.showModal();
    }
}

Then, include the script in your page. Open _Host.cshtml or wwwroot/index.html and add this line before blazor.server.js:

Razor
    <!-- 👇 Include the JavaScript file before "_framework/blazor.server.js" -->
    <script src="~/blazor-modal.js"></script>
    <script src="_framework/blazor.server.js"></script>

Now you can call the JS function from C# using JSRuntime.InvokeVoidAsync:

Razor
@inject IJSRuntime JSRuntime

<button @onclick="OpenModal">Open</button>

@* 👇 Use @ref to keep a reference to the html element in order to use it in JS *@
<dialog @ref="_element">My modal</dialog>

@code {
    private ElementReference _element;

    private async Task OpenModal()
    {
        // 👇 Call the JS function with the html element (dialog) as parameter
        await JSRuntime.InvokeVoidAsync("blazorShowModal", _element);
    }
}

Clicking the button should now open the modal. That completes the first step.

##Getting notified when the user close the dialog

The next step is to be notified when the dialog has been closed. There are multiple ways to close a dialog:

  • Pressing escape,
  • Calling dialog.close() in JS,
  • Using a form with method dialog.

Using the close event, you can be notified when the dialog closes. You then need to notify Blazor that the modal has closed by calling a C# method from the JavaScript event handler.

To call a C# instance method from JavaScript, you need an object reference. Blazor provides a way to pass this reference to JavaScript using DotNetObjectReference.Create(instance). In JS, this object reference exposes an invokeMethodAsync method to call a .NET method on it. The C# method must be decorated with [JSInvokable] to be callable. Here's what it looks like:

Razor
    public async Task InitializeModal()
    {
        // Create a reference to this .NET object, so you can invoke its methods from JavaScript
        var reference = DotNetObjectReference.Create(this);

        // Send the HTML element reference and the instance of the current component as parameters of the blazorInitializeModal JS function
        await JSRuntime.InvokeVoidAsync("blazorInitializeModal", _element, reference);
    }

    // 👇 This method will be called from JavaScript, so it needs to be decorated by [JSInvokable]
    [JSInvokable]
    public async Task OnClose(string returnValue)
    {
        // Called from JavaScript
    }

You can then call the OnClose method of the component from JavaScript:

JavaScript
function blazorInitializeModal(dialog, reference) {
    dialog.addEventListener("close", async e => {
        // 👇 Call the C# method from JavaScript
        await reference.invokeMethodAsync("OnClose", dialog.returnValue);
    });
}

Now that we can use the dialog element methods from the Blazor component and be notified when the user closes the dialog, we can wrap that in the Modal component!

#Creating the Modal component

  1. Let's write the JavaScript code the file /wwwroot/js/blazor-modal.js:

    JavaScript
    function blazorInitializeModal(dialog, reference) {
        dialog.addEventListener("close", async e => {
            await reference.invokeMethodAsync("OnClose", dialog.returnValue);
        });
    }
    
    function blazorOpenModal(dialog) {
        if (!dialog.open) {
            dialog.showModal();
        }
    }
    
    function blazorCloseModal(dialog) {
        if (dialog.open) {
            dialog.close();
        }
    }
  2. Reference the JS file in the Pages/_Host.cshtml or wwwroot/index.html file:

    Razor
    @page "/"
    @namespace BlazorApp2.Pages
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        ...
    </head>
    <body>
        <app>
            <component type="typeof(App)" render-mode="ServerPrerendered" />
        </app>
    
        ...
    
        <!-- 👇 Include the JavaScript file -->
        <script src="~/js/blazor-modal.js"></script>
        <script src="_framework/blazor.server.js"></script>
    </body>
    </html>
  3. Create a new file named Shared/Modal.razor with the following content:

    Razor
    @inject IJSRuntime JSRuntime
    
    <dialog @ref="_element">@ChildContent</dialog>
    
    @code {
        private DotNetObjectReference<Modal> _this;
        private ElementReference _element;
    
        // Content of the dialog
        [Parameter]
        public RenderFragment ChildContent { get; set; }
    
        [Parameter]
        public bool Open { get; set; }
    
        // This parameter allows to use @bind-Open=... as explained in the previous post
        // https://www.meziantou.net/two-way-binding-in-blazor.htm
        [Parameter]
        public EventCallback<bool> OpenChanged { get; set; }
    
        [Parameter]
        public EventCallback<string> Close { get; set; }
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            // Initialize the dialog events the first time th ecomponent is rendered
            if (firstRender)
            {
                _this = DotNetObjectReference.Create(this);
                await JSRuntime.InvokeVoidAsync("blazorInitializeModal", _element, _this);
            }
    
            if (Open)
            {
                await JSRuntime.InvokeVoidAsync("blazorOpenModal", _element);
            }
            else
            {
                await JSRuntime.InvokeVoidAsync("blazorCloseModal", _element);
            }
    
            await base.OnAfterRenderAsync(firstRender);
        }
    
        [JSInvokable]
        public async Task OnClose(string returnValue)
        {
            if (Open == true)
            {
                Open = false;
                await OpenChanged.InvokeAsync(Open);
            }
    
            await Close.InvokeAsync(returnValue);
        }
    }

#How to use the component?

You can now use the Modal component in any page or component:

Razor
@page "/"

<button @onclick="e => IsModalOpened = true">Open modal</button>

@if (SelectedButton != null)
{
    <p>You have selected @SelectedButton</p>
}

@* 👇 Use the modal component *@
<Modal @bind-Open="IsModalOpened" Close="OnClose">
    <form method="dialog">
        <p>
            Do you really want to do this?
        </p>
        <menu>
            <button value="cancel">Cancel</button>
            <button value="confirm">I'm sure</button>
        </menu>
    </form>
</Modal>

@code{
    public bool IsModalOpened { get; set; }
    public string SelectedButton { get; set; }

    void OnClose(string value)
    {
        SelectedButton = value;
    }
}

When running the application, you should see the same result as the video at the beginning of the post.

#Additional references

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?