15 KiB
sidebar_position | title |
---|---|
2 | Generic component |
Generic components are components that accept one or more UI templates as parameters, which can then be used as part of the component's rendering logic. Generic components allow you to author higher-level components that are more reusable than regular components. A couple of examples include:
- A table component that allows a user to specify templates for the table's header, rows, and footer.
- A list component that allows a user to specify a template for rendering items in a list.
Component creation
Create a new component that will serve as the basis for the generic component:
<h3>Card</h3>
<div class="card text-center">
@CardHeader
@CardBody
@CardFooter
</div>
public partial class Card
{
[Parameter]
public RenderFragment CardBody { get; set; }
[Parameter]
public RenderFragment CardFooter { get; set; }
[Parameter]
public RenderFragment CardHeader { get; set; }
}
@CardHeader
, @CardBody
and @CardFooter
are parameter template components of type RenderFragment
.
...
<Card>
<CardHeader>
<div class="card-header">
My Templated Component
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<h5>Welcome To Template Component</h5>
</div>
</CardBody>
<CardFooter>
<div class="card-footer text-muted">
Click Here
</div>
</CardFooter>
</Card>
...
Each Template parameter of type RenderFragment
will have a corresponding Html tag. The HTML tag must match the parameter name.
Render:
Html tag duplication
The Html tag which represents the parameter of the Template component, duplicating it will result in the rendering of the last Html tag or the bottom one in priority.
...
<Card>
<CardHeader>
<div class="card-header">
Templated Component
</div>
</CardHeader>
<CardHeader>
<div class="card-header">
Hi I'm duplicated header
</div>
</CardHeader>
</Card>
...
Render:
Parameter typed RenderFragment<T>
RenderFragmetnt<T>
is a typed parameter that represents a portion of UI rendering with dynamic content of the type it implements.- Declare the generic type parameter at the top of the component using
@typeparam
. This represents the component type at runtime. - This type of typed component parameter that is passed as an element will have an implicit parameter called
context
. - Implicit context parameter helps with dynamic data binding. It has access to the property type implemented by
RenderFragment<T>
.
@typeparam TItem
<div class="card text-center">
@CardHeader(Item)
@CardBody(Item)
@CardFooter
</div>
public partial class Card<TItem>
{
[Parameter]
public RenderFragment<TItem> CardBody { get; set; }
[Parameter]
public RenderFragment CardFooter { get; set; }
[Parameter]
public RenderFragment<TItem> CardHeader { get; set; }
[Parameter]
public TItem Item { get; set; }
}
CardHeader
, CardBody
are parameters of type RenderFragment<T>
. These Html modeled properties have access to the implicit context
parameter for dynamic binding data.
public class Cake
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Cost { get; set; }
}
...
<Card Item="CakeItem">
<CardHeader>
<div class="card-header">
Cake Token Number - @context.Id
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<div>@context.Name</div>
<div>$ @context.Cost</div>
</div>
</CardBody>
<CardFooter>
<div class="card-footer text-muted">
Click Here
</div>
</CardFooter>
</Card>
...
public partial class Index
{
private Cake CakeItem = new Cake
{
Id = 1,
Name = "Black Forest",
Cost = 50
};
}
Render:
Context attribute on Template HTML element
In the component, the context can be declared explicitly as an Html attribute. The value assigned to the context (Html attribute) will be used for dynamic linking in all component templates.
Declaration of the Context attribute at the component level.
...
<Card Item="CakeItem" Context="cakeContext">
<CardHeader>
<div class="card-header">
Cake Token Number - @cakeContext.Id
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<div>@cakeContext.Name</div>
<div>$ @cakeContext.Cost</div>
</div>
</CardBody>
</Card>
...
Declaration context attribute at the level of each template.
...
<Card Item="CakeItem">
<CardHeader Context="headContext">
<div class="card-header">
Cake Token Number - @headContext.Id
</div>
</CardHeader>
<CardBody Context="bodyContext">
<div class="card-body">
<div>@bodyContext.Name</div>
<div>$ @bodyContext.Cost</div>
</div>
</CardBody>
</Card>
...
Generic component
The template component is itself a generic component. @typeparam
defines the type of the generic component.
The generic template component will be defined by our own implementation and it can be reused for different types.
The best choice of its implementation is like the component with only two properties.
The first property is of type RenderFragment<T>
to render the content.
The second property is of type List<T>
a collection of data to bind to render.
The most widely used scenarios for generic component are display elements, this will help avoid creating new components to display items in an application. A single component for all display elements.
@typeparam TItem
<div>
@if ((Items?.Count ?? 0) != 0)
{
@foreach (var item in Items)
{
@ShowTemplate(item);
}
}
</div>
public partial class ShowItems<TItem>
{
[Parameter]
public List<TItem> Items { get; set; }
[Parameter]
public RenderFragment<TItem> ShowTemplate { get; set; }
}
ShowTemplate
and Items
are two parameters of our generic type component.
ShowTemplate
is a property of type RenderFragment<TItem>
which will render the Html to be displayed (this Html can be an array, a list, a content, etc.).
Items
is a collection whose type matches our component type, this data will be used in data binding inside our rendered Html.
...
<ShowItems Items="Cakes" >
<ShowTemplate Context="CakeContext">
<div class="card text-center">
<div class="card-header">
Cake Token Id - @CakeContext.Id
</div>
<div class="card-body">
<h5 class="card-title">@CakeContext.Name</h5>
<p class="card-text">Price $@CakeContext.Cost</p>
</div>
<div class="card-footer text-muted">
Click Here
</div>
</div>
</ShowTemplate>
</ShowItems>
...
The ShowItems
template represents our generic type component. ShowTemplate
represents the HTML rendering property of the component.
public partial class Index
{
...
public List<Cake> Cakes { get; set; }
protected override Task OnAfterRenderAsync(bool firstRender)
{
LoadCakes();
StateHasChanged();
return base.OnAfterRenderAsync(firstRender);
}
public void LoadCakes()
{
Cakes = new List<Cake>
{
// items hidden for display purpose
new Cake
{
Id = 1,
Name = "Red Velvet",
Cost = 60
},
};
}
...
}
Inside the OnAfterRenderAsync
method we call the StageHasChanged()
method to reflect the data changes in the component.
You can try removing the StateHasChanged()
method and you may observe an empty page without displaying any data because we are filling some data before rendering Html.
Render:
Concept: RenderFragment
Template components are components that accept one or more UI templates as parameters, which can then be used as part of the component's rendering logic. Template components allow you to create higher level components that are more reusable than normal components.
Here are some examples :
- Table component that allows a user to specify templates for the table header, rows, and footer.
- List component that allows a user to specify a template to display items in a list.
A template component is defined by specifying one or more component parameters of type RenderFragment
or RenderFragment<TValue>
.
A render fragment represents a segment of the user interface to be rendered. RenderFragment<TValue>
takes a type parameter which can be specified when calling the render fragment.
Often, template components are generically typed, as shown in the following TableTemplate
component.
The <T>
generic type in this example is used to render IReadOnlyList<T>
values, which in this case is a series of PET rows in a component that displays a table of pets.
@typeparam TItem
@using System.Diagnostics.CodeAnalysis
<table class="table">
<thead>
<tr>@TableHeader</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
if (RowTemplate is not null)
{
<tr>@RowTemplate(item)</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public RenderFragment? TableHeader { get; set; }
[Parameter]
public RenderFragment<TItem>? RowTemplate { get; set; }
[Parameter, AllowNull]
public IReadOnlyList<TItem> Items { get; set; }
}
When using a template component, template parameters can be specified using child elements that correspond to parameter names.
In the following example, <TableHeader>...</TableHeader>
and <RowTemplate>...<RowTemplate>
provide RenderFragment<TValue>
templates for TableHeader
and RowTemplate
of component TableTemplate
.
Specify the Context
attribute on the component element when you want to specify the content parameter name for implicit child content (without encapsulating child element).
In the following example, the Context
attribute appears on the TableTemplate
element and applies to all RenderFragment<TValue>
template parameters.
@page "/pets1"
<h1>Pets</h1>
<TableTemplate Items="pets" Context="pet">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
You can also change the parameter name using the Context
attribute on the RenderFragment<TValue>
child element.
In the following example, the Context
is set to RowTemplate
instead of TableTemplate
:
@page "/pets2"
<h1>Pets</h1>
<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate Context="pet">
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
Component arguments of type RenderFragment<TValue>
have an implicit parameter named context
, which can be used.
In the following example, Context
is not set. @context.{PROPERTY}
provides PET
values to the model, where {PROPERTY}
is a Pet
property:
@page "/pets3"
<h1>Pets</h1>
<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
When using generic components, the type parameter is inferred if possible.
However, you can explicitly specify the type with an attribute whose name matches the type parameter, which is TItem
in the previous example:
@page "/pets4"
<h1>Pets</h1>
<TableTemplate Items="pets" TItem="Pet">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
By default a new blazor component does not accept content if you do not declare a RenderFragment
named ChildContent
.
<h3>TestRenderFragment</h3>
@code {
}
...
<TestRenderFragment>
<div>Content of my TestRenderFragment</div>
</TestRenderFragment>
...
This code does not generate a compilation error but will trigger the following error at runtime:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Object of type 'MyBeautifulAdmin.Components.TestRenderFragment' does not have a property matching the name 'ChildContent'.
System.InvalidOperationException: Object of type 'MyBeautifulAdmin.Components.TestRenderFragment' does not have a property matching the name 'ChildContent'.
In order to render the child code, use the following code:
<h3>TestRenderFragment</h3>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@ChildContent