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
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();
}
}
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>
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!