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:
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.
.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:
.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.
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"
.pagination-centered
ul.pagination
if canStepBackward
li.arrow: a click="stepBackward" «
else
li.arrow.unavailable: a «
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" »
else
li.arrow.unavailable: a »
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.
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"
.pagination-centered
ul.pagination
if canStepBackward
li.arrow: a click="stepBackward" «
else
li.arrow.unavailable: a «
each item in pageItems
if item.ellipses
li.unavailable: a …
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" »
else
li.arrow.unavailable: a »
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.
Thanks a lot for sharing! A live demo or a jsbin would be really helpful :) So we can see the whole picture in action. Thanks a lot again.
thanks, and yes, a jsbin would be nice
I wrote an Ember-CLI addon inspired by this code.
https://github.com/mharris717/ember-cli-pagination
Nice article. Could you please show us how you integrated Foundation and Ember together? A github url will do the task. I’m in confused how to use Foundation with Ember 1.13 which deprecates the views altogether. Thanks.