Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to render multiple child components in parent component without calling @ChildContent in Blazor

In our Blazor WebAssembly-App, we have a ParentComponent which nests multiple ChildComponents. There are 3 Files:

  1. Test.razor which is the client and uses the ParentComponent and ClientComponent.
  2. ParentComponent.razor which nests multiple ChildComponent.
  3. ChildComponent.razor which is a child-component of the ParentComponent.

enter image description here

File: Test.razor

<ParentComponent DataSource="@AccountList">
    <ChildComponent TData="Account"><span>@context.FirstName</span></ChildComponent>
    <ChildComponent TData="Account"><i>@context.LastName</i></ChildComponent>
    <ChildComponent TData="Account"><b>@context.Age</b></ChildComponent>
</ParentComponent>

@code {

    // The datasource:
    private List<Account> AccountList = new List<Account>() {
        new Account() { FirstName = "Sam", LastName = "Soldier", Age = 33},
        new Account() { FirstName = "Lisa", LastName = "Johnson", Age = 25},
        new Account() { FirstName = "Jonas", LastName = "Peer", Age = 50 }
    };

    // A Simple Account-Class for the datasource
    class Account
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

The file above shows the following data as expected:

result

File: ParentComponent.razor

@typeparam TData

<CascadingValue Value="this" Name="ParentComponent">
    @ChildContent
    <div class="parent-component">
        @foreach (var lData in DataSource)
        {
            @foreach (var lChild in m_Children)
            {
                <div class="child-component">
                    @lChild.ChildContent(lData);
                </div>
            }
        }

    </div>
</CascadingValue>

@code {

    [Parameter]
    public List<TData> DataSource { get; set; }

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

    /// Collection of all added child components
    private List<ChildComponent<TData>> m_Children = new List<ChildComponent<TData>>();

    /// Add a child component (will be done by the child itself)
    public void AddChildComponent(ChildComponent<TData> pChildComponent)
    {
        m_Children.Add(pChildComponent);
        StateHasChanged();
    }
}

File: ChildComponent.razor

@typeparam TData

@code {

    /// Reference to the parent component
    [CascadingParameter(Name = "ParentComponent")]
    ParentComponent<TData> ParentComponent { get; set; }

    /// Child content of the component.
    [Parameter]
    public RenderFragment<TData> ChildContent { get; set; }

    // add ChildComponent to ParentComponent
    protected override void OnInitialized()
    {
        Console.WriteLine("ChildComponent.OnInitialized");
        ParentComponent.AddChildComponent(this);
    }
}

Question: The file ParentComponent.razor contains the statement @ChildContent which makes no sense here, because we want to render the data of the children by @lChild.ChildContent(lData):

// [...]
<CascadingValue Value="this" Name="ParentComponent">
@ChildContent // <- required, but want to remove, because here is othing to show
<div class="parent-component">
    @foreach (var lData in DataSource)
    {
        @foreach (var lChild in m_Children)
        {
            <div class="child-component">
                @lChild.ChildContent(lData); // We wender the child here
            </div>
        }
    }
// [...]

When we remove @ChildContent, then nothing will be rendered. So the invocation of @ChildContent seem to be required to render the child components, but seem to be unnecessary, because we call @lChild.ChildContent(lData);. How can we render the data without the statement @ChildContent?

Perhaps is there any invocation in the @code-block possible that does the same as @ChildContent? Example:

ParentComponent.razor:

@code {

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        if (firstRender)
        {
            ChildContent.Invoke(...); // ???
        }
    }
}
like image 693
Simon Avatar asked Jan 27 '21 13:01

Simon


People also ask

How do you pass data between components in Blazor?

There are three steps to this solution: Define an event callback in your first component. Define a parameter in your second component. Define a property in your parent component (your page).

What is RenderFragment in Blazor?

RenderFragment is used to render components or content at run time in Blazor. The RenderFragment class allows you to create the required content or component in a dynamic manner at runtime.

What are blazor components?

Blazor apps are built using Razor components, informally known as Blazor components. A component is a self-contained portion of user interface (UI) with processing logic to enable dynamic behavior. Components can be nested, reused, shared among projects, and used in MVC and Razor Pages apps.


1 Answers

Component Instances

Components are classes, and you need an instance of a Component class to be created before you can access its members - such as its ChildContent.

The Component instance is created by Blazor when it is first rendered.

In order to render the Child Component(s), they need to be part of a RenderTree batch.

If the Component (Parent) that "owns" the RenderFragment (Parent.ChildContent) containing the Child Component does not itself render that ChildContent, then the Child Component(s) instance will not get created.

A line of code like this is accessing an instance Property of the Child Component, and this requires the Child Component instance to exist prior to being called.

@lChild.ChildContent(lData); // We render the child here

So, in short, you need to render ChildContent in your Parent Component to create the instances of the Child Components and register them in the RenderTree.

Slight Variation

If your Child Component really has no internal logic - as in the example in your question - then it does not need to be a Component at all - it can be a RenderFragment, which then makes your requirement to wrap each child in extra markup much easier, as a RenderFragment does not get instantiated like a Component.

Multi-child RenderFragments

If you write your Parent to accept multiple child RenderFragments, like this:

@typeparam TData

<div class="parent-component">
  @foreach (var lData in DataSource)
    {
        @foreach (var lChild in m_Children)
        {
            <div class="child-component">
                @lChild(lData);
            </div>
        }
    }

</div>

@code {
    [Parameter]
    public List<TData> DataSource { get; set; }

    [Parameter]
    public RenderFragment<TData> ChildFragment { set => m_Children.Add(value); }

    /// Collection of all added child fragments
    private List<RenderFragment<TData>> m_Children = new List<RenderFragment<TData>>();
}

You can use it like this:

<ParentComponent DataSource="@AccountList">
    <ChildFragment><span>@context.FirstName</span></ChildFragment>
    <ChildFragment><i>@context.LastName</i></ChildFragment>
    <ChildFragment><b>@context.Age</b></ChildFragment>
</ParentComponent>

As an added bonus, you no longer need to specify the type for TData as it can be inferred.

** Note: This technique will only work for RenderFragments, but you can, of course put any content you like inside the ChildFragment(s), including Child Components and still have control over the output rendering.

like image 79
Mister Magoo Avatar answered Oct 19 '22 06:10

Mister Magoo