Friday, November 18, 2016

Some attempts in game dev :-)

I and colleague of mine Alexander Zhak, we've decided to create a game in honor of ending of project we'v participated in.
This project was related to IOT: gateways, devices, mobiles and so on. Besides that we've added comic description of the team who worked on this project.
Game is based on phaser.io framework. That's what we've got in the end: we.katim-build.com 

Tuesday, February 2, 2016

Pagination and filtering with Backbone.Marionette

  Recently I've got a task to show grid data with pagination using Backbone.Marionette. Moreover we were needed to filter data by some flag(active/finished).I am not a guru of UI :-) I am mostly backend developer, but had some experience in UI and it was funny and interesting for me to resolve this issue.
  I've a little bit simplified the final code of pagination/filtering module in demo purposes and prepared real running sample on my Amazon ec2 instance, so you can analyse js source and networks calls under F12/firebug or whatever you like to use. Try (Demo)Pagination/filtering (http://52.32.134.99:3000)
  I've decided to use Backbone.PageableCollection to configure paging with its server-side mode. See backbone-pageable for more details. Below is how events collection looks like:

var EventsCollection = Backbone.PageableCollection.extend({
url: '/api/events',
model: Event,
//borodaalex: see https://github.com/backbone-paginator/backbone-pageable
//for more details
state: {
// You can use 0-based or 1-based indices, the default is 1-based.
// You can set to 0-based by setting ``firstPage`` to 0.
firstPage: 0,
// Set this to the initial page index if different from `firstPage`. Can
// also be 0-based or 1-based.
currentPage: 0,
pageSize: 10,
showActiveEventsOnly: false,
// Required under server-mode
totalRecords: 200,
offset: function () { return this.currentPage * this.pageSize; }
},
// You can configure the mapping from a `Backbone.PageableCollection#state`
// key to the query string parameters accepted by your server API.
queryParams: {
pageSize: "pageSize",
showActiveEventsOnly: function () {
return this.state.showActiveEventsOnly;
},
offset: function () {
return this.state.currentPage * this.state.pageSize;
}
},
parseState: function (resp, queryParams, state, options) {
return {totalRecords: resp.totalCount};
},
// get the actual records
parseRecords: function (resp, options) {
return resp.items;
}
});
  Pay attention to parseState and parseRecords methods. They allowed us to recognize totalCount and items fields get passed from server. From ui side we will send pageSize, showActiveEventsOnly and offset to server side and then use them to fetch appropriate subset of data.
  On server side(Java) I've used simple grizzly server and method which returns paging data as follows:

package com.greenwave.load.simulator.web.resources;
import java.net.SocketException;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import mint.rest.common.util.MediaTypeEx;
import org.fluttercode.datafactory.impl.DataFactory;
import org.springframework.stereotype.Component;
import com.google.common.collect.Lists;
import com.greenwave.load.simulator.web.resources.dto.EventDTO;
import com.greenwave.load.simulator.web.resources.dto.EventsList;
@Component
@Path("/api/events")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8)
public class EventResource {
private List<String> statuses = Lists.newArrayList();
private Random random = new Random();;
public EventResource() {
statuses.add("active");
statuses.add("finished");
}
@GET
/**
* Regarding offset, pageSize, showActiveEventsOnly queryParams look at ui side,
* see https://github.com/backbone-paginator/backbone-pageable for more details
*/
public EventsList list(@QueryParam("offset") String offset, @QueryParam("pageSize") String pageSize,
@QueryParam("showActiveEventsOnly") String showActiveEventsOnly)
throws SocketException {
Boolean showActiveEventsOnlyParsed = Boolean.valueOf(showActiveEventsOnly);
List<EventDTO> result = Lists.newArrayList();
DataFactory df = new DataFactory();
for (int i = 0; i < (showActiveEventsOnlyParsed ? 25 : 50); i++) {
EventDTO dto = new EventDTO();
dto.setName(df.getRandomWord() + " event");
String address = df.getAddress() + ", " + df.getCity() + ", " + df.getNumberText(5);
String business = df.getBusinessName();
dto.setDescription(business + " located at " + address);
dto.setId(random.nextInt(100));
dto.setStatus(showActiveEventsOnlyParsed ? "active" : statuses.get(random.nextInt(2)));
Calendar calendar = Calendar.getInstance(); // this would default to now
calendar.add(Calendar.DAY_OF_MONTH, -30);
dto.setStart(df.getDate(calendar.getTime(), 1, 15));
dto.setStop(df.getDate(calendar.getTime(), 15, 30));
result.add(dto);
}
Collections.sort(result, new Comparator<EventDTO>() {
@Override
public int compare(EventDTO o1, EventDTO o2) {
int result = o1.getStatus().compareTo(o2.getStatus());
if (result == 0) {
return o2.getStart().compareTo(o1.getStart());
}
return result;
}
});
final int totalCount = result.size();
List<EventDTO> paginatedResult;
final Integer pageSizeParsed = Integer.valueOf(pageSize);
final Integer offsetParsed = Integer.valueOf(offset);
final int boundPageSize = offsetParsed + pageSizeParsed;
if (result.size() >= boundPageSize) {
paginatedResult = result.subList(offsetParsed, pageSizeParsed + offsetParsed);
} else {
paginatedResult = result.subList(offsetParsed, result.size());
}
return new EventsList(paginatedResult, totalCount);
}
}
  So as you can see we use parameters get passed as query params from ui side to determine which part of data we need to return as per given offset/filtering flag. I made screenshot describing what happened under the hood, when you, for example, click second page  on paging control:


  It depends on you how to implement server side related logic to get paged data. The one this I should mention the final set of data we will return to ui is wrapped with custom object that contains two key fields: result items and totalCount. I described those fields above.
  They are used by ui to build corresponding ui(remember parseState/parseRecords methods ? :-))

public class EventsList {
private List<EventDTO> items;
private int totalCount;
public EventsList(List<EventDTO> items, int totalCount) {
this.items = items;
this.totalCount = totalCount;
}
public EventsList() {
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public List<EventDTO> getItems() {
return items;
}
public void setItems(List<EventDTO> items) {
this.items = items;
}
}
The most efficient way to get subset of data is to resolve it on database level, but I repeat it depends on your decision and details of your project. Okay, now we have all the data from the server and we are ready to show it on ui side. Let's see how our filter and paging controls are defined. I've used RequireJS as module loader, so the module looks like as follows:

define (function(){
var FilterView = Marionette.ItemView.extend({
template: '#running-events-filter',
events: {
'change #events-filter': 'changed'
},
changed: function (ev) {
ev.preventDefault();
if ($(ev.currentTarget).is(":checked")) {
this.collection.state.showActiveEventsOnly = true;
} else {
this.collection.state.showActiveEventsOnly = false;
}
var self = this;
this.collection.getFirstPage().done(function () {
self.collection.trigger('updated', this.collection, {});
});
}
});
var PaginationView = Marionette.ItemView.extend({
template: '#pagination',
events: {
'click a.link.previous': 'previousPage',
'click a.link.next': 'nextPage',
'click a.link.page': 'goToPage'
},
initialize: function () {
this.collection.on('reset', this.render);
this.collection.on('updated', this.render);
this.template = _.template($('#pagination').html());
},
render: function () {
var pages = [];
if (this.collection.length > 0) {
var i, max_page = this.collection.state.totalRecords / this.collection.state.pageSize;
for (i = 0; i < max_page; i++) {
pages.push({
current: (this.collection.state.currentPage === i),
page: i + 1,
index: i
});
}
}
this.$el.html(this.template({
pages: pages
}));
},
previousPage: function (ev) {
ev.preventDefault();
this.collection.getPreviousPage();
this.render();
},
nextPage: function (ev) {
ev.preventDefault();
this.collection.getNextPage();
this.render();
},
goToPage: function (ev) {
ev.preventDefault();
this.collection.getPage($(ev.currentTarget).data('page'));
this.render();
}
});
return {
PaginationView: PaginationView,
FilterView: FilterView
}
});
view raw views.js hosted with ❤ by GitHub
and the corresponding templates:

<script type="text/template" id="running-events-filter">
<div class="input-field input-field-checkbox padding-small">
<input type="checkbox" name="events-filter" id="events-filter">
<label for="events-filter">Show Only Active Events</label>
</div>
</script>
<script type="text/template" id="pagination">
<ul class="pagination">
<% if (pages.length != 0) { %>
<li class="waves-effect"><a href="#!" class="link previous"><i class="material-icons">chevron_left</i></a></li>
<% } %>
<% _.each(pages, function(page) { %>
<li <% if (page.current) { %> class="active"<% } %> >
<a href="#" class="link page" data-page="<%= page.index %>">
<%= page.page %>
</a>
</li>
<% }) %>
<% if (pages.length != 0) { %>
<li class="waves-effect"><a class="link next" href="#!"><i class="material-icons">chevron_right</i></a></li>
<% } %>
</ul>
</script>
view raw templates.html hosted with ❤ by GitHub
 I've used materializecss so paging controls looks in Material Design style.
 Model and View are linked using CompositeView for grid data and simple ItemView for pagination/filtering logic:

require(['app', 'models', 'common-views'], function(App, Models, CommonViews) {
var events = new Models.EventsCollection();
var EventView = Backbone.Marionette.LayoutView.extend({
template: '#events-template',
tagName: 'tbody',
className: 'event',
events: {
'click .stop': 'stopEvent'
},
templateHelpers: {
startTime: function() {
return moment(this.start).format('MMM DD YYYY HH:mm');
},
stopTime: function() {
return this.stop ? moment(this.stop).format('MMM DD YYYY HH:mm') : '';
}
},
stopEvent: function() {
var name = this.model.get('name');
if (confirm('Are you sure you want to stop "' + name + '"?')) {
$.ajax({
method: 'POST',
contentType: 'application/json',
dataType: 'json',
url: Models.Event.prototype.urlRoot + '/' + this.model.get('id') + '/stop'
}).then(function () {
Materialize.toast('Event "' + name + '" was stopped', 5000);
events.fetch();
});
}
}
});
var EventsListView = Backbone.Marionette.CompositeView.extend({
template: '#event-list-template',
tagName: 'table',
childView: EventView,
emptyView: Backbone.Marionette.ItemView.extend({
template: '#empty-list-template',
tagName: 'tr'
})
});
App.addRegions({
events: '.event-list'
});
App.addRegions({
pagination: '.pagination'
});
App.addRegions({
eventsFilter: '.running-events-filter'
});
App.addInitializer(function(){
events.getFirstPage().done(function(){
App.events.show(new EventsListView({
collection: events
}));
App.pagination.show(new CommonViews.PaginationView({
collection: events
}));
App.eventsFilter.show(new CommonViews.FilterView({
collection: events
}));
});
});
App.start();
});
view raw event-list.js hosted with ❤ by GitHub
And the final screen where everything is together:


P.S. I hope my solution will be helpful to someone :-)