Search code examples
c#blazorblazor-webassemblyasp.net-blazor

Is it possible to load dynamically existing page in a Bootstrap Modal body


I'm new to a Blazor and now I'm working to Blazor WebAssembly project. I have couple razor page with a table where I'm displaying data from SQL. When I click in one of the table rows it opens a page where I can do the CRUD operation.

Now, instead of opening a page to do the CRUD operation I need to open a bootstarp modal and do the CRUD operation. I'm doing a generic ModalComponent where I have the header and the footer of the modal. Is it possibile to load dynamically the body of the model, which in this case it will be the CRUD operation pages that I already had done?


Solution

  • The code below demonstrates how to implement a generic modal dialog i.e. a wrapper for any component/page that you want to display in modal mode. The concrete implementation of IModalDialog shows how to implement a Bootstrap version.

    I use a more complex version of this. My edit/View components are written to run in modal or full page mode.

    Support Classes

    First three support classes that are fairly self-evident:

    public class ModalResult
    {
        public ModalResultType ResultType { get; private set; } = ModalResultType.NoSet;
    
        // Whatever object you wish to pass back
        public object? Data { get; set; } = null;
    
        // A set of static methods to build a BootstrapModalResult
        public static ModalResult OK() => new ModalResult() { ResultType = ModalResultType.OK };
        public static ModalResult Exit() => new ModalResult() { ResultType = ModalResultType.Exit };
        public static ModalResult Cancel() => new ModalResult() { ResultType = ModalResultType.Cancel };
        public static ModalResult OK(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.OK };
        public static ModalResult Exit(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Exit };
        public static ModalResult Cancel(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Cancel };
    }
    
    public class ModalOptions
    {
        // Whatever data you want to pass
        // my complex version uses a <string, object> dictionary
    }
    
    
    public enum ModalResultType
    {
        NoSet,
        OK,
        Cancel,
        Exit
    }
    

    IModalDialog Interface

    Now the modal dialog Interface.

    using Microsoft.AspNetCore.Components;
    
    public interface IModalDialog
    {
        ModalOptions Options { get; }
    
        //  Method to display a Modal Dialog
        Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent;
    
        // Method to update the Modal Dialog during display
        void Update(ModalOptions? options = null);
    
        // Method to dismiss - normally called by the dismiss button in the header bar
        void Dismiss();
    
        // Method to close the dialog - normally called by the child component TModal
        void Close(ModalResult result);
    }
    

    The Base Bootstrap Implementation

    1. There's no JS.
    2. I use a TaskCompletionSource to make the open/close an async process.
    3. The component to display is passed as part of the open call.
    @inherits ComponentBase
    @implements IModalDialog
    
    @if (this.Display)
    {
        <CascadingValue Value="(IModalDialog)this">
            <div class="modal" tabindex="-1" style="display:block;">
                <div class="modal-dialog">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">Modal title</h5>
                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @onclick="() => Close(ModalResult.Exit())"></button>
                        </div>
                        <div class="modal-body">
                            @this.Content
                        </div>
                    </div>
                </div>
            </div>
        </CascadingValue>
    }
    @code {
        public ModalOptions Options { get; protected set; } = new ModalOptions();
        public bool Display { get; protected set; }
        protected RenderFragment? Content { get; set; }
    
        protected TaskCompletionSource<ModalResult> _ModalTask { get; set; } = new TaskCompletionSource<ModalResult>();
    
        public Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent
        {
            this.Options = options ??= this.Options;
            this._ModalTask = new TaskCompletionSource<ModalResult>();
            this.Content = new RenderFragment(builder =>
            {
                builder.OpenComponent(1, typeof(TModal));
                builder.CloseComponent();
            });
            this.Display = true;
            InvokeAsync(StateHasChanged);
            return this._ModalTask.Task;
        }
    
        public void Update(ModalOptions? options = null)
        {
            this.Options = options ??= this.Options;
            InvokeAsync(StateHasChanged);
        }
    
        public async void Dismiss()
        {
            _ = this._ModalTask.TrySetResult(ModalResult.Cancel());
            this.Display = false;
            this.Content = null;
            await InvokeAsync(StateHasChanged);
        }
    
        public async void Close(ModalResult result)
        {
            _ = this._ModalTask.TrySetResult(result);
            this.Display = false;
            this.Content = null;
            await InvokeAsync(StateHasChanged);
        }
    }
    

    Demo

    A simple "Edit" component:

    @inject NavigationManager NavManager
    
    <h3>EditForm</h3>
    <div class="p-3">
        <button class="btn btn-success" @onclick=Close>Close</button>
    </div>
    
    @code {
        [CascadingParameter] private IModalDialog? modal { get; set; }
    
        private void Close()
        {
            if (modal is not null)
                modal.Close(ModalResult.OK());
            else
                this.NavManager.NavigateTo("/");
        }
    }
    

    And a test page:

    @page "/"
    
    <div class="p-2">
        <button class="btn btn-primary" @onclick=OpenDialog>Click</button>
    </div>
    
    <div class="p-2">
        result: @Value
    </div>
    
    <ModalDialog @ref=this.modal />
    
    @code {
        private string Value { get; set; } = "Fred";
    
        private IModalDialog? modal;
        private IModalDialog Modal => modal!;
    
        private async void OpenDialog()
        {
            var options = new ModalOptions();
            var ret = await Modal.ShowAsync<EditorForm>(new ModalOptions());
            if (ret is not null)
            {
                Value = ret.ResultType.ToString();
            }
            StateHasChanged();
        }
    }