Skip to content

Commit 65b9246

Browse files
authored
Merge pull request #13 from linkdotnet/feature/pagination
Feature/pagination
2 parents fc9ef0c + 4101283 commit 65b9246

File tree

14 files changed

+237
-26
lines changed

14 files changed

+237
-26
lines changed

LinkDotNet.Blog.IntegrationTests/SqlDatabaseTestBase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ protected SqlDatabaseTestBase()
1919
BlogPostRepository = new BlogPostRepository(new BlogPostContext(options));
2020
}
2121

22-
protected BlogPostRepository BlogPostRepository { get; private set; }
22+
protected BlogPostRepository BlogPostRepository { get; }
2323

24-
protected BlogPostContext DbContext { get; private set; }
24+
protected BlogPostContext DbContext { get; }
2525

2626
public Task InitializeAsync()
2727
{

LinkDotNet.Blog.IntegrationTests/Web/Pages/Admin/DraftBlogPostPageTests.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Threading.Tasks;
1+
using System.Linq;
2+
using System.Threading.Tasks;
23
using Bunit;
34
using FluentAssertions;
45
using LinkDotNet.Blog.TestUtilities;
@@ -23,6 +24,7 @@ public async Task ShouldOnlyShowPublishedPosts()
2324
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
2425
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
2526
var cut = ctx.RenderComponent<DraftBlogPosts>();
27+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
2628

2729
var blogPosts = cut.FindComponents<ShortBlogPost>();
2830

LinkDotNet.Blog.IntegrationTests/Web/Pages/IndexTests.cs

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Threading.Tasks;
1+
using System.Linq;
2+
using System.Threading.Tasks;
23
using Bunit;
34
using FluentAssertions;
45
using LinkDotNet.Blog.TestUtilities;
@@ -26,6 +27,7 @@ public async Task ShouldShowAllBlogPostsWithLatestOneFirst()
2627
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
2728
ctx.Services.AddScoped(_ => CreateSampleAppConfiguration());
2829
var cut = ctx.RenderComponent<Index>();
30+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
2931

3032
var blogPosts = cut.FindComponents<ShortBlogPost>();
3133

@@ -46,13 +48,67 @@ public async Task ShouldOnlyShowPublishedPosts()
4648
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
4749
ctx.Services.AddScoped(_ => CreateSampleAppConfiguration());
4850
var cut = ctx.RenderComponent<Index>();
51+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
4952

5053
var blogPosts = cut.FindComponents<ShortBlogPost>();
5154

5255
blogPosts.Should().HaveCount(1);
5356
blogPosts[0].Find(".description h1").InnerHtml.Should().Be("Published");
5457
}
5558

59+
[Fact]
60+
public async Task ShouldOnlyLoadTenEntities()
61+
{
62+
await CreatePublishedBlogPosts(11);
63+
using var ctx = new TestContext();
64+
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
65+
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
66+
ctx.Services.AddScoped(_ => CreateSampleAppConfiguration());
67+
var cut = ctx.RenderComponent<Index>();
68+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
69+
70+
var blogPosts = cut.FindComponents<ShortBlogPost>();
71+
72+
blogPosts.Count.Should().Be(10);
73+
}
74+
75+
[Fact]
76+
public async Task ShouldLoadNextBatchOnClick()
77+
{
78+
await CreatePublishedBlogPosts(11);
79+
using var ctx = new TestContext();
80+
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
81+
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
82+
ctx.Services.AddScoped(_ => CreateSampleAppConfiguration());
83+
var cut = ctx.RenderComponent<Index>();
84+
85+
cut.FindComponent<BlogPostNavigation>().Find("li:last-child a").Click();
86+
87+
cut.WaitForState(() => cut.FindAll(".blog-card").Count == 1);
88+
var blogPosts = cut.FindComponents<ShortBlogPost>();
89+
blogPosts.Count.Should().Be(1);
90+
}
91+
92+
[Fact]
93+
public async Task ShouldLoadPreviousBatchOnClick()
94+
{
95+
await CreatePublishedBlogPosts(11);
96+
using var ctx = new TestContext();
97+
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
98+
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
99+
ctx.Services.AddScoped(_ => CreateSampleAppConfiguration());
100+
var cut = ctx.RenderComponent<Index>();
101+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
102+
cut.FindComponent<BlogPostNavigation>().Find("li:last-child a").Click();
103+
cut.WaitForState(() => cut.FindAll(".blog-card").Count == 1);
104+
105+
cut.FindComponent<BlogPostNavigation>().Find("li:first-child a").Click();
106+
107+
cut.WaitForState(() => cut.FindAll(".blog-card").Count > 1);
108+
var blogPosts = cut.FindComponents<ShortBlogPost>();
109+
blogPosts.Count.Should().Be(10);
110+
}
111+
56112
private static AppConfiguration CreateSampleAppConfiguration()
57113
{
58114
return new()
@@ -66,5 +122,14 @@ private static AppConfiguration CreateSampleAppConfiguration()
66122
},
67123
};
68124
}
125+
126+
private async Task CreatePublishedBlogPosts(int amount)
127+
{
128+
for (var i = 0; i < amount; i++)
129+
{
130+
var blogPost = new BlogPostBuilder().IsPublished().Build();
131+
await BlogPostRepository.StoreAsync(blogPost);
132+
}
133+
}
69134
}
70135
}

LinkDotNet.Blog.IntegrationTests/Web/Pages/SearchByTagTests.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using System.Threading.Tasks;
34
using Bunit;
45
using FluentAssertions;
@@ -21,6 +22,7 @@ public async Task ShouldOnlyDisplayTagsGivenByParameter()
2122
await AddBlogPostWithTagAsync("Tag 2");
2223
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
2324
var cut = ctx.RenderComponent<SearchByTag>(p => p.Add(s => s.Tag, "Tag 1"));
25+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
2426

2527
var tags = cut.FindAll(".blog-card");
2628

@@ -34,6 +36,7 @@ public async Task ShouldHandleSpecialCharacters()
3436
await AddBlogPostWithTagAsync("C#");
3537
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
3638
var cut = ctx.RenderComponent<SearchByTag>(p => p.Add(s => s.Tag, Uri.EscapeDataString("C#")));
39+
cut.WaitForState(() => cut.FindAll(".blog-card").Any());
3740

3841
var tags = cut.FindAll(".blog-card");
3942

LinkDotNet.Blog.UnitTests/Web/Shared/Admin/CreateNewBlogPostTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Linq;
2-
using AngleSharp.Dom;
32
using Bunit;
43
using FluentAssertions;
54
using LinkDotNet.Blog.TestUtilities;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Bunit;
2+
using FluentAssertions;
3+
using LinkDotNet.Blog.Web.Shared;
4+
using LinkDotNet.Domain;
5+
using Moq;
6+
using X.PagedList;
7+
using Xunit;
8+
9+
namespace LinkDotNet.Blog.UnitTests.Web.Shared
10+
{
11+
public class BlogPostNavigationTests : TestContext
12+
{
13+
[Fact]
14+
public void ShouldFireEventWhenGoingToNextPage()
15+
{
16+
var actualNewPage = 0;
17+
var page = CreatePagedList(2, 3);
18+
var cut = RenderComponent<BlogPostNavigation>(p => p.Add(param => param.CurrentPage, page.Object)
19+
.Add(param => param.OnPageChanged, newPage => actualNewPage = newPage));
20+
21+
cut.Find("li:last-child a").Click();
22+
23+
actualNewPage.Should().Be(3);
24+
}
25+
26+
[Fact]
27+
public void ShouldFireEventWhenGoingToPreviousPage()
28+
{
29+
var actualNewPage = 0;
30+
var page = CreatePagedList(2, 3);
31+
var cut = RenderComponent<BlogPostNavigation>(p => p.Add(param => param.CurrentPage, page.Object)
32+
.Add(param => param.OnPageChanged, newPage => actualNewPage = newPage));
33+
34+
cut.Find("li:first-child a").Click();
35+
36+
actualNewPage.Should().Be(1);
37+
}
38+
39+
[Fact]
40+
public void ShouldNotFireNextWhenOnLastPage()
41+
{
42+
var page = CreatePagedList(2, 2);
43+
var cut = RenderComponent<BlogPostNavigation>(p =>
44+
p.Add(param => param.CurrentPage, page.Object));
45+
46+
cut.Find("li:last-child").ClassList.Should().Contain("disabled");
47+
}
48+
49+
[Fact]
50+
public void ShouldNotFireNextWhenOnFirstPage()
51+
{
52+
var page = CreatePagedList(1, 2);
53+
var cut = RenderComponent<BlogPostNavigation>(p =>
54+
p.Add(param => param.CurrentPage, page.Object));
55+
56+
cut.Find("li:first-child").ClassList.Should().Contain("disabled");
57+
}
58+
59+
private static Mock<IPagedList<BlogPost>> CreatePagedList(int currentPage, int pageCount)
60+
{
61+
var page = new Mock<IPagedList<BlogPost>>();
62+
page.Setup(p => p.PageNumber).Returns(currentPage);
63+
page.Setup(p => p.PageCount).Returns(pageCount);
64+
page.Setup(p => p.IsFirstPage).Returns(currentPage == 1);
65+
page.Setup(p => p.IsLastPage).Returns(currentPage == pageCount);
66+
67+
return page;
68+
}
69+
}
70+
}

LinkDotNet.Blog.Web/Pages/Index.razor

+13-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@using LinkDotNet.Domain
33
@using LinkDotNet.Infrastructure.Persistence
44
@using Markdig
5+
@using X.PagedList
56
@inject IRepository _repository
67
@inject AppConfiguration _appConfiguration
78
@inject NavigationManager _navigationManager
@@ -13,17 +14,19 @@
1314
<IntroductionCard Introduction="_appConfiguration.Introduction"></IntroductionCard>
1415

1516
<div class="content px-4">
16-
@for (var i = 0; i < _blogPosts.Count; i++)
17+
@for (var i = 0; i < _currentPage.Count; i++)
1718
{
18-
<ShortBlogPost BlogPost="_blogPosts[i]" UseAlternativeStyle="@(i % 2 != 0)"></ShortBlogPost>
19+
<ShortBlogPost BlogPost="_currentPage[i]" UseAlternativeStyle="@(i % 2 != 0)"></ShortBlogPost>
1920
}
2021
</div>
22+
<BlogPostNavigation CurrentPage="@_currentPage" OnPageChanged="@GetPage"></BlogPostNavigation>
2123
</section>
2224
@code {
23-
IList<BlogPost> _blogPosts = new List<BlogPost>();
25+
IPagedList<BlogPost> _currentPage = new PagedList<BlogPost>(Array.Empty<BlogPost>(), 1, 1);
26+
2427
protected override async Task OnInitializedAsync()
2528
{
26-
_blogPosts = (await _repository.GetAllAsync(p => p.IsPublished, b => b.UpdatedDate)).ToList();
29+
_currentPage = await _repository.GetAllAsync(p => p.IsPublished, b => b.UpdatedDate, pageSize: 10);
2730
}
2831

2932
private string GetAbsolutePreviewImageUrl()
@@ -42,4 +45,10 @@
4245
{
4346
return Uri.TryCreate(url, UriKind.Absolute, out _);
4447
}
48+
49+
private async Task GetPage(int newPage)
50+
{
51+
_currentPage = await _repository.GetAllAsync(p => p.IsPublished, b => b.UpdatedDate, pageSize: 10, page:
52+
newPage);
53+
}
4554
}

LinkDotNet.Blog.Web/Pages/SearchByTag.razor

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
protected override async Task OnInitializedAsync()
1919
{
2020
Tag = Uri.UnescapeDataString(Tag);
21-
_blogPosts = (await _repository.GetAllAsync(b => b.Tags.Any(t => t.Content == Tag), b => b.UpdatedDate)).ToList();
21+
_blogPosts = (await _repository.GetAllAsync(
22+
b => b.Tags.Any(t => t.Content == Tag),
23+
b => b.UpdatedDate))
24+
.ToList();
2225
}
2326
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@using LinkDotNet.Domain
2+
@using X.PagedList
3+
<nav aria-label="Page navigation example">
4+
<ul class="pagination justify-content-center">
5+
<li class="page-item @(!CurrentPage.IsFirstPage ? string.Empty : "disabled")">
6+
<a class="page-link" href="#" tabindex="-1" @onclick="PreviousPage">Previous</a>
7+
</li>
8+
<li class="page-item @(!CurrentPage.IsLastPage ? string.Empty : "disabled")">
9+
<a class="page-link"
10+
@onclick="NextPage"
11+
href="#">Next</a>
12+
</li>
13+
</ul>
14+
</nav>
15+
16+
@code {
17+
[Parameter]
18+
public IPagedList<BlogPost> CurrentPage { get; set; }
19+
20+
[Parameter]
21+
public EventCallback<int> OnPageChanged { get; set; }
22+
23+
private async Task PageHasChanged(int newPage)
24+
{
25+
await OnPageChanged.InvokeAsync(newPage);
26+
}
27+
28+
private async Task PreviousPage()
29+
{
30+
await PageHasChanged(CurrentPage.PageNumber - 1);
31+
}
32+
33+
private async Task NextPage()
34+
{
35+
await PageHasChanged(CurrentPage.PageNumber + 1);
36+
}
37+
}

LinkDotNet.Infrastructure/LinkDotNet.Infrastructure.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PrivateAssets>all</PrivateAssets>
1616
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1717
</PackageReference>
18+
<PackageReference Include="X.PagedList" Version="8.1.0" />
1819
</ItemGroup>
1920

2021
<ItemGroup>

LinkDotNet.Infrastructure/Persistence/IRepository.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq.Expressions;
43
using System.Threading.Tasks;
54
using LinkDotNet.Domain;
5+
using X.PagedList;
66

77
namespace LinkDotNet.Infrastructure.Persistence
88
{
99
public interface IRepository
1010
{
1111
Task<BlogPost> GetByIdAsync(string blogPostId);
1212

13-
Task<IEnumerable<BlogPost>> GetAllAsync(Expression<Func<BlogPost, bool>> filter = null, Expression<Func<BlogPost, object>> orderBy = null, bool descending = true);
13+
Task<IPagedList<BlogPost>> GetAllAsync(
14+
Expression<Func<BlogPost, bool>> filter = null,
15+
Expression<Func<BlogPost,
16+
object>> orderBy = null,
17+
bool descending = true,
18+
int page = 1,
19+
int pageSize = 5);
1420

1521
Task StoreAsync(BlogPost blogPost);
1622

LinkDotNet.Infrastructure/Persistence/InMemory/InMemoryRepository.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq.Expressions;
55
using System.Threading.Tasks;
66
using LinkDotNet.Domain;
7+
using X.PagedList;
78

89
namespace LinkDotNet.Infrastructure.Persistence.InMemory
910
{
@@ -17,7 +18,12 @@ public Task<BlogPost> GetByIdAsync(string blogPostId)
1718
return Task.FromResult(blogPost);
1819
}
1920

20-
public Task<IEnumerable<BlogPost>> GetAllAsync(Expression<Func<BlogPost, bool>> filter = null, Expression<Func<BlogPost, object>> orderBy = null, bool descending = true)
21+
public Task<IPagedList<BlogPost>> GetAllAsync(
22+
Expression<Func<BlogPost, bool>> filter = null,
23+
Expression<Func<BlogPost, object>> orderBy = null,
24+
bool descending = true,
25+
int page = 1,
26+
int pageSize = 5)
2127
{
2228
var result = blogPosts.AsEnumerable();
2329
if (filter != null)
@@ -29,13 +35,13 @@ public Task<IEnumerable<BlogPost>> GetAllAsync(Expression<Func<BlogPost, bool>>
2935
{
3036
if (descending)
3137
{
32-
return Task.FromResult(result.OrderByDescending(orderBy.Compile()).AsEnumerable());
38+
return Task.FromResult(result.OrderByDescending(orderBy.Compile()).ToPagedList(page, pageSize));
3339
}
3440

35-
return Task.FromResult(result.OrderBy(orderBy.Compile()).AsEnumerable());
41+
return Task.FromResult(result.OrderBy(orderBy.Compile()).ToPagedList(page, pageSize));
3642
}
3743

38-
return Task.FromResult(blogPosts.AsEnumerable());
44+
return Task.FromResult(blogPosts.ToPagedList(page, pageSize));
3945
}
4046

4147
public Task StoreAsync(BlogPost blogPost)

0 commit comments

Comments
 (0)