Porting Jquery Plugins with Backbone and Deep Model Merging
I’m currently working on an analytics project at VMware where I was lucky enough to redo the front end with backbone. Though one thing I couldn’t get away from was the jQuery plugins we were using to do the graphs and tables. Much of the data being passed from the back end to the front end was stable and we didn’t want to redo the graphs in D3 or other libraries. I really didn’t want to have a mix of jquery plugin syntax with this clean backbone setup, so I had to come up with some sort of solution with porting these jquery options with backbone. At the same time I wanted a clean way of keeping things modular and reusing code, because the project had started to become ridden with conditional statements for different graphs/reports
So the mission was to port plugins and make it more modular
Solution To Porting
jQuery plugins usually consist of many options, and I have seen examples of people placing these options in Backbone.View’s render method for rendering. This doesn’t seem clean to me, because we use a lot of different options with highcharts, and would clog up the render method.
I basically asked myself what are options in a jQuery plugin?
Options are basically attributes. What has attributes?
Models!
So lets store the highcharts options in a Backbone Model under the default method.
Our Model
Wow thats a lot of options! Good thing we didn’t clog our view up with them.
var HighChartsGraph = Backbone.Model.extend({
// Deep merge needed. Backbone only has a shallow merge available, which will remove
// the nested data. So had to use Jqueries extend to merge the objects in a deep
// recursive manner by setting the deep option to true.
deepSet: function(data){
// Deep merge of data
$.extend(true, this.attributes, data);
this.trigger('change', this);
},
defaults: {
chart: {
renderTo: 'graph-2',
defaultSeriesType: 'column',
marginBotton: 30,
reflow: true,
zoomType: 'xy',
backgroundColor: 'transparent',
style: {
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
fontSize: '13px',
lineHeight: '18px',
color: '#333333'
},
height: null
},
colors: [
'#4572A7',
'#AA4643',
'#89A54E',
'#80699B',
'#3D96AE',
'#DB843D',
'#92A8CD',
'#A47D7C',
'#B5CA92',
'#6baed6',
'#9ecae1',
'#c6dbef',
'#e6550d',
'#fd8d3c',
'#fdae6b',
'#fdd0a2',
'#31a354',
'#74c476',
'#a1d99b',
'#c7e9c0',
'#756bb1',
'#9e9ac8',
'#bcbddc',
'#dadaeb',
'#636363',
'#969696',
'#bdbdbd',
'#d9d9d9'
],
credits: {
enabled: false
},
exporting: {
buttons: {
exportButton: {
enabled:false
}
}
},
plotOptions: {
column: {
pointPadding: 0.2,
borderWidth: 0,
pointWidth: 13
},
pie : {
allowPointSelect: true,
cursor: 'pointer',
point: {
events: {
click: function() {
DrilldownView.graphDrilldown($('#graph'), false)
}
}
},
size: DASHBOARD ? '60%' : '80%',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
formatter: function() {
return ''+ this.point.name +': '+ this.point.y;
},
verticalAlign:'bottom',
distance: 20,
}
},
series: {
borderWidth: 1,
borderColor: '#fff',
cursor: 'pointer',
point: {
events: {
click: function(e) {
var graphWrapper = $('#'+container).parents('.graph');
get_specific_data(this.x, this.y, this.series, graphWrapper);
DrilldownView.graphDrilldown(graphWrapper);
}
}
}
},
line: {
lineWidth: 4,
states: {
hover: {
lineWidth: 5
}
},
marker: {
enabled: false,
states: {
hover: {
enabled: true,
symbol: 'circle',
radius: 5,
lineWidth: 1
}
}
}
}
},
subtitle: {
margin: 20,
style: {
fontSize: '13px',
color: '#999999',
marginBottom: '20px'
}
},
title: {
margin: 5,
style: {
fontSize: '16px',
lineHeight: '18px',
color: '#333333'
}
},
tooltip: {
formatter: function() {
return this.series.name + ' has ' + this.y + ' on ' + this.x;
}
},
xAxis: {
allowDecimals: false,
tickWidth: 0,
labels: {
rotation: -45,
style: {
color:'#333333'
}
}
},
yAxis: {
title: {
style: {
color: '#89A54E'
}
},
gridLineWidth: 1,
gridLineColor: '#EEEEEE',
allowDecimals: false
}
}
For simplicity sake we are going to just stick with models and not add a collection
Deep Set Function
You may have noticed that I added a deepSet function to the model. Thats because when you use the function model.set(data), it won’t deep merge the attributes. Meaning if the parent attribute changes it will remove all of its nested attributes, whether or not they are different. So I used extend and set the deep copy to true. I also created a trigger, so that we get similar backbone model behavior.
// Deep merge needed. Backbone only has a shallow merge available, which will remove
// the nested data. So had to use Jqueries extend to merge the objects in a deep
// recursive manner by setting the deep option to true.
deepSet: function(data){
this._previousAttributes = _.clone(this.attributes);
// Deep recursive merge of data
$.extend(true, this.attributes, data);
this.trigger('change', this);
}
Note: On the change event we won’t be able to filter by specific attribute, ie. this.model.on(‘change:attribute’, etc….. That would require some more work on my side, for my use I don’t need to use this functionality, I just want the basic model structure.
IMPORTANT!: Merged JSON objects
One very important thing to note is that the json objects that we merge have the same structure and sequence. We use the same options structure that can be found in the plugin’s docs. This keeps it very clean and organized, for both front end and back end.
Our View
Set up a basic view to set the data with our new deepSet function, and have it listen to the model change, where we pass the model.toJSON() into the jQuery plugin, and then append to our el.
var GraphView = Backbone.View.extend({
el: $('.chart-wrapper'),
initialize: function(){
_.bindAll(this, 'render');
this.model = new HighChartsGraph();
this.model.on('change', _.bind(function(data){
this.$el.prepend(this.template(data.toJSON()));
// Intialize our high charts
var chart = new Highcharts.Chart(data.toJSON());
}, this));
},
template: function(data){
var graphSection = ich.reportSection(data.layout);
return graphSection
},
render: function(data){
this.model.deepSet(data)
return this
}
});
Complete Picture
To show you a more complete picture of how I render the highcharts graph:
I setup a report model and collections
I setup a report view, and fire off a fetch on intialize of the view. Then parse the report data model appropriately and render the graph view among other views that I won’t go into detail about.
var Report = Backbone.Model.extend({
url: '/get-report/'
});
var Reports = Backbone.Collection.extend({
url: '/get-report/',
model:ReportData
});
var ReportView = Backbone.View.extend({
el: $('body'),
intialize: function(){
_.bindAll(this);
this.collection = new Reports();
this.collection.on('add', function(report){
this.render(report)
});
this.collection.fetch();
},
render: function(report){
var d = report.toJSON();
if(d.graph){
GraphView.render(d.graph);
}
if(d.table){
TableView.render(d.table);
}
}
});
var HighChartsGraph = Backbone.Model.extend({
// Deep merge needed. Backbone only has a shallow merge available, which will remove
// the nested data. So had to use Jqueries extend to merge the objects in a deep
// recursive manner by setting the deep option to true.
deepSet: function(data){
// Deep recursive merge of data
$.extend(true, this.attributes, data);
this.trigger('change', this);
},
defaults: {
chart: {
renderTo: 'graph-2',
defaultSeriesType: 'column',
marginBotton: 30,
reflow: true,
zoomType: 'xy',
backgroundColor: 'transparent',
style: {
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
fontSize: '13px',
lineHeight: '18px',
color: '#333333'
},
height: null
},
colors: [
'#4572A7',
'#AA4643',
'#89A54E',
'#80699B',
'#3D96AE',
'#DB843D',
'#92A8CD',
'#A47D7C',
'#B5CA92',
'#6baed6',
'#9ecae1',
'#c6dbef',
'#e6550d',
'#fd8d3c',
'#fdae6b',
'#fdd0a2',
'#31a354',
'#74c476',
'#a1d99b',
'#c7e9c0',
'#756bb1',
'#9e9ac8',
'#bcbddc',
'#dadaeb',
'#636363',
'#969696',
'#bdbdbd',
'#d9d9d9'
],
credits: {
enabled: false
},
exporting: {
buttons: {
exportButton: {
enabled:false
}
}
},
plotOptions: {
column: {
pointPadding: 0.2,
borderWidth: 0,
pointWidth: 13
},
pie : {
allowPointSelect: true,
cursor: 'pointer',
point: {
events: {
click: function() {
DrilldownView.graphDrilldown($('#graph'), false)
}
}
},
size: DASHBOARD ? '60%' : '80%',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
formatter: function() {
return ''+ this.point.name +': '+ this.point.y;
},
verticalAlign:'bottom',
distance: 20,
}
},
series: {
borderWidth: 1,
borderColor: '#fff',
cursor: 'pointer',
point: {
events: {
click: function(e) {
var graphWrapper = $('#'+container).parents('.graph');
get_specific_data(this.x, this.y, this.series, graphWrapper);
DrilldownView.graphDrilldown(graphWrapper);
}
}
}
},
line: {
lineWidth: 4,
states: {
hover: {
lineWidth: 5
}
},
marker: {
enabled: false,
states: {
hover: {
enabled: true,
symbol: 'circle',
radius: 5,
lineWidth: 1
}
}
}
}
},
subtitle: {
margin: 20,
style: {
fontSize: '13px',
color: '#999999',
marginBottom: '20px'
}
},
title: {
margin: 5,
style: {
fontSize: '16px',
lineHeight: '18px',
color: '#333333'
}
},
tooltip: {
formatter: function() {
return this.series.name + ' has ' + this.y + ' on ' + this.x;
}
},
xAxis: {
allowDecimals: false,
tickWidth: 0,
labels: {
rotation: -45,
style: {
color:'#333333'
}
}
},
yAxis: {
title: {
style: {
color: '#89A54E'
}
},
gridLineWidth: 1,
gridLineColor: '#EEEEEE',
allowDecimals: false
}
}
});
var GraphView = Backbone.View.extend({
el: $('.chart-wrapper'),
initialize: function(){
_.bindAll(this, 'render');
this.model = new HighChartsGraph();
this.model.on('change', _.bind(function(data){
this.$el.prepend(this.template(data.toJSON()));
// Intialize our high charts
var chart = new Highcharts.Chart(data.toJSON());
}, this));
},
template: function(data){
var graphSection = ich.reportSection(data.layout);
return graphSection
},
render: function(data){
this.model.deepSet(data)
return this
}
});
Review
So to review, I set up a model and insert the jQuery plugin options I will be always be using in the default function of the model. I then fetch my saved data options from the backend. Do a deep merge of the default attributes and new attributes. Then pass that model to the jQuery plugin in the view and append the template to our el.
Boom! Clean and done..
All options changes and new data show up on the front end in a nice clean manner.




![Mercedes Rolls Out Invisible Car [VIDEO]](http://mashable.com/wp-content/uploads/2012/03/125,invisible-car600.jpg)
