Travis Pettry
Travis Pettry

Reputation: 1352

Blazor Nested Components

I want to be able to take in a list of blazor components as a parameter in a declarative way.

Component 1:

@foreach(Component2 input in Inputs)
{
    @input
}
 
@code{
    [Parameter]
    public List<Component2> Inputs {get; set;}
}

Component 2:

<h1>@Header</h1>

@code{
    [Parameter]
    public string Header {get; set;}
}

How I want to use it:

<Component1>
    <Inputs>
        <Component2 Header="hello" />
        <Component2 Header="world" />
    </Inputs>
</Component1>

Does anyone know if this is possible?

Thank you, Travis

Upvotes: 0

Views: 5659

Answers (3)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30177

Here's a simplistic but more generic way to achieve what you want. It renders three instances of your HeaderComponent and one Counter.

@page "/"
<h3>MultiHeader</h3>
@MyComponents

@code {

    private List<Tuple<Type, Dictionary<string, object>>> _components = new List<Tuple<Type, Dictionary<string, object>>>();

    protected override void OnInitialized()
    {
        _components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 1" } }));
        _components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 2" } }));
        _components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 3" } }));
        _components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(Counter), new Dictionary<string, object>()));
    }

    private RenderFragment MyComponents => builder =>
    {
        foreach (var component in _components)
        {
            builder.OpenComponent(0, component.Item1);
            foreach (var parameter in component.Item2)
            {
                builder.AddAttribute(1, parameter.Key, parameter.Value);
            }
            builder.CloseComponent();
        }
    };
}

I've used a Tuple to keep things simple: you could have an object to hold your component information if you want to get a bit more complex.

Upvotes: 2

Travis Pettry
Travis Pettry

Reputation: 1352

While digging around the web I found a link to a Blazor University page that described this perfectly.

https://blazor-university.com/templating-components-with-renderfragements/creating-a-tabcontrol/

https://github.com/mrpmorris/blazor-university/tree/master/src/TemplatedComponents/CreatingATabControl

The gist of the solution is you send a reference of the parent to the child component. Then when the child is initialized you tell the parent to add the child to the list of components.

Parent Component:

<CascadingValue Value="this">
    <div class="btn-group" role="group">
        @foreach (Tab tabPage in Pages)
        {
            <button type="button"
                    class="btn @GetButtonClass(tabPage)"
                    @onclick=@( () => ActivatePage(tabPage) )>
                @tabPage.Text
            </button>
        }
    </div>
    @ChildContent
</CascadingValue>

@code {
    // Next line is needed so we are able to add <TabPage> components inside
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public Tab ActivePage { get; set; }
    List<Tab> Pages = new List<Tab>();
    internal void AddPage(Tab tabPage)
    {
        Pages.Add(tabPage);
        if (Pages.Count == 1)
            ActivePage = tabPage;
        StateHasChanged();
    }

    string GetButtonClass(Tab page)
    {
        return page == ActivePage ? "btn-primary" : "btn-secondary";
    }

    void ActivatePage(Tab page)
    {
        ActivePage = page;
    }
}

Child Component:

@if (Parent.ActivePage == this)
{
    @ChildContent
}

@code {
    [CascadingParameter]
    private Tabs Parent { get; set; }
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public string Text { get; set; }

    protected override void OnInitialized()
    {
        if (Parent == null)
            throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
        base.OnInitialized();

        Parent.AddPage(this);
    }
}

Usage:

<ParentComponent>
    <ChildComponent Text="Tab 1">
        <h1>This is a test</h1>
    </ChildComponent >
    <ChildComponent Text="2">
        <h3>This is a test</h3>
    </ChildComponent >
    <ChildComponent Text="3 Tab">
        <h5>This is a test</h5>
    </ChildComponent >
</ParentComponent>

Upvotes: 2

RB.
RB.

Reputation: 37202

One option is to override BuildRenderTree. Here we define a MessagesComponent that is supplied with a list of MessageComponents, each of which has a single message:

// YourPage.Razor
<p>Here are some messages:</p>
<MessagesComponent MessageComponents="messageComponents" />

@code {
    private readonly IEnumerable<MessageComponent> messageComponents = new List<MessageComponent>
    {
        new MessageComponent { Message = "Hello" },
        new MessageComponent { Message = "World" }
    };
}

// MessagesComponent.cs
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

namespace WebApplication3.Pages
{
    public class MessagesComponent : ComponentBase
    {
        [Parameter]
        public IEnumerable<MessageComponent> MessageComponents { get; set; }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            base.BuildRenderTree(builder);

            // Render each component.
            foreach (var component in MessageComponents)
            {
                builder.OpenComponent(0, typeof(MessageComponent));
                builder.AddAttribute(1, nameof(MessageComponent.Message), component.Message);
                builder.CloseComponent();
            }
        }
    }
}

// MessageComponent.razor
@using Microsoft.AspNetCore.Components.Rendering 

<p>This message says "@Message"</p>

@code {
    [Parameter]
    public string Message { get; set; }
}

This outputs like so: enter image description here

Upvotes: 0

Related Questions