Creating a Modal component in Blazor

  • Gérald Barré

Blazor is the shinny new framework to create web application. 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 WebForm, WinForms or WPF. Components allow reusability, and sharing among projects.

The component we'll build is a modal dialog. You can find lots of way to do it based on CSS and JS, but in this post we'll use the standard dialog HTML element (reference). This element provides what we want to display a modal and get a return value of type string. It is still experimental but the support is not that bad. In my case, it's for an internal website where all users use chromium-based browsers, so the support is good.

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

Here's the final result:

Table of contents:

#First attempt

Here's the code of my initial attempt:

<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 raise an error

So, I have to find another solution. The solution is to use the interop between Blazor and JavaScript. Let's first explore how the JS interop works before creating the component.

#JavaScript interop

Blazor allows to interop with JavaScript. You can call a JavaScript function from a C# method, and you can call a C# method from JavaScript. This allows to interop with existing JavaScript code.

##Opening the dialog using showModal

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

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

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

Then, you need to include the function in your page. Open the file _Host.cshtml or wwwroot/index.html and add this line before the razor.js file:

    <!-- 👇 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:

@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);
    }
}

Now clicking on the button should open the modal. That's ok 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 has been closed. But you need to notify Blazor that the modal is closed. So, you need to call a C# method from the JavaScript event handler.

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

    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:

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:

    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:

    @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:

    @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:

@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?Buy Me A Coffee