Blazor component: change classes based on number of children

88 views Asked by At

I think it should be relatively simple but I could not find how to do it.

I have this:

<div class="row">
    <div class="col-6 col-12-small">
        <p>
            Lorem lipsum.
        </p>
    </div>
    <div class="col-6 col-12-small">
        <span class="image fit">
            <img src="images/test.jpg"/>
        </span>
    </div>
</div>

and I would like to create a component that would help make coding faster and cleaner. I would like something like this:

<MyGrid>
    <MyGridColumn>
        <p>
            Lorem lipsum.
        </p>
    </MyGridColumn>
    <MyGridColumn>
        <span class="image fit">
            <img src="images/test.jpg"/>
        </span>
    </MyGridColumn>
</MyGrid>

The component would change the classes depending on the number of GridColumn. If 2 children: "col-6 col-12-small", if three, "col-4 col-12-small",... and add maybe later more complexity.

2

There are 2 answers

3
Brian Parker On BEST ANSWER

Use a registration process with the parent component. This is a basic example the registration process could exchange a lot more information, parameters, sizes etc.

Parent.razor

<div>I have @Count children</div>
<CascadingValue Value="this">
    @ChildContent
</CascadingValue>
@code {
    private List<ComponentBase> children = new();

    [Parameter, EditorRequired]
    public RenderFragment ChildContent { get; set; } = default!;

    internal int Count => children.Count;
    internal void Register(ComponentBase componentBase)
    {
        children.Add(componentBase);
        StateHasChanged();
    }
}

Child.razor

@ChildContent/@Parent.Count
@code {
    [CascadingParameter]
    public Parent Parent { get; set; } = default!;

    [Parameter, EditorRequired]
    public RenderFragment ChildContent { get; set; } = default!;

    protected override void OnInitialized()
    {
        if (Parent is null) throw new SpitTheDummyException();

        Parent.Register(this);
    }

    public class SpitTheDummyException() :
        Exception("I need a parent! Place the Child within a Parent"); // C# 12
}

Usage

<Parent>
    <Child>One</Child>
    <Child>Two</Child>
    <Child>Three</Child>
</Parent>

Output

enter image description here

0
MrC aka Shaun Curtis On

Here's a different version of Brian Parker's answer that uses the Blazor Component Registration process as used in the QuickGrid component.

First the Defer component. I'll explain what it does shortly.

@ChildContent

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

Next your MyGridColumn. See the inline comments.

public class MyGridColumn : ComponentBase
{
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [CascadingParameter] private Action<RenderFragment>? Register { get; set; }

    private bool _isRegistered;

    public override Task SetParametersAsync(ParameterView parameters)
    {
        // Only do this on the first pass.
        if (!_isRegistered)
        {
            parameters.SetParameterProperties(this);
            if (this.ChildContent is not null)
                this.Register?.Invoke(this.ChildContent);
        }

        // Short circuit the lifecycle stuff.
        // There's nothing for the component to render so don't waste CPU cycles doing nothing
        return Task.CompletedTask;
    }
}

MyGrid. The trick here is that Defer is deferring the rendering of the MyGrid markup code in it's ChildContent until after the MyGridColumn components in MyGrid's ChildContent has been handled by the Renderer and registered. Defer is at the same level in the render tree as the MyGridColumn's, and thus is handled sequentially with them.

Each MyGridColumn registers it's ChildContent render fragment delegate. Defer walks through the list and renders each in turn.

I've added the column stuff and some logic to sort out the column formatting based on the number of render fragments registered.

<CascadingValue Value="Register" IsFixed>
    @ChildContent
</CascadingValue>

<Defer>
    <div class="row">
        @foreach (var item in _items)
        {
            <div class="@_colCss">
                @item
            </div>
        }
    </div>
</Defer>

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

    private List<RenderFragment> _items = new();

    private string _colCss => _items.Count switch
    {
        > 6 => "col-12 col-lg-1",
        > 4 => "col-12 col-lg-2",
        > 3 => "col-12 col-lg-3",
        > 2 => "col-12 col-lg-4",
        > 1 => "col-12 col-lg-6",
        _ => "col-12",
    };

    private void Register(RenderFragment option)
    {
        if (!_items.Contains(option))
            _items.Add(option);
    }
}

Finally, a demo page:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<MyGrid>
    <MyGridColumn>
        <p>
            Lorem lipsum.
        </p>
    </MyGridColumn>
    <MyGridColumn>
        <span class="image fit">
            <img src="images/test.jpg"/>
        </span>
    </MyGridColumn>
    <MyGridColumn>
        <button class="btn btn-primary">Click Me</button>
    </MyGridColumn>
</MyGrid>

You can find more detail on the Blazor Component Registration Pattern here: https://github.com/ShaunCurtis/Blazr.ComponentRegistration

By coincidence - I was working on the Repo today.