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
568 lines
15 KiB
![]()
2 years ago
|
---
|
||
|
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:**
|
||
|
|
||
|

|
||
|
|
||
|
## 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:**
|
||
|
|
||
|

|
||
|
|
||
|
|
||
|
## 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:**
|
||
|
|
||
|

|
||
|
|
||
|
|
||
|
## 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:**
|
||
|
|
||
|

|
||
|
|
||
|
## 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
|
||
|
```
|