Templating components with RenderFragments in Blazor

Sometimes we need to create components that mix consumer-supplied mark-up with their own rendered output.
It would be very messy to pass content to a component as an HTML encoded string parameter:

<Collapsible content="Lots of encoded HTML for your entire view here"/>

And, in addition to the maintenance nightmare, the embedded HTML could only be basic HTML mark-up too, no Blazor components. Basically, it’d be useless, and obviously that’s not how it should be done. The correct approach is to use a RenderFragment.

Without RenderFragment

Index.razor

<table class="table">
    <tr>
        <th>Name</th>
        <th>Gender</th>
        <th>Age</th>
    </tr>
    <tr>
        <td>John</td>
        <td>Male</td>
        <td>37</td>
    </tr>
    <tr>
        <td>Rose</td>
        <td>Female</td>
        <td>32</td>
    </tr>
    <tr>
        <td>Martin</td>
        <td>Male</td>
        <td>1</td>
    </tr>
</table>

Child Content

These are the criteria Blazor uses to inject embedded content into a component. The embedded content may be anything you wish; plain text, HTML elements, more razor mark-up (including more components), and the content of that embedded content may be output anywhere in your component’s mark-up simply by adding @ChildContent.

TableTemplate.razor

<table class="table">
    <tr>
        <th>Name</th>
        <th>Gender</th>
        <th>Age</th>
    </tr>

    @ChildContent
</table>

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

Index.razor

@page "/"

<TableTemplate>
    <tr>
        <td>John</td>
        <td>Male</td>
        <td>37</td>
    </tr>
    <tr>
        <td>Rose</td>
        <td>Female</td>
        <td>32</td>
    </tr>
    <tr>
        <td>Martin</td>
        <td>Male</td>
        <td>1</td>
    </tr>
</TableTemplate>

Multiple RenderFragments

When we write mark-up inside a component, Blazor will assume it should be assigned to a Parameter on the component that is descended from the RenderFragment class and is named ChildContent. If we wish to use a different name, or multiple render fragments, then we must explicitly specify the parameter’s name in our mark-up.

TableTemplate.razor

<table class="table">
    <tr>
        @TableHeader
    </tr>

    @ChildContent
</table>

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

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

Index.razor

@page "/"

<TableTemplate>
    <TableHeader>
        <th>Name</th>
        <th>Gender</th>
        <th>Age</th>
    </TableHeader>

    <ChildContent>
        <tr>
            <td>John</td>
            <td>Male</td>
            <td>37</td>
        </tr>
        <tr>
            <td>Rose</td>
            <td>Female</td>
            <td>32</td>
        </tr>
        <tr>
            <td>Martin</td>
            <td>Male</td>
            <td>1</td>
        </tr>
    </ChildContent>

</TableTemplate>

Passing data to a RenderFragment

As well as the standard RenderFragment class, there is also a generic RenderFragment<T> class that can be used to pass data into the RenderFragment.

TableTemplate.razor

@using System.Diagnostics.CodeAnalysis
@typeparam TItem

<table class="table">
    <tr>
        @TableHeader
    </tr>

    @foreach (var item  in Items)
    {
        if (RowTemplate is not null)
        {
            <tr>@RowTemplate(item)</tr>
        }
    }
</table>

@code {

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

    [Parameter, AllowNull]
    public IReadOnlyList<TItem> Items { get; set; }

    [Parameter]
    public RenderFragment<TItem>? RowTemplate { get; set; }

}

Index.razor

@page "/"

<TableTemplate Items="people" Context="person">
    <TableHeader>
        <th>Name</th>
        <th>Gender</th>
        <th>Age</th>
    </TableHeader>

    <RowTemplate>
        <td>@person.Name</td>
        <td>@person.Gender</td>
        <td>@person.Age</td>
    </RowTemplate>

</TableTemplate>

@code
{
    private List<Person> people = new()
    {
        new Person() { Name = "John", Gender = "Male", Age = 37 },
        new Person() { Name = "Rose", Gender = "Female", Age = 32 },
        new Person() { Name = "Martin", Gender = "Male", Age = 1 },
    };

    private class Person
    {
        public string Name { get; set; }
        public string Gender { get; set; }
        public int Age { get; set; }
    }
}

References
https://blazor-university.com/templating-components-with-renderfragements/
https://blazor-university.com/templating-components-with-renderfragements/passing-data-to-a-renderfragement/
https://docs.microsoft.com/en-us/aspnet/core/blazor/components/templated-components?view=aspnetcore-6.0