Angular: поиск и пагинация страниц
В реальных приложениях очень распространено работать с большими объемами данных и предоставлять пользователю возможность поиска по ним и представления их в разбивке по страницам. Поэтому в этой статье я покажу один подход к созданию компонента поиска, который обрабатывает входные данные поиска с помощью debounce и компонент пагинации на страницы, который будет обрабатывать запросы на разные страницы данных; оба работают вместе.
Компонент поиска
Давайте начнем с нашего компонента поиска. Наша цель состоит в том, чтобы предоставить входные данные пользователю, чтобы он мог ввести строку поиска, чтобы отфильтровать результаты в большой коллекции. Однако мы должны заметить, что при использовании прямой привязки свойств мы не получаем промежуток времени между каждым введенным пользователем символом (время отката).
В большинстве случаев это является проблемой, так как мы можем делать запросы к серверу на основе предоставленного ввода, и вы хотите дождаться, пока пользователь закончит ввод; и для того, чтобы не делать много ненужных запросов, а также для обеспечения лучшего удобства использования.
Мы можем решить эту проблему довольно просто через rxjs. Во - первых, нам нужно создать в нашем компоненте Subject c типом string. Это то, что мы собираемся использовать для подписки на изменения введенного значения и обработки нашего отката.
Каждый Subject является Наблюдаемым и Наблюдателем. Вы можете подписываться на изменения с помощью subscribe или вызвать next передав ему значение.
private _searchSubject: Subject= new Subject();
Кроме того, мы собираемся добавить к нашему компоненту Output, который будет генерировать событие с введенным значением с заданным debounce. Таким образом, будет возможно сделать любой запрос (или обработать логику фильтра, если разбиение на страницы выполняется во внешнем интерфейсе), привязанный к предоставленной строке поиска после того, как пользователь закончит ввод.
@Output() setValue: EventEmitter= new EventEmitter();
Затем нам нужно создать саму подписку. Мы сделаем это с помощью функции pipe() из rxjs.
constructor() { this._setSearchSubscription(); } private _setSearchSubscription() { this._searchSubject.pipe( debounceTime(500) ).subscribe((searchValue: string) => { // Filter Function }); }
Мы также использовали debounceTime, оператор, предоставляемый библиотекой rxjs; который получает данные о том, сколько миллисекунд он должен ждать, прежде чем активировать подписку. Здесь мы используем 500мс , что, я считаю, является довольно приличным периодом ожидания «между нажатиями клавиш».
В Angular Docs есть хорошая простая страница, рассказывающая о библиотеке rxjs, с которой вы можете ознакомится по следующей ссылке: https://angular.io/guide/rx-library.
Подробное объяснение RxJS не является целью этой статьи, но вы можете узнать больше в их документации: https://rxjs-dev.firebaseapp.com/api
Затем нам нужно создать метод, который мы собираемся связать с HTML input, который будет запускать наш объект, когда пользователь вводит в строке поиска (элемент ввода html).
public updateSearch(searchTextValue: string) { this._searchSubject.next( searchTextValue ); }
И давайте не забудем отписаться от него на onDestroy, чтобы избежать утечек памяти:
ngOnDestroy() { this._searchSubject.unsubscribe(); }
Наконец, нам нужна разметка нашего шаблона, которая в этом примере будет довольно простой; но, конечно, в реальном приложении можно настроить и стилизовать его соответственно. Чтобы связать с методом updateSearch, мы собираемся использовать метод keyup.
Окончательный код
Таким образом, собрав все наши части вместе, это был бы окончательный код для компонента ввода поиска с реализованной стратегией debounce. При использовании этот компонент предоставляет input, который будет инициировать событие, указывающее, что пользователь закончил печатать, поэтому мы можем добавить к нему любую логику, которая нам нужна.
@Component({ selector: 'app-search-input', template: ` ` }) export class SearchInputComponent implements OnDestroy { // Опционально, я добавил ввод placeholder для настройки @Input() readonly placeholder: string = ''; @Output() setValue: EventEmitter= new EventEmitter(); private _searchSubject: Subject = new Subject(); constructor() { this._setSearchSubscription(); } public updateSearch(searchTextValue: string) { this._searchSubject.next( searchTextValue ); } private _setSearchSubscription() { this._searchSubject.pipe( debounceTime(500) ).subscribe((searchValue: string) => { this.setValue.emit( searchValue ); }); } ngOnDestroy() { this._searchSubject.unsubscribe(); } }
Компонент пагинации
Для нашего компонента пагинации страниц мы должны сделать две основные вещи: отрендерить все возможные номера страниц, чтобы пользователь мог выбрать нужную а мы могли определить, когда страница изменилась, чтобы получить данные с выбранной страницы.
Прежде всего, мы собираемся добавить к нашему компоненту свойство Input, чтобы получать информацию, необходимую для создания нумерации страниц: количество элементов на странице (pageSize) и общее количество элементов (itemsCount).
export interface MyPagination { itemsCount: number; pageSize: number; } export class PaginationComponent { public pagesArray: Array= []; public currentPage: number = 1; @Input() set setPagination(pagination: MyPagination) { if (pagination) { const pagesAmount = Math.ceil( pagination.itemsCount / pagination.pageSize ); this.pagesArray = new Array(pagesAmount).fill(1); } } }
Вы заметите, что вместо свойства прямого ввода я использовал функцию set для перехвата полученного свойства ввода. Это было сделано по двум причинам: чтобы обработать pagesAmount, а также заполнить массив чисел с pagesAmount.
Хорошо, так зачем нам нужен массив чисел? Для того, чтобы показать все возможные страницы для пользователя. В Angular мы не можем напрямую брать число и запрашивать цикл *ngFor определенное количество раз, так что это одна из стратегий, которую я обычно использую для преодоления этого.
Что мы делаем: используя массив чисел, мы можем пройти по нему и использовать индекс для получения нужного числа. Поскольку нам нужен просто упорядоченный список страниц, этого легко достичь, как показано в разметке ниже.
{{ index + 1 }}
В этой разметке мы отображаем все возможные страницы для выбора пользователем. Мы добавили ngClass, чтобы установить какой-то стиль на текущей выбранной странице, чтобы пользователь знал, на какой странице он находится в данный момент. Кроме того, мы подписались на событие click, которое выполняя функцию setPage, сообщает родительскому компоненту, что выбранная страница изменилась.
@Output() goToPage = new EventEmitter(); public setPage(pageNumber: number): void { // Запретить изменения, если была выбрана та же страница if (pageNumber === this.currentPage) return; this.currentPage = pageNumber; this.goToPage.emit(pageNumber); }
Теперь давайте добавим две стрелки, чтобы облегчить жизнь нашим пользователям; одна для перехода на одну страницу назад, другая для перехода на одну страницу вперед. Левую стрелку мы будем прятать, если в настоящее время пользователь находится на первой странице и соответственно скрывать правую если находимся на последней странице.
< {{ index + 1 }} >
Но у нас все еще есть здесь одна проблема! Что делать, если в itemsAmount сотни элементов, а pageSize маленький? Или даже тысячи элементов? Мы будем рендерить все страницы одновременно, и у нас будет довольно неудобное для использования полотно со всеми этими числами, которые просто висят там.
Есть несколько возможных дизайнерских решений этой проблемы, например, скрытие средних страниц или скрытие последних страниц после определенного числа. Тот, который я собираюсь показать, прост для реализации, и я считаю, что в некоторых случаях он может быть интересным. Мы заменим список страниц на select.
Итак, вернемся к нашей разметке, мы собираемся добавить следующие изменения в часть, где мы отображаем номера наших страниц:
{{ index + 1 }} 10" >
Окончательный код
Таким образом, собрав все наши части вместе, это был бы окончательный код для простого, но эффективного реализованного компонента разбиения на страницы. Он получает входные данные с размером страниц и общим количеством элементов и позволяет пользователю выбрать, какую страницу он хочет просмотреть, вызывая событие, указывающее выбранную страницу, для обработки требуемой логики / запросов разбиения на страницы
export interface MyPagination { itemsCount: number; pageSize: number; } @Component({ selector: 'app-pagination', template: `<#Angular{{ index + 1 }} 10" > > `, styleUrls: ['./pagination.component.scss'] }) export class PaginationComponent { public pagesArray: Array= []; public currentPage: number = 1; @Input() set setPagination(pagination: MyPagination) { if (pagination) { const pagesAmount = Math.ceil( pagination.itemsCount / pagination.pageSize ); this.pagesArray = new Array(pagesAmount).fill(1); } } public setPage(pageNumber: number): void { if (pageNumber === this.currentPage) return; this.currentPage = pageNumber; this.goToPage.emit(pageNumber); } } Пример использования
Чтобы предоставить практический пример, я создам пример компонента, в котором у нас есть список людей, разбитых на страницы, и мы хотим, чтобы пользователь мог искать имя человека, выбирать страницу и фильтровать результаты списка.
Модуль поиска и пагинации
Во-первых, мы создали бы модуль для этих компонентов, который мы можем импортировать в модули, которые нам понадобятся.
@NgModule({ declarations: [ SearchInputComponent, PaginationComponent ], imports: [ BrowserModule ], exports: [ SearchInputComponent, PaginationComponent ] }) export class SearchAndPaginationModule { }Затем импортируем сам модуль.
... @NgModule({ declarations: [ ... ListComponent ], imports: [ ... SearchAndPaginationModule ], providers: [ ... MyService ], ... }) export class ExampleModule { }Теперь давайте предположим, что у нас есть сервис для связи с сервером для получения информации о пользователях. Предположим, что этот метод получает строку поиска и текущую страницу в качестве параметров для фильтрации результатов списка.
Мы собираемся сделать нашу разметку очень простой, чтобы показать, как могут использоваться созданные нами компоненты. Ниже, как наш окончательный код будет выглядеть для компонента, который использует поиск и пагинацию вместе.
@Component({ selector: 'app-list', template: `
- {{ user.name }}
`}) export class ListComponent implements OnInit { public users: Array ; public totalUsersAmount: number = 0; private _currentPage: number = 1; private _currentSearchValue: string = ''; constructor( private _myService: MyService ) { } ngOnInit() { this._loadUsers( this._currentPage, this._currentSearchValue ); } public filterList(searchParam: string): void { this._currentSearchValue = searchParam; this._loadUsers( this._currentPage, this._currentSearchValue ); } public goToPage(page: number): void { this._currentPage = page; this._loadUsers( this._currentPage, this._currentSearchValue ); } private _loadUsers( page: number = 1, searchParam: string = '' ) { this._myService.getUsers( page, searchParam ).subscribe((response) => { this.users = response.data.users; this.totalUsersAmount = response.data.totalAmount; }, (error) => console.error(error)); } } Надеюсь, это вам поможет в работе 😉