web-dev-qa-db-ja.com

Knockoutでページネーションを処理する方法

observeableArrayにバインドするように設定されたdivがありますが、そのobserveableArrayから最大50項目のみを表示したい場合があります。ユーザーがコレクションのアイテムのページを循環できるように、ページのインデックスと一緒に「前へ」ボタンと「次へ」ボタンでページ分割してこれを処理したいと思います。
私はおそらくcomputedObservableとカスタムデータバインディングでこれを行うことができることを知っていますが、それを行う方法がわかりません(私はまだノックアウトの初心者です)。
誰かが私を正しい方向に向けることができますか?

これが私のコードです(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();
        }
    }
}
19
noname

Knockoutを使用すると、ページ分割は非常に簡単です。私は個人的にそれをこの方法で達成します:

  • すべての要素を含むobservableArrayがある
  • 現在のページを含むオブザーバブルを持っている(0に初期化)
  • ページごとの要素数を宣言する変数を用意する
  • ページあたりの要素数と要素の総数に基づいて計算された、ページ数を返す計算機を用意します。
  • 最後に、すべての要素を含む配列をスライスする計算結果を追加します。

そのため、現在のページをインクリメント(次へ)またはデクリメント(前へ)する関数を追加できます。

ここに簡単な例があります:

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/ (少しバグがあるかもしれませんが、アイデアはそこにあります)。

42
mael

実際、私は多くのテーブルがあるWebサイトで作業しています(ほとんどはページングが必要です)。
実際には、ページングが必要なすべてのケースで使用するために、ページングに_reusable-component_が必要でした。
また、この質問への回答で認められているよりも高度な機能が必要でした。

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

現在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コンポーネントは表示されなくなります。
    これは、statement _data-bind="visible: NeedPaging"_によって確立されます。

  2. 必要に応じて無効にする
    たとえば、すでに最後のページを選択している場合、なぜ_last page_またはNextボタンを押すことができるはずなのですか?
    私はこれを処理しており、その場合、次のバインディングを適用してこれらのボタンを無効にしますdata-bind="css: { disabled: !PreviousPageActive() }"

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

  4. 最後と最初
    最初と最後のページへの移動は、専用の簡単なボタンでも実行できます。

  5. 表示されるボタンの制限
    1000ページなど、多くのページがあるとすると、どうなりますか?それらをすべてユーザーに表示しますか? 絶対にありません現在のページに従ってそれらのいくつかだけを表示する必要があります。たとえば、選択したページの前の3ページと後の3ページを表示します。
    このケースはここで処理されました<!-- ko foreach: GetPages() -->
    すべてのページを表示する必要があるかどうかを判断するために単純なアルゴリズムを適用するGetPages関数(ページ数がしきい値を下回っており、簡単に判断できる)、または一部のみを表示するボタン。
    maxPageCount変数の値を変更することで、しきい値を決定できます
    今、次の_var maxPageCount = 7;_として割り当てました。つまり、ユーザー(SelectedPageの前に3つ、Selected Pageの後に3つ)とSelected Page自体に表示できるボタンは7つまでです。

    疑問に思うかもしれませんが、現在のページの前に[〜#〜]または[〜#〜]の後に十分なページがなかった場合表示する? 心配しないでください私はこれをアルゴリズムで処理しています
    たとえば、_11 pages_があり、_maxPageCount = 7_と現在の_selected page is 10_がある場合、次のページが表示されます

    5,6,7,8,9,10(selected page),11

    そのため、選択したページの前に_5_ページを表示し、選択したページの後に_1_ページを表示する前の例では、常にmaxPageCountを階層化しています。

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

  7. すでにきれい
    私はpureComputedプロパティのみを使用しており、computedプロパティは使用していません。つまり、これらのプロパティのクリーニングや破棄に煩わされる必要はありません。 ただし、以下の例でわかるように、コンポーネント自体の外部にある他のサブスクリプションを破棄する必要があります

注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();
    }
}
_

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

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

乾杯

9
Hakan Fıstık

この質問は、依然として「ノックアウトページネーション」の上位の検索の1つなので、ノックアウト拡張機能 knockout-paginggit )は言及する価値があります。
これは、ko.observableArray。十分に文書化されており、使いやすいです。
使用例は here です。

1
Eli Algranti

小さなJQueryプラグイン( ここ )を使用してページネーションを作成する方法の詳細な説明をブログ記事に作成しました。

基本的に、私はAJAXで通常のノックアウトデータバインディングを使用しており、サーバーからデータが取得された後、プラグインを呼び出します。プラグインを見つけることができます ここ 。それはSimple Paginationと呼ばれます。

1