Creating a Modal component in Blazor
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 ways 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:
#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 us 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
Let's write the JavaScript code the file
/wwwroot/js/blazor-modal.js
:JavaScriptfunction 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(); } }
Reference the JS file in the
Pages/_Host.cshtml
orwwwroot/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>
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:
@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!