Skip to content

Commit e2ae1ad

Browse files
authored
Merge pull request #47 from czBalazs98/pagination
feat(posts, series): load more posts on scroll
2 parents 93ee7e3 + fc3fe9b commit e2ae1ad

File tree

11 files changed

+215
-55
lines changed

11 files changed

+215
-55
lines changed
Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
<div class="posts-view">
1+
<div class="posts-view" infiniteScroll [isActiveInfiniteScroll]="isActiveInfiniteScroll" (scrolled)="loadMorePosts()">
22
<div class="cards-wrapper grid">
3-
@for (post of posts$ | async; track post.id) {
4-
<a [routerLink]="['post', post.slug]">
5-
<p-card class="post-card" header="{{ post.title }}">
6-
<ng-template pTemplate="header">
7-
<img class="card-image" [src]="post.coverImage.url" [alt]="post.title + ' image'" />
8-
</ng-template>
9-
</p-card>
10-
</a>
3+
@for (post of posts; track post) {
4+
<a [routerLink]="['post', post.slug]">
5+
<p-card class="post-card" header="{{ post.title }}">
6+
<ng-template pTemplate="header">
7+
<img class="card-image" [src]="post.coverImage.url" [alt]="post.title + ' image'"/>
8+
</ng-template>
9+
</p-card>
10+
</a>
1111
}
1212
</div>
13+
14+
@if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
15+
<div class="load-more-posts">
16+
<p-button (click)="loadMorePosts()">Load more</p-button>
17+
</div>
18+
}
1319
</div>

angular-primeng-app/src/app/components/posts/posts.component.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
display: flex;
1414
}
1515
}
16+
17+
.load-more-posts {
18+
display: flex;
19+
justify-content: center;
20+
margin-bottom: 2rem;
21+
}
1622
}
1723

1824

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
1-
import { Component, OnInit, inject } from '@angular/core';
1+
import { Component, inject, OnInit } from '@angular/core';
22
import { BlogService } from '../../services/blog.service';
33
import { RouterLink } from '@angular/router';
4-
import { Observable } from 'rxjs';
54
import { AsyncPipe } from '@angular/common';
65
import { CardModule } from 'primeng/card';
7-
import { Post } from '../../models/post';
6+
import { PageInfo, Post } from '../../models/post';
7+
import { InfiniteScrollDirective } from "../../directives/infinite-scroll.directive";
8+
import { ButtonModule } from "primeng/button";
89

910
@Component({
1011
selector: 'app-posts',
1112
standalone: true,
12-
imports: [AsyncPipe, RouterLink, CardModule],
13+
imports: [AsyncPipe, RouterLink, CardModule, InfiniteScrollDirective, ButtonModule],
1314
templateUrl: './posts.component.html',
1415
styleUrl: './posts.component.scss'
1516
})
1617
export class PostsComponent implements OnInit {
1718
blogURL!: string;
18-
posts$!: Observable<Post[]>;
19+
posts!: Post[];
20+
paginationInfo: PageInfo = { hasNextPage: true, endCursor: ''};
21+
isHiddenLoadMore: boolean = true;
22+
isActiveInfiniteScroll: boolean = false;
23+
1924
private blogService = inject(BlogService);
2025

2126
ngOnInit() {
2227
this.blogURL = this.blogService.getBlogURL();
23-
this.posts$ = this.blogService.getPosts(this.blogURL);
28+
this.loadPosts();
2429
}
30+
31+
private loadPosts(): void {
32+
this.blogService.getPosts(this.blogURL, this.paginationInfo.endCursor).subscribe(postsPageInfo => {
33+
this.paginationInfo = postsPageInfo.pagination;
34+
this.isHiddenLoadMore = !postsPageInfo.pagination.hasNextPage;
35+
this.posts = postsPageInfo.posts;
36+
});
37+
}
38+
39+
loadMorePosts(): void {
40+
if (!this.paginationInfo.hasNextPage) {
41+
return;
42+
}
43+
44+
this.isHiddenLoadMore = true;
45+
46+
this.blogService.getPosts(this.blogURL, this.paginationInfo.endCursor)
47+
.subscribe(newPosts => {
48+
this.isActiveInfiniteScroll = true;
49+
this.paginationInfo = newPosts.pagination;
50+
this.posts = this.posts.concat(newPosts.posts);
51+
});
52+
}
2553
}
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
<div class="series-view">
1+
<div class="series-view" infiniteScroll [isActiveInfiniteScroll]="isActiveInfiniteScroll"
2+
(scrolled)="loadMorePostsFromSeries()">
23
<div class="cards-wrapper grid">
3-
@for (post of postsInSeries$ | async; track post.id) {
4+
@for (post of postsInSeries; track post) {
45
<a [routerLink]="['/post', post.slug]">
56
<p-card class="post-card" header="{{ post.title }}">
67
<ng-template pTemplate="header">
@@ -10,4 +11,10 @@
1011
</a>
1112
}
1213
</div>
14+
15+
@if (paginationInfo.hasNextPage && !isHiddenLoadMore) {
16+
<div class="load-more-posts">
17+
<p-button (click)="loadMorePostsFromSeries()">Load more</p-button>
18+
</div>
19+
}
1320
</div>

angular-primeng-app/src/app/components/series/series.component.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@
1313
display: flex;
1414
}
1515
}
16+
17+
.load-more-posts {
18+
display: flex;
19+
justify-content: center;
20+
margin-bottom: 2rem;
21+
}
1622
}
Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,54 @@
11
import { Component, inject } from '@angular/core';
2-
import { ActivatedRoute, Params, RouterLink } from '@angular/router';
3-
import { Observable, switchMap } from 'rxjs';
4-
import { Post } from '../../models/post';
2+
import { ActivatedRoute, RouterLink } from '@angular/router';
3+
import { PageInfo, Post } from '../../models/post';
54
import { AsyncPipe } from "@angular/common";
65
import { BlogService } from '../../services/blog.service';
76
import { CardModule } from 'primeng/card';
7+
import { InfiniteScrollDirective } from "../../directives/infinite-scroll.directive";
8+
import { ButtonModule } from "primeng/button";
89

910
@Component({
1011
selector: 'app-series',
1112
standalone: true,
12-
imports: [RouterLink, AsyncPipe, CardModule],
13+
imports: [RouterLink, AsyncPipe, CardModule, InfiniteScrollDirective, ButtonModule],
1314
templateUrl: './series.component.html',
1415
styleUrl: './series.component.scss'
1516
})
1617
export class SeriesComponent {
1718
blogURL!: string;
1819
slug: string = "";
19-
postsInSeries$!: Observable<Post[]>;
20+
postsInSeries: Post[] = [];
21+
paginationInfo: PageInfo = { hasNextPage: true, endCursor: '' };
22+
isHiddenLoadMore: boolean = true;
23+
isActiveInfiniteScroll: boolean = false;
24+
2025
blogService: BlogService = inject(BlogService);
2126
route: ActivatedRoute = inject(ActivatedRoute);
2227

2328
ngOnInit(): void {
2429
this.blogURL = this.blogService.getBlogURL();
25-
this.postsInSeries$ = this.route.params.pipe(
26-
switchMap((params: Params) => {
27-
this.slug = params["slug"];
28-
return this.blogService.getPostsInSeries(this.blogURL, this.slug);
29-
})
30-
);
30+
this.route.params.subscribe(params => {
31+
this.slug = params['slug'];
32+
this.loadPostsInSeries();
33+
})
3134
}
35+
36+
private loadPostsInSeries():void{
37+
this.blogService.getPostsInSeries(this.blogURL, this.slug).subscribe(seriesPageInfo => {
38+
this.paginationInfo = seriesPageInfo.pagination;
39+
this.isHiddenLoadMore = !seriesPageInfo.pagination.hasNextPage;
40+
this.postsInSeries = seriesPageInfo.posts;
41+
})
42+
}
43+
44+
loadMorePostsFromSeries():void {
45+
if (!this.paginationInfo.hasNextPage) return;
46+
this.isHiddenLoadMore = true;
47+
this.blogService.getPostsInSeries(this.blogURL, this.slug, this.paginationInfo.endCursor).pipe(
48+
).subscribe(newPosts => {
49+
this.isActiveInfiniteScroll = true;
50+
this.paginationInfo = newPosts.pagination;
51+
this.postsInSeries = this.postsInSeries.concat(newPosts.posts);
52+
});
53+
}
3254
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { InfiniteScrollDirective } from './infinite-scroll.directive';
2+
3+
describe('InfiniteScrollDirective', () => {
4+
it('should create an instance', () => {
5+
const directive = new InfiniteScrollDirective();
6+
expect(directive).toBeTruthy();
7+
});
8+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Directive, effect, EventEmitter, input, Input, InputSignal, OnDestroy, Output } from '@angular/core';
2+
import { fromEvent, Subject, takeUntil } from 'rxjs';
3+
4+
@Directive({
5+
selector: '[infiniteScroll]',
6+
standalone: true
7+
})
8+
export class InfiniteScrollDirective implements OnDestroy {
9+
isActiveInfiniteScroll: InputSignal<boolean> = input(false);
10+
@Input() infiniteScrollDistance: number = 20;
11+
12+
@Output() scrolled = new EventEmitter<void>;
13+
14+
private unsubscribe: Subject<void> = new Subject<void>();
15+
16+
constructor() {
17+
this._listenSignals();
18+
}
19+
20+
ngOnDestroy(): void {
21+
this.unsubscribe.next();
22+
this.unsubscribe.complete();
23+
}
24+
25+
private listenScrollWindow(): void {
26+
let isActivePercentageBeforeEnd = false;
27+
fromEvent(window, 'scroll').pipe(
28+
takeUntil(this.unsubscribe))
29+
.subscribe(() => {
30+
let percentageBeforeEnd = this.calculatePositionScroll();
31+
if (!isActivePercentageBeforeEnd && percentageBeforeEnd <= this.infiniteScrollDistance) {
32+
isActivePercentageBeforeEnd = true;
33+
this.scrolled.emit();
34+
} else if (percentageBeforeEnd >= this.infiniteScrollDistance) {
35+
isActivePercentageBeforeEnd = false;
36+
}
37+
});
38+
}
39+
40+
private calculatePositionScroll(): number {
41+
let scrollHeight = document.documentElement.scrollHeight;
42+
let innerHeight = window.innerHeight;
43+
let scrollY = window.scrollY || 0;
44+
return (scrollHeight - scrollY - innerHeight) / scrollHeight * 100;
45+
}
46+
47+
private _listenSignals(): void {
48+
effect(() => {
49+
this.unsubscribe.next();
50+
if (this.isActiveInfiniteScroll()) {
51+
this.listenScrollWindow();
52+
}
53+
});
54+
}
55+
}

angular-primeng-app/src/app/graphql.operations.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ export const GET_AUTHOR_INFO = gql`
4848
`;
4949

5050
export const GET_POSTS = gql`
51-
query Publication($host: String!) {
51+
query Publication($host: String!, $after: String!) {
5252
publication(host: $host) {
5353
id
5454
isTeam
5555
title
56-
posts(first: 10) {
56+
posts(first: 10, after: $after) {
5757
edges {
5858
node {
5959
id
@@ -68,6 +68,10 @@ export const GET_POSTS = gql`
6868
}
6969
}
7070
}
71+
pageInfo {
72+
endCursor
73+
hasNextPage
74+
}
7175
}
7276
}
7377
}
@@ -92,13 +96,13 @@ export const GET_SERIES_LIST = gql`
9296
`;
9397

9498
export const GET_POSTS_IN_SERIES = gql`
95-
query Publication($host: String!, $slug: String!) {
99+
query Publication($host: String!, $slug: String!, $after: String!) {
96100
publication(host: $host) {
97101
id
98102
isTeam
99103
title
100104
series(slug: $slug) {
101-
posts(first: 10) {
105+
posts(first: 10, after: $after) {
102106
edges {
103107
node {
104108
id
@@ -109,6 +113,10 @@ export const GET_POSTS_IN_SERIES = gql`
109113
}
110114
}
111115
}
116+
pageInfo {
117+
endCursor
118+
hasNextPage
119+
}
112120
}
113121
}
114122
}

angular-primeng-app/src/app/models/post.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@ export interface Content {
4747
html: string;
4848
}
4949

50+
export interface PostsPageInfo {
51+
posts: Post[];
52+
pagination: PageInfo;
53+
}
54+
55+
export interface PageInfo {
56+
hasNextPage: boolean;
57+
endCursor: string;
58+
}

0 commit comments

Comments
 (0)