ウェブサイト検索

javascript - Knockoutでページネーションをどのように扱うか


私は、observableArray にバインドするように設定された div を持っていますが、任意の時点でその observableArray から最大 50 件のアイテムだけを表示したいです。これを、前へ・次へボタンとページ上のインデックスを使ってページネーションで処理し、ユーザーがコレクションのアイテムをページごとに切り替えられるようにしたいと考えています。
おそらく computedObservable とカスタムデータバインディングでこれを実装できると思うのですが、やり方がよくわかりません(まだ Knockout の初心者です)。
どなたか正しい方向に導いてくれませんか?

こちらが私のコードです(JSはTypeScriptです):

<div class="container-fluid">
    <div class="row-fluid">
        <div class="span12">
            <%=
            if params[:q]
              render 'active_search.html.erb'
            else
              render 'passive_search.html.erb'
            end
            %>
            <%= form_tag("/search", method: "get", :class => "form-search form-inline") do %>
            <%= label_tag(:q, "Search for:") %>
            <%= text_field_tag(:q, nil, class:"input-medium search-query") %>
            <%= submit_tag("Search", :class=>"btn") %>
            <% end %>

            <div class="media" data-bind="foreach: tweetsArray">
                <%= image_tag('twitter-icon.svg', :class=>"tweet_img", :style=>"display:inline;") %>
                <div class="media-body" style="display:inline;">
                    <h4 class="media-heading" data-bind="text: user.screen_name" style="display:inline;"></h4>
                    <span data-bind="text:text" style="display:inline;"></span> <br />
                    <span data-bind="text:'Created at '+created_at"></span> <br />
                </div>
            </div>

            <div class="pagination pagination-centered">
                <ul>
                    <li>
                        <a href="#">Prev</a>
                    </li>
                    <li>
                        <a href="#">1</a>
                    </li>
                    <li>
                        <a href="#">Next</a>
                    </li>
                </ul>
            </div>

        </div>
    </div>
</div>

<script>
    var viewModel = new twitterResearch.TweetViewModel();
    ko.applyBindings(viewModel);

    //TODO: notes to self, use custom binding for pagination along with a computed observable to determine where at in the list you are

    //document.onReady callback function
    $(function() {
        $.getJSON('twitter', {}, function(data) {
            viewModel.pushTweet(data);
            console.log(data.user);
        });
    });
</script>

declare var $: any;
declare var ko: any;

module twitterResearch {
    class Tweet {
        text: string;
        created_at: string;
        coordinates: string;
        user: string;
        entities: string;
        id: number;
        id_str: string;

        constructor(_text: string, _created_at: string, _coordinates: any, _user: any,
                    _entities: any, _id_str: string, _id: number){

            this.text = _text;
            this.created_at = _created_at;
            this.coordinates = _coordinates;
            this.user = _user;
            this.entities = _entities;
            this.id_str = _id_str;
            this.id = _id;
        }
    }

    export class TweetViewModel{

        tweetsArray: any;
        constructor()
        {
            this.tweetsArray = ko.observableArray([]);
        }

        //tweet is going to be the JSON tweet we return
        //from the server
        pushTweet(tweet)
        {
            var _tweet = new Tweet(tweet.text, tweet.created_at, tweet.coordinates,
                                    tweet.user, tweet.entities, tweet.id_str, tweet.id);
            this.tweetsArray.push(_tweet);
            this.tweetsArray.valueHasMutated();
        }
    }
}

回答 #1

Knockoutを使ったページネーションは非常に簡単です。私なら個人的にこのように実現します:

  • すべての要素を含む observableArray を持つ
  • 現在のページを含むオブザーバブルを持つ(初期値は0)
  • ページごとの要素数を宣言する変数を持つ
  • 1ページあたりの要素数と要素の総数に基づいて計算される、ページ数を返すコンピューテッドを持つ。
  • 最後に、すべての要素を含む配列をスライスする計算済みプロパティを追加します。

それを踏まえて、現在のページを増加(次へ)または減少(前へ)させる関数を追加することができます。

こちらは簡単な例です:

var Model = function() {
    var self = this;
    this.all = ko.observableArray([]);
    this.pageNumber = ko.observable(0);
    this.nbPerPage = 25;
    this.totalPages = ko.computed(function() {
        var div = Math.floor(self.all().length / self.nbPerPage);
        div += self.all().length % self.nbPerPage > 0 ? 1 : 0;
        return div - 1;
    });

    this.paginated = ko.computed(function() {
        var first = self.pageNumber() * self.nbPerPage;
        return self.all.slice(first, first + self.nbPerPage);
    });

    this.hasPrevious = ko.computed(function() {
        return self.pageNumber() !== 0;
    });

    this.hasNext = ko.computed(function() {
        return self.pageNumber() !== self.totalPages();
    });

    this.next = function() {
        if(self.pageNumber() < self.totalPages()) {
            self.pageNumber(self.pageNumber() + 1);
        }
    }

    this.previous = function() {
        if(self.pageNumber() != 0) {
            self.pageNumber(self.pageNumber() - 1);
        }
    }
}

ここで、シンプルで完全な例が見つかります: http://jsfiddle.net/LAbCv/ (少しバグがあるかもしれませんが、考え方は示されています)。

回答 #2

実際、私は多くのテーブル(ほとんどがページングを必要とするもの)を含むウェブサイトに取り組んでいます。
ですので、実際には、ページングが必要なすべてのケースで使用するための再利用可能なコンポーネントが必要でした。
また、この質問の承認された回答で提供されているものよりも高度な機能も必要でした。

そこで、この問題を解決するために自分のコンポーネントを開発しました。こちらです。

現在Githubで

JsFiddle

さらに詳しい情報については、(ここではなくGitHubからコードを取得することを検討してください。GitHubのコードは、ここに置いた後に更新および強化されています) を読み続けてください

JavaScript

function PagingVM(options) {
    var self = this;

    self.PageSize = ko.observable(options.pageSize);
    self.CurrentPage = ko.observable(1);
    self.TotalCount = ko.observable(options.totalCount);

    self.PageCount = ko.pureComputed(function () {
        return Math.ceil(self.TotalCount() / self.PageSize());
    });

    self.SetCurrentPage = function (page) {
        if (page < self.FirstPage)
            page = self.FirstPage;

        if (page > self.LastPage())
            page = self.LastPage();

        self.CurrentPage(page);
    };

    self.FirstPage = 1;
    self.LastPage = ko.pureComputed(function () {
        return self.PageCount();
    });

    self.NextPage = ko.pureComputed(function () {
        var next = self.CurrentPage() + 1;
        if (next > self.LastPage())
            return null;
        return next;
    });

    self.PreviousPage = ko.pureComputed(function () {
        var previous = self.CurrentPage() - 1;
        if (previous < self.FirstPage)
            return null;
        return previous;
    });

    self.NeedPaging = ko.pureComputed(function () {
        return self.PageCount() > 1;
    });

    self.NextPageActive = ko.pureComputed(function () {
        return self.NextPage() != null;
    });

    self.PreviousPageActive = ko.pureComputed(function () {
        return self.PreviousPage() != null;
    });

    self.LastPageActive = ko.pureComputed(function () {
        return (self.LastPage() != self.CurrentPage());
    });

    self.FirstPageActive = ko.pureComputed(function () {
        return (self.FirstPage != self.CurrentPage());
    });

    // this should be odd number always
    var maxPageCount = 7;

    self.generateAllPages = function () {
        var pages = [];
        for (var i = self.FirstPage; i <= self.LastPage() ; i++)
            pages.push(i);

        return pages;
    };

    self.generateMaxPage = function () {
        var current = self.CurrentPage();
        var pageCount = self.PageCount();
        var first = self.FirstPage;

        var upperLimit = current + parseInt((maxPageCount - 1) / 2);
        var downLimit = current - parseInt((maxPageCount - 1) / 2);

        while (upperLimit > pageCount) {
            upperLimit--;
            if (downLimit > first)
                downLimit--;
        }

        while (downLimit < first) {
            downLimit++;
            if (upperLimit < pageCount)
                upperLimit++;
        }

        var pages = [];
        for (var i = downLimit; i <= upperLimit; i++) {
            pages.push(i);
        }
        return pages;
    };

    self.GetPages = ko.pureComputed(function () {
        self.CurrentPage();
        self.TotalCount();

        if (self.PageCount() <= maxPageCount) {
            return ko.observableArray(self.generateAllPages());
        } else {
            return ko.observableArray(self.generateMaxPage());
        }
    });

    self.Update = function (e) {
        self.TotalCount(e.TotalCount);
        self.PageSize(e.PageSize);
        self.SetCurrentPage(e.CurrentPage);
    };

    self.GoToPage = function (page) {
        if (page >= self.FirstPage && page <= self.LastPage())
            self.SetCurrentPage(page);
    }

    self.GoToFirst = function () {
        self.SetCurrentPage(self.FirstPage);
    };

    self.GoToPrevious = function () {
        var previous = self.PreviousPage();
        if (previous != null)
            self.SetCurrentPage(previous);
    };

    self.GoToNext = function () {
        var next = self.NextPage();
        if (next != null)
            self.SetCurrentPage(next);
    };

    self.GoToLast = function () {
        self.SetCurrentPage(self.LastPage());
    };
}

HTML

<ul data-bind="visible: NeedPaging" class="pagination pagination-sm">
    <li data-bind="css: { disabled: !FirstPageActive() }">
        <a data-bind="click: GoToFirst">First</a>
    </li>
    <li data-bind="css: { disabled: !PreviousPageActive() }">
        <a data-bind="click: GoToPrevious">Previous</a>
    </li>

    <!-- ko foreach: GetPages() -->
    <li data-bind="css: { active: $parent.CurrentPage() === $data }">
        <a data-bind="click: $parent.GoToPage, text: $data"></a>
    </li>
    <!-- /ko -->

    <li data-bind="css: { disabled: !NextPageActive() }">
        <a data-bind="click: GoToNext">Next</a>
    </li>
    <li data-bind="css: { disabled: !LastPageActive() }">
        <a data-bind="click: GoToLast">Last</a>
    </li>
</ul>

特徴

  1. 必要時に表示
    ページングが全く必要ない場合(例えば、表示するアイテムがページサイズより少ない場合)、HTMLコンポーネントは消えます。
    これは、data-bind="visible: NeedPaging" という記述で設定されます。

  2. 必要に応じて無効化
    例えば、すでに最後のページを選択している場合、なぜ 最後のページ や 次へ ボタンを押せる状態にしておく必要があるのでしょうか?
    私はこれを処理しており、その場合は次のバインディングを適用してこれらのボタンを無効化しています data-bind="css: { disabled: !PreviousPageActive() }"

  3. 選択されたページを区別する
    選択されたページには特別なクラス(この場合は active クラス)が適用され、ユーザーが現在どのページにいるかを認識できるようにします。
    これはバインディング data-bind="css: { active: $parent.CurrentPage() === $data }" によって実現されます。

  4. 最初と最後
    最初のページと最後のページに移動することも、これ専用の簡単なボタンで可能です。



  5. 現在のページの前後に表示するページが十分でなかったらどうするか疑問に思うかもしれません。心配しないでください、このアルゴリズムで処理しています
    例えば、11ページあり、maxPageCount=7 で、現在の 選択されたページが10 の場合、以下のページが表示されます。

    5,6,7,8,9,10(選択されたページ),11

    このように、常に maxPageCount を基準に分割しています。前の例では選択されたページの前に 5 ページ、後に 1 ページだけを表示しています。

  6. 選択されたページの検証
    ユーザーによって選択されたページを決定する CurrentPage オブザーバブルに対するすべての設定操作は、SetCurrentPage 関数を通じて行われます。この関数だけでオブザーバブルを設定し、コードからわかるように、値を設定する前に、ページの利用可能範囲を超えないように検証操作を行っています。

  7. すでにクリーン
    私は computed プロパティではなく pureComputed のみを使用しているため、これらのプロパティのクリーンアップや破棄に気を使う必要はありません。 ただし、以下の例でわかるように、コンポーネント自体の外にあるいくつかの他の購読は破棄する必要があります

注意 1
このコンポーネントでいくつかの bootstrap クラスを使用していることに気づくかもしれません。これは私にとって適していますが、もちろん、 bootstrap クラスの代わりにご自身のクラスを使用することもできます。
ここで使用した bootstrap クラスは pagination、pagination-sm、active、disabled です。
必要に応じて自由に変更してください。

注2
では、このコンポーネントを紹介しましたので、次はどのように動作するかを見てみましょう。
このコンポーネントをあなたのメインのViewModelに次のように統合します。

function MainVM() {
    var self = this;

    self.PagingComponent = ko.observable(new Paging({
        pageSize: 10,      // how many items you would show in one page
        totalCount: 100,   // how many ALL the items do you have.
    }));

    self.currentPageSubscription = self.PagingComponent().CurrentPage.subscribe(function (newPage) {
        // here is the code which will be executed when the user changes the page.
        // you can handle this in the way you need.
        // for example, in my case, I am requesting the data from the server again by making an ajax request
        // and then updating the component

        var data = /*bring data from server , for example*/
        self.PagingComponent().Update({

            // we need to set this again, why? because we could apply some other search criteria in the bringing data from the server, 
            // so the total count of all the items could change, and this will affect the paging
            TotalCount: data.TotalCount,

            // in most cases we will not change the PageSize after we bring data from the server
            // but the component allows us to do that.
            PageSize: self.PagingComponent().PageSize(),

            // use this statement for now as it is, or you have to made some modifications on the 'Update' function.
            CurrentPage: self.PagingComponent().CurrentPage(),
        });
    });

    self.dispose = function () {
        // you need to dispose the manual created subscription, you have created before.
        self.currentPageSubscription.dispose();
    }
}

最後になりましたが、必ず自分の特別な viewModel に従って HTML コンポーネントのバインディングを変更することを忘れないでください、またはすべてのコンポーネントをこのように with binding でラップしてください

<div data-bind="with: PagingComponent()">
    <!-- put the component here -->
</div>

乾杯

関連記事