Недавно делал проект для портфолио и искал способ как сделать пагинацию. К сожалению, в сети советуют в основном использование сторонних библиотек. Я же захотел сделать без них.
Это не туториал, не учебник по TS/Angular/Rxjs. Это просто заметка о том как относительно просто реализовать пагинацию без сторонних зависимостей. Сработает не только с Angular-ом.
В примере я пользуюсь популярной заглушкой для разработки WebAPI dummyjson. Сервис позволяет отправлять параметры limit и skip для подгрузки данных порциями, что нам и нужно. Я делал сайт по поиску работы, поэтому и код покажу оттуда.
Работа этого варианта пагинации основана на IntersectionObserver. Этот "обсервер" следит за пересечением указанных элементов на странице или - что нам и интересно - за попаданием элемента на экран. То есть при попадании определённого элемента на экран(в моём случае, вы можете реализовать другое условие) будет срабатывать entry.isIntersecting
в написанной директиве.
Собственно, вот директива, которая и будет править балом.
enter-the-viewport-notifier.directive.ts
@Directive({
selector: '[appEnterTheViewportNotifier]',
})
export class EnterTheViewportNotifierDirective {
// Издатель события, который сработает при попадании элемента с директивой на экран
// Издаёт true, когда элемент появляется на экране и flase когда выходит с области видимости.
@Output() visibilityChange = new EventEmitter<boolean>();
private observer!: IntersectionObserver;
// Ссылка на HTML элемент, содержащий директиву
constructor(@Host() private elementRef: ElementRef) {}
// Здесь мы создадим сам наблюдатель и настройки к нему.
// А еще подпишемся на отлсеживание компонента.
ngAfterViewInit(): void {
const options = { root: null, rootMargin: '0px', threshold: 0.0 };
this.observer = new IntersectionObserver(this.callback, options);
this.observer.observe(this.elementRef.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
// Обыкновенный колбэк, который проверяет наличие на экране IntersectionObserver,
// как только элемент попадёт на экран, сработает издаст true,
// выйдет за пределы false.
/**
* Издаст событие при попадании элумента на экран.
* @param entries
* @param observer
*/
private callback = (entries: any) => {
entries.forEach((entry: any) => {
this.visibilityChange.emit(entry.isIntersecting ? true : false);
});
};
На понадобится:
Модель данных. У меня это соискатель.
stored-user.interface.ts
export interface StoredUser {
firstName: string;
lastName: string;
email: string;
image?: string;
}
Сервис по загрузке данных. Позже мы на него подпишемся в компоненте.
applicants.service.ts
export class ApplicantsService {
constructor(private httpClient: HttpClient) {}
private URL: string = 'https://dummyjson.com/users';
/**
* Сервис для пагинации.
* @param limit Кол-во загружаемых сущностей.
* @param skip Кол-во пропускаемых сущностей.
* @returns Загруженные сущности.
*/
getUsers(limit: number, skip: number) {
return this.httpClient.get<StoredUser>(
this.URL + `?limit=${limit}&skip=${skip}`
);
}
}
Сам класс компонента.
applicants-list.component.ts
export class ApplicantsListComponent {
constructor(
private applicantsService: ApplicantsService
) {}
users: StoredUser[] = []; // для хранения полученных данных
// те самые "порции" данных. При желании их можно вынести во вью,
// чтобы пользователь сам устанавливал их размер
private skip: number = 0;
private limit = 10;
// Здесь мы подписываемся на сервис, получаем загруженные данные,
// и добавляем их к уже имеющимся. Именно добавляем, чтобы, если у вас
// данные отображаются в строку, при подгрузке эта строка не начиналась сначала,
// а дополнялась.
getAll(limit: number, skip: number) {
this.applicantsService.getUsers(limit, skip).subscribe({
next: (res: any) => {
res['users'].forEach((element: StoredUser) => {
this.users.push(element);
});
}
});
// Метод, который будет подгружать новые данные.
/**
* Обновляет параметры загрузки, загружает обновлённые данные
* @param e Событие - срабатывает в конце списка.
*/
onScroll(e: any) {
if (e) {
this.getAll(this.limit, this.skip);
this.skip += this.limit;
console.log(`skip - ${this.skip}`);
console.log(`limit - ${this.limit}`);
}
}
}
Вьюха с отображением данных.
applicants-list.component.html
<div class="row">
<div class="col-sm-3"
*ngFor="let user of users">
<div class="card"
style="width: 18rem;">
<img class="card-img-top"
src={{user.image}}
alt="Card image cap">
<div class="card-body">
<h4 class="card-title">{{getUserName(user.firstName, user.lastName)}}</h4>
<h5 class="card-text">{{truncateAddress($any(user)['address']['address'], 20)}}</h5>
<h5 class="card-text">{{truncateAddress($any(user)['address']['city'], 20)}}</h5>
<div class="d-grid gap-2">
<button (click)="showUserInfo(user)"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
class="btn btn-primary">Подробнее</button>
</div>
</div>
</div>
</div>
</div>
<!-- САМОЕ ИНТЕРЕСНОЕ ТУТ -->
<!-- div издаёт событие, о том, что надо подгружать пользователей -->
<!-- добавим жёлтый цвет просто, чтобы видеть область на экране. Для теста -->
<div appEnterTheViewportNotifier
style="width: 100%; height: 50px; background-color: yellow;"
(visibilityChange)="onScroll($event)"></div>
По сути, везде где написано элемент появляется на экране следует понимать, что появление элемента на экране есть пересечение элемента с директивой с родительским элементом. Или с тем элементом, который указан в const options = { root: null, rootMargin: '0px', threshold: 0.0 };
где root это и есть элемент, с которым пересекается директива.
Теперь при прокрутке страницы вниз, будет появляться жёлтый div и с ним подгружаться новые данные, увеличиваться параметр skip на величину limit.
Надеюсь написано не слишком сумбурно, и кто-то воспользуется таким способом пагинации.