Angular2で汎用的なPagerComponentを作ってみた

# まずは作ったファイル

## pager.component.ts

`@Component()`内の`selector`, `templateUrl`は適宜書き換えるべし。

```ts:pager.component.ts
import { Component, Input, EventEmitter, Output, OnChanges } from '@angular/core';

@Component({
    selector: 'pager',
    templateUrl: '/pager.component.html'
})
export class PagerComponent implements OnChanges
{
    /** 現在のページ番号 */
    @Input()
    public currentPage: number;

    /** 合計アイテム数 */
    @Input()
    public totalItemCount: number;

    /** ページを切り替えた際の通知用 */
    @Output()
    public pageChanged = new EventEmitter<number>();

    /** 1ページに何個のアイテムを表示するか（初期値: 15） */
    @Input()
    public perPage: number = 15;

    /** ページャーの表示サイズ（初期値: 7） */
    @Input()
    public size: number = 7;

    /** 総ページ数 */
    public totalPages: number;

    /** 表示すべきページ番号群 */
    public displayPages: number[];

    public constructor() {}

    /**
     * プロパティバインドした変数の変更を検知し
     * totalPages、displayPagesを更新する
     * @param changes
     * @returns {void}
     */
    public ngOnChanges(changes): void
    {
        if (changes.totalItemCount) {
            this.totalPages = Math.ceil(changes.totalItemCount.currentValue / this.perPage);
            this.updateDisplayPages();
        }
    }

    /**
     * ページ番号を直接指定して移動
     * @param pageNumber
     * @returns {void}
     */
    public jump(pageNumber: number): void
    {
        this.currentPage = pageNumber;
        this.updateDisplayPages();
    }

    /**
     * 「次」ボタンを押して移動
     * @returns {void}
     */
    public next(): void
    {
        if (this.currentPage < this.totalPages) {
            this.currentPage++;
            this.updateDisplayPages();
        }
    }

    /**
     * 「前」ボタンを押して移動
     * @returns {void}
     */
    public prev(): void
    {
        if (this.currentPage > 1) {
            this.currentPage--;
            this.updateDisplayPages();
        }
    }


    /**
     * displayPagesを正しい状態に更新する
     * @returns {void}
     */
    private updateDisplayPages(): void
    {
        this.displayPages = this.calcDisplayPages();

        // EventEmitterを通じて変更を通知
        this.pageChanged.emit(this.currentPage);
    }

    /**
     * sizeを考慮して表示するべきページ配列を返却する
     * @returns {number[]}
     */
    private calcDisplayPages(): number[]
    {
        if (this.size >= this.totalPages) {
            return this.range(1, this.totalPages);
        }

        // 中間を求める（切り上げ）
        let mid = Math.ceil(this.size / 2);

        if (this.currentPage <= mid) {
            return this.range(1, this.size);
        }

        // 現在のページを中心に前後を埋める
        let zero = ((this.size - 1) % 2) === 0;

        let before, after;
        if (zero) {
            before = (this.size - 1) / 2;
            after = before;
        } else {
            before = Math.floor((this.size - 1) / 2);
            after = before + 1;
        }

        if (this.totalPages - after < this.currentPage) {
            return this.range(this.totalPages - this.size + 1, this.size);
        }

        let start = this.currentPage - before;

        return this.range(start, this.size);
    }

    /**
     * 開始値と長さを受け取り配列を生成する
     * @param start
     * @param length
     * @returns {number[]}
     */
    private range(start: number, length: number): number[]
    {
        let arr = [];
        for (let i = start; i < start + length; i++) {
            arr.push(i);
        }

        return arr;
    }

}
```

## pager.component.html

`ul`、`li`を用いたベーシックなpagerテンプレート。
現在位置の`li`には`active`クラスを付与、また`a`を`span`に変更。

```html:pager.component.html
<ul class="pagination">
    <li><a href="javascript:;" (click)="prev()">&laquo;</a></li>

    <li *ngFor="let page of displayPages" [ngClass]="{'active': page === currentPage}">
        <a href="javascript:;" *ngIf="page !== currentPage" (click)="jump(page)">{{ page }}</a>
        <span *ngIf="page === currentPage">{{ page }}</span>
    </li>

    <li><a href="javascript:;" (click)="next()">&raquo;</a></li>
</ul>
```

# 使い方

使う側のコンポーネントは例えばこんな感じ。

## lists.component.ts

```ts:lists.component.ts
import { Component, OnInit } from '@angular/core';
import { PagerComponent } from './pager.component';

@Component({
    selector: 'lists',
    templateUrl: '/lists.component.html',
    directives: [
        PagerComponent
    ]
})
export class ListsComponent implements OnInit {

    public page: number;

    public totalItemCount: number;

    public constructor() {}

    public ngOnInit()
    {
        // 例えばapiよりデータを取得してきたとする
        setTimeout(() => {
            // ページ番号をセット
            this.page = 1;
            // 全アイテム件数をセット
            this.totalItemCount = 123;
        }, 1000);
    }

    public update(page: number)
    {
        // ページャーを操作した際の処理を記述
        alert(`現在${page}ページです`);
    }

}
```

## lists.component.html

`currentPage`、`totalItemCount`は必須。`perPage`、`size`は任意。
`pageChanged`にページ変更時に実行したい関数を渡す。`$event`でページ番号を受け取れる。

```html:lists.component.html
<pager
    *ngIf="totalItemCount"
    [currentPage]="page"
    [totalItemCount]="totalItemCount"
    [perPage]="10"
    (pageChanged)="update($event)"></pager>
```