This time, I would like to show you how to add a simple pagination
helper to your Ember.js
application.
For this example, I will be using Rails + Solr for the backend
and Ember.js as my frontend
framework.
I am doing this with Rails
and Solr
, but you can do it using other backend frameworks, as long as the JSON's response resembles what we have here:
{
"resource_collection":
[
{
"id": 1,
"page": 1,
"per_page": 5,
"total_entries": 15,
"total_pages": 3,
"resource_ids": [1,2,3,4,5]
}
],
"resources":
[
{
"id": 1,
"property": "value,
"property_2": "other value"
},
{
// each collection's element ...
}
]
}
Don't worry if this JSON
looks weird to you, its structure will be explained in a while.
I am considering that you already have a Rails
+ Ember.js
web application. So I would like to start showing you my Solr
setup.
Solr setup
First things first, if you don't have Solr
in your Rails
app, you must add the following gems to the Gemfile
:
gem 'sunspot_rails'
gem 'sunspot_solr'
Then run the following commands:
bundle install
bundle exec rails generate sunspot_rails:install
bundle exec rake sunspot:solr:start
The first command installs the gems. The second one sets up Solr
in your Rails
app. Finally, the third one starts the Solr
service.
If this is your first contact with Solr
, you can read more about it here
Adding the searchable
block to the Model to paginate
In my case, I wanted to paginate my Post
resource (model). So I added this code to my app/models/post.rb
:
searchable do
text :title, :content, :status
integer :id
integer :author_id
time :published_on
end
Creating the Solr
search model
The next thing I did, was to create a Solr
search model, this class is intended to return a paginated collection of Posts
. I called the file as app/models/post_search.rb
and here is the code:
class PostSearch
attr_accessor :page, :per_page, :filters
def initialize(params)
@page = params[:page] || 1
@per_page = params[:per_page] || 5
@filters = params[:filters] || {}
end
def search
Post.search do
paginate page: page, per_page: per_page
# Other search parameters... Irrelevant to this example
end
end
end
If you are interested in adding more customization to your Solr Search
model or knowing more about the Sunspot
gem, you can read this great blog post.
Adding the special Serializer
This is a very important class, it will help us send the correct JSON
to Ember.js
, I put the following code inside of app/serializers/post_search_serialiazer.rb
:
class PostSearchSerializer
attr_accessor :search
delegate :current_page, :per_page, :total_entries, :total_pages, to: :pagination_info
def initialize(params)
@search = PostSearch.new(params).search
end
def serialize
{
post_search:
[
{
id: current_page,
page: current_page,
per_page: per_page,
total_entries: total_entries,
total_pages: total_pages,
post_ids: post_ids
}
],
posts: search_results
}
end
private
def post_ids
search_results.map(&:id)
end
def search_results
search.results
end
def pagination_info
search.hits
end
end
If you remember the first JSON
example, the resource_collection
key makes reference to my post_search
key from the example above. Also, the resources
key is the same than my posts
key. You will need to change this according to your model 😉
Customizing the Route
This is easy, just add the following code to your config/routes.rb
:
resources :post_search, defaults: {format: :json}
Creating the Controller
As you can imagine, it's necessary to create a Rails
controller in order to provide an API to our Ember.js
application.
I have just created the following file app/controllers/post_search_controller.rb
:
class PostSearchController < ApplicationController
def index
@post_search = PostSearchSerializer.new(params).serialize
render json: @post_search
end
end
As you can observe, I just used the special serializer to return the paginated collection
, also known as the Solr search results
.
And that's it! We are ready to start working with Ember.js
and the pagination helper.
The Frontend
Before we start, there's something I want to say, just in case you are using the ember-rails
gem. Please make sure that you are requiring the helpers before the models, in your app/assets/javascripts/your_app.js.coffee
file. For instance, I changed it to this:
#= require ./store
#= require_tree ./helpers
#= require_tree ./models
#= require_tree ./controllers
#= require_tree ./views
#= require_tree ./helpers
#= require_tree ./templates
#= require_tree ./routes
#= require_tree ./initializers
Paginated Collection Helper
I have created a new Model
, Serializer
and Controller
on Rails
. So, all this means that I'm going to need a new Ember.js
model, in order to get it working. Remember, the store
is in charge of finding the models through the adapter. It will communicate with the new Rails
controller to serialize the new model.
From my point of view, we can have many paginated collections inside an application. So, one way to avoid code duplication is the use of Ember.js
Mixins. The following mixin app/assets/javascripts/helpers/paginated_collection_helper.js.coffee
has the responsibility of defining the pagination's attributes:
App.PaginatedCollection = Ember.Mixin.create
perPage: DS.attr('number')
totalPages: DS.attr('number')
totalEntries: DS.attr('number')
page: DS.attr('number')
Then, I am ready to include it in my new Ember.js
model. This model is called PostSearch
and it's located at app/assets/javascripts/models/post_search.js.coffee
, please check it out:
App.PostSearch = DS.Model.extend(App.PaginatedCollection,
posts: DS.hasMany('post')
)
This way, if your application needs more paginated collections
, you just will need to create the corresponding models and include the PaginatedCollection
mixin in the recently created models.
Paginate It helper
This is where the magic happens! This mixin holds all the logic for the pagination. It handles pages setup and page changes. The file is called app/assets/javascripts/helpers/paginate_it_helper.js.coffee
and here is the code:
App.PaginateIt = Ember.Mixin.create
maxPerPage: 20,
setup: (resource) ->
@set('currentPage', @get('content.content.firstObject'))
@set(resource, @get("currentPage.#{resource}"))
perPage: (->
@get('pagination.perPage')
).property('pagination')
hasPrevious: (->
@get('pageNumber') > 1
).property('pageNumber')
hasNext: (->
@get('pageNumber') < @get('pagination.totalPages')
).property('pageNumber')
pagination: (->
@setup(@get('resource'))
@get('content.content.firstObject')
).property('content')
pageNumber: (->
parseInt(@get('pagination.page'))
).property('pagination')
pages: (->
self = @
pages = Em.A()
[@get('firstPage')..@get('lastPage')].forEach (page) ->
pages.pushObject {
pageNumber: page,
active: page is self.get('pageNumber')
}
pages
).property('@each')
firstPage: (->
firstPage = 1
firstPage = @get('pageNumber') - 5 if @get('pageNumber') - 5 > 0
firstPage
).property('pageNumber')
lastPage: (->
lastPage = @get('pagination.totalPages')
lastPage = @get('firstPage') + 5 if @get('firstPage') + 5 < @get('pagination.totalPages')
lastPage
).property('pagination.totalPages')
changePage: (pageNumber) ->
params = {
page: pageNumber,
per_page: @get('perPage')
}
self = @
@store.find('postSearch', params).then (postSearch) ->
self.set('content', postSearch)
switchPerPage: (perPage) ->
@set('maxPerPage', @get('perPage'))
@set('perPage', perPage)
actions:
goPage: (page) ->
@changePage(page)
goNext: ->
@changePage(@get('pageNumber') + 1) if @get('hasNext')
goPrev: ->
@changePage(@get('pageNumber') - 1) if @get('hasPrevious')
goFirst: ->
@changePage(1) if @get('hasPrevious')
goLast: ->
@changePage(@get('pagination.totalPages')) if @get('hasNext')
changePerPage: (newPerPage) ->
@switchPerPage(newPerPage)
@changePage(1)
The partial
I have almost all the necessary code to get the pagination working. Let me show you the template for this pagination helper. The file is located at app/assets/javascripts/templates/helpers/_pagination.handlebars
:
<div class="pagination-container">
<ul class="pagination">
<li {{bindAttr class="hasPrevious::disabled"}}>
<span {{action "goFirst"}}>«</span>
</li>
<li {{bindAttr class="hasPrevious::disabled"}}>
<span {{action "goPrev"}}>←</span>
</li>
{{#each page in pages}}
{{#with page}}
<li {{bindAttr class="active"}}>
<span {{action "goPage" pageNumber}}>{{pageNumber}}</span>
</li>
{{/with}}
{{/each}}
<li {{bindAttr class="hasNext::disabled"}}>
<span {{action "goNext"}}>→</span>
</li>
<li {{bindAttr class="hasNext::disabled"}}>
<span {{action "goLast"}}>»</span>
</li>
</ul>
<button class='btn btn-success btn-xs change-per-page' {{action changePerPage maxPerPage}}>Show {{maxPerPage}}</button>
</div>
I'm making use of Bootstrap's CSS
classes for pagination.
Preparing the Route
One last necessary step before including the PaginateIt
mixin in our controllers, is to change the Route
to use our custom PaginatedCollection
model. For instance, I changed my PostsRoute
from this:
App.PostsRoute = Ember.Route.extend
model: ->
@store.find('post')
To this:
App.PostsRoute = Ember.Route.extend
model: ->
@store.find('postSearch')
By doing this we are switching to the model that brings the Paginated Collection
.
You will also need to append this line to the app/assets/javascripts/store.js.coffee
file:
Ember.Inflector.inflector.uncountable('post_search')
That's to prevent the Assertion failed: Error while loading route...
error. This is caused by the conventions that Ember.js
has defined. The particular convention is to make an HTTP
request to the plural form of the model name, which in this case is post_searches
but in our server I haven't declared such resource.
In a nutshell, the previous line is setting the post_search
word as uncountable, which means that it does not have a plural form, it remains unchanged.
How to use it?
It is not as hard as it seems. You just need to attach the PaginateIt mixin to the corresponding controller, add a property and that's it! You got it working like a charm. I want to show you an example. This was my PostsController
without pagination:
App.PostsController = Ember.ArrayController.extend(
needs: ['currentUser']
currentUser: Ember.computed.alias('controllers.currentUser')
)
This is my PostsController
with pagination:
App.PostsController = Ember.ArrayController.extend(App.PaginateIt,
needs: ['currentUser']
currentUser: Ember.computed.alias('controllers.currentUser')
resource: 'posts'
)
The resource
property specifies the name of the property which I will use on my templates.
So, instead of my old Posts
index template:
{{#each post in content}}
<h4>{{#linkTo 'post' post}}{{post.title}}{{/linkTo}}</h4>
<p class='text-right'>
<small>
Published: {{friendlyDate post.publishedOn}}
<em>by</em>
<strong>{{fullName post.author}}</strong>
</small>
</p>
<p>{{post.content}}</p>
{{/each}}
Now I have this:
{{#each post in posts}}
<h4>{{#linkTo 'post' post}}{{post.title}}{{/linkTo}}</h4>
<p class='text-right'>
<small>
Published: {{friendlyDate post.publishedOn}}
<em>by</em>
<strong>{{fullName post.author}}</strong>
</small>
</p>
<p>{{post.content}}</p>
{{/each}}
<br>
{{ partial "helpers/pagination" }}
As mentioned previously, I won't use {{#each post in content}}
anymore. Instead, I will be using {{#each post in posts}}
now that I have included the PaginateIt mixin.
We are done!
A working example?
Yes, of course! You can take a look at this repo and follow the Readme's instructions.
- Pro-tip: if you are applying this tutorial on your own application, please remember to run:
bundle exec rake sunspot:reindex
before sending hate mail my way because thesearch
method onYourSearch
model does not return anything.
Please feel free to comment!
What topics would you like me to cover in the next blogpost? I will be back soon with an article covering your suggestions.
Notes
This example is running under the following setup.
- Ember : 1.4.0-beta.1+canary.f3a696df
- Ember Data : 1.0.0-beta.4+canary.7af6fcb0
- Handlebars : 1.0.0
- jQuery : 1.10.2
Thanks for reading!