Skip to content

Instantly share code, notes, and snippets.

@terion-name
Last active August 29, 2015 14:01
Show Gist options
  • Save terion-name/ab53d6c20043dd58774b to your computer and use it in GitHub Desktop.
Save terion-name/ab53d6c20043dd58774b to your computer and use it in GitHub Desktop.
Ember REST pagination and filtering

Pagination and filtering for REST in ember.js

Conventions:

  • Backend accepts pagination and filters in GET query: page=2&name=foo&email=example.com

  • Backend accepts filter (search) values with sign prefixes: '=', '<', '>', '~', '!~', '<=', '>=', '!=', where '~' is LIKE. E.g. name=John&age=>=18&job=~manager

  • Pagination is outputed in meta and has format:

    total: 60,
    count: 10,
    per_page: 10,
    current_page: 2,
    total_pages: 6,
    links: {
        previous: "http://localhost:8081/api/users?page=1",
        next: "http://localhost:8081/api/users?page=3"
    }

Requirements

Screenshots

Paging: paging

Filtering: filtering

(function() {
var App;
App = Ember.Application.create();
window.App = App;
App.Store = DS.Store.extend({
revision: 1,
adapter: DS.RESTAdapter.extend({
namespace: 'api'
})
});
Ember.Handlebars.registerBoundHelper('getObjValue', function(obj, key) {
return obj.get(key);
});
/*
To make a route filterable
specify @modelName property in your route as in example below
App.UsersIndexRoute = Ember.Route.extend Ember.FilterableRouteMixin,
modelName: 'user'
*/
Ember.FilterableRouteMixin = Ember.Mixin.create({
filterParams: {},
modelName: '',
model: function(params) {
if (this.filterParams) {
params = $.extend(params, this.filterParams);
}
return this.get('store').find(this.modelName, params);
},
actions: {
filterModel: function(params) {
var query;
this.set('filterParams', params);
query = $.extend({}, params);
if (this.get('context').query && this.get('context').query.page) {
query.page = this.get('context').query.page;
}
return this.get('controller').set('content', this.model(query));
}
}
});
Ember.PageableControllerMixin = Ember.Mixin.create({
queryParams: ['page'],
currentPage: 1,
totalPages: 1,
_getContent: function() {
var content;
if (this.get('content').content instanceof Array) {
return content = this.get('content');
} else {
return content = this.get('content').content;
}
},
currentPage: (function() {
var content;
content = this._getContent();
if (!content || !content.meta) {
return 1;
} else {
return content.meta.current_page;
}
}).property('content.content'),
totalPages: (function() {
var content;
content = this._getContent();
if (!content || !content.meta) {
return 1;
} else {
return content.meta.total_pages;
}
}).property('content.content')
});
/*
There are two methods to make route reload when query parameter changes.
1. Refresh model on any query parameter change:
actions:
queryParamsDidChange: ->
@refresh()
2. Change only on specified parameters change
queryParams:
page:
refreshModel: true
*/
Ember.PageableRouteMixin = Ember.Mixin.create({
queryParams: {
page: {
refreshModel: true
}
}
});
App.User = DS.Model.extend({
first_name: DS.attr('string'),
last_name: DS.attr('string'),
group: DS.attr('string'),
email: DS.attr('string'),
bio: DS.attr('string'),
photo: DS.attr('string')
});
App.Router.reopen({
rootURL: '/admin/',
location: 'auto'
});
App.Router.map(function() {
this.resource('users', function() {
return this.route('show', {
path: ':user_id'
});
});
});
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(err) {
return console.log(err);
}
}
});
App.UsersIndexRoute = Ember.Route.extend(Ember.FilterableRouteMixin, Ember.PageableRouteMixin, {
modelName: 'user'
});
App.UsersShowRoute = Ember.Route.extend({
model: function(params) {
return this.get('store').find('user', params.user_id);
}
});
App.UsersIndexController = Ember.ArrayController.extend(Ember.PageableControllerMixin, {
users: (function() {
return this.get('content');
}).property('content')
});
App.PaginatorLinksComponent = Ember.Component.extend({
layoutName: 'components/paginator-links/paginator-links',
linksCount: 5,
pages: [],
next: 1,
prev: 1,
calculate: (function() {
var current, i, pageEnd, pagesStart, total, _i, _results;
current = this.get('currentPage');
total = this.get('totalPages');
this.next = current < total ? current + 1 : 1;
this.prev = current > 1 ? current - 1 : total;
pagesStart = current - this.linksCount;
if (pagesStart < 1) {
pagesStart = 1;
}
pageEnd = current + this.linksCount;
if (pageEnd > total) {
pageEnd = total;
}
this.pages = [];
_results = [];
for (i = _i = pagesStart; pagesStart <= pageEnd ? _i <= pageEnd : _i >= pageEnd; i = pagesStart <= pageEnd ? ++_i : --_i) {
_results.push(this.pages.push({
page: i,
active: i === current
}));
}
return _results;
}).on('init'),
contentDidChange: (function() {
this.calculate();
return this.rerender();
}).observes('currentPage', 'totalPages'),
didInsertElement: function() {
if (this.get('totalPages') <= 1) {
return this.$().hide();
} else {
$().show();
if (this.get('currentPage') < 2) {
this.$().find('.prev').addClass('disabled');
}
if (this.get('currentPage') === this.get('totalPages')) {
return this.$().find('.next').addClass('disabled');
}
}
}
});
App.TableFilterableComponent = Ember.Component.extend(Ember.TargetActionSupport, {
layoutName: 'components/table-filterable/table-filterable',
title: '&nbsp',
searchType: '~',
columns: [
{
column: 'id',
name: '#'
}
],
showFilters: function() {
var $filters, $tbody;
$filters = this.$().find('.filters input');
$tbody = this.$().find('.table tbody');
if ($filters.prop('disabled') === true) {
$filters.prop('disabled', false);
return $filters.first().focus();
} else {
$filters.val('').prop('disabled', true);
$tbody.find('.no-result').remove();
return $tbody.find('tr').show();
}
},
runFilter: function() {
var $filters, actionContext, self;
self = this;
$filters = this.$().find('.filters input');
actionContext = {};
$filters.each(function() {
var column, name, searchType, val;
name = $(this).attr('name');
val = $(this).val();
if (val.length === 0) {
return;
}
column = self.columns.filter(function(e) {
return e.column === name;
});
if (column[0] && column[0].searchType) {
searchType = column[0].searchType;
} else {
searchType = self.searchType;
}
return actionContext[name] = searchType + val;
});
return this.triggerAction({
action: 'filterModel',
actionContext: actionContext
});
},
didInsertElement: function() {
var self;
self = this;
return this.$().each(function() {
var $filters, $widget;
$widget = $(this);
$filters = $widget.find('.filters input');
$widget.find('.btn-filter').on('click', function() {
return self.showFilters();
});
return $filters.on('keyup', $.debounce(250, function() {
return self.runFilter();
}));
});
}
});
App.UsersTableComponent = App.TableFilterableComponent.extend({
title: 'Список пользователей',
linkTo: 'users.show',
columns: [
{
column: 'id',
name: '#',
linked: true
}, {
column: 'first_name',
name: 'Имя'
}, {
column: 'last_name',
name: 'Фамилия'
}, {
column: 'email',
name: 'email'
}, {
column: 'group',
name: 'Группа',
searchType: '='
}
]
});
}).call(this);
###
To make a route filterable
specify @modelName property in your route as in example below
App.UsersIndexRoute = Ember.Route.extend Ember.FilterableRouteMixin,
modelName: 'user'
###
Ember.FilterableRouteMixin = Ember.Mixin.create
filterParams: {}
modelName: ''
model: (params)->
if @filterParams
params = $.extend params, @filterParams
@get('store').find(@modelName, params)
actions:
filterModel: (params)->
@set 'filterParams', params
# clone params to query to not affect them in further processing
query = $.extend {}, params
# check do we have pagination here
if @get('context').query && @get('context').query.page
query.page = @get('context').query.page
# run
@get('controller').set('content', @model(query))
Ember.Handlebars.registerBoundHelper 'getObjValue', (obj, key)->
obj.get key
Ember.PageableControllerMixin = Ember.Mixin.create
queryParams: ['page']
currentPage: 1
totalPages: 1
_getContent: ->
if @get('content').content instanceof Array
content = @get('content')
else
content = @get('content').content
# properties are binded to content.content to be compatible with filterable routes
currentPage: (->
content = @_getContent()
content?.meta?.current_page or 1
).property('content.content')
totalPages: (->
content = @_getContent()
content?.meta?.total_pages or 1
).property('content.content')
###
There are two methods to make route reload when query parameter changes.
1. Refresh model on any query parameter change:
actions:
queryParamsDidChange: ->
@refresh()
2. Change only on specified parameters change
queryParams:
page:
refreshModel: true
###
Ember.PageableRouteMixin = Ember.Mixin.create
queryParams:
page:
refreshModel: true
# base TableFilterableComponent
App.TableFilterableComponent = Ember.Component.extend Ember.TargetActionSupport,
layoutName: 'components/table-filterable/table-filterable'
title: '&nbsp'
searchType: '~' # this evaluated at backend as LIKE
columns: [
{column: 'id', name: '#'}
]
showFilters: ->
$filters = @$().find '.filters input'
$tbody = @$().find '.table tbody'
if $filters.prop('disabled') == true
$filters.prop 'disabled', false
$filters.first().focus()
else
$filters.val('').prop 'disabled', true
$tbody.find('.no-result').remove()
$tbody.find('tr').show()
runFilter: ->
self = @
$filters = @$().find '.filters input'
actionContext = {}
# loop over all filters
$filters.each ->
# take the input value
name = $(@).attr 'name'
val = $(@).val()
# if value is empty simply skip
if (val.length == 0) then return
# see if there is a special search type for this column
column = self.columns.filter((e)->e.column == name)
if column[0] && column[0].searchType
searchType = column[0].searchType
else
searchType = self.searchType
# add this pair to action context
# values are prefixed with search type keys that should be evaluated at backend
actionContext[name] = searchType + val
# fire action to make route run the query at backend
@triggerAction
action: 'filterModel'
actionContext: actionContext
didInsertElement: ->
self = @
@$().each ->
$widget = $(@)
$filters = $widget.find '.filters input'
$widget.find '.btn-filter'
.on 'click', ->
self.showFilters()
$filters
.on 'keyup', $.debounce 250, ->
self.runFilter()
<div class="row">
<div class="col-md-12">
<div class="panel filterable">
<div class="panel-heading">
<h3 class="panel-title">{{title}}</h3>
<div class="pull-right">
<button class="btn btn-default btn-xs btn-filter">
<span class="glyphicon glyphicon-filter"></span>
Фильтр
</button>
</div>
</div>
<table class="table table-striped table-hover">
<thead>
<tr class="filters">
{{#each col in columns}}
<th>{{input name=col.column placeholder=col.name disabled="disabled" class="form-control"}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each item in model}}
<tr>
{{#each col in columns}}
<td>
{{#if col.linked}}
{{#link-to linkTo item}} {{getObjValue item col.column}} {{/link-to}}
{{else}}
{{getObjValue item col.column}}
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
// based on twitter bootstrap
.filterable {
margin-top: 15px;
.panel-heading {
padding-left: 8px;
padding-right: 8px;
.pull-right {
margin-top: -20px;
}
}
.filters {
input[disabled] {
background-color: transparent;
border: none;
cursor: auto;
box-shadow: none;
padding: 0;
height: auto;
}
input[disabled]::-webkit-input-placeholder {
color: #333;
}
input[disabled]::-moz-placeholder {
color: #333;
}
input[disabled]:-ms-input-placeholder {
color: #333;
}
}
&.transitionable {
tbody {
tr {
cursor: pointer;
}
}
}
}
# this component extends base TableFilterableComponent
App.UsersTableComponent = App.TableFilterableComponent.extend
title: 'Users List'
linkTo: 'users.show'
columns: [
{column: 'id', name: '#', linked: true} #this coumn will be linked
{column: 'first_name', name: 'Name'}
{column: 'last_name', name: 'Surname'}
{column: 'email', name: 'email'}
{column: 'group', name: 'Group', searchType: '='} #strict search for this column
]
# usage example
App.UsersIndexController = Ember.ArrayController.extend Ember.PageableControllerMixin,
# any logick here. in this case simply binding model to users property
users: (->
@get('content')
).property('content')
# usage example
App.UsersIndexRoute = Ember.Route.extend Ember.FilterableRouteMixin, Ember.PageableRouteMixin,
modelName: 'user' # for FilterableRouteMixin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment