David S
David S

Reputation: 113

Templating Blazor Components

Just thinking about coming up with a templated blazor component to do a CRUD style Single Page App which can have a specific object passed in so I don't have to write the same boilerplate code over and again.

so for instance as below parts of it can be templated by using RenderFragment objects:

@typeparam TItem

<div>
    @if (AddObjectTemplate != null)
    {
        @AddObjectTemplate
    }
    else
    {
        <div style="float:left">
            <button class="btn btn-primary" @onclick="AddObject">Add Object</button>
        </div>
    }
</div>

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

    [Parameter]
    public IList<TItem> Items { get; set; }

}

However further down I might want to have something like this:

<button class="btn btn-default" @onclick="@(() => EditObject(item.Id))">Edit</button>

protected void EditObject(int id)
{
    TItem cust = _itemServices.Details(id);
}

The issue is that the above call to EditObject(item.Id) cannot resolve to a specific object at this moment because it does not know what TItem is. Is there a way to use a specific interface in the template component that each object must implement or is there another way of doing this?

The idea would be to have AddObject, EditObject, DeleteObject etc which all basically do the same thing but with different types of object.

Upvotes: 3

Views: 1351

Answers (1)

Nik FP
Nik FP

Reputation: 3063

Since you have the IList<TItem> as a parameter, the list exists at another level of the component structure outside of this component. Because of this you might be better off using the EventCallBack<T> properties for your Add, Edit, and Delete methods, and having the actual methods set as you wire the component up. This makes your template component a rendering object only, and you keep the real "work" to be done close to the actual list that needs the work done.

When you set up your template component, you might try something like this which I've had good results with.

Templator.razor

@typeparam TItem

<h3>Templator</h3>

@foreach (var item in Items)
{

    @ItemTemplate(item)

    <button @onclick="@(() => EditItemCallBack.InvokeAsync(item))">Edit Item</button>
}

@code {

    [Parameter]
    public IList<TItem> Items { get; set; }

    [Parameter]
    public EventCallback<TItem> EditItemCallBack { get; set; }

    [Parameter]
    public RenderFragment<TItem> ItemTemplate
}

Container.Razor

<h3>Container</h3>

<Templator TItem="Customer" Items="Customers" EditItemCallBack="@EditCustomer">

    <ItemTemplate Context="Cust">
        <div>@Cust.Name</div>
    </ItemTemplate>

</Templator>

@code {

    public List<Customer> Customers { get; set; }

    void EditCustomer(Customer customer)
    {
        var customerId = customer.Id;

        //Do something here to update the customer

    }

}

Customer.cs

public class Customer
{
    public int Id { get; set; }

    public string Name { get; set; }

}

The key points here would be as follows:

  1. The actual root list is living outside the templated component, as are the methods to work on that list, so all of the above are at the same level of abstraction and the same chunk of code.

  2. The template component receives a list, a type to specify what type of the list items will be, and a method callback to execute on each item. (or series of methods, you can add the "Add" and "Delete" methods as you see fit using the same approach). It also receives the <ItemTemplate> render fragment that you specify when calling the code in the Container.razor file.

  3. The 'foreach' in the templated item makes sure each TItem gets set up for it's own RenderFragment, set of buttons and callback functions.

  4. Using the EventCallBack<TItem> as a parameter means that you assign a method to it that expects back the whole object of TItem. This is good, as the template now doesn't care what type the TItem is, only that is has the ability to call a method that takes a TItem as an argument! Handling the instance of whatever TItem is is now the responsibility of the calling code, and you don't have to try to constrain the generic type TItem. (Which I haven't had any luck doing in Blazor yet, maybe future releases)

As for how to render whatever TItem you feed into it, that is explained well in the documentation HERE.

Hope this helps!

Upvotes: 3

Related Questions