Comment gérer la pagination avec Knockout

J'ai un div qui est configuré pour se lier à un observeableArray , mais je veux seulement montrer au plus 50 éléments de cet observeableArray à un moment donné. Je veux gérer cela avec la pagination avec un bouton précédent et suivant avec des indices sur la page pour permettre aux utilisateurs de parcourir les pages d'éléments de la collection.
Je sais que je pourrais probablement le faire avec un computedObservable et une liaison de données personnalisée, mais je ne sais pas comment le faire (je suis encore un néophyte Knockout).
Est-ce que quelqu'un peut-il me montrer la bonne direction?

Voici mon code (le JS est dans 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(); } } } 

La pagination est assez simple avec Knockout. Je le ferais personnellement ainsi:

  • Avoir un tableau observable contenant tous vos éléments
  • Avoir un observable contenant la page actuelle (initialisé à 0)
  • Avoir une variable déclarant le nombre d'éléments par page
  • Avoir calculé qui renvoie le nombre de pages, calculé grâce au nombre d'éléments par page et au nombre total d'éléments.
  • Enfin, ajoutez un calcul qui coupe le tableau contenant tous les éléments.

Compte tenu de cela, vous pouvez maintenant ajouter une fonction qui augmente (suivant) ou décrémente (précédente) la page en cours.

Voici un exemple rapide:

 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); } } } 

Vous trouverez ici un exemple simple et complet: http://jsfiddle.net/LAbCv/ (peut-être un peu buggy, mais l'idée est là).

J'ai créé un blogpost avec des explications détaillées sur la façon de créer une pagination à l'aide d'un petit plugin JQuery ( ici ).

Fondamentalement, j'ai utilisé la liaison de données knock-out normal avec AJAX et après que les données ont été extraites du serveur, j'appelle le plugin. Vous pouvez trouver le plugin ici . C'est ce qu'on appelle la pagination simple .

En fait, je travaille sur le site Web, qui a beaucoup de tables (la plupart d'entre elles ont besoin de pagination), alors j'ai vraiment besoin d'un reusable-component pour la pagination pour l'utiliser dans tous les cas dont j'ai besoin de pagination.
Aussi, j'avais besoin de fonctionnalités plus avancées que celles fournies dans la réponse acceptée de cette question.

J'ai donc développé mon propre composant pour résoudre ce problème, voilà.

Maintenant sur Github

JsFiddle

Et pour plus de détails, continuez à lire

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> 

Caractéristiques

  1. Montre sur le besoin
    Lorsqu'il n'y a pas besoin de paginer du tout (par exemple, les éléments qui doivent afficher moins que la taille de la page), le composant HTML disparaîtra.
    Ceci sera établi par instruction data-bind="visible: NeedPaging" .

  2. Désactiver le besoin
    Par exemple, si vous avez déjà sélectionné la dernière page, pourquoi la last page ou le bouton Next devraient-ils être disponibles pour appuyer sur?
    Je suis en train de gérer cela et dans ce cas, je désactive ces boutons en appliquant la liaison suivante data-bind="css: { disabled: !PreviousPageActive() }"

  3. Distinguer la page sélectionnée
    Une classe spéciale (dans ce cas, appelée classe active ) est appliquée sur la page sélectionnée, afin de faire savoir à l'utilisateur dans quelle page il est en ce moment.
    Ceci est établi par la liaison data-bind="css: { active: $parent.CurrentPage() === $data }"

  4. Derniers seront les premiers
    Aller à la première et dernière page est également disponible par des boutons simples dédiés à cela.

  5. Limites pour les boutons affichés
    Supposons que vous avez beaucoup de pages, par exemple 1000 pages, alors qu'est-ce qui se passera? Les afficheriez-vous tous pour l'utilisateur? Absolument, vous n'avez qu'à afficher quelques-uns d'entre eux en fonction de la page actuelle. Par exemple en affichant 3 pages avant la page et d'autres 3 pages après la page sélectionnée.
    Cette affaire a été traitée ici <!-- ko foreach: GetPages() -->
    GetPages fonction GetPages applique un algorithme simple pour déterminer si nous devons afficher toutes les pages (le nombre de pages est sous le seuil, qui peut être déterminé facilement), ou pour afficher quelques-uns des boutons.
    Vous pouvez déterminer le seuil en modifiant la valeur de la variable maxPageCount
    À l'heure actuelle, je l'ai assigné comme suit var maxPageCount = 7; Ce qui signifie que pas plus de 7 boutons pourraient être affichés pour l'utilisateur (3 avant le SelectedPage et 3 après la page sélectionnée) et la page sélectionnée elle-même.

    Vous vous demandez peut-être, et s'il n'y avait pas assez de pages après OR avant que la page actuelle ne s'affiche? Ne t'inquiète pas, je manipule ceci dans l'algorithme
    Par exemple, si vous avez 11 pages et que vous avez maxPageCount = 7 et que la selected page is 10 actuelle selected page is 10 , les pages suivantes seront affichées

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

    Nous maxPageCount toujours stratifiant le maxPageCount , dans l'exemple précédent montrant 5 pages avant la page sélectionnée et seulement 1 page après la page sélectionnée.

  6. Validation de la page sélectionnée
    Toutes les opérations définies pour la CurrentPage qui déterminent la page sélectionnée par l'utilisateur , sont passées par la fonction SetCurrentPage . Dans cette seule fonction, nous définissons cette valeur observable et, comme vous pouvez le voir à partir du code, avant de définir la valeur, nous effectuons des opérations de validation pour nous assurer que nous n'allons pas au-delà de la page disponible des pages.

  7. Déjà propre
    J'utilise uniquement des propriétés pureComputed non computed , ce qui signifie que vous n'avez pas besoin de vous ennuyer avec le nettoyage et l'élimination de ces propriétés. Bien que, comme vous le verrez dans l'exemple ci-dessous, vous devez disposer d'autres abonnements qui sont en dehors du composant lui-même

NOTE 1
Vous remarquerez peut-être que j'utilise certaines classes de bootstrap dans ce composant, cela convient à moi, mais bien sûr, vous pouvez utiliser vos propres classes au lieu des classes bootstrap.
Les classes bootstrap que j'ai utilisées ici sont la pagination , la pagination-sm , active et disabled
N'hésitez pas à les changer selon vos besoins.

NOTE 2
J'ai donc présenté le composant pour vous, il est temps de voir comment cela pourrait fonctionner.
Vous intégreriez ce composant dans votre ViewModel principal comme ceci.

 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 change 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 allow 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(); } } 

Last but not least , Bien sûr, n'oubliez pas de modifier la liaison dans le composant html selon votre modèle de vue spécial, ou d'enrouler tout le composant avec le with binding

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

À votre santé