Author : MD TAREQ HASSAN | Updated : 2020/08/26
Reusing components
- Blazor is designed with modularity/re-usability in mind
- Blazor supports UI encapsulation through components
- Each component in Blazor:
- self-contained (chunk of UI)
- Maintains its own state and rendering logic
- Can define UI event handlers
- manage its own lifecycle.
- Each component is contained in a
.razor
file (if we seperate C# code from UI, then there will be.razor.cs
file too)
Best practices for creating components
- Use
.razor.cs
file for seperating C# code but inline code when needed - Define reusable RenderFragments in code
- Create lightweight, optimized components
- Don’t receive too many parameters
- Ensure cascading parameters are fixed
- Don’t trigger events too rapidly
- Optimize JavaScript interop speed
- More details (Microsoft doc): https://docs.microsoft.com/en-us/aspnet/core/blazor/webassembly-performance-best-practices
Managing multiple email addresses in a modal
- Scenario: we have a Create/Edit page where we would like to manage (create, update, delete) multiple email addresses for “To” addresses (in DB, receipients -> multiple addresses seperated by “;”)
- Custom component overview:
EmailAddressesModal.razor.cs
: code behind base classEmailAddressesModal.razor
: scrollable bootstrap modal (razor syntax)- input field in head (for creating, editing email entry)
- list view in body (for displaying email entries in body)
EmailEntry
: model for input field, uses DataAnnotations for validation (file:EmailAddressesModal.razor.cs
)EmailAddressesModalEditButton.razor
&EmailAddressesModalEditButton.razor.cs
: button to trigger Bootstrap modal (modal will be shown when botton clicked - button has data-toggle)
- Has dependency to jQuery & Boostrap: https://getbootstrap.com/docs/4.5/getting-started/introduction/#starter-template
EmailAddressesModal.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace MultipleEmailAddressManagement.Components
{
public class EmailAddressesModalBase: ComponentBase
{
#region Parameters
[Parameter]
public string ModalId { get; set; }
[Parameter]
public string Title { get; set; }
[Parameter]
public List<string> EmailAddressList { get; set; } = new List<string>();
[Parameter]
public EventCallback<List<string>> EmailAddressListChanged { get; set; }
#endregion
#region Undo & Reset
protected Stack<KeyValuePair<int, string>> UndoStack { get; set; }
#endregion
#region Edit context
protected EmailEntry ActiveEntry { get; set; } = new EmailEntry();
protected EditContext _editContext;
private void ResetEditContext()
{
ActiveEntry = new EmailEntry();
_editContext = new EditContext(ActiveEntry);
}
#endregion
protected override void OnInitialized()
{
UndoStack = new Stack<KeyValuePair<int, string>>();
ActiveEntry = new EmailEntry();
_editContext = new EditContext(ActiveEntry);
}
protected void AddOrUpdate()
{
var isInvalidForm = !_editContext.Validate();
if (isInvalidForm)
{
Debug.WriteLine($"Submitted form is not valid");
return;
}
var insertedAddress = ActiveEntry.Address;
if (ActiveEntry.index.HasValue)
{
EmailAddressList[ActiveEntry.index.Value] = insertedAddress;
}
else
{
EmailAddressList.Add(insertedAddress);
}
ResetEditContext();
}
protected void ClearActiveEntry()
{
ResetEditContext();
}
protected void UpdateEntry(MouseEventArgs eArgs, int key)
{
Debug.WriteLine($"Entry with key '{key}' needs to be updated");
ResetEditContext();
var entryToUpdate = EmailAddressList[key];
EmailAddressList[key] = string.Empty;
ActiveEntry.index = key;
ActiveEntry.Address = entryToUpdate;
}
protected void DeleteEntry(MouseEventArgs eArgs, int key)
{
Debug.WriteLine($"Entry with key '{key}' needs to be deleted");
// Retain data for undo before removing
UndoStack.Push(new KeyValuePair<int, string>(key, EmailAddressList[key]));
// Remove entry for given key (emty values would not be shown)
EmailAddressList[key] = string.Empty;
}
protected void UndoDelete(MouseEventArgs eArgs)
{
Debug.WriteLine($"Undo deleted");
if (UndoStack.Any())
{
var lastDeleted = UndoStack.Pop();
EmailAddressList[lastDeleted.Key] = lastDeleted.Value;
}
}
protected async Task OnCompleteEdit(MouseEventArgs eArgs)
{
UndoStack = new Stack<KeyValuePair<int, string>>();
ResetEditContext();
// remove emty entries
EmailAddressList.RemoveAll(item => string.IsNullOrWhiteSpace(item));
await EmailAddressListChanged.InvokeAsync(EmailAddressList);
}
}
}
public class EmailEntry
{
public int? index { get; set; } = null;
[Required]
[EmailAddress]
public string Address { get; set; } = string.Empty;
}
EmailAddressesModal.razor
@inherits EmailAddressesModalBase
<!-- Modal -->
<div class="modal fade" id="@ModalId" tabindex="-1" role="dialog" aria-labelledby="@Title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document" style="overflow-y: scroll; max-height:85%; margin-top: 50px; margin-bottom:50px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="@Title">@Title</h5>
</div>
<div class="panel panel-default py-sm-2 border border-secondary border-bottom">
<div class="panel-body container-sm">
<EditForm EditContext="@_editContext" OnSubmit="@AddOrUpdate">
<DataAnnotationsValidator />
<div class="row">
<div class="col-sm">
<InputText id="@($"{ModalId}Modal_ActiveEntryField")" @bind-Value="@ActiveEntry.Address" class="form-control" placeholder="Enter email address" />
</div>
<div class="col-sm-2 my-sm-auto text-sm-center">
<button type="submit"
id="@(ModalId)Modal_Button_Submit"
class="btn btn-sm rounded bg-warning text-white">
<span class="oi @(ActiveEntry.index.HasValue ? "oi-hard-drive" : "oi-plus")" aria-hidden="true"></span>
</button>
</div>
<div class="col-sm-2 my-sm-auto text-sm-center">
<button @onclick="@ClearActiveEntry"
type="button"
class="btn btn-sm btn-secondary rounded"
id="@(ModalId)Modal_Button_Clear">
Clear
</button>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<ValidationMessage For="() => ActiveEntry.Address" class="" />
</div>
</div>
</EditForm>
</div>
</div>
<div class="modal-body">
<div class="panel panel-default">
<div class="panel-body container-sm">
@if (EmailAddressList == null || !EmailAddressList.Any())
{
<div class="row mt-sm-5">
<div class="col-sm-12 text-sm-center my-sm-auto">
No Entries
</div>
</div>
}
else
{
@for (var i = EmailAddressList.Count - 1; i >= 0; i--)
{
var index = i;
var entryData = EmailAddressList[index];
<div class="row" hidden="@string.IsNullOrWhiteSpace(entryData)">
<div class="col-sm-2 my-1 text-sm-center">
<button @onclick="@(e => DeleteEntry(e, key: index))"
class="btn btn-sm btn-outline-danger rounded"
id="@(ModalId)Modal_Button_Delete_@(index)">
<span class="oi oi-trash" aria-hidden="true"></span>
</button>
</div>
<div class="col-sm-8 my-1 text-sm-center">
<input type="text" value="@entryData" class="form-control" disabled="disabled" />
</div>
<div class="col-sm my-1 text-sm-center">
<button @onclick="@(e => UpdateEntry(e, key: index))"
class="btn btn-sm btn-outline-info rounded"
id="@(ModalId)Modal_Button_Update_@(index)">
<span class="oi oi-pencil" aria-hidden="true"></span>
</button>
</div>
</div>
}
}
</div>
</div>
</div>
<div class="modal-footer">
<button @onclick="UndoDelete"
id="@(ModalId)Modal_Button_Undo"
type="button"
class="btn btn-sm btn-info rounded mr-sm-auto"
disabled="@(!UndoStack.Any())">
Undo Delete
</button>
<button @onclick="OnCompleteEdit"
id="@(ModalId)Modal_Button_Done"
type="button"
class="btn btn-sm btn-dark rounded"
data-dismiss="modal"
disabled="@(!string.IsNullOrWhiteSpace(ActiveEntry.Address))">
Done
</button>
</div>
</div>
</div>
</div>
EmailAddressesModalButton.razor.cs
using Microsoft.AspNetCore.Components;
namespace MultipleEmailAddressManagement.Components
{
public class EmailAddressesModalButtonBase : ComponentBase
{
[Parameter]
public string ModalId { get; set; }
}
}
EmailAddressesModalButton.razor
@inherits EmailAddressesModalButtonBase
<button class="btn btn-sm btn-outline-secondary"
type="button"
id="@ModalId"
data-toggle="modal"
data-target="#@ModalId"
data-backdrop='static'
data-keyboard='false'>
Edit
</button>
site.css
// ... ... ...
/* CSS for scrollable modal body */
.modal-dialog {
overflow-y: initial !important;
}
.modal-body {
max-height: 60vh;
min-height: 60vh;
overflow-y: auto;
}
Edit.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MultipleEmailAddressManagement.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MultipleEmailAddressManagement.Pages
{
public class EditBase : ComponentBase
{
[Inject]
protected NavigationManager NavigationManager { get; set; }
protected EditEmailModel EditEmailModel { get; set; } = new EditEmailModel();
public List<string> ToEmailAddressList { get; set; } = new List<string>();
public List<string> CcEmailAddressList { get; set; } = new List<string>();
public List<string> BccEmailAddressList { get; set; } = new List<string>();
protected override async Task OnInitializedAsync()
{
// fetch data from database
EditEmailModel = await Task.Run(new Func<EditEmailModel>(() =>
{
return new EditEmailModel() {
ToAddresses = "aaa@hovermind.com;bbb@hovermind.com;ccc@hovermind.com;ddd@hovermind.com",
CcAddresses = "xxx@hovermind.com;yyy@hovermind.com;zzz@hovermind.com;www@hovermind.com",
BccAddresses = "mmm@hovermind.com;nnn@hovermind.com;ppp@hovermind.com;qqq@hovermind.com"
};
}));
// prepare parameters for modal component
await Task.Run(() =>
{
var toAddresses = EditEmailModel.ToAddresses?.Split(";");
if (toAddresses.Any())
{
ToEmailAddressList = toAddresses.ToList();
}
var ccAddresses = EditEmailModel.CcAddresses?.Split(";");
if (ccAddresses.Any())
{
CcEmailAddressList = ccAddresses.ToList();
}
var bccAddresses = EditEmailModel.BccAddresses?.Split(";");
if (bccAddresses.Any())
{
BccEmailAddressList = bccAddresses.ToList();
}
});
}
protected void HandleValidSubmit(EditContext editContext)
{
// ... ... ...
NavigationManager.NavigateTo("/");
}
}
}
Edit.razor
@page "/"
@inherits EditBase
<h1>Custom component for managing email addresses</h1>
<EmailAddressesModal ModalId="@nameof(ToEmailAddressList)"
Title="To addresses"
@bind-EmailAddressList="@ToEmailAddressList" />
<EmailAddressesModal ModalId="@nameof(CcEmailAddressList)"
Title="CC addresses"
@bind-EmailAddressList="@CcEmailAddressList" />
<EditForm Model="@EditEmailModel" Context="CreateFormContext" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-sm-1 text-sm-right font-weight-bolder my-sm-auto">To:</div>
<div class="col-sm-1 text-sm-left my-sm-auto p-0"><span class="badge badge-pill badge-warning">@ToEmailAddressList.Count</span> entries</div>
<div class="col-sm text-sm-left my-sm-auto">
<EmailAddressesModalEditButton ModalId="@nameof(ToEmailAddressList)" />
</div>
</div>
<div class="row">
<div class="col-sm-1 text-sm-right font-weight-bolder my-sm-auto">CC:</div>
<div class="col-sm-1 text-sm-left my-sm-auto p-0"><span class="badge badge-pill badge-warning">@CcEmailAddressList.Count</span> entries</div>
<div class="col-sm text-sm-left my-sm-auto">
<EmailAddressesModalEditButton ModalId="@nameof(CcEmailAddressList)" />
</div>
</div>
<div class="row mt-5">
<div class="col offset-1 form-group pl-0">
<input type="submit" value="Create" class="btn btn-success" />
<input type="button" value="Cancel" class="btn btn-danger ml-3" @onclick="@( () => NavigationManager.NavigateTo("/") )" />
</div>
</div>
</EditForm>
Text Avatar
Dependency:
- Bootstrap
- TextUtil
Components/TextAvatar.razor.cs
using Microsoft.AspNetCore.Components;
using System.Text.RegularExpressions;
public class TextAvatarBase: ComponentBase
{
[Parameter]
public string FullName { get; set; }
public string InitialLetters { get; set; }
protected override void OnParametersSet()
{
InitialLetters = TextUtil.GetInitials(FullName);
}
}
Components/TextAvatar.razor
@inherits TextAvatarBase
<div class="bg-orange-dark rounded mx-sm-3 p-sm-2">
<span class="text-white">@InitialLetters</span>
</div>
Usage
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="" target="_blank">About</a>
<TextAvatar FullName="hassan md tareq" />
</div>
<div class="content px-4">
@Body
</div>
</div>