【问题标题】:How to handle pagination with Knockout如何使用 Knockout 处理分页
【发布时间】:2013-07-14 03:48:32
【问题描述】:

我有一个设置为绑定到 observeableArray 的 div,但我只想在任何给定时间显示来自该 observeableArray 的最多 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();
        }
    }
}

【问题讨论】:

    标签: javascript knockout.js pagination typescript


    【解决方案1】:

    使用 Knockout 进行分页非常简单。我个人会以这种方式实现它:

    • 拥有一个包含所有元素的 observableArray
    • 拥有一个包含当前页面的 observable(初始化为 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/(可能有点错误,但想法就在那里)。

    【讨论】:

    • 这太棒了.. 谢谢!我已经扩展它以显示每个页码,并允许用户从第 5 页单击到第 1 页。jsfiddle.net/LAbCv/31
    • 删除后半页对吗? Math.ceil 应该在页数计算中,而不是 Math.floor?编辑:啊,你重新添加余数/模数,我只需要把它加起来就可以了。
    • 如果您正在处理可能具有相当大结果集的任何事物,请注意这种方法。在客户端加载整个结果列表然后只在 javascript 中进行页面加载不是一个好主意。对于大型结果集,您需要某种仅加载当前页面所需数据的 AJAX 解决方案。
    【解决方案2】:

    我创建了一篇博文,详细解释了如何借助一个小 JQuery 插件 (here) 创建分页。

    基本上,我使用 AJAX 的普通敲除数据绑定,在从服务器检索数据后,我调用插件。你可以找到插件here。它被称为简单分页

    【讨论】:

      【解决方案3】:

      实际上,我正在一个网站上工作,该网站有很多表格(其中大多数需要分页)。
      所以实际上,我需要一些 reusable-component 用于分页,以便在我需要分页的所有情况下使用它.
      此外,我需要比这个问题的已接受答案中提供的更高级的功能。

      所以我开发了自己的组件来解决这个问题,就是这样。

      Now on 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 组件将消失。
        这将由 statementdata-bind="visible: NeedPaging" 建立。

      2. 按需禁用
        例如,如果您已经选择了最后一页,为什么应该可以按下last pageNext 按钮?
        我正在处理这个问题,在这种情况下,我将通过应用以下绑定来禁用这些按钮 data-bind="css: { disabled: !PreviousPageActive() }"

      3. 区分所选页面
        一个特殊的类(在本例中称为active 类)应用于所选页面,以使用户知道他/她现在在哪个页面中。
        这是通过绑定data-bind="css: { active: $parent.CurrentPage() === $data }"

      4. 建立的
      5. 最后和第一个
        也可以通过专用于此的简单按钮访问第一页和最后一页。

      6. 显示按钮的限制
        假设你有很多页,例如 1000 页,那么会发生什么?你会为用户显示它们吗? 绝对不是你必须根据当前页面显示其中的几个。例如,显示所选页面之前的 3 页和之后的其他 3 页。
        此案已在此处理&lt;!-- ko foreach: GetPages() --&gt;
        GetPages 函数应用一个简单的算法来确定我们是否需要显示所有页面(页面计数低于阈值,这很容易确定),或者只显示一些按钮。
        您可以通过更改 maxPageCount 变量的值来确定阈值
        现在,我将其分配为以下var maxPageCount = 7;,这意味着不能为用户显示超过 7 个按钮(SelectedPage 之前 3 个,Selected Page 之后 3 个)和 Selected Page 本身。

        您可能想知道,如果当前页面之前 OR 之后没有足够的页面显示怎么办? 别担心我在算法中处理这个
        例如,如果你有11 pages,你有maxPageCount = 7和当前的selected page is 10,那么会显示以下页面
        5,6,7,8,9,10(selected page),11

        所以我们总是对maxPageCount进行分层,在前面的示例中,在所选页面之前显示5页面,而在所选页面之后仅显示1页面。

      7. 选定页面验证
        CurrentPage observable确定用户选择的页面的所有设置操作都通过函数SetCurrentPage。仅在这个函数中我们设置了这个 observable,从代码中可以看出,在设置值之前我们会进行验证操作,以确保我们不会超出页面的可用页面。

      8. 已经干净了
        我只使用pureComputed 而不是computed 属性,这意味着您无需费心清理和处理这些属性。 尽管您将在下面的示例中看到,您需要处理组件本身之外的一些其他订阅

      注意 1
      你可能注意到我在这个组件中使用了一些bootstrap 类, 这对我来说很合适,但是,当然,你可以使用自己的类而不是引导类。
      我在这里使用的引导类是paginationpagination-smactivedisabled
      您可以根据需要随意更改它们。

      注意 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>
      

      干杯

      【讨论】:

      • 我没有使用可重用组件的概念,但 knockout.js 中的代码对我来说非常有效。谢谢!
      【解决方案4】:

      这个问题仍然是“淘汰赛分页”的热门搜索之一,因此淘汰赛扩展knockout-paginggit)值得一提。
      它通过扩展ko.observableArray 来提供分页。它有据可查且易于使用。
      用法示例为here

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-07
        • 1970-01-01
        • 2015-03-18
        • 2019-05-23
        • 2016-08-29
        • 2014-02-21
        • 2021-10-06
        相关资源
        最近更新 更多