3 Comments

Creating a Pagination Component with Ember.js and Foundation

For my current Ember.js project, I found myself needing some pagination controls. Thankfully,Zurb Foundation provides me some markup and CSS to base my pagination controls on, so I was free to focus more on the functionality. Essentially, all I needed was a little widget with three properties:

  • current page number
  • total number of pages
  • optionally, the maximum number of pages to display before the list is truncated

This looked like the perfect use case for an Ember Component. I wouldn’t even need to have the component trigger any actions, because anything using it could simply observe changes to the current page property! Let’s take a look at how I solved this.

Displaying a List of Clickable Page Numbers

To begin, I created a component with two properties: currentPage and totalPages. In order to actually display a list from this, I created a computed property named pageItems that would generate some objects to drive the UI. I also added an action handler to support selecting a new page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LabCompass.PageNumbersComponent = Ember.Component.extend
 
  currentPage: null
  totalPages: null
 
  pageItems: (->
    currentPage = Number @get "currentPage"
    totalPages = Number @get "totalPages"
 
    for pageNumber in [1..totalPages]
      page: pageNumber
      current: currentPage == pageNumber
  ).property "currentPage", "totalPages"
 
  actions:
    pageClicked: (number) ->
      @set "currentPage", number

Then, my user interface was as simple as iterating over this list and displaying some markup. I’d recommend you take a look at the documentation for Foundation’s pagination to have a better idea of why things are being structured as they are.

1
2
3
4
5
6
7
.pagination-centered
  ul.pagination
    each item in pageItems
      if item.current
        li.current: a = item.page
      else
        li: a click="pageClicked item.page" = item.page

You may notice that I’m not using Ember objects to represent the individual pages. This is because I’m returning a new array each time the property is recomputed after being invalidated by its dependent keys, causing everything in the template to re-render. Since I don’t see any other reason why I’d be updating the individual pageItem objects, I left them as regular javascript objects, which the template can handle just fine.

Using the component is easy:

1
2
3
.row
  .small-12.column
    page-numbers currentPage=myCurrentPage totalPages=myTotalPages

Adding Buttons for Previous/Next

The next step was adding some arrow controls for stepping forward or backwards in the list of pages. This was easy enough; add some markup for the arrows and a couple of action handlers. I also wanted to have them reflect to the user whether they are currently clickable, so I added two computed properties (canStepForward, canStepBackward) to inform the template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LabCompass.PageNumbersComponent = Ember.Component.extend
 
  currentPage: null
  totalPages: null
 
  pageItems: (->
    currentPage = Number @get "currentPage"
    totalPages = Number @get "totalPages"
 
    for pageNumber in [1..totalPages]
      page: pageNumber
      current: currentPage == pageNumber
  ).property "currentPage", "totalPages"
 
  canStepForward: (->
    page = Number @get "currentPage"
    totalPages = Number @get "totalPages"
    page < totalPages
  ).property "currentPage", "totalPages"
 
  canStepBackward: (->
    page = Number @get "currentPage"
    page > 1
  ).property "currentPage"
 
  actions:
    pageClicked: (number) ->
      @set "currentPage", number
 
    stepForward: ->
      @incrementProperty "page"
 
    stepBackward: ->
      @decrementProperty "page"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.pagination-centered
  ul.pagination
    if canStepBackward
      li.arrow: a click="stepBackward" &laquo;
    else
      li.arrow.unavailable: a &laquo;
    each item in pageItems
      if item.current
        li.current: a = item.page
      else
        li: a click="pageClicked item.page" = item.page
    if canStepForward
      li.arrow: a click="stepForward" &raquo;
    else
      li.arrow.unavailable: a &raquo;

Truncating the Full List of Pages

Now things are looking good and working well. There’s one caveat, though: if there are a very large number of pages, things become a bit unwieldy. The next step is to specify a maximum number of pages to display, after which the clickable numbers get truncated and ellipses are inserted.

I decided to fix this by specifying a maximum number of options to display. If the number of pages exceeded this, then I would intelligently trim out some options and replace them with ellipses. This was easily the most complicated part of the entire process. E.g., if you’ve selected page number 4, it’s unlikely you want to see page 2 or 3 truncated out of the list. Similarly for the last few pages.

The solution I came up with was to take the array of objects we were already generating, then strip out and replace excess items with an ellipses marker. I also added a new property, maxPagesToDisplay, that controls when the page numbers will begin to be truncated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
LabCompass.PageNumbersComponent = Ember.Component.extend
 
  currentPage: null
  totalPages: null
 
  maxPagesToDisplay: 11 #should be odd
 
  pageItems: (->
    currentPage = Number @get "currentPage"
    totalPages = Number @get "totalPages"
    maxPages = Number @get "maxPagesToDisplay"
    # ensure that maxPages is odd
    maxPages += 1 - maxPages % 2
 
    pages = for pageNumber in [1..totalPages]
      ellipses: false
      page: pageNumber
      current: currentPage == pageNumber
 
    if pages.length > maxPages
      # determine position in truncated array (1 to max)
      positionOfCurrent = ((maxPages - 1) / 2) + 1
      # does the position need to be shifted left?
      if positionOfCurrent > currentPage
        positionOfCurrent = currentPage
      # does the position need to be shifted right?
      if (totalPages - currentPage) < (maxPages - positionOfCurrent)
        positionOfCurrent = maxPages - (totalPages - currentPage)
 
      # if distance from max is greater than delta of values, truncate
      if (totalPages - currentPage) > (maxPages - positionOfCurrent)
        maxDistance = maxPages - positionOfCurrent
        overspill = totalPages - currentPage - maxDistance
        toRemove = overspill + 1
        idx = totalPages - 1 - toRemove
        pages.replace idx, toRemove, [
          ellipses: true
        ]
 
 
      # if distance from 1 is greater than delta of values, truncate
      if currentPage > positionOfCurrent
        maxDistance = positionOfCurrent
        overspill = currentPage - positionOfCurrent
        toRemove = overspill + 1
        idx = 1
        pages.replace idx, toRemove, [
          ellipses: true
        ]
 
    pages
  ).property "currentPage", "totalPages", "maxPagesToDisplay"
 
  canStepForward: (->
    page = Number @get "currentPage"
    totalPages = Number @get "totalPages"
    page < totalPages
  ).property "currentPage", "totalPages"
 
  canStepBackward: (->
    page = Number @get "currentPage"
    page > 1
  ).property "currentPage"
 
  actions:
    pageClicked: (number) ->
      @set "currentPage", number
 
    stepForward: ->
      @incrementProperty "currentPage"
 
    stepBackward: ->
      @decrementProperty "currentPage"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  .pagination-centered
    ul.pagination
      if canStepBackward
        li.arrow: a click="stepBackward" &laquo;
      else
        li.arrow.unavailable: a &laquo;
      each item in pageItems
        if item.ellipses
          li.unavailable: a &hellip;
        else
          if item.current
            li.current: a = item.page
          else
            li: a click="pageClicked item.page" = item.page
      if canStepForward
        li.arrow: a click="stepForward" &raquo;
      else
        li.arrow.unavailable: a &raquo;

This could definitely be done more efficiently. The best way would be to generate only items needed, deciding beforehand which ranges to cut out. Frankly, the performance appears to be fine, so I’m willing to leave things as-is until I notice a good reason to improve it.

Wrap-up

I’m really quite happy with how this turned out — largely due to Ember’s awesomeness. Ember’s computed properties and bindings are excellent. Without them, the implementation of a pagination control couldn’t have been so easy or fast to create, and it couldn’t have provided such a simple interface to its client code.