diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 89f27de279af..9bd2088454b7 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -18,6 +18,10 @@ + + + + diff --git a/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs b/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs index eeae2040d2ed..769756ffc882 100644 --- a/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs +++ b/src/Components/Components/src/Routing/SupplyParameterFromQueryValueProvider.cs @@ -74,9 +74,9 @@ private bool TryUpdateUri() return false; } - var query = GetQueryString(navigationManager.Uri); + var query = QueryParameterValueSupplier.GetQueryString(navigationManager.Uri); - if (!query.Span.SequenceEqual(GetQueryString(_lastUri).Span)) + if (!query.Span.SequenceEqual(QueryParameterValueSupplier.GetQueryString(_lastUri).Span)) { _queryChanged = true; _queryParameterValueSupplier.ReadParametersFromQuery(query); @@ -84,20 +84,6 @@ private bool TryUpdateUri() _lastUri = navigationManager.Uri; return true; - - static ReadOnlyMemory GetQueryString(string? url) - { - var queryStartPos = url?.IndexOf('?') ?? -1; - - if (queryStartPos < 0) - { - return default; - } - - Debug.Assert(url is not null); - var queryEndPos = url.IndexOf('#', queryStartPos); - return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); - } } private void SubscribeToLocationChanges() diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor index f9d50dfdc2b7..46beae4969ed 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @namespace Microsoft.AspNetCore.Components.QuickGrid @@ -24,10 +24,26 @@ if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault()) { - + @if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting && Title is not null) + { + +
@Title
+ +
+ } + else if (!QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting && Title is not null) + { + + } + else + { +
+
@Title
+
+ } } else { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css index 11477eb380d6..756257d3d570 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor.css @@ -6,12 +6,15 @@ padding: 0; } -/* If the column is sortable, its title is rendered as a button element for accessibility and to support navigation by tab */ +/* If the column is sortable, its title is rendered as a link element for accessibility and to support navigation by tab */ +a.col-title, button.col-title { border: none; background: none; position: relative; cursor: pointer; + text-decoration: none; + color: inherit; } .col-justify-center .col-title { diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj index 8603bc6d164e..16dc8318a297 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Microsoft.AspNetCore.Components.QuickGrid.csproj @@ -17,6 +17,11 @@ <_CurrentProjectDiscoveredScopedCssFiles Include="@(ThemeCssFiles)" RelativePath="%(Identity)" BasePath="_content/$(AssemblyName)" /> + + + + + diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs index 2da161a7af9a..a86eb2b5c017 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/PaginationState.cs @@ -36,6 +36,7 @@ public class PaginationState /// public event EventHandler? TotalItemCountChanged; + internal string QueryName { get; set; } = ""; internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor index 054b09ba20a5..51b229f27909 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web @namespace Microsoft.AspNetCore.Components.QuickGrid
@@ -15,14 +15,28 @@ }
} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs index bd34bec6272d..fadf0784b88f 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -12,6 +13,10 @@ public partial class Paginator : IDisposable { private readonly EventCallbackSubscriber _totalItemCountChanged; + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + private string QueryName => State.QueryName; + /// /// Specifies the associated . This parameter is required. /// @@ -29,6 +34,15 @@ public Paginator() { // The "total item count" handler doesn't need to do anything except cause this component to re-render _totalItemCountChanged = new(new EventCallback(this, null)); + _queryParameterValueSupplier = new(); + } + + private readonly QueryParameterValueSupplier _queryParameterValueSupplier; + + private string GetPageUrl(int pageIndex) + { + int? pageValue = pageIndex == 0 ? null : pageIndex + 1; + return NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); } private Task GoFirstAsync() => GoToPageAsync(0); @@ -38,15 +52,68 @@ public Paginator() private bool CanGoBack => State.CurrentPageIndex > 0; private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; - private Task GoToPageAsync(int pageIndex) => State.SetCurrentPageIndexAsync(pageIndex); /// - protected override void OnParametersSet() - => _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + protected override void OnInitialized() + { + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged += OnLocationChanged; + } + } + + /// + protected override Task OnParametersSetAsync() + { + _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + return State.SetCurrentPageIndexAsync(pageFromQuery); + } + } + + return Task.CompletedTask; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + await InvokeAsync(async () => + { + if (pageFromQuery != State.CurrentPageIndex) + { + await State.SetCurrentPageIndexAsync(pageFromQuery); + } + StateHasChanged(); + }); + } + + private int? ReadPageIndexFromQueryString() + { + var value = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), QueryName) as string; + if (value is not null && int.TryParse(value, out var page) && page > 0) + { + return page - 1; + } + + return null; + } /// public void Dispose() - => _totalItemCountChanged.Dispose(); + { + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged -= OnLocationChanged; + } + _totalItemCountChanged.Dispose(); + } } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css index d996a6d1beb7..a15905bfc6b8 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Pagination/Paginator.razor.css @@ -17,22 +17,32 @@ nav { align-items: center; } + nav a, nav button { border: 0; background: none center center / 1rem no-repeat; width: 2rem; height: 2rem; + display: inline-block; + text-decoration: none; + font-size: 0; + cursor: pointer; + padding: 0; } - nav button[disabled] { + nav a[aria-disabled="true"], + nav button:disabled { opacity: 0.4; + pointer-events: none; } - nav button:not([disabled]):hover { + nav a:not([aria-disabled="true"]):hover, + nav button:not(:disabled):hover { background-color: #eee; } - nav button:not([disabled]):active { + nav a:not([aria-disabled="true"]):active, + nav button:not(:disabled):active { background-color: #aaa; } diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt index 8aa8edca4b77..bc71853880cf 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/PublicAPI.Unshipped.txt @@ -1,3 +1,9 @@ #nullable enable +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryParameterNamePrefix.get -> string! +Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.QueryParameterNamePrefix.set -> void Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.get -> Microsoft.AspNetCore.Components.EventCallback Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnRowClick.set -> void +override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnInitialized() -> void +override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSetAsync() -> System.Threading.Tasks.Task! +override Microsoft.AspNetCore.Components.QuickGrid.QuickGrid.OnInitialized() -> void +*REMOVED*override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSet() -> void diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs index f8d34dc34925..af620bc09e17 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.JSInterop; using Microsoft.AspNetCore.Components.Forms; @@ -114,8 +115,15 @@ public partial class QuickGrid : IAsyncDisposable /// [Parameter] public EventCallback OnRowClick { get; set; } + /// + /// The parameter from which the page and sorting URL parameters are derived. The default value is an empty string, which results in query parameters named "page", "sort", and "order". If you provide a non-empty value, for example "products", + /// then the query parameters will be "products_page", "products_sort", and "products_order". This allows you to use multiple components on the same page without their URL parameters conflicting with each other. + /// + [Parameter] public string QueryParameterNamePrefix { get; set; } = ""; + [Inject] private IServiceProvider Services { get; set; } = default!; [Inject] private IJSRuntime JS { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; private ElementReference _tableReference; private Virtualize<(int, TGridItem)>? _virtualizeComponent; @@ -136,6 +144,8 @@ public partial class QuickGrid : IAsyncDisposable private ColumnBase? _displayOptionsForColumn; private ColumnBase? _sortByColumn; private bool _sortByAscending; + private ColumnBase? _defaultSortColumn; + private bool _defaultSortAscending; private bool _checkColumnOptionsPosition; // The associated ES6 module, which uses document-level event listeners @@ -162,6 +172,13 @@ public partial class QuickGrid : IAsyncDisposable private bool _firstRefreshDataAsync = true; + private (string ColumnTitle, bool Ascending)? _cachedSortFromQuery; + + private string SortQueryParameterNameBy => QueryParameterNamePrefix == "" ? "sort" : $"{QueryParameterNamePrefix}_sort"; + private string SortQueryParameterNameOrder => QueryParameterNamePrefix == "" ? "order" : $"{QueryParameterNamePrefix}_order"; + private string PageQueryParameterName => QueryParameterNamePrefix == "" ? "page" : $"{QueryParameterNamePrefix}_page"; + private readonly QueryParameterValueSupplier _queryParameterValueSupplier; + /// /// Constructs an instance of . /// @@ -172,6 +189,7 @@ public QuickGrid() _currentPageItemsChanged = new(EventCallback.Factory.Create(this, RefreshDataCoreAsync)); _renderColumnHeaders = RenderColumnHeaders; _renderNonVirtualizedRows = RenderNonVirtualizedRows; + _queryParameterValueSupplier = new(); // As a special case, we don't issue the first data load request until we've collected the initial set of columns // This is so we can apply default sort order (or any future per-column options) before loading data @@ -181,12 +199,27 @@ public QuickGrid() columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected); } + /// + protected override void OnInitialized() + { + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + NavigationManager.LocationChanged += OnLocationChanged; + } + } + /// protected override Task OnParametersSetAsync() { // The associated pagination state may have been added/removed/replaced _currentPageItemsChanged.SubscribeOrMove(Pagination?.CurrentPageItemsChanged); + if (Pagination is { } pagination) + { + pagination.QueryName = PageQueryParameterName; + } + if (Items is not null && ItemsProvider is not null) { throw new InvalidOperationException($"{nameof(QuickGrid)} requires one of {nameof(Items)} or {nameof(ItemsProvider)}, but both were specified."); @@ -243,6 +276,16 @@ internal void AddColumn(ColumnBase column, SortDirection? initialSort { _sortByColumn = column; _sortByAscending = initialSortDirection.Value != SortDirection.Descending; + _defaultSortColumn = column; + _defaultSortAscending = _sortByAscending; + } + + if (_cachedSortFromQuery is { } sortFromQuery + && sortFromQuery.ColumnTitle == column.Title) + { + _sortByColumn = column; + _sortByAscending = sortFromQuery.Ascending; + _cachedSortFromQuery = null; } } } @@ -251,6 +294,7 @@ private void StartCollectingColumns() { _columns.Clear(); _collectingColumns = true; + _cachedSortFromQuery ??= ReadSortFromQueryString(); } private void FinishCollectingColumns() @@ -276,10 +320,75 @@ public Task SortByColumnAsync(ColumnBase column, SortDirection direct _sortByColumn = column; - StateHasChanged(); // We want to see the updated sort order in the header, even before the data query is completed + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + var newUri = GetSortQueryStringUrl(_sortByColumn, _sortByAscending); + NavigationManager.NavigateTo(newUri); + } + return RefreshDataAsync(); } + internal string GetSortUrl(ColumnBase column) + { + var ascending = _sortByColumn != column || !_sortByAscending; + return GetSortQueryStringUrl(column, ascending); + } + + private string GetSortQueryStringUrl(ColumnBase? column, bool ascending) + { + return NavigationManager.GetUriWithQueryParameters(new Dictionary + { + [SortQueryParameterNameBy] = column?.Title, + [SortQueryParameterNameOrder] = ascending ? "asc" : "desc", + }); + } + + private (string ColumnTitle, bool Ascending)? ReadSortFromQueryString() + { + var column = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), SortQueryParameterNameBy) as string; + var order = _queryParameterValueSupplier.GetQueryParameterValue(typeof(string), SortQueryParameterNameOrder) as string; + if (column is not null && order is not null) + { + return order switch + { + var d when d.Equals("asc", StringComparison.OrdinalIgnoreCase) => (column, true), + var d when d.Equals("desc", StringComparison.OrdinalIgnoreCase) => (column, false), + _ => null, + }; + } + return null; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + _queryParameterValueSupplier.ReadParametersFromQuery(QueryParameterValueSupplier.GetQueryString(NavigationManager.Uri)); + var sortFromQuery = ReadSortFromQueryString(); + + if (sortFromQuery is { } sort + && _columns.FirstOrDefault(c => c.Title == sort.ColumnTitle) is { } column + && (column != _sortByColumn || sort.Ascending != _sortByAscending)) + { + await InvokeAsync(async () => + { + _sortByColumn = column; + _sortByAscending = sort.Ascending; + await RefreshDataCoreAsync(); + }); + } + else if (sortFromQuery is null + && (_sortByColumn != _defaultSortColumn || _sortByAscending != _defaultSortAscending)) + { + await InvokeAsync(async () => + { + _sortByColumn = _defaultSortColumn; + _sortByAscending = _defaultSortAscending; + await RefreshDataCoreAsync(); + }); + } + await InvokeAsync(StateHasChanged); + } + /// /// Displays the UI for the specified column, closing any other column /// options UI that was previously displayed. @@ -457,6 +566,10 @@ private string GridClass() /// public async ValueTask DisposeAsync() { + if (QuickGridFeatureFlags.EnableUrlBasedQuickGridNavigationAndSorting) + { + NavigationManager.LocationChanged -= OnLocationChanged; + } _wasDisposed = true; _currentPageItemsChanged.Dispose(); diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs new file mode 100644 index 000000000000..1c91fe421ac1 --- /dev/null +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGridFeatureFlags.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.QuickGrid; + +internal static class QuickGridFeatureFlags +{ + internal static bool EnableUrlBasedQuickGridNavigationAndSorting => + !AppContext.TryGetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", out var isEnabled) || isEnabled; +} diff --git a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs index ffa4e6d3b409..fb335d294c46 100644 --- a/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs +++ b/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/GridRaceConditionTest.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Xunit.Sdk; +using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components.QuickGrid.Tests; @@ -21,6 +22,7 @@ public async Task CanCorrectlyDisposeAsync() var testJsRuntime = new TestJsRuntime(moduleLoadCompletion, moduleImportStarted); var serviceProvider = new ServiceCollection() .AddSingleton(testJsRuntime) + .AddSingleton() .BuildServiceProvider(); var renderer = new TestRenderer(serviceProvider); @@ -57,6 +59,7 @@ public async Task FailingQuickGridCallsInitAfterDisposal() var testJsRuntime = new TestJsRuntime(moduleLoadCompletion, moduleImportStarted); var serviceProvider = new ServiceCollection() .AddSingleton(testJsRuntime) + .AddSingleton() .BuildServiceProvider(); var renderer = new TestRenderer(serviceProvider); @@ -177,6 +180,15 @@ public ValueTask InvokeAsync(string identifier, CancellationToke InvokeAsync(identifier, args); } +internal class TestNavigationManager : NavigationManager, IHostEnvironmentNavigationManager +{ + public TestNavigationManager() => Initialize("https://localhost/", "https://localhost/"); + + void IHostEnvironmentNavigationManager.Initialize(string baseUri, string uri) => Initialize(baseUri, uri); + + protected override void NavigateToCore(string uri, bool forceLoad) => Uri = uri; +} + internal class TestJSObjectReference(TestJsRuntime jsRuntime) : IJSObjectReference { private readonly TestJsRuntime _jsRuntime = jsRuntime; diff --git a/src/Components/Components/src/Routing/QueryParameterNameComparer.cs b/src/Components/Shared/src/QueryParameterNameComparer.cs similarity index 100% rename from src/Components/Components/src/Routing/QueryParameterNameComparer.cs rename to src/Components/Shared/src/QueryParameterNameComparer.cs diff --git a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs b/src/Components/Shared/src/QueryParameterValueSupplier.cs similarity index 81% rename from src/Components/Components/src/Routing/QueryParameterValueSupplier.cs rename to src/Components/Shared/src/QueryParameterValueSupplier.cs index d5e75c9003f6..6367c0ee6a2c 100644 --- a/src/Components/Components/src/Routing/QueryParameterValueSupplier.cs +++ b/src/Components/Shared/src/QueryParameterValueSupplier.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Internal; @@ -51,4 +52,18 @@ public void ReadParametersFromQuery(ReadOnlyMemory query) return default; } + + public static ReadOnlyMemory GetQueryString(string? url) + { + var queryStartPos = url?.IndexOf('?') ?? -1; + + if (queryStartPos < 0) + { + return default; + } + + Debug.Assert(url is not null); + var queryEndPos = url.IndexOf('#', queryStartPos); + return url.AsMemory(queryStartPos..(queryEndPos < 0 ? url.Length : queryEndPos)); + } } diff --git a/src/Components/Components/src/Routing/StringSegmentAccumulator.cs b/src/Components/Shared/src/StringSegmentAccumulator.cs similarity index 100% rename from src/Components/Components/src/Routing/StringSegmentAccumulator.cs rename to src/Components/Shared/src/StringSegmentAccumulator.cs diff --git a/src/Components/Components/src/Routing/UrlValueConstraint.cs b/src/Components/Shared/src/UrlValueConstraint.cs similarity index 100% rename from src/Components/Components/src/Routing/UrlValueConstraint.cs rename to src/Components/Shared/src/UrlValueConstraint.cs diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs new file mode 100644 index 000000000000..ac0e0673ad3d --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveCompatTest.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +/// +/// Tests QuickGrid with URL-driven navigation disabled via the +/// Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting AppContext switch. +/// Mirrors but asserts button-based sorting and pagination +/// instead of anchor-based navigation with URL query parameters. +/// +public class QuickGridInteractiveCompatTest : ServerTestBase>> +{ + public QuickGridInteractiveCompatTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + _serverFixture.AdditionalArguments.Add("--DisableUrlDrivenNavigation=true"); + base.InitializeAsyncCore(); + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // URL should not change in compat mode + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + + // URL should still not change + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void CanColumnSortByString() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > button.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > button.col-title")); + + Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void CanColumnSortByDateOnly() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var initialUrl = Browser.Url; + + // Click button to sort ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > button.col-title")); + Browser.Equal("11205", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal(initialUrl, Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > button.col-title")); + + Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Equal(initialUrl, Browser.Url); + } + + [Fact] + public void PaginatorCorrectItemsPerPage() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + + Browser.Click(By.CssSelector(".first-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal(10, () => Browser.FindElement(By.CssSelector("#grid .quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); + + // URL should not change in compat mode + Assert.DoesNotContain("page=", Browser.Url); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + Browser.Exists(By.CssSelector("#grid2 > table")); + + var initialUrl = Browser.Url; + + // Sort first grid by PersonId ascending (button, not anchor) + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > button.col-title")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by FirstName ascending (button, not anchor) + Browser.Click(By.CssSelector("#grid2 > table thead > tr > th:nth-child(2) > div > button.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid2 > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // First grid should remain sorted + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // URL should not change in compat mode + Assert.Equal(initialUrl, Browser.Url); + } +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs new file mode 100644 index 000000000000..d9f31d046afd --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridInteractiveTest.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +public class QuickGridInteractiveTest : ServerTestBase>> +{ + public QuickGridInteractiveTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + + //Compare first row to expected result + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Matti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Karttunen", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-06-04", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=desc", Browser.Url); + } + + [Fact] + public void CanColumnSortByString() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=FirstName", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(2) > div > a.col-title")); + + //Compare first row to expected result + Browser.Equal("12379", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Zbyszek", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Piestrzeniewicz", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("1981-04-02", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=FirstName", Browser.Url); + Assert.Contains("order=desc", Browser.Url); + } + + [Fact] + public void CanColumnSortByDateOnly() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + // Click to sort ascending, wait for result + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); + Browser.Equal("11205", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=BirthDate", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + + // Click again to sort descending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(4) > div > a")); + + //Compare first row to expected result + Browser.Equal("12364", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Browser.Equal("Paolo", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Browser.Equal("Accorti", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(3)")).Text); + Browser.Equal("2018-05-18", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(4)")).Text); + Assert.Contains("sort=BirthDate", Browser.Url); + Assert.Contains("order=desc", Browser.Url); + } + + [Fact] + public void PaginatorCorrectItemsPerPage() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + + Browser.Click(By.CssSelector(".first-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal(10, () => Browser.FindElement(By.CssSelector("#grid .quickgrid")).FindElements(By.CssSelector("tbody > tr")).Count); + } + + [Fact] + public void DualPaginatorsNavigatesAndBothUpdate() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid2 > table")); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.Click(By.CssSelector(".top-paginator .go-next")); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Assert.Contains("people_page=2", Browser.Url); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid-interactive"); + Browser.Exists(By.CssSelector("#grid > table")); + Browser.Exists(By.CssSelector("#grid2 > table")); + + // Sort first grid by PersonId ascending + Browser.Click(By.CssSelector("#grid > table thead > tr > th:nth-child(1) > div > a")); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by FirstName ascending (uses QueryParameterNamePrefix="people" prefix) + Browser.Click(By.CssSelector("#grid2 > table thead > tr > th:nth-child(2) > div > a.col-title")); + Browser.Equal("12372", () => Browser.FindElement(By.CssSelector("#grid2 > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // First grid should remain sorted + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid > table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Verify URL contains both unprefixed and prefixed sort parameters + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("people_sort=FirstName", Browser.Url); + Assert.Contains("people_order=asc", Browser.Url); + } +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs new file mode 100644 index 000000000000..b4998f47ae2c --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.E2ETesting; +using Xunit.Abstractions; +using OpenQA.Selenium; +using TestServer; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests; + +public class QuickGridNoInteractivityTest : ServerTestBase>> +{ + public QuickGridNoInteractivityTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + + [Fact] + public void PaginatorDisplaysCorrectItemCount() + { + Navigate($"{ServerPathBase}/quickgrid"); + + var paginator = Browser.FindElement(By.CssSelector(".first-paginator .paginator")); + + var paginatorCount = paginator.FindElement(By.CssSelector("div > strong")).Text; + var currentPageNumber = paginator.FindElement(By.CssSelector("nav > div > strong:nth-child(1)")).Text; + var totalPageNumber = paginator.FindElement(By.CssSelector("nav > div > strong:nth-child(2)")).Text; + + Assert.Equal("43", paginatorCount); + Assert.Equal("1", currentPageNumber); + Assert.Equal("5", totalPageNumber); + } + + [Fact] + public void PaginatorGoNextShowsNextPage() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector(".first-paginator .go-next")).Click(); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Assert.Contains("page=2", Browser.Url); + + var grid = Browser.FindElement(By.ClassName("quickgrid")); + var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; + Assert.Equal(10, rowCount); + } + + [Fact] + public void PaginatorLinkLoadsCorrectPage() + { + Navigate($"{ServerPathBase}/quickgrid?page=3"); + + Browser.Equal("3", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorGoPreviousFromSecondPage() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector(".first-paginator .go-next")).Click(); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).Click(); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorNavigationLinksDisabledCorrectly() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("aria-disabled")); + + Browser.FindElement(By.CssSelector(".first-paginator .go-last")).Click(); + Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("aria-disabled")); + Assert.Equal("false", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("aria-disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("aria-disabled")); + } + + [Fact] + public void MultiplePaginatorsWorkIndependently() + { + Navigate($"{ServerPathBase}/quickgrid"); + Browser.FindElement(By.CssSelector(".second-paginator .go-next")).Click(); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".second-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + var grid1 = Browser.FindElement(By.CssSelector("#grid .quickgrid")); + Assert.Equal(10, grid1.FindElements(By.CssSelector("tbody > tr")).Count); + var grid2 = Browser.FindElement(By.CssSelector("#grid2 .quickgrid")); + Assert.Equal(5, grid2.FindElements(By.CssSelector("tbody > tr")).Count); + } + + [Fact] + public void PaginatorOutOfRangePageClampsToLastPage() + { + Navigate($"{ServerPathBase}/quickgrid?page=999"); + + Browser.Equal("5", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void PaginatorInvalidPageValueDefaultsToFirstPage() + { + Navigate($"{ServerPathBase}/quickgrid?page=abc"); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } + + [Fact] + public void CanColumnSortByInt() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=desc", Browser.Url); + + var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); + Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); + Assert.Equal("Karttunen", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); + Assert.Equal("1981-06-04", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); + } + + [Fact] + public void SortLinkLoadsCorrectOrder() + { + Navigate($"{ServerPathBase}/quickgrid?sort=PersonId&order=desc"); + + Browser.Equal("12381", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Equal("Matti", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + } + + [Fact] + public void SortNavigationWithPagination() + { + Navigate($"{ServerPathBase}/quickgrid?page=2"); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".first-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); + Browser.True(() => Browser.Url.Contains("sort=PersonId")); + Browser.True(() => Browser.Url.Contains("order=asc")); + var firstRow = Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1)")); + Assert.NotNull(firstRow); + } + + [Fact] + public void TwoGridsSortIndependently() + { + Navigate($"{ServerPathBase}/quickgrid"); + + // Sort first grid by PersonId ascending + Browser.FindElement(By.CssSelector("#grid table thead > tr > th:nth-child(1) a.col-title")).Click(); + Browser.Equal("10895", () => Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + // Sort second grid by Name ascending + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) a.col-title")).Click(); + Browser.Equal("Bangalore", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=Name", Browser.Url); + Assert.Contains("QuickGrid2_order=asc", Browser.Url); + + // Sort second grid by Name descending + Browser.FindElement(By.CssSelector("#grid2 table thead > tr > th:nth-child(2) a.col-title")).Click(); + Browser.Equal("Tokyo", () => Browser.FindElement(By.CssSelector("#grid2 table tbody > tr:nth-child(1) > td:nth-child(2)")).Text); + + // First grid should remain unchanged + Assert.Equal("10895", Browser.FindElement(By.CssSelector("#grid table tbody > tr:nth-child(1) > td:nth-child(1)")).Text); + Assert.Contains("sort=PersonId", Browser.Url); + Assert.Contains("order=asc", Browser.Url); + Assert.Contains("QuickGrid2_sort=Name", Browser.Url); + Assert.Contains("QuickGrid2_order=desc", Browser.Url); + } + + [Fact] + public void DualPaginatorsNavigatesAndBothUpdate() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".third-top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("1", () => Browser.FindElement(By.CssSelector(".third-bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + + Browser.FindElement(By.CssSelector(".third-top-paginator .go-next")).Click(); + + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".third-top-paginator .paginator nav > div > strong:nth-child(1)")).Text); + Browser.Equal("2", () => Browser.FindElement(By.CssSelector(".third-bottom-paginator .paginator nav > div > strong:nth-child(1)")).Text); + } +} diff --git a/src/Components/test/E2ETest/Tests/QuickGridTest.cs b/src/Components/test/E2ETest/Tests/QuickGridTest.cs index fcafb8c4ec1b..6eee73bb5127 100644 --- a/src/Components/test/E2ETest/Tests/QuickGridTest.cs +++ b/src/Components/test/E2ETest/Tests/QuickGridTest.cs @@ -31,76 +31,6 @@ protected override void InitializeAsyncCore() app = Browser.MountTestComponent(); } - [Fact] - public void CanColumnSortByInt() - { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var idColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(1) > div > button")); - - // Click twice to sort by descending - idColumnSortButton.Click(); - idColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); - - //Compare first row to expected result - Assert.Equal("12381", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Matti", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Karttunen", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("1981-06-04", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); - } - - [Fact] - public void CanColumnSortByString() - { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var firstNameColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(2) > div > button.col-title")); - - // Click twice to sort by descending - firstNameColumnSortButton.Click(); - firstNameColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); - - //Compare first row to expected result - Assert.Equal("12379", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Zbyszek", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Piestrzeniewicz", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("1981-04-02", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); - } - - [Fact] - public void CanColumnSortByDateOnly() - { - var grid = app.FindElement(By.CssSelector("#grid > table")); - var birthDateColumnSortButton = grid.FindElement(By.CssSelector("thead > tr > th:nth-child(4) > div > button")); - - // Click twice to sort by descending - birthDateColumnSortButton.Click(); - birthDateColumnSortButton.Click(); - - var firstRow = grid.FindElement(By.CssSelector("tbody > tr:nth-child(1)")); - - //Compare first row to expected result - Assert.Equal("12364", firstRow.FindElement(By.CssSelector("td:nth-child(1)")).Text); - Assert.Equal("Paolo", firstRow.FindElement(By.CssSelector("td:nth-child(2)")).Text); - Assert.Equal("Accorti", firstRow.FindElement(By.CssSelector("td:nth-child(3)")).Text); - Assert.Equal("2018-05-18", firstRow.FindElement(By.CssSelector("td:nth-child(4)")).Text); - } - - [Fact] - public void PaginatorCorrectItemsPerPage() - { - var grid = app.FindElement(By.ClassName("quickgrid")); - var rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; - Assert.Equal(10, rowCount); - - app.FindElement(By.ClassName("go-next")).Click(); - - rowCount = grid.FindElements(By.CssSelector("tbody > tr")).Count; - Assert.Equal(10, rowCount); - } - [Fact] public void PaginatorDisplaysCorrectItemCount() { @@ -245,7 +175,7 @@ public void FilterUsingRefreshDataDoesNotCauseExtraRefresh() Browser.Equal("2", () => app.FindElement(By.Id("items-provider-calls")).Text); } - + [Fact] public void OnRowClickTriggersCallback() { diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 093215e0b2c6..a3f6de839849 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -101,6 +101,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor new file mode 100644 index 000000000000..74ef09c5726d --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridDualPaginatorComponent.razor @@ -0,0 +1,70 @@ +@using Microsoft.AspNetCore.Components.QuickGrid + +

QuickGrid Dual Paginator Component

+ +
+ +
+
+ + + + + + +
+
+ +
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + PaginationState pagination = new PaginationState { ItemsPerPage = 5 }; + + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +} diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj index a147aeeb1a07..5afa2d99fdff 100644 --- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj @@ -85,6 +85,7 @@ + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 3361507131f3..220260c9066c 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -31,6 +31,15 @@ public RazorComponentEndpointsStartup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + if (Configuration.GetValue("DisableUrlDrivenNavigation")) + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", false); + } + else + { + AppContext.SetSwitch("Microsoft.AspNetCore.Components.QuickGrid.EnableUrlBasedQuickGridNavigationAndSorting", true); + } + services.AddValidation(); services.AddRazorComponents(options => diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor new file mode 100644 index 000000000000..392401ecf9dc --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -0,0 +1,149 @@ +@page "/quickgrid" +@using Microsoft.AspNetCore.Components.QuickGrid +@using Microsoft.AspNetCore.Components.Forms + +
+ + + + + + +
+
+ +
+ +
+ + + + + +
+
+ +
+ + +
+ +
+
+
+ + + + + + +
+ +
+ +
+
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + record City(int CityId, string Name, string Country); + + PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; + PaginationState pagination3 = new PaginationState { ItemsPerPage = 5 }; + + GridItemsProvider cityProvider; + + static readonly City[] allCities = new[] + { + new City(1, "Tokyo", "Japan"), + new City(2, "Delhi", "India"), + new City(3, "Shanghai", "China"), + new City(4, "São Paulo", "Brazil"), + new City(5, "Mexico City", "Mexico"), + new City(6, "Cairo", "Egypt"), + new City(7, "Mumbai", "India"), + new City(8, "Beijing", "China"), + new City(9, "Dhaka", "Bangladesh"), + new City(10, "Osaka", "Japan"), + new City(11, "New York", "USA"), + new City(12, "Karachi", "Pakistan"), + new City(13, "Buenos Aires", "Argentina"), + new City(14, "Istanbul", "Turkey"), + new City(15, "Chongqing", "China"), + new City(16, "Lagos", "Nigeria"), + new City(17, "Kolkata", "India"), + new City(18, "Manila", "Philippines"), + new City(19, "Guangzhou", "China"), + new City(20, "Tianjin", "China"), + new City(21, "Lahore", "Pakistan"), + new City(22, "Bangalore", "India"), + new City(23, "Paris", "France"), + new City(24, "Bogotá", "Colombia"), + new City(25, "Jakarta", "Indonesia"), + new City(26, "Lima", "Peru"), + new City(27, "Bangkok", "Thailand"), + new City(28, "Hyderabad", "India"), + new City(29, "Seoul", "South Korea"), + new City(30, "London", "United Kingdom"), + }; + + protected override void OnInitialized() + { + cityProvider = req => + { + var query = allCities.AsQueryable(); + query = req.ApplySorting(query); + var totalCount = query.Count(); + var items = query.Skip(req.StartIndex).Take(req.Count ?? totalCount).ToList(); + return ValueTask.FromResult(GridItemsProviderResult.From(items, totalCount)); + }; + } + + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor new file mode 100644 index 000000000000..3d8614f27f40 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridInteractive.razor @@ -0,0 +1,138 @@ +@page "/quickgrid-interactive" +@rendermode RenderMode.InteractiveServer +@using Microsoft.AspNetCore.Components.QuickGrid + +

Sample QuickGrid Component

+ +
+ + + + + + + + + + + + +
+
+ +
+ +
+ +
+
+ + + + + + +
+
+ +
+ +
+ @if (clickedPerson is not null) + { +

PersonId: @clickedPerson.PersonId

+

Name: @clickedPerson.FirstName @clickedPerson.LastName

+

Click count: @clickCount

+ } + else + { +

No row clicked yet

+ } +
+ +@code { + record Person(int PersonId, string FirstName, string LastName, DateOnly BirthDate); + PaginationState pagination = new PaginationState { ItemsPerPage = 10 }; + PaginationState pagination2 = new PaginationState { ItemsPerPage = 5 }; + string firstNameFilter; + QuickGrid quickGridRef; + Person clickedPerson; + int clickCount; + + void HandleRowClick(Person person) + { + clickedPerson = person; + clickCount++; + } + + int ComputeAge(DateOnly birthDate) + => DateTime.Now.Year - birthDate.Year - (birthDate.DayOfYear < DateTime.Now.DayOfYear ? 0 : 1); + + string HighlightJulie(Person person) => person.FirstName == "Julie" ? "highlight" : null; + + IQueryable FilteredPeople + { + get + { + var result = people; + + if (!string.IsNullOrEmpty(firstNameFilter)) + { + result = result.Where(p => p.FirstName.Contains(firstNameFilter, StringComparison.CurrentCultureIgnoreCase)); + } + + return result; + } + } + + // Changes to this list affect the E2E tests. + // If you change this list, you must also update the E2E tests. + IQueryable people = new[] + { + new Person(11203, "Julie", "Smith", new DateOnly(1958, 10, 10)), + new Person(11205, "Nur", "Sari", new DateOnly(1922, 4, 27)), + new Person(11898, "Jose", "Hernandez", new DateOnly(2011, 5, 3)), + new Person(10895, "Jean", "Martin", new DateOnly(1985, 3, 16)), + new Person(10944, "António", "Langa", new DateOnly(1991, 12, 1)), + new Person(12130, "Kenji", "Sato", new DateOnly(2004, 1, 9)), + new Person(12238, "Sven", "Ottlieb", new DateOnly(1973, 11, 15)), + new Person(12345, "Liu", "Wang", new DateOnly(1999, 6, 30)), + new Person(12346, "Giovanni", "Rovelli", new DateOnly(2000, 7, 31)), + new Person(12347, "Eduardo", "Martins", new DateOnly(2001, 8, 1)), + new Person(12348, "Martín", "Sommer", new DateOnly(2002, 9, 2)), + new Person(12349, "Victoria", "Ashworth", new DateOnly(2003, 10, 3)), + new Person(12350, "Hannah", "Moos", new DateOnly(2004, 11, 4)), + new Person(12351, "Palle", "Ibsen", new DateOnly(2005, 12, 5)), + new Person(12352, "Lúcia", "Carvalho", new DateOnly(2006, 1, 6)), + new Person(12353, "Horst", "Kloss", new DateOnly(2007, 2, 7)), + new Person(12354, "Sergio", "Gutiérrez", new DateOnly(2008, 3, 8)), + new Person(12355, "Janine", "Labrune", new DateOnly(2009, 4, 9)), + new Person(12356, "Ann", "Devon", new DateOnly(2010, 5, 10)), + new Person(12357, "Roland", "Mendel", new DateOnly(2011, 6, 11)), + new Person(12358, "Aria", "Cruz", new DateOnly(2012, 7, 12)), + new Person(12359, "Diego", "Roel", new DateOnly(2001, 8, 13)), + new Person(12360, "Martine", "Rancé", new DateOnly(2005, 9, 14)), + new Person(12361, "Maria", "Larsson", new DateOnly(1998, 10, 15)), + new Person(12362, "Peter", "Lewis", new DateOnly(2016, 11, 16)), + new Person(12363, "Carine", "Schmitt", new DateOnly(2017, 12, 13)), + new Person(12364, "Paolo", "Accorti", new DateOnly(2018, 5, 18)), + new Person(12365, "Lino", "Rodriguez", new DateOnly(1980, 2, 19)), + new Person(12367, "Bernardo", "Batista", new DateOnly(1979, 4, 21)), + new Person(12368, "Lúcia", "Carvalho", new DateOnly(1976, 5, 22)), + new Person(12369, "Guillermo", "Fernández", new DateOnly(1983, 6, 23)), + new Person(12370, "Georg", "Pipps", new DateOnly(1982, 7, 24)), + new Person(12371, "Mario", "Pontes", new DateOnly(1981, 8, 25)), + new Person(12372, "Anabela", "Camino", new DateOnly(1980, 9, 26)), + new Person(12380, "Karl", "Jablonski", new DateOnly(1981, 5, 3)), + new Person(12381, "Matti", "Karttunen", new DateOnly(1981, 6, 4)), + new Person(12373, "Helvetius", "Nagy", new DateOnly(1980, 10, 27)), + new Person(12374, "Rita", "Müller", new DateOnly(1980, 11, 28)), + new Person(12375, "Pirkko", "Koskitalo", new DateOnly(1980, 12, 29)), + new Person(12376, "Paula", "Parente", new DateOnly(1981, 1, 30)), + new Person(12377, "Karl", "Jablonski", new DateOnly(1981, 2, 10)), + new Person(12378, "Matti", "Karttunen", new DateOnly(1981, 3, 1)), + new Person(12379, "Zbyszek", "Piestrzeniewicz", new DateOnly(1981, 4, 2)), + }.AsQueryable(); +}