読者です 読者をやめる 読者になる 読者になる

M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

UI BootstrapのPaginationの現在ページが強制リセットされる

stackoverflowのこの記事に載っている問題で半日を無駄にしてしまいました。

UI BootstrapのPaginationディレクティブの実装上の問題で──というか実際のところこのようにしか実装しようがないと思いますが──ページ番号をクエリ文字列などで永続化させていても、F5リロードを含む画面初期表示時に現在ページ番号の値が強制的に1にリセットされてしまうという問題です。

原因はPaginationディレクティブにおける「すべてのアイテム数とページあたりアイテム数から、可能な現在のページ番号」を求めるロジックで、画面初期表示の「すべてのアイテム数」も「ページあたりアイテム数」も0の状態では「可能な現在のページ番号」は1になってしまうというもの。

So I found a solution after drilling down into the angular-bootstrap code. Their code has a watch on totalPages that checks if the current page is greater than the totalPages value.(UI Bootstrapのコードを掘り下げていったところ原因がわかった。そのコードでは「現在ページ番号」が「合計ページ数」を超過しないよう監視している)

angular-bootstrap code:

if ( $scope.page > value ) {
  $scope.selectPage(value);
} else {
  ngModelCtrl.$render();
}

What was happening was if I refreshed the page on page 3 (for example) my controller was reloading the items for that page temporarily causing total-items to be 0 and totalPages to be calculated as 1. This triggered the watch and the above code.(そこで何が起きていたかというと、まず私が〔例えば〕3ページ目で画面のリロードを行う、するとコントローラがページ上の項目をリロードする、まさにその時「合計アイテム数」が0となって、計算の結果「合計ページ数」は1であるということになってしまう。ここで上述の監視コードが作動して「現在ページ番号」を強制的に1にしてしまう)

この記事にはいろいろな解法が乗っていますが、私の開発中のアプリケーションで実際に効力があったのは以下の方法:

手順1:外側スコープで$scopeで「すべてのアイテム数」と「ページあたりアイテム数」を初期化しておく。外側スコープの$scopeは内側スコープの$scopeprototypeとして継承するオブジェクトであり、$scope.$parentとしてもアクセスできる。

// 外側スコープのためのコントローラ
...
.controller('outer', function($scope) {
    $scope.totalItems = Number.MAX_VALUE; // すべてのアイテム数
    $scope.itemsPerPage = 25; // ページあたりアイテム数
})

手順2:内側スコープにPaginationディレクティブを配置し「すべてのアイテム数」と「ページあたりアイテム数」を$scopeを通じて参照させる。これらの値は内側スコープのディレクティブが初期化されるまえにすでに初期化済みなので、前述の「可能な現在のページ番号」の算出の際に利用される。「すべてのアイテム数」にはデフォルト値として十分に大きな数値(整数として表現可能な最大値)が設定されているため、結果として画面初期表示時に「現在ページ番号」がリセットされてしまう問題は回避できる。

<uib-pagination total-items="totalItems"
	ng-model="page"
	ng-change="pageChange()"
	max-size="5"
	class="pagination-sm"
	boundary-links="true"
	items-per-page="itemsPerPage"
	first-text="最初"
	previous-text="前"
	next-text="次"
	last-text="最後"
></uib-pagination>
// 内側スコープのためのコントローラ
...
.controller('inner', function($scope, $location, $http) {
    // ここでURLのクエリ文字列に永続化されたページ情報を取得し$scopeに設定
    $scope.page = ...;
    // コールバックを作成・登録して現在ページ番号が変化したときURLに反映されるようにする
    $scope.pageChange = function() { ... };
    // クエリ文字列の変化をトリガーに新しいページのための一覧を画面表示するロジックを用意する
    $scope.$watch(function () { return $location.search().page; },
        function (page) { $scope.list = $http. ...; });
}

前述のとおり内側スコープの$scopeは外側スコープのそれに含まれるメンバをprototype継承している状態。$scope.totalItemsという形式で値を参照すると外側スコープで初期化済みの値が得られる。一方$scope.totalItems = 3のような値の設定を行うと内側スコープの$scopeに新規にメンバが追加され、外側スコープのそれを覆い隠す(シャドウ化する)。これにより外側スコープの初期化済みの値はデフォルト値として変更されずに保たれる。