Datagrids with Symfony 2,
Backbone and Backgrid
Eugenio Pombi & Giorgio Cefaro
requirements - composer
http://getcomposer.org
Run this in your terminal to get the latest Composer version:
curl -sS https://getcomposer.org/installer | php
Or if you don't have curl:
php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"
requirements - symfony
http://symfony.com/download
Create a symfony 2.3.1 project in path/:
php composer.phar create-project symfony/framework-standard-edition path/ 2.3.1
requirements - dependencies
composer.json:
"require": {
[...]
"friendsofsymfony/rest-bundle": "0.12",
"jms/serializer-bundle": "dev-master",
"jms/di-extra-bundle": "dev-master",
"friendsofsymfony/jsrouting-bundle": "~1.1"
},
requirements - FOSRestBundle
https://github.com/FriendsOfSymfony/FOSRestBundle
app/config/config.yml:
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
view_response_listener: 'force'
requirements - javascript libs
Download the required libs:
http://backbonejs.org/
http://underscorejs.org/
http://jquery.com/
http://backgridjs.com/
http://twitter.github.io/bootstrap/
requirements - javascript libs
Place the libraries in src/Acme/MyBundle/Resources/public/js/ and include
them with Assetic:
base.html.yml:
{% block javascripts %}
{% javascripts
'bundles/mwtbrokertool/js/di-lite.js'
'bundles/mwtbrokertool/js/jquery.js'
'bundles/mwtbrokertool/js/underscore.js'
'bundles/mwtbrokertool/js/bootstrap.js'
'bundles/mwtbrokertool/js/backbone.js'
%}
<script src="{{ asset_url }}" type="text/javascript"></script>
{% endjavascripts %}
<script src="{{ asset('/js/fos_js_routes.js') }}"></script>
{% endblock %}
controllers - index
/**
* @ParamConverter("user", class="MyBundle:User", options={"id" = "userId"})
* @FosRestGet("/ticket.{_format}",
* name="mwt_brokertool_ticket",
* defaults={"_format": "json"},
* options={"expose"=true})
*/
public function indexAction(User $user)
{
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('MyBundle:Ticket');
$tickets = $repo->findBySellerJoinAll($user);
return $tickets;
}
controllers - new
/**
* @ParamConverter("user", class="MyBundle:User", options={"id" = "userId"})
* @FosRestPost("/ticket.{_format}",
* name="My_bundle_ticket_new",
* defaults={"_format": "json"},
* options={"expose"=true}
* )
* @FosRestView
* @param User $user
*/
public function newAction(User $user)
{
[...]
}
controllers - new ticket
$ticket = new Ticket();
$form = $this->createForm(new TicketType(), $ticket);
$data = $this->getRequest()->request->all();
$children = $form->all();
$data = array_intersect_key($data, $children);
$form->submit($data);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($ticket);
$em->flush();
return View::create($ticket, 201);
}
return View::create($form, 400);
test index
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET','/'.$this->user1->getId().'/ticket');
$this->assertTrue($client->getResponse()->isSuccessful());
$json_response = json_decode($client->getResponse()->getContent(), true);
$this->assertTrue(is_array($json_response));
$this->assertTrue(isset($json_response[0]['event_id']));
$this->assertTrue(isset($json_response[1]['event_id']));
$this->assertTrue(isset($json_response[2]['event_id']));
}
test new ticket
$client = static::createClient();
$client->request(
'POST',
'/' . $this->user1->getId() . '/ticket',
array(),
array(),
array('CONTENT_TYPE' => 'application/json'),
'[aJsonString]'
);
$this->assertEquals(201, $client->getResponse()->getStatusCode());
json_response = json_decode($client->getResponse()->getContent(), true);
$this->assertTrue(is_array($json_response));
$ticket = $this->em->getRepository('ACMEMyBundle:Ticket')->findOneBy(array
(...);
$this->assertNotNull($ticket);
backgrid
backgrid
backgrid
backgridjs.com
The goal of Backgrid.js is to produce a set of
core Backbone UI elements that offer you all
the basic displaying, sorting and editing
functionalities you'd expect, and to create an
elegant API that makes extending Backgrid.js
with extra functionalities easy.
backgrid
Backgrid.js depends on 3 libraries to function:
● jquery >= 1.7.0
● underscore.js ~ 1.4.0
● backbone.js >= 0.9.10
backgrid
● Solid foundation. Based on Backbone.js.
● Semantic and easily stylable. Just style with plain CSS like
you would a normal HTML table.
● Low learning curve. Works with plain old Backbone models
and collections. Easy things are easy, hards things possible.
● Highly modular and customizable. Componenets are just
simple Backbone View classes, customization is easy if you
already know Backbone.
● Lightweight. Extra features are separated into extensions,
which keeps the bloat away.
di-lite.js
minimalistic dependency injection container
ctx.register("name", instance);
ctx.get("name");
My.Stuff = Backbone.Collection.extend({
dependencies: "name",
[...]
});
di-lite.js - example
var ctx = di.createContext();
var user = function () {
this.id = $("#grid").attr('data-user);
};
ctx.register("user", user);
var App.Collections.Articles = Backbone.Collection.extend({
dependencies: "user",
model: App.Models.Article,
url: function() {
return '/article?userId=' + this.user.id;
}
[...]
});
ctx.register("articles", App.Collections.Articles);
backbone model + collection
var Ticket = Backbone.Model.extend({});
var Tickets = Backbone.Collection.extend({
model: Territory,
url: Routing.generate('my_bundle_ticket', { userId: App.userId })
});
var tickets = new Tickets();
backbone associations
Associations allows Backbone applications to model 1:1 & 1:
N associations between application models and Collections.
https://github.com/dhruvaray/backbone-associations
var TicketGroup = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.Many,
key: 'tickets',
relatedModel: 'Ticket'
}]
});
backgrid columns
var columns = [{
name: "event_name",
label: "Event",
cell: "string" ,
editable: false,
}, {
name: "event_datetime",
label: "Event Date",
cell: "datetime"
}];
backgrid initialize
var grid = new Backgrid.Grid({
columns: columns,
collection: tickets
});
$("#my-list").append(grid.render().$el);
// Fetch some tickets from the url
tickets.fetch({reset: true});
backgrid - computed fields
https://github.com/alexanderbeletsky/backbone-computedfields
var CartItem = Backbone.Model.extend({
initialize: function () {
this.computedFields = new Backbone.ComputedFields(this);
},
computed: {
grossPrice: {
depends: ['netPrice', 'vatRate'],
get: function (fields) {
return fields.netPrice * (1 + fields.vatRate / 100);
}
}
}
});
backgrid - computed fields
var columns = [{
name: "netPrice",
label: "Net Price",
cell: "number"
}, {
name: "vatRate",
label: "VAT Rate",
cell: "integer"
}, {
name: "grossPrice",
label: "Gross price",
cell: "number"
}];
backgrid - select editor
{
name: "country",
label: "Country",
cell: Backgrid.SelectCell.extend({
optionValues: ctx.get('countries').getAsOptions()
})
}
backgrid - select editor
App.Collections.Countries = Backbone.Collection.extend({
getAsOptions: function () {
var options = new Array();
this.models.forEach(function(item) {
options.push([item.get('name'), item.get('id')])
});
return options;
}
});
toggle cell - column definition
{
name: 'nonModelField',
label: 'Details',
editable: false,
cell: Backgrid.ToggleCell,
subtable: function(el, model) {
var subtable = new Backgrid.Grid({
columns: columns,
collection: model.get('tickets')
});
el.append(subtable.render().$el);
return subtable;
}
toggle cell - cell extension
Backgrid.ToggleCell = Backgrid.Cell.extend({
[...]
});
toggle cell - cell extension -
render
Backgrid.ToggleCell = Backgrid.Cell.extend({
[...]
render: function() {
this.$el.empty();
var new_el = $('<span class="toggle"></span>');
this.$el.append(new_el);
this.set_toggle().delegateEvents();
return this;
}
});
toggle cell - cell extension -
event
set_toggle: function() {
var self = this;
var td_el = this.$el;
td_el.find('.toggle').click( function() {
var details_row = td_el.closest('tr').next('.child-table');
if (details_row.length > 0) {
$(details_row).remove();
} else {
details_row = $('<tr class="child-table"><td colspan="100"></td></tr>');
$(this).closest('tr').after(details_row);
self.subtable = self.column.get('subtable')(details_row.find('td'), self.model);
}
});
return this;
}
retrieve data - model
App.Models.TicketGroup = Backbone.AssociatedModel.extend({
relations: [
{
type: Backbone.Many,
key: tickets,
relatedModel: 'App.Models.Ticket'
}
],
[...]
});
retrieve data - collection
App.Collections.TicketGroups = Backbone.Collection.extend({
model: App.Models.TicketGroup,
parse: function(tickets, options) {
[...]
return ticketGroups;
},
});
retrieve data - collection
var ticketGroups = [];
_.each(tickets, function (element, index, list) {
var foundElement = _.findWhere(
ticketGroups,
{event_id: element.event_id}
)
if (foundElement == null) {
ticketGroups.push({
"event_id": element.event_id,
"event_name": element.event_name,
"tickets": [element]
});
} else {
foundElement.tickets.push(element);
}
}, this);
testing!
describe("TicketGroups Collection", function () {
describe("parse", function () {
beforeEach(function () {
this.ticketGroupCollection = new App.Collections.TicketGroups();
});
it("parse should return a ticketGroup with nested tickets", function ()
{
var jsonWith3Records = [...];
var result = this.ticketGroupCollection.parse(jsonWith3Records, {});
result.should.have.length(2);
var firstResult = result[0];
firstResult.event_name.should.equal("Concerto Iron Maiden");
firstResult.tickets.should.have.length(2);
var secondResult = result[1];
secondResult.event_name.should.equal("Battle Hymns Tour");
secondResult.tickets.should.have.length(1);
//close brackets
thanks
@giorrrgio
giorgiocefaro.com
@euxpom
nerd2business.net

Datagrids with Symfony 2, Backbone and Backgrid

  • 1.
    Datagrids with Symfony2, Backbone and Backgrid Eugenio Pombi & Giorgio Cefaro
  • 2.
    requirements - composer http://getcomposer.org Runthis in your terminal to get the latest Composer version: curl -sS https://getcomposer.org/installer | php Or if you don't have curl: php -r "eval('?>'.file_get_contents('https://getcomposer.org/installer'));"
  • 3.
    requirements - symfony http://symfony.com/download Createa symfony 2.3.1 project in path/: php composer.phar create-project symfony/framework-standard-edition path/ 2.3.1
  • 4.
    requirements - dependencies composer.json: "require":{ [...] "friendsofsymfony/rest-bundle": "0.12", "jms/serializer-bundle": "dev-master", "jms/di-extra-bundle": "dev-master", "friendsofsymfony/jsrouting-bundle": "~1.1" },
  • 5.
  • 6.
    requirements - javascriptlibs Download the required libs: http://backbonejs.org/ http://underscorejs.org/ http://jquery.com/ http://backgridjs.com/ http://twitter.github.io/bootstrap/
  • 7.
    requirements - javascriptlibs Place the libraries in src/Acme/MyBundle/Resources/public/js/ and include them with Assetic: base.html.yml: {% block javascripts %} {% javascripts 'bundles/mwtbrokertool/js/di-lite.js' 'bundles/mwtbrokertool/js/jquery.js' 'bundles/mwtbrokertool/js/underscore.js' 'bundles/mwtbrokertool/js/bootstrap.js' 'bundles/mwtbrokertool/js/backbone.js' %} <script src="{{ asset_url }}" type="text/javascript"></script> {% endjavascripts %} <script src="{{ asset('/js/fos_js_routes.js') }}"></script> {% endblock %}
  • 8.
    controllers - index /** *@ParamConverter("user", class="MyBundle:User", options={"id" = "userId"}) * @FosRestGet("/ticket.{_format}", * name="mwt_brokertool_ticket", * defaults={"_format": "json"}, * options={"expose"=true}) */ public function indexAction(User $user) { $em = $this->getDoctrine()->getManager(); $repo = $em->getRepository('MyBundle:Ticket'); $tickets = $repo->findBySellerJoinAll($user); return $tickets; }
  • 9.
    controllers - new /** *@ParamConverter("user", class="MyBundle:User", options={"id" = "userId"}) * @FosRestPost("/ticket.{_format}", * name="My_bundle_ticket_new", * defaults={"_format": "json"}, * options={"expose"=true} * ) * @FosRestView * @param User $user */ public function newAction(User $user) { [...] }
  • 10.
    controllers - newticket $ticket = new Ticket(); $form = $this->createForm(new TicketType(), $ticket); $data = $this->getRequest()->request->all(); $children = $form->all(); $data = array_intersect_key($data, $children); $form->submit($data); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($ticket); $em->flush(); return View::create($ticket, 201); } return View::create($form, 400);
  • 11.
    test index public functiontestIndex() { $client = static::createClient(); $crawler = $client->request('GET','/'.$this->user1->getId().'/ticket'); $this->assertTrue($client->getResponse()->isSuccessful()); $json_response = json_decode($client->getResponse()->getContent(), true); $this->assertTrue(is_array($json_response)); $this->assertTrue(isset($json_response[0]['event_id'])); $this->assertTrue(isset($json_response[1]['event_id'])); $this->assertTrue(isset($json_response[2]['event_id'])); }
  • 12.
    test new ticket $client= static::createClient(); $client->request( 'POST', '/' . $this->user1->getId() . '/ticket', array(), array(), array('CONTENT_TYPE' => 'application/json'), '[aJsonString]' ); $this->assertEquals(201, $client->getResponse()->getStatusCode()); json_response = json_decode($client->getResponse()->getContent(), true); $this->assertTrue(is_array($json_response)); $ticket = $this->em->getRepository('ACMEMyBundle:Ticket')->findOneBy(array (...); $this->assertNotNull($ticket);
  • 13.
  • 14.
  • 15.
    backgrid backgridjs.com The goal ofBackgrid.js is to produce a set of core Backbone UI elements that offer you all the basic displaying, sorting and editing functionalities you'd expect, and to create an elegant API that makes extending Backgrid.js with extra functionalities easy.
  • 16.
    backgrid Backgrid.js depends on3 libraries to function: ● jquery >= 1.7.0 ● underscore.js ~ 1.4.0 ● backbone.js >= 0.9.10
  • 17.
    backgrid ● Solid foundation.Based on Backbone.js. ● Semantic and easily stylable. Just style with plain CSS like you would a normal HTML table. ● Low learning curve. Works with plain old Backbone models and collections. Easy things are easy, hards things possible. ● Highly modular and customizable. Componenets are just simple Backbone View classes, customization is easy if you already know Backbone. ● Lightweight. Extra features are separated into extensions, which keeps the bloat away.
  • 18.
    di-lite.js minimalistic dependency injectioncontainer ctx.register("name", instance); ctx.get("name"); My.Stuff = Backbone.Collection.extend({ dependencies: "name", [...] });
  • 19.
    di-lite.js - example varctx = di.createContext(); var user = function () { this.id = $("#grid").attr('data-user); }; ctx.register("user", user); var App.Collections.Articles = Backbone.Collection.extend({ dependencies: "user", model: App.Models.Article, url: function() { return '/article?userId=' + this.user.id; } [...] }); ctx.register("articles", App.Collections.Articles);
  • 20.
    backbone model +collection var Ticket = Backbone.Model.extend({}); var Tickets = Backbone.Collection.extend({ model: Territory, url: Routing.generate('my_bundle_ticket', { userId: App.userId }) }); var tickets = new Tickets();
  • 21.
    backbone associations Associations allowsBackbone applications to model 1:1 & 1: N associations between application models and Collections. https://github.com/dhruvaray/backbone-associations var TicketGroup = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.Many, key: 'tickets', relatedModel: 'Ticket' }] });
  • 22.
    backgrid columns var columns= [{ name: "event_name", label: "Event", cell: "string" , editable: false, }, { name: "event_datetime", label: "Event Date", cell: "datetime" }];
  • 23.
    backgrid initialize var grid= new Backgrid.Grid({ columns: columns, collection: tickets }); $("#my-list").append(grid.render().$el); // Fetch some tickets from the url tickets.fetch({reset: true});
  • 24.
    backgrid - computedfields https://github.com/alexanderbeletsky/backbone-computedfields var CartItem = Backbone.Model.extend({ initialize: function () { this.computedFields = new Backbone.ComputedFields(this); }, computed: { grossPrice: { depends: ['netPrice', 'vatRate'], get: function (fields) { return fields.netPrice * (1 + fields.vatRate / 100); } } } });
  • 25.
    backgrid - computedfields var columns = [{ name: "netPrice", label: "Net Price", cell: "number" }, { name: "vatRate", label: "VAT Rate", cell: "integer" }, { name: "grossPrice", label: "Gross price", cell: "number" }];
  • 26.
    backgrid - selecteditor { name: "country", label: "Country", cell: Backgrid.SelectCell.extend({ optionValues: ctx.get('countries').getAsOptions() }) }
  • 27.
    backgrid - selecteditor App.Collections.Countries = Backbone.Collection.extend({ getAsOptions: function () { var options = new Array(); this.models.forEach(function(item) { options.push([item.get('name'), item.get('id')]) }); return options; } });
  • 28.
    toggle cell -column definition { name: 'nonModelField', label: 'Details', editable: false, cell: Backgrid.ToggleCell, subtable: function(el, model) { var subtable = new Backgrid.Grid({ columns: columns, collection: model.get('tickets') }); el.append(subtable.render().$el); return subtable; }
  • 29.
    toggle cell -cell extension Backgrid.ToggleCell = Backgrid.Cell.extend({ [...] });
  • 30.
    toggle cell -cell extension - render Backgrid.ToggleCell = Backgrid.Cell.extend({ [...] render: function() { this.$el.empty(); var new_el = $('<span class="toggle"></span>'); this.$el.append(new_el); this.set_toggle().delegateEvents(); return this; } });
  • 31.
    toggle cell -cell extension - event set_toggle: function() { var self = this; var td_el = this.$el; td_el.find('.toggle').click( function() { var details_row = td_el.closest('tr').next('.child-table'); if (details_row.length > 0) { $(details_row).remove(); } else { details_row = $('<tr class="child-table"><td colspan="100"></td></tr>'); $(this).closest('tr').after(details_row); self.subtable = self.column.get('subtable')(details_row.find('td'), self.model); } }); return this; }
  • 32.
    retrieve data -model App.Models.TicketGroup = Backbone.AssociatedModel.extend({ relations: [ { type: Backbone.Many, key: tickets, relatedModel: 'App.Models.Ticket' } ], [...] });
  • 33.
    retrieve data -collection App.Collections.TicketGroups = Backbone.Collection.extend({ model: App.Models.TicketGroup, parse: function(tickets, options) { [...] return ticketGroups; }, });
  • 34.
    retrieve data -collection var ticketGroups = []; _.each(tickets, function (element, index, list) { var foundElement = _.findWhere( ticketGroups, {event_id: element.event_id} ) if (foundElement == null) { ticketGroups.push({ "event_id": element.event_id, "event_name": element.event_name, "tickets": [element] }); } else { foundElement.tickets.push(element); } }, this);
  • 35.
    testing! describe("TicketGroups Collection", function() { describe("parse", function () { beforeEach(function () { this.ticketGroupCollection = new App.Collections.TicketGroups(); }); it("parse should return a ticketGroup with nested tickets", function () { var jsonWith3Records = [...]; var result = this.ticketGroupCollection.parse(jsonWith3Records, {}); result.should.have.length(2); var firstResult = result[0]; firstResult.event_name.should.equal("Concerto Iron Maiden"); firstResult.tickets.should.have.length(2); var secondResult = result[1]; secondResult.event_name.should.equal("Battle Hymns Tour"); secondResult.tickets.should.have.length(1); //close brackets
  • 36.