Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
Expand All @@ -17,6 +17,7 @@
<_CurrentProjectDiscoveredScopedCssFiles Include="@(ThemeCssFiles)" RelativePath="%(Identity)" BasePath="_content/$(AssemblyName)" />

<Compile Include="$(ComponentsSharedSourceRoot)src\AttributeUtilities.cs" LinkBase="Infrastructure" />
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Infrastructure" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ public override int GetHashCode()
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
public Task SetCurrentPageIndexAsync(int pageIndex)
{
CurrentPageIndex = pageIndex;
if (pageIndex < 0 || (LastPageIndex.HasValue && pageIndex > LastPageIndex.Value))
{
CurrentPageIndex = 0;
}
else
{
CurrentPageIndex = pageIndex;
}
return CurrentPageItemsChanged.InvokeCallbacksAsync(this);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web
@namespace Microsoft.AspNetCore.Components.QuickGrid

<div class="paginator">
Expand All @@ -15,14 +16,40 @@
}
</div>
<nav role="navigation">
<button class="go-first" type="button" @onclick="GoFirstAsync" disabled="@(!CanGoBack)" title="Go to first page" aria-label="Go to first page"></button>
<button class="go-previous" type="button" @onclick="GoPreviousAsync" disabled="@(!CanGoBack)" title="Go to previous page" aria-label="Go to previous page"></button>
<div class="pagination-text">
Page <strong>@(State.CurrentPageIndex + 1)</strong>
of <strong>@(State.LastPageIndex + 1)</strong>
</div>
<button class="go-next" type="button" @onclick="GoNextAsync" disabled="@(!CanGoForwards)" title="Go to next page" aria-label="Go to next page"></button>
<button class="go-last" type="button" @onclick="GoLastAsync" disabled="@(!CanGoForwards)" title="Go to last page" aria-label="Go to last page"></button>
@if (RendererInfo.IsInteractive)
{
<button class="go-first" type="button" @onclick="GoFirstAsync" disabled="@(!CanGoBack)" title="Go to first page" aria-label="Go to first page"></button>
<button class="go-previous" type="button" @onclick="GoPreviousAsync" disabled="@(!CanGoBack)" title="Go to previous page" aria-label="Go to previous page"></button>
<div class="pagination-text">
Page <strong>@(State.CurrentPageIndex + 1)</strong>
of <strong>@(State.LastPageIndex + 1)</strong>
</div>
<button class="go-next" type="button" @onclick="GoNextAsync" disabled="@(!CanGoForwards)" title="Go to next page" aria-label="Go to next page"></button>
<button class="go-last" type="button" @onclick="GoLastAsync" disabled="@(!CanGoForwards)" title="Go to last page" aria-label="Go to last page"></button>
}
else
{
<form data-enhance method="post" @formname="@FormNameFirst" @onsubmit="GoFirstAsync">
<AntiforgeryToken />
<button class="go-first" type="submit" disabled="@(!CanGoBack)" title="Go to first page" aria-label="Go to first page"></button>
</form>
<form data-enhance method="post" @formname="@FormNamePrevious" @onsubmit="GoPreviousAsync">
<AntiforgeryToken />
<button class="go-previous" type="submit" disabled="@(!CanGoBack)" title="Go to previous page" aria-label="Go to previous page"></button>
</form>
<div class="pagination-text">
Page <strong>@(State.CurrentPageIndex + 1)</strong>
of <strong>@(State.LastPageIndex + 1)</strong>
</div>
<form data-enhance method="post" @formname="@FormNameNext" @onsubmit="GoNextAsync">
<AntiforgeryToken />
<button class="go-next" type="submit" disabled="@(!CanGoForwards)" title="Go to next page" aria-label="Go to next page"></button>
</form>
<form data-enhance method="post" @formname="@FormNameLast" @onsubmit="GoLastAsync">
<AntiforgeryToken />
<button class="go-last" type="submit" disabled="@(!CanGoForwards)" title="Go to last page" aria-label="Go to last page"></button>
</form>
}
</nav>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -11,12 +13,28 @@ namespace Microsoft.AspNetCore.Components.QuickGrid;
public partial class Paginator : IDisposable
{
private readonly EventCallbackSubscriber<PaginationState> _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";

/// <summary>
/// Specifies the associated <see cref="PaginationState"/>. This parameter is required.
/// </summary>
[Parameter, EditorRequired] public PaginationState State { get; set; } = default!;

/// <summary>
/// Name of the query string parameter used to persist the current page index in the URL.
/// Defaults to <c>"page"</c>. When set, the paginator reads the current page from this query parameter on
/// initialization and updates the URL when navigating to a different page.
/// </summary>
[Parameter] public string QueryName { get; set; } = "page";

/// <summary>
/// Optionally supplies a template for rendering the page count summary.
/// </summary>
Expand All @@ -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);
}

/// <inheritdoc />
protected override void OnParametersSet()
=> _totalItemCountChanged.SubscribeOrMove(State.TotalItemCountChangedSubscribable);
protected override void OnInitialized()
{
NavigationManager.LocationChanged += OnLocationChanged;
}

/// <inheritdoc />
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;
}

/// <inheritdoc />
public void Dispose()
=> _totalItemCountChanged.Dispose();
{
NavigationManager.LocationChanged -= OnLocationChanged;
_totalItemCountChanged.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -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<TGridItem>.OnRowClick.get -> Microsoft.AspNetCore.Components.EventCallback<TGridItem>
Microsoft.AspNetCore.Components.QuickGrid.QuickGrid<TGridItem>.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
126 changes: 126 additions & 0 deletions src/Components/test/E2ETest/Tests/QuickGridNoInteractivityTest.cs
Original file line number Diff line number Diff line change
@@ -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<BasicTestAppServerSiteFixture<RazorComponentEndpointsNoInteractivityStartup<App>>>
{
public QuickGridNoInteractivityTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsNoInteractivityStartup<App>> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<Reference Include="Microsoft.Extensions.Caching.Hybrid" />
<Reference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
<Reference Include="Microsoft.AspNetCore.Session" />
<Reference Include="Microsoft.AspNetCore.Components.QuickGrid" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading