Author : HASSAN MD TAREQ | Updated : 2020/08/26

Reusing components

Best practices for creating components

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 class
    • EmailAddressesModal.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:

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>