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..5050a8001cb7 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 @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -17,6 +17,7 @@ <_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..7a4a94bf041e 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 @@ -51,7 +51,14 @@ public override int GetHashCode() /// A representing the completion of the operation. public Task SetCurrentPageIndexAsync(int pageIndex) { - CurrentPageIndex = pageIndex; + if (pageIndex < 0 || (LastPageIndex.HasValue && pageIndex > LastPageIndex.Value)) + { + CurrentPageIndex = 0; + } + else + { + CurrentPageIndex = pageIndex; + } return CurrentPageItemsChanged.InvokeCallbacksAsync(this); } 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..73ef33bc782c 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,5 @@ -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web @namespace Microsoft.AspNetCore.Components.QuickGrid
@@ -15,14 +16,40 @@ }
} 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..9a91f9acbc0f 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,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.QuickGrid.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Internal; namespace Microsoft.AspNetCore.Components.QuickGrid; @@ -11,12 +13,28 @@ namespace Microsoft.AspNetCore.Components.QuickGrid; public partial class Paginator : IDisposable { private readonly EventCallbackSubscriber _totalItemCountChanged; + private bool _hasReadQueryString; + private bool _isNavigating; + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + private string FormNameFirst => $"Paginator_{QueryName}_GoFirst"; + private string FormNamePrevious => $"Paginator_{QueryName}_GoPrevious"; + private string FormNameNext => $"Paginator_{QueryName}_GoNext"; + private string FormNameLast => $"Paginator_{QueryName}_GoLast"; /// /// Specifies the associated . This parameter is required. /// [Parameter, EditorRequired] public PaginationState State { get; set; } = default!; + /// + /// Name of the query string parameter used to persist the current page index in the URL. + /// Defaults to "page". When set, the paginator reads the current page from this query parameter on + /// initialization and updates the URL when navigating to a different page. + /// + [Parameter] public string QueryName { get; set; } = "page"; + /// /// Optionally supplies a template for rendering the page count summary. /// @@ -39,14 +57,84 @@ public Paginator() private bool CanGoBack => State.CurrentPageIndex > 0; private bool CanGoForwards => State.CurrentPageIndex < State.LastPageIndex; - private Task GoToPageAsync(int pageIndex) - => State.SetCurrentPageIndexAsync(pageIndex); + private async Task GoToPageAsync(int pageIndex) + { + int? pageValue = pageIndex == 0 ? null : pageIndex + 1; + var newUri = NavigationManager.GetUriWithQueryParameter(QueryName, pageValue); + await State.SetCurrentPageIndexAsync(pageIndex); + _isNavigating = true; + NavigationManager.NavigateTo(newUri, replace: true); + } /// - protected override void OnParametersSet() - => _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + /// + protected override Task OnParametersSetAsync() + { + _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable); + if (!_hasReadQueryString) + { + _hasReadQueryString = true; + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + return State.SetCurrentPageIndexAsync(pageFromQuery); + } + } + return Task.CompletedTask; + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + if (_isNavigating) + { + _isNavigating = false; + return; + } + var pageFromQuery = ReadPageIndexFromQueryString() ?? 0; + if (pageFromQuery != State.CurrentPageIndex) + { + await InvokeAsync(async () => + { + await State.SetCurrentPageIndexAsync(pageFromQuery); + StateHasChanged(); + }); + } + } + + private int? ReadPageIndexFromQueryString() + { + var uri = NavigationManager.Uri; + var queryStart = uri.IndexOf('?'); + if (queryStart < 0) + { + return null; + } + + var queryEnd = uri.IndexOf('#', queryStart); + var query = uri.AsMemory(queryStart..((queryEnd < 0) ? uri.Length : queryEnd)); + var enumerable = new QueryStringEnumerable(query); + + foreach (var pair in enumerable) + { + if (pair.DecodeName().Span.Equals(QueryName, StringComparison.OrdinalIgnoreCase) + && int.TryParse(pair.DecodeValue().Span, out var page) + && page > 0) + { + return page - 1; + } + } + return null; + } /// public void Dispose() - => _totalItemCountChanged.Dispose(); + { + NavigationManager.LocationChanged -= OnLocationChanged; + _totalItemCountChanged.Dispose(); + } } 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..52adfbc4ca1d 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,8 @@ #nullable enable +Microsoft.AspNetCore.Components.QuickGrid.Paginator.QueryName.get -> string! +Microsoft.AspNetCore.Components.QuickGrid.Paginator.QueryName.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! +*REMOVED*override Microsoft.AspNetCore.Components.QuickGrid.Paginator.OnParametersSet() -> void diff --git a/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs new file mode 100644 index 000000000000..3ec10f0690b9 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs @@ -0,0 +1,126 @@ +// 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 PaginatorNavigationButtonsDisabledCorrectly() + { + Navigate($"{ServerPathBase}/quickgrid"); + + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("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.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-first")).GetDomAttribute("disabled")); + Assert.Null(Browser.FindElement(By.CssSelector(".first-paginator .go-previous")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-next")).GetDomAttribute("disabled")); + Assert.Equal("true", Browser.FindElement(By.CssSelector(".first-paginator .go-last")).GetDomAttribute("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); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj index bbd94ca13df1..b4f37687efa9 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/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor new file mode 100644 index 000000000000..cb950625fcf2 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/QuickGrid/QuickGridComponent.razor @@ -0,0 +1,101 @@ +@page "/quickgrid" +@using Microsoft.AspNetCore.Components.QuickGrid + +

QuickGrid Test

+ +
+ + + + + + +
+
+ +
+ +

Second QuickGrid

+ +
+ + + + + +
+
+ +
+ +@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 }; + + IQueryable cities = 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"), + }.AsQueryable(); + + 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(); +}