I’m working on an application that allows a user to add and manipulate individual elements on a SVG canvas. I have used Backbone JS for other projects. So why not use Backbone to wrap a little structure around this app.
Extending the Backbone View
I’m going to start by creating a Backbone View for the root SVG element.
1 2 3 4 5 6 7 8 9 10 11 12 |
(function(window, Backbone) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.SvgCanvas = Backbone.View.extend({ tagName:"svg", render:function() { return this; } }); }(window, Backbone)); |
My HTML looks like this…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>SVG + Backbone JS</title> </head> <body> <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.10/backbone-min.js"></script> <script src="scripts/svg-canvas.js"></script> <script> $(document).ready(function() { var svgCanvas = new NoCircleNo.SvgCanvas(); $("body").prepend(svgCanvas.render().el); }); </script> </body> </html> |
When I view and inspect the page, the SVG element is in the body as expected. However I’m going to have a problem once I try to add anything to that SVG. Backbone created the SVG element when a new instance of the view was spun up. This is a great feature of Backbone, but that element isn’t being created with the SVG namespace.
Looking through the Backbone source I found the function that creates an element if one wasn’t supplied to the view.
1 2 3 4 5 6 7 8 9 10 11 |
_ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } } |
This is the line that does the element making.
1 |
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); |
In my setup, Backbone is using jQuery to create the element. So all I need to do is change this function to create the element with the proper namespace. Luckily you can just override this function in the view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
(function(window, $, _, Backbone) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.SvgCanvas = Backbone.View.extend({ tagName:"svg", nameSpace: "http://www.w3.org/2000/svg", _ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); var $el = $(window.document.createElementNS(_.result(this, 'nameSpace'), _.result(this, 'tagName'))).attr(attrs); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } }, render:function() { return this; } }); }(window, $, _, Backbone)); |
Making it Reusable
I don’t want to override this function in each Backbone View that renders SVG. Lets create a SVG Backbone View that can be used to extend other views.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
(function(window, $, _, Backbone) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.SvgBackboneView = Backbone.View.extend({ nameSpace: "http://www.w3.org/2000/svg", _ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, 'attributes')); if (this.id) attrs.id = _.result(this, 'id'); if (this.className) attrs['class'] = _.result(this, 'className'); var $el = $(window.document.createElementNS(_.result(this, 'nameSpace'), _.result(this, 'tagName'))).attr(attrs); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } } }); }(window, $, _, Backbone)); |
Now I can update the SvgCanvas object to extend the new SvgBackboneView object.
1 2 3 4 5 6 7 8 9 10 11 12 |
(function(window) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.SvgCanvas = window.NoCircleNo.SvgBackboneView.extend({ tagName:"svg", render:function() { return this; } }); }(window)); |
Ok, display something already!
Lets create another view with some actual graphics to display as a subview in SvgCanvas. This view will display a face from my Auto Andy project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(function(window) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.FaceView = window.NoCircleNo.SvgBackboneView.extend({ tagName:"path", render:function() { this.$el.attr("d", "M24.514,98.772C11.98,72.085,48.905,64.897,52.987,84.797 C55.742,98.235,32.866,108.729,24.514,98.772z M25.799,90.235c0.912,10.577,18.277,11.611,21.071,0.992 C45.817,72.755,24.65,76.938,25.799,90.235 M199,106.758c-6.294,8.146-17.307,11.576-26.476,16.849 c0.592,5.404-0.61,9.018-1.204,13.238c-3.97-1.099-6.205-5.757-10.83-8.425c-47.752,20.97-97.11,33.497-157.653,13.238 c22.04,3.313,43.696,8.971,69.8,7.221c47.627-3.194,90.074-23.532,120.346-42.121c0.298-1.308,2.393-0.817,2.407-2.407 C196.971,104.776,197.283,106.47,199,106.758z M164.1,127.216c1.45,3.562,3.605,5.221,6.018,7.221c0.214-4.964,0.836-5.832,0-9.627 C167.289,125.436,166.172,125.933,164.1,127.216 M147.24,82.985c-2.712-28.381-49.209-9.002-28.549,14.07 C126.663,104.554,148.298,94.072,147.24,82.985z M131.245,94.32c-5.613,0-10.164-4.551-10.164-10.163 c0-5.614,4.551-10.164,10.164-10.164s10.163,4.55,10.163,10.164C141.408,89.769,136.858,94.32,131.245,94.32z"); this.$el.attr("fill", "#666"); return this; } }); }(window)); |
Adding in the Face Subview, the SvgCanvas object now displays a face when rendered.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(function(window) { window.NoCircleNo = window.NoCircleNo || {}; window.NoCircleNo.SvgCanvas = window.NoCircleNo.SvgBackboneView.extend({ tagName:"svg", render:function() { this.faceView = new NoCircleNo.FaceView(); this.$el.append(this.faceView.render().el); return this; } }); }(window)); |