Skip to content

Commit

Permalink
Merge pull request #731 from havit/feature/HxListLayout-NamedViewsOve…
Browse files Browse the repository at this point in the history
…rhaul

HxListLayout - Named Views overhaul (HxNamedViewList component removed)
  • Loading branch information
hakenr authored Jan 12, 2024
2 parents a58f848 + e6ff9fa commit 56bda19
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 168 deletions.
38 changes: 19 additions & 19 deletions BlazorAppTest/Pages/HxListLayoutTest.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@

<h1>HxListLayout</h1>

<HxListLayout Title="Title" TFilterModel="FilterModelDto" FilterModel="filterModel" FilterModelChanged="HandleFilterModelChanged">
<NamedViewsTemplate>
<HxNamedViewList TFilterModel="FilterModelDto" FilterModel="filterModel" FilterModelChanged="HandleFilterModelChanged" NamedViews="namedViews" OnNamedViewSelected="NamedViewSelected" />
</NamedViewsTemplate>
<FilterTemplate Context="filterContext">
<HxInputNumber Label="MinimumItemId" @bind-Value="filterContext.MinimumItemId" />
<HxInputText Label="Name containing" @bind-Value="filterContext.NameContains" />
<HxInputNumber Label="Minimum Age" @bind-Value="filterContext.MinimumAge" />
<HxInputNumber Label="Maximum Age" @bind-Value="filterContext.MaximumAge" />
<HxInputTags Label="Tags (ThemeColor)" @bind-Value="filterContext.Tags" DataProvider="GetTagsSuggestions"/>
</FilterTemplate>
<HxListLayout Title="Employees"
@bind-FilterModel="filterModel"
@bind-FilterModel:after="RefreshDataAsync"
NamedViews="namedViews">
<FilterTemplate Context="filterContext">
<HxInputNumber Label="MinimumItemId" @bind-Value="filterContext.MinimumItemId" />
<HxInputText Label="Name containing" @bind-Value="filterContext.NameContains" />
<HxInputNumber Label="Minimum Age" @bind-Value="filterContext.MinimumAge" />
<HxInputNumber Label="Maximum Age" @bind-Value="filterContext.MaximumAge" />
<HxInputTags Label="Tags (ThemeColor)" @bind-Value="filterContext.Tags" DataProvider="GetTagsSuggestions" />
</FilterTemplate>
<CommandsTemplate>
<HxButton Text="Add" Icon="BootstrapIcon.Plus" Color="ThemeColor.Secondary" OnClick="NewItemClicked" />
</CommandsTemplate>
<DataTemplate>
<HxGrid @ref="gridComponent" TItem="DataItemDto" PageSize="20" DataProvider="LoadDataItems" SelectedDataItem="currentItem" SelectedDataItemChanged="HandleSelectedDataItemChanged">
<Columns>
<HxGridColumn TItem="DataItemDto" HeaderText="Id" ItemTextSelector="@(item => item.ItemId.ToString())" SortString="@nameof(DataItemDto.ItemId)" IsDefaultSortColumn="true" />
<HxGridColumn TItem="DataItemDto" HeaderText="Name" ItemTextSelector="@(item => item.Name)" SortString="@nameof(DataItemDto.Name)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Email" ItemTextSelector="@(item => item.Email)" SortString="@nameof(DataItemDto.Email)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Phone number" ItemTextSelector="@(item => item.PhoneNumber)" SortString="@nameof(DataItemDto.PhoneNumber)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Age" ItemTextSelector="@(item => item.Age.ToString())" SortString="@nameof(DataItemDto.Age)" />
<Columns>
<HxGridColumn TItem="DataItemDto" HeaderText="Id" ItemTextSelector="@(item => item.ItemId.ToString())" SortString="@nameof(DataItemDto.ItemId)" IsDefaultSortColumn="true" />
<HxGridColumn TItem="DataItemDto" HeaderText="Name" ItemTextSelector="@(item => item.Name)" SortString="@nameof(DataItemDto.Name)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Email" ItemTextSelector="@(item => item.Email)" SortString="@nameof(DataItemDto.Email)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Phone number" ItemTextSelector="@(item => item.PhoneNumber)" SortString="@nameof(DataItemDto.PhoneNumber)" />
<HxGridColumn TItem="DataItemDto" HeaderText="Age" ItemTextSelector="@(item => item.Age.ToString())" SortString="@nameof(DataItemDto.Age)" />
<HxContextMenuGridColumn Context="item">
<HxContextMenu>
<HxContextMenuItem Text="Delete" OnClick="async () => await DeleteItemClicked(item)" ConfirmationQuestion="@($"Do you realy want to delete {item.Name}?")" />
<HxContextMenuItem Text="Delete" OnClick="async () => await DeleteItemClicked(item)" ConfirmationQuestion="@($"Do you really want to delete {item.Name}?")" />
</HxContextMenu>
</HxContextMenuGridColumn>
</Columns>
</Columns>
</HxGrid>
</DataTemplate>
<DetailTemplate>
Expand Down
15 changes: 4 additions & 11 deletions BlazorAppTest/Pages/HxListLayoutTest.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public partial class HxListLayoutTest

private readonly IEnumerable<NamedView<FilterModelDto>> namedViews = new List<NamedView<FilterModelDto>>()
{
new NamedView<FilterModelDto>("Minimum = 1", () => new FilterModelDto { MinimumItemId = 1 }),
new NamedView<FilterModelDto>("Minimum = 2", () => new FilterModelDto { MinimumItemId = 2 }),
new NamedView<FilterModelDto>("Minimum = 3", () => new FilterModelDto { MinimumItemId = 3 })
new NamedView<FilterModelDto>("Minimum ID = 1", () => new FilterModelDto { MinimumItemId = 1 }),
new NamedView<FilterModelDto>("Minimum ID = 2", () => new FilterModelDto { MinimumItemId = 2 }),
new NamedView<FilterModelDto>("Minimum ID = 3", () => new FilterModelDto { MinimumItemId = 3 })
};

private Task<GridDataProviderResult<DataItemDto>> LoadDataItems(GridDataProviderRequest<DataItemDto> request)
Expand Down Expand Up @@ -48,15 +48,8 @@ private Task<GridDataProviderResult<DataItemDto>> LoadDataItems(GridDataProvider
});
}

private async Task HandleFilterModelChanged(FilterModelDto newFilterModel)
private async Task RefreshDataAsync()
{
filterModel = newFilterModel;
await gridComponent.RefreshDataAsync();
}

protected async Task NamedViewSelected(NamedView<FilterModelDto> namedView)
{
filterModel = namedView.Filter();
await gridComponent.RefreshDataAsync();
}

Expand Down
83 changes: 49 additions & 34 deletions Havit.Blazor.Components.Web.Bootstrap/Layouts/HxListLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,50 @@
<HxCard Settings="this.CardSettingsEffective">
<BodyTemplate>
<div class="hx-list-layout-header hstack gap-2">
@if (NamedViewsTemplate != null)
{
<div class="hx-list-layout-header-dropdown">
<div class="hx-list-layout-named-view dropdown">
<span role="button" id="dropdownMenuLink" class="hx-list-layout-dropdown-menu-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<h5 class="card-title">
@if (TitleTemplate != null)
{
@TitleTemplate
}
else if (!String.IsNullOrEmpty(Title))
{
@Title
}
<HxIcon CssClass="ms-1" Icon="@BootstrapIcon.ChevronDown" />
</h5>
</span>
@NamedViewsTemplate
@if ((NamedViews != null) && NamedViews.Any())
{
<div class="hx-list-layout-header-dropdown">
<div class="hx-list-layout-named-view dropdown">
<span role="button" id="dropdownMenuLink" class="hx-list-layout-dropdown-menu-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<h5 class="card-title">
@if (TitleFromNamedView && (SelectedNamedView is not null))
{
@SelectedNamedView.Name
}
else if (TitleTemplate != null)
{
@TitleTemplate
}
else if (!String.IsNullOrEmpty(Title))
{
@Title
}
<HxIcon CssClass="ms-1" Icon="@BootstrapIcon.ChevronDown" />
</h5>
</span>
<div class="hx-named-view dropdown-menu" aria-labelledby="dropdownMenuLink">
@foreach (var namedView in NamedViews)
{
<a @key="@namedView"
class="@CssClassHelper.Combine("dropdown-item", namedView.Equals(SelectedNamedView) ? "active" : null)"
role="button"
@onclick="async () => await HandleNamedViewClickAsync(namedView)"
@onclick:stopPropagation="true">
@namedView.Name
</a>
}
</div>
</div>
}
else if (TitleTemplate != null)
{
<h5 class="card-title me-auto">@TitleTemplate</h5>
}
else if (!String.IsNullOrEmpty(Title))
{
<h5 class="card-title me-auto">@Title</h5>
}
</div>
</div>
}
else if (TitleTemplate != null)
{
<h5 class="card-title me-auto">@TitleTemplate</h5>
}
else if (!String.IsNullOrEmpty(Title))
{
<h5 class="card-title me-auto">@Title</h5>
}
@if (SearchTemplate != null)
{
@SearchTemplate
Expand All @@ -56,12 +71,12 @@
<HxOffcanvas @ref="filterOffcanvasComponent" RenderMode="OffcanvasRenderMode.Always" Title="@Localizer["FilterHeaderTitle"]" Settings="this.FilterOffcanvasSettingsEffective">
<BodyTemplate>
<HxFilterForm @ref="filterForm"
Id="@filterFormId"
TModel="TFilterModel"
Model="FilterModel"
ModelChanged="HandleFilterFormModelChanged"
Context="filterContext"
OnChipsUpdated="HandleChipUpdated">
Id="@filterFormId"
TModel="TFilterModel"
Model="FilterModel"
ModelChanged="HandleFilterFormModelChanged"
Context="filterContext"
OnChipsUpdated="HandleChipUpdated">
@FilterTemplate(filterContext)
</HxFilterForm>
</BodyTemplate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Havit.Blazor.Components.Web.Bootstrap;

/// <summary>
/// Data presentation component composed of <see cref="HxGrid"/> for data, <see cref="HxOffcanvas"/> for manual filtering, and <see cref="HxNamedViewList{T}"/> for pre-defined filters.<br />
/// Data presentation component composed of <see cref="HxGrid"/> for data, <see cref="HxOffcanvas"/> for manual filtering, and named-views for pre-defined filters.<br />
/// Full documentation and demos: <see href="https://havit.blazor.eu/components/HxListLayout">https://havit.blazor.eu/components/HxListLayout</see>
/// </summary>
/// <typeparam name="TFilterModel"></typeparam>
Expand All @@ -28,11 +28,49 @@ public partial class HxListLayout<TFilterModel>
/// </remarks>
protected virtual ListLayoutSettings GetSettings() => this.Settings;

/// <summary>
/// Title of the component.
/// If <see cref="TitleFromNamedView"/> is <c>true</c> and <see cref="SelectedNamedView"/> is not <c>null</c>, the component's title displays the name of the currently selected Named View.
/// </summary>
[Parameter] public string Title { get; set; }

/// <summary>
/// Title of the component (in form of RenderFragment).
/// If <see cref="TitleFromNamedView"/> is <c>true</c> and <see cref="SelectedNamedView"/> is not <c>null</c>, the component's title displays the name of the currently selected Named View.
/// </summary>
[Parameter] public RenderFragment TitleTemplate { get; set; }

[Parameter] public RenderFragment NamedViewsTemplate { get; set; }
/// <summary>
/// Represents the collection of Named Views available for selection.
/// Each Named View defines a pre-set filter configuration that can be applied to the data.
/// </summary>
/// <remarks>
/// Named Views provide a convenient way for users to quickly apply commonly used filters to the data set.
/// Ensure that each Named View in the collection has a unique name which accurately describes its filter criteria.
/// </remarks>
[Parameter] public IEnumerable<NamedView<TFilterModel>> NamedViews { get; set; }

/// <summary>
/// Selected named view (highlighted in the list with <c>.active</c> CSS class).
/// </summary>
[Parameter] public NamedView<TFilterModel> SelectedNamedView { get; set; }
[Parameter] public EventCallback<NamedView<TFilterModel>> SelectedNamedViewChanged { get; set; }
/// <summary>
/// Triggers the <see cref="SelectedNamedViewChanged"/> event. Allows interception of the event in derived components.
/// </summary>
protected virtual Task InvokeSelectedNamedViewChangedAsync(NamedView<TFilterModel> itemSelected) => SelectedNamedViewChanged.InvokeAsync(itemSelected);

/// <summary>
/// Indicates whether the name of the selected Named View (<see cref="SelectedNamedView"/>) is automatically used as title.
/// If <c>true</c>, the component's title changes to match the name of the currently selected Named View.
/// Useful for dynamic title updates based on user selections from predefined views.
/// The default value is <c>true</c>.
/// </summary>
/// <remarks>
/// This update occurs upon the selection of a new Named View. It allows the Title to reflect the
/// current data filtering context provided by the Named Views, enhancing user understanding of the active filter.
/// </remarks>
[Parameter] public bool TitleFromNamedView { get; set; } = true;

[Parameter] public RenderFragment SearchTemplate { get; set; }

Expand Down Expand Up @@ -116,4 +154,17 @@ private async Task HandleFilterFormModelChanged(TFilterModel newFilterModel)
await InvokeFilterModelChangedAsync(newFilterModel);
await filterOffcanvasComponent.HideAsync();
}

private async Task HandleNamedViewClickAsync(NamedView<TFilterModel> namedView)
{
SelectedNamedView = namedView;
await InvokeSelectedNamedViewChangedAsync(namedView);

TFilterModel newFilterModel = namedView.FilterModelFactory();
if (newFilterModel != null)
{
FilterModel = newFilterModel;
await InvokeFilterModelChangedAsync(newFilterModel);
}
}
}

This file was deleted.

This file was deleted.

30 changes: 17 additions & 13 deletions Havit.Blazor.Components.Web.Bootstrap/NamedViews/NamedView.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
namespace Havit.Blazor.Components.Web.Bootstrap;

/// <summary>
/// Represents a named view for <see cref="HxListLayout{TFilterModel}" />.
/// </summary>
public class NamedView<TFilterModel>
{
/// <summary>
/// Name of the view. Used as a label in the list. Can be used as a title for the <see cref="HxListLayout{TFilterModel}" />.
/// </summary>
public string Name { get; }

public Func<TFilterModel> Filter { get; }
/// <summary>
/// Creates new filter model for the view.
/// </summary>
public Func<TFilterModel> FilterModelFactory { get; }

public NamedView(string name) : this(name, () => default)
{
// NOOP
}

public NamedView(string name, TFilterModel filter) : this(name, () => filter)
{
// NOOP
}

public NamedView(string name, Func<TFilterModel> filterFunc)
/// <summary>
/// Creates a new instance of <see cref="NamedView{TFilterModel}" /> which uses a factory to build a filter model.
/// </summary>
/// <param name="name">Name of the view.</param>
/// <param name="filterModelFactory">Function which builds a new filter model to be applied when the view is selected.</param>
public NamedView(string name, Func<TFilterModel> filterModelFactory)
{
Name = name;
Filter = filterFunc;
FilterModelFactory = filterModelFactory;
}
}
Loading

0 comments on commit 56bda19

Please sign in to comment.