hultqvist
hultqvist

Reputation: 18409

Attach blazor component dynamically

Can I create an instance of a Blazor component in C# and attach it afterwards? Alternatively can from C# code dynamically create a component in the DOM and get back a reference to it?

I'm creating a "popup dialog" triggered from the C# code.

Dialog.razor

<div>
    Dialog Text: @Text
</div>

@code{
    public string Text { get; set; }
}

Failed attempt

<div>
    @foreach (var d in List)
    {
        <div>@d</div><!--this doesn't work since d is not a RenderFragment-->
    }
</div>

@code{
    public List<Dialog> List { get; set; } = new List<Dialog>();

    void AddDialog()
    {
        var d = new Dialog();
        d.Text = "Hello " + List.Count;
        List.Add(d);
    }
}

Problem here is that I haven't found a way to attach an instance of ComponentBase onto the DOM. Is there a way to get the RenderFragment?

Secondary problem is that child components aren't initialized until after the dialog is attached thus limiting what can be done in the first place.

Upvotes: 1

Views: 5604

Answers (2)

enet
enet

Reputation: 45586

This is another way to do it...

AlertMessage.razor

<div style="border: 1px solid red; width: 500px; height:auto; margin: 3px; 
  padding:0px;">
    <div style="height:auto; width:inherit; padding:5px; border: 1px solid 
    blue; text-align:right;">
        <span style="float:left">@Title</span>
        <a href="#" @onclick="@(() => Close.InvokeAsync(ID))" 
   role="button">X</a>
        <div><input type="text" value="@content" /></div>
    </div>
     <div style="padding:25px; ">@ChildContent</div> 

 </div>

@code {
  [Parameter]
  public int ID { get; set; }
  [Parameter]
  public string Title { get; set; }
  [Parameter]
  public RenderFragment ChildContent { get; set; }
  [Parameter]
  public EventCallback <int> Close {get; set;}

}

AlertMessageGroup.razor

@using Microsoft.AspNetCore.Components.CompilerServices;


<h3>AlertMessageGroup</h3>
@if (alerts.Count > 0)
{
<p>Contains @alerts.Count AlertMessage Components</p>

@foreach (var alert in alerts)
 {
       <p>Alert ID: @alert.ID</p>

 }
}

<div>

    @foreach (var alert in alerts)
    {
        @RenderAlert(alert);

    }


</div>



@code {

List<Alert> alerts = new List<Alert>
        {
            new Alert{ ID = 1, Title = "First Message", Message = "This is my 
                                                            first message" },
            new Alert{ ID = 2, Title = "Second Message", Message = "This is 
                                                        my second message" },
            new Alert{ ID = 3, Title = "Third Message", Message = "This is my 
                                                             third message" }
        };


private RenderFragment RenderAlert(Alert alert) => builder =>
{

    builder.OpenComponent(0, typeof(AlertMessage));
    builder.AddAttribute(1, "ID", alert.ID);
    builder.AddAttribute(2, "Title", alert.Title);


    builder.AddAttribute(3, "ChildContent", (RenderFragment)((builder) =>
    {
        builder.AddContent(4, alert.Message);


    }
    ));

     builder.AddAttribute(5, "Close", EventCallback.Factory.Create<int>
                                          (this, RemoveAlertMessage));
     builder.CloseComponent();

  };



public void RemoveAlertMessage(int ID)
{

    alerts.Remove( alerts.Where(alert => alert.ID == ID).FirstOrDefault());
    StateHasChanged();
}


public class Alert
{
    public int ID { get; set; }
    public string Title { get; set; }
    public string Message { get; set; }

}

}

Usage

@page "/"

<AlertMessageGroup />

Upvotes: 5

hultqvist
hultqvist

Reputation: 18409

This is one way to do it

  1. Create a Placeholder object and place it in a list
  2. From the list the razor page will create a new instance.
  3. Catch the instance reference and pass it back to the calling code
  4. Wait for OnAfterRender before operating on the instance.

Dialog.razor
Unaware of it being loaded dynamically.

<div>
    Dialog Text: @Text
    <Red @ref="Red"></Red>
</div>

@code{
    public Red Red { get; set; }

    public string Text { get; set; }

    public void SetText(string text)
    {
        this.Text = text;
        StateHasChanged();
    }
}

DynamicWrapper.razor For notifying when the component is fully loaded

@ChildContent

@code{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public EventCallback AfterFirstRender { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await AfterFirstRender.InvokeAsync(null);

        base.OnAfterRender(firstRender);
    }
}

TestPage.razor

<div><button @onclick="AddDialog">Add one more</button></div>

<div>
    @foreach (var p in List)
    {
        <DynamicWrapper AfterFirstRender="p.AfterFirstRender">
            <Dialog @ref="p.Dialog"></Dialog>
        </DynamicWrapper>
    }
</div>

@code{
    public class Placeholder<T>
    {
        public T Dialog { get; set; }

        TaskCompletionSource<T> task = new TaskCompletionSource<T>();

        public void AfterFirstRender(object args)
        {
            task.SetResult(Dialog);
        }

        public Task<T> GetDialog() => task.Task;
    }

    public List<Placeholder<Dialog>> List { get; set; } = new List<Placeholder<Dialog>>();

    async Task AddDialog()
    {
        var p = new Placeholder<Dialog>();
        List.Add(p);
        var d = await p.GetDialog();
        d.SetText("Hello");
        d.Red.Show();
    }
}

Upvotes: 2

Related Questions