You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

568 lines
15 KiB

---
sidebar_position: 2
title: 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:
```cshtml title="Components/Card.razor"
<h3>Card</h3>
<div class="card text-center">
@CardHeader
@CardBody
@CardFooter
</div>
```
```csharp title="Components/Card.razor.cs"
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`.
```cshtml title="Pages/Index.razor"
...
<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:**
![Render](/img/blazor-component/RenderFragmentCard.png)
## 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.
```cshtml title="Pages/Index.razor"
...
<Card>
<CardHeader>
<div class="card-header">
Templated Component
</div>
</CardHeader>
<CardHeader>
<div class="card-header">
Hi I'm duplicated header
</div>
</CardHeader>
</Card>
...
```
**Render:**
![Render](/img/blazor-component/duplicate.png)
## 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>`.
```cshtml title="Components/Card.razor"
@typeparam TItem
<div class="card text-center">
@CardHeader(Item)
@CardBody(Item)
@CardFooter
</div>
```
```csharp title="Components/Card.razor.cs"
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.
```csharp title="Models/Cake.cs"
public class Cake
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Cost { get; set; }
}
```
```cshtml title="Pages/Index.razor"
...
<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>
...
```
```csharp title="Pages/Index.razor.cs"
public partial class Index
{
private Cake CakeItem = new Cake
{
Id = 1,
Name = "Black Forest",
Cost = 50
};
}
```
**Render:**
![Render](/img/blazor-component/typeRenderFragment.png)
## 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.
```cshtml title="Pages/Index.razor"
...
<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.
```cshtml title="Pages/Index.razor"
...
<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.
```cshtml title="Components/ShowItems.razor"
@typeparam TItem
<div>
@if ((Items?.Count ?? 0) != 0)
{
@foreach (var item in Items)
{
@ShowTemplate(item);
}
}
</div>
```
```cshtml title="Components/ShowItems.razor.cs"
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.
```cshtml title="Pages/Index.razor"
...
<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.
```csharp title="Pages/Index.razor.cs"
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:**
![Render](/img/blazor-component/genericStatic.png)
## 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.
```cshtml title="Shared/TableTemplate.razor"
@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.
```cshtml title="Pages/Pets1.razor"
@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`:
```cshtml title="Pages/Pets2.razor"
@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:
```cshtml title="Pages/Pets3.razor"
@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:
```cshtml title="Pages/Pets4.razor"
@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`.
```cshtml title="Components/TestRenderFragment.razor"
<h3>TestRenderFragment</h3>
@code {
}
```
```cshtml title="Pages/Index.razor"
...
<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:
```cshtml title="Components/TestRenderFragment.razor"
<h3>TestRenderFragment</h3>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@ChildContent
```