с примерами использования

В реальных приложениях очень часто приходится работать с большими объемами данных и предоставлять пользователю возможность выполнять поиск по ним и представлять их с разбивкой на страницы. Поэтому в этой статье я покажу один подход к созданию компонента поиска, который обрабатывает входные данные поиска с помощью debounce и компонента разбивки на страницы. который будет обрабатывать запросы разных страниц данных; оба работают вместе.

Компонент поиска

Начнем с нашего компонента поиска. Наша цель здесь - предоставить пользователю ввод, чтобы он мог ввести строку поиска для фильтрации результатов в большой коллекции. Однако мы должны заметить, что, используя прямую привязку свойств, мы не получаем промежуток времени между каждым набранным пользователем символом (время устранения ошибки).

В большинстве случаев это является проблемой, поскольку мы можем делать запросы к серверу на основе предоставленных входных данных, а вы хотите дождаться, пока пользователь закончит ввод; как для того, чтобы не выполнять много ненужных запросов, так и для повышения удобства использования.

Мы можем решить эту проблему довольно просто с помощью rxjs. Во-первых, нам нужно создать в нашем компоненте Subject типа string. Это то, что мы собираемся использовать, чтобы подписаться на изменения введенного значения и обработать наш дебонс.

«Каждый субъект является наблюдаемым и наблюдателем. Вы можете подписаться на тему и вызвать далее, чтобы передать значения, а также сообщение об ошибке и завершение. ”

private _searchSubject: Subject<string> = new Subject();

Кроме того, мы собираемся добавить к нашему компоненту Выход, который будет генерировать событие с введенным значением после устранения ошибки. Таким образом, можно будет сделать любой запрос, который нам нужен (или обработать логику фильтрации, если разбивка на страницы выполняется во внешнем интерфейсе), привязанную к указанной строке поиска после того, как пользователь закончил вводить.

@Output() setValue: EventEmitter<string> = new EventEmitter();

Затем нам нужно создать саму подписку. Мы сделаем это с помощью функции pipe () из rxjs.

«Вы можете использовать каналы, чтобы связать операторов вместе. Каналы позволяют объединить несколько функций в одну. Функция pipe() принимает в качестве аргументов функции, которые вы хотите объединить, и возвращает новую функцию, которая при выполнении последовательно запускает составленные функции ».

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, который будет запускать наши Тема, когда пользователь вводит текст в строке поиска (элемент ввода html).

public updateSearch(searchTextValue: string) {
  this._searchSubject.next( searchTextValue );
}

И не забудьте отказаться от подписки на onDestroy, чтобы избежать утечки памяти ; , как я ранее писал в статье Понимание крючков жизненного цикла Angular.

ngOnDestroy() {
  this._searchSubject.unsubscribe();
}

Наконец, нам нужна разметка нашего шаблона, которая в этом примере будет довольно простой; но, конечно, в реальном приложении его можно будет соответствующим образом настроить и стилизовать. Чтобы привязать метод updateSearch, мы будем использовать keyup метод.

<input
  type="text"
  (keyup)="updateSearch($event.target.value)"
/>

Окончательный код

Итак, если собрать все наши части вместе, это будет окончательный код для компонента ввода поиска с реализованной стратегией debounce. При использовании этот компонент предоставляет входные данные, которые запускают событие, указывающее, что пользователь закончил вводить текст, поэтому мы можем добавить к нему любую логику, которая нам нужна.

@Component({
  selector: 'app-search-input',
  template: `
    <input
      type="text"
      [placeholder]="placeholder"
      (keyup)="updateSearch($event.target.value)"
    />`
})
export class SearchInputComponent implements OnDestroy {
  // Optionally, I have added a placeholder input for customization 
  @Input() readonly placeholder: string = '';
  @Output() setValue: EventEmitter<string> = new EventEmitter();
  private _searchSubject: Subject<string> = 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 для получения информации, необходимой для создания разбивки на страницы: размер страниц и общее количество элементов.

export interface MyPagination {
  itemsCount: number;
  pageSize: number;
}
export class PaginationComponent {
  public pagesArray: Array<number> = [];
  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);
    }
  }
}

Вы заметите, что вместо свойства прямого ввода я использовал сеттер для перехвата полученного свойства ввода. Это произошло по двум причинам: округлить pagesAmount ( в случае, если по какой-либо причине его еще не было), а также заполнить массив чисел с помощью pagesAmount.

Итак, зачем нам массив чисел? Чтобы отобразить все возможные страницы для пользователя. В Angular мы не можем напрямую брать число и запрашивать цикл * ngFor определенное количество раз, так что это одна из стратегий, которую я обычно использую для решения этой проблемы.

Что мы делаем: используя массив чисел, мы можем перебрать его и использовать index для получения нужного числа. Поскольку нам нужен обычный список упорядоченных номеров, этого легко добиться, как показано в разметке ниже.

<span
  *ngFor="let page of pagesArray; let index = index"
  [ngClass]="{ 'active': currentPage === index + 1 } 
  (click)="setPage(index + 1)"
>
  {{ index + 1 }}
</span>

В этой разметке мы отображаем все возможные страницы на выбор пользователя. Мы добавили ngClass для установки некоторого стиля на текущей выбранной странице, чтобы пользователь знал, на какой странице он сейчас находится. Кроме того, мы вставили действие click, которое будет генерировать событие, сообщающее родительскому компоненту об изменении выбранной страницы.

@Output() goToPage = new EventEmitter<number>();
public setPage(pageNumber: number): void {
  // Prevent changes if the same page was selected
  if (pageNumber === this.currentPage)
    return;
  this.currentPage = pageNumber;
  this.goToPage.emit(pageNumber);
}

Теперь давайте также добавим две стрелки, чтобы облегчить жизнь нашим пользователям; один для возврата на одну страницу и один для перехода вперед на одну страницу. Однако мы собираемся скрыть стрелку влево, когда в настоящее время находимся на первой странице и скрыть стрелку вправо, когда мы сейчас на последней странице.

<span
 *ngIf="currentPage !== 1"
 (click)="setPage(currentPage - 1)"
>
  &lt; <!-- This is the simbol for '<' icon -->
</span>
<span
  *ngFor="let page of pagesArray; let index = index"
  [ngClass]="{ 'active': currentPage === index + 1 } 
  (click)="setPage(index + 1)"
>
  {{ index + 1 }}
</span>
<span 
  *ngIf="currentPage !== pagesArray.length"
  (click)="setPage(currentPage + 1)"
>
  &gt; <!-- This is the simbol for '>' icon -->
</span>

Но у нас все еще есть одна проблема! Что, если наше itemsAmount равно сотням, а наш pageSize невелик? Или даже тысячи предметов? Мы бы отображали все страницы сразу, и у нас было бы довольно плохое удобство использования, когда все эти числа просто висели бы там.

Есть несколько возможных дизайнерских решений этой проблемы, например, скрытие средних страниц или скрытие последних страниц после определенного числа. То, что я собираюсь показать, легко осуществить, и я считаю, что в некоторых случаях он может быть интересным; который меняет числа с печати на использование элемента select html с каждой страницей в качестве опции.

Итак, возвращаясь к нашей разметке, мы собираемся добавить следующие изменения в ту часть, где мы отображаем номера наших страниц:

<!-- Here I decided the max pages amount before changing the rendering strategy to be 10, but you could change it however you want it and even create an environment variable if necessary -->
<ng-container *ngIf="pagesArray.length <= 10" >
  <span
    *ngFor="let page of pagesArray; let index = index"
    [ngClass]="{ 'active': currentPage === index + 1 }"
    (click)="setPage(index + 1)"
  >
    {{ index + 1 }}
  </span>
</ng-container>
<ng-container *ngIf="pagesArray.length > 10" >
  <select
    [ngModel]="currentPage"
    (ngModelChange)="setPage($event.target.value)"
  >
    <option
      *ngFor="let p of pagesArray; let index = index"
      [value]="(index + 1)" >
      {{ index + 1 }}
    </option>
  </select>
</ng-container>

Окончательный код

Итак, если собрать все наши части вместе, это будет окончательный код для простого, но эффективного компонента нумерации страниц. Он получает входные данные с размером страниц и общим количеством элементов и позволяет пользователю выбрать, какую страницу он хочет просмотреть, инициируя событие, указывающее выбранную страницу, для обработки необходимой логики / запросов разбивки на страницы.

export interface MyPagination {
  itemsCount: number;
  pageSize: number;
}
@Component({
  selector: 'app-pagination',
  template: `
    <div class="pagination" >
      <span
        *ngIf="currentPage !== 1"
        (click)="setPage(currentPage - 1)"
      >
        &lt;
      </span>
      <ng-container *ngIf="pagesArray.length <= 10" >
        <span
          *ngFor="let page of pagesArray; let index = index"
          [ngClass]="{ 'active': currentPage === index + 1 }"
          (click)="setPage(index + 1)"
        >
          {{ index + 1 }}
        </span>
      </ng-container>
      <ng-container *ngIf="pagesArray.length > 10" >
        <select
          [ngModel]="currentPage"
          (ngModelChange)="setPage($event.target.value)"
        >
          <option
            *ngFor="let p of pagesArray; let index = index"
            [value]="(index + 1)" >
            {{ index + 1 }}
          </option>
        </select>
      </ng-container>
      <span 
        *ngIf="currentPage !== pagesArray.length"
        (click)="setPage(currentPage + 1)"
      >
        &gt;
      </span>`,
  styleUrls: ['./pagination.component.scss']
})
export class PaginationComponent {
  public pagesArray: Array<number> = [];
  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: `
    <app-search-input
      placeholder="Search by name"
      (setValue)="filterList($event)"
    ></app-search-input>
    <ul>
      <li *ngFor="let user of users" >
        {{ user.name }}
      </li>
    </ul>
    <app-pagination
      [setPagination]="{
        'itemsCount': totalUsersAmount,
        'pageSize': 10
      }"
      (goToPage)="goToPage($event)"
    ></app-pagination>
`
})
export class ListComponent implements OnInit {
  public users: Array<User>;
  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));
  }
}

Надеюсь, это поможет 😉

Ссылки

https://angular.io/guide/rx-library
https://itnext.io/understanding-angular-life-cycle-hooks-91616f8946e3 последняя
«Https://angular.io/guide/component-interaction#intercept-input-property-changes-with-a-setter