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 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 .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 .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" 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 » .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" 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 » .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.

