PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/_posts/2011-02-11-backbone.js-makes-building-javascript-applications-fun.md

https://github.com/alexrothenberg/alexrothenberg.github.com
Markdown | 355 lines | 276 code | 79 blank | 0 comment | 0 complexity | ac205978f225eebf9e4241712be0cc8a MD5 | raw file
  1. ---
  2. layout: post
  3. title: Backbone.js Makes Building JavaScript Applications Fun
  4. ---
  5. Like many developers I've had a long, complicated relationship with Javascript.
  6. Especially with libraries like [jquery](http://jquery.com) it's incredibly easy to add interesting behavior to your pages, but unless you're very careful
  7. its also likely that you'll end up with a mess of spaghetti javascript. I know as I've gotten myself into that mess and abandoned many projects
  8. because they were just too hard to change.
  9. All this has changed with some of new libraries out there that help you write your javascript following the MVC pattern.
  10. Today I'm going to talk about [backbone.js](http://documentcloud.github.com/backbone/)
  11. and show how it helped me and two friends build a rich one-page application to understand an exception stack trace.
  12. Backbone.js describes itself as supplying structure to JavaScript-heavy
  13. applications by providing models with key-value binding and custom events, collections with a rich API of enumerable
  14. functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON
  15. interface.
  16. There are a number of [tutorials](http://liquidmedia.ca/blog/2011/01/backbone-js-part-1/) and
  17. [examples](http://documentcloud.github.com/backbone/#examples) available out there to get you started.
  18. The basic idea is to uses backbone.js to organize your code into models, views and controllers.
  19. ## What is CodeBuddy
  20. [CodeBuddy](http://rubygems.org/gems/code_buddy) is the application I'll describe.
  21. It helps you navigate an exception stack raised by your Rails app or any other stack you paste in.
  22. I worked on it with [Pat Shaughnessy](http://patshaughnessy.net) and [Daniel Higginbotham](http://www.flyingmachinestudios.com/)
  23. and Pat produced a super article describing
  24. [what CodeBuddy is and how to use it](http://patshaughnessy.net/2010/12/13/codebuddy-see-your-ruby-stack-come-alive).
  25. There is some server-side code that replaces the Rails Show Exceptions page and syntax highlights a snippet of source code for each line in the stack but
  26. for this article I'm going to ignore that and focus on the interactive page and the javascript behind it.
  27. Below is a picture showing a stack trace the background and the code snippet for the currently selected line in front.
  28. Now a picture is okay but to really get a sense of it I suggest you follow this link to
  29. <a href="/examples/code_buddy" target="_blank">experience Code Buddy in action</a>
  30. - try pressing or or double clicking a few lines in the stack then press `s`.
  31. ![Code Buddy](http://www.alexrothenberg.com/examples/code_buddy/images/code_buddy_screenshot.png)
  32. There's a lot going on here and if we had tried building this before discovering backbone.js we probably would have had created a mess -
  33. mixing javascript and html that quickly would have become hard to change.
  34. I'm going to show you how backbone let us separate the models from the views and create something that was not hard to grow.
  35. Let's get into the technical details!
  36. ## Organizing our data in Backbone Models
  37. We want to follow good OO design principles and thinking about this our exception stack is really just a few objects:
  38. * A `Stack`
  39. * has many `Addresses`
  40. * knows which `Address` is selected
  41. * Each `Address` has
  42. * path to a file
  43. * line number
  44. * snippet of code
  45. We can build this as a JSON object like
  46. {% highlight javascript %}
  47. var stackJson = {
  48. "stack_frames": [
  49. { "path": "/Users/alex/ruby/github/test_app/app/controllers/users_controller.rb",
  50. "line": 5,
  51. "code": "class UsersController < ApplicationController\n..."
  52. },
  53. { "path": "/Users/alex/.rvm/.../lib/action_controller/metal/implicit_render.rb",
  54. "line": 4,
  55. "code": "module ActionController\n..."
  56. }
  57. ],
  58. "selected": 0
  59. }
  60. {% endhighlight %}
  61. The first step is to turn this into backbone models.
  62. {% highlight javascript %}
  63. // Stack, Address and Addresses
  64. CodeBuddy.backbone.Stack = Backbone.Model.extend({
  65. initialize: function() {
  66. this.set({
  67. addresses: new CodeBuddy.backbone.Addresses(this.get('stack_frames'))
  68. })
  69. }
  70. })
  71. CodeBuddy.backbone.Address = Backbone.Model.extend({
  72. })
  73. CodeBuddy.backbone.Addresses = Backbone.Collection.extend({
  74. model:CodeBuddy.backbone.Address
  75. })
  76. {% endhighlight %}
  77. The way we build a model in backbone is by extending `Backbone.Model`. When extending we can add custom behavior
  78. if we want. In our example we tell the `Stack` to contain a collection of `Address` objects in the `Addresses` collection.
  79. We use the backbone collection framework to define `Addresses` and tell it that it is a collection of `Address` model objects.
  80. These models will use the default backbone behavior for the rest which includes read/write access to its properties `get` or `set`.
  81. Now that they're defined, we're ready to interact with these models. For example below is what you'd see using the Chrome javascript console
  82. (I'm showing the output as comments for readability)
  83. {% highlight javascript %}
  84. // Using our models in a console
  85. CodeBuddy.stack = new CodeBuddy.backbone.Stack({
  86. "stack_frames": [
  87. { "path": "/Users/alex/ruby/github/test_app/app/controllers/users_controller.rb",
  88. "line": 5,
  89. "code": "class UsersController < ApplicationController\n..."
  90. },
  91. { "path": "/Users/alex/.rvm/.../lib/action_controller/metal/implicit_render.rb",
  92. "line": 4,
  93. "code": "module ActionController\n..."
  94. }
  95. ],
  96. "selected": 0
  97. })
  98. // inherits.child
  99. CodeBuddy.stack.get('addresses')
  100. // inherits.child
  101. CodeBuddy.stack.get('addresses').first().get('path')
  102. // "/Users/alex/ruby/github/test_app/app/controllers/users_controller.rb"
  103. CodeBuddy.stack.get('addresses').first().get('code')
  104. // "class UsersController < ApplicationController
  105. // ..."
  106. {% endhighlight %}
  107. We now have an object hierarchy with default behavior which we can (and will) extend in a bit but first let's build some views and get a page we can look at.
  108. ## Building the UI with Backbone Views
  109. We're going to build
  110. * A `StackView` that contains many `AddressViews`
  111. * Each `AddressView` will display a single line from the stack.
  112. * Each view will be tied to one of our model objects
  113. It will look something like this:
  114. ![Code Buddy views on the page](http://www.alexrothenberg.com/examples/code_buddy/images/annotated_screenshot.png)
  115. We can start with a `StackView` that's tied to the `Stack` model.
  116. Creating a backbone view is very similar to how we created our models - we extend `Backbone.View` and can
  117. override behavior if we want.
  118. {% highlight javascript %}
  119. CodeBuddy.backbone.StackView = Backbone.View.extend({
  120. el: $("#stack"),
  121. initialize: function() {
  122. this.model.get('addresses').each(this.addOneAddress);
  123. },
  124. addOneAddress: function(address, index) {
  125. var view = new CodeBuddy.backbone.AddressView({model: address});
  126. this.$("#stack").append(view.render().el);
  127. }
  128. })
  129. {% endhighlight %}
  130. In this case we overrode the `initializer` function to create an `AddressView` for each address.
  131. It also uses jQuery to add the `AddressView's` html within the page's `#stack` element.
  132. To do the iteration we use another powerful javascript library called
  133. [underscore.js](http://documentcloud.github.com/underscore/). Underscore.js gives us a ruby-like collection methods letting us
  134. write `.each(this.addOneAddress)`. This will iterate over all the `addressses` calling the `addOneAddress` function on each one.
  135. Underscore.js also gives us erb-like templating we'll use in the `AddressView`...let's take a look at that view.
  136. {% highlight javascript %}
  137. CodeBuddy.backbone.AddressView = Backbone.View.extend({
  138. tagName: "li",
  139. template: _.template("<span class='container'><%= path %>:<%= line%></span>"),
  140. initialize: function() {
  141. },
  142. render: function() {
  143. var html = this.template(this.model.toJSON())
  144. $(this.el).html(html);
  145. return this;
  146. }
  147. })
  148. {% endhighlight %}
  149. You can see the template and how it does look like erb. It defines the html that will be displayed for each address and its able to access
  150. properties in the model like `path` and `line`. The template gets applied in the `render` function with the line `this.template(this.model.toJSON())`.
  151. Finally, `tagName` is used to wrap this html in an `li` tag.
  152. Now we can put it all together to see on a page. We can start with a toplevel page that has an element with `stack`, includes our models and views and tells them to load.
  153. {% highlight html %}
  154. <html>
  155. <body>
  156. <ul id="stack"></ul>
  157. <script src="javascripts/code_buddy.js" type="text/javascript"></script>
  158. <script>
  159. CodeBuddy.setup({
  160. "stack_frames": [
  161. { "path": "/Users/alex/ruby/github/test_app/app/controllers/users_controller.rb",
  162. "line": 5,
  163. "code": "class UsersController < ApplicationController\n..."
  164. },
  165. { "path": "/Users/alex/.rvm/.../lib/action_controller/metal/implicit_render.rb",
  166. "line": 4,
  167. "code": "module ActionController\n..."
  168. }
  169. ],
  170. "selected": 0
  171. })
  172. </script>
  173. </body>
  174. </html>
  175. {% endhighlight %}
  176. After the page loads and the views render it changes the `#stack` div to have all this
  177. {% highlight html %}
  178. <ul id="stack">
  179. <li>
  180. <span class="container">/Users/alex/ruby/github/test_app/app/controllers/users_controller.rb:5</span>
  181. </li>
  182. <li>
  183. <span class="container">/Users/alex/.rvm/.../lib/action_controller/metal/implicit_render.rb:4</span>
  184. </li>
  185. </ul>
  186. {% endhighlight %}
  187. So far this is a lot of framework and structure for a simple page but now is when it gets interesting and backbone reveals its true power!
  188. ## Showing code for the selected address
  189. Let's say we want to mark one address selected and show the code for that one.
  190. ![Showing Code for selected Addresss](http://www.alexrothenberg.com/examples/code_buddy/images/code_buddy_screenshot_with_selection.png)
  191. We can start by adding an element to the html called `code-viewer`
  192. {% highlight html %}
  193. <html>
  194. <body>
  195. <ul id="stack"></ul>
  196. <div id="code-viewer">
  197. <div id="code"></div>
  198. </div>
  199. <!-- all the javascript omitted for clarity -->
  200. </body>
  201. </html>
  202. {% endhighlight %}
  203. Now we want to create a `StackView` to put the right code in there
  204. {% highlight javascript %}
  205. CodeBuddy.backbone.CodeView = Backbone.View.extend({
  206. el:$("#code-viewer"),
  207. initialize: function() {
  208. },
  209. render: function() {
  210. this.$("#code").html(CodeBuddy.stack.selectedAddress().get('code'))
  211. }
  212. })
  213. {% endhighlight %}
  214. In this view we used our models to find which address was selected `CodeBuddy.stack.selectedAddress()` so let's add that method.
  215. {% highlight javascript %}
  216. CodeBuddy.backbone.Stack = Backbone.Model.extend({
  217. initialize: function() {
  218. // same code as before
  219. },
  220. addresses: function() {
  221. return this.get('addresses')
  222. },
  223. selectedAddress: function() {
  224. var selected = this.get('selected')
  225. return this.addresses().at(selected)
  226. }
  227. })
  228. {% endhighlight %}
  229. That's it, now we have a code view on top of the stack view and our code is still clean and well organized. I'm not afraid to keep going and make our next change.
  230. ## Changing the selected address
  231. Oh, we just got a new requirement to be able to scroll up and down through the stack and see the code window change as we go.
  232. We add some more functions to our Stack Model so we can change the selection and we also tell it to call the `selectionChanged` function whenever
  233. the `selected` property changes.
  234. Calling a function on a property change is something that's given to us with [backbone events](http://backbonejs.org/#Events).
  235. Getting the view to update itself is also easy we just tell it to with `CodeBuddy.codeView.render()`
  236. {% highlight javascript %}
  237. CodeBuddy.backbone.Stack = Backbone.Model.extend({
  238. initialize: function() {
  239. this.bind('change:selected', this.selectionChanged);
  240. // same code as before
  241. },
  242. setSelection: function(newSelected) {
  243. if (newSelected >= 0 && newSelected < this.addresses().size()) {
  244. this.set({ selected: newSelected })
  245. }
  246. },
  247. selectPrevious: function() {
  248. this.setSelection(this.get('selected') - 1)
  249. },
  250. selectNext: function() {
  251. this.setSelection(this.get('selected') + 1)
  252. },
  253. selectionChanged: function(x) {
  254. this.addresses().at(x.previousAttributes().selected).view.render()
  255. this.addresses().at(x.changedAttributes().selected).view.render()
  256. CodeBuddy.codeView.render()
  257. },
  258. // all the existing functions remain
  259. })
  260. {% endhighlight %}
  261. The last thing we need to do is give some way for the user to change the selection.
  262. We decided to use [jQuery Hotkeys](http://code.google.com/p/js-hotkeys/) to bind to
  263. the up and down arrows ( or ).
  264. {% highlight javascript %}
  265. CodeBuddy.setStackKeyBindings = function(){
  266. $(document).bind('keydown', 'up', CodeBuddy.stack.selectPrevious)
  267. $(document).bind('keydown', 'down', CodeBuddy.stack.selectNext)
  268. }
  269. {% endhighlight %}
  270. ## Conclusion
  271. I hope this example has shown a little of the power of using backbone.js when you need to write a complex javascript application.
  272. If you want to see the actual application its all on Github in
  273. [code_buddy.js](https://github.com/patshaughnessy/code_buddy/blob/master/lib/code_buddy/public/javascripts/code_buddy.js)
  274. This was our first project working with backbone.js and I'm sure there's much more we can learn but we've found that it
  275. lets us write modular code with many small methods which feels much closer to writing Ruby than my previous forays into javascript.
  276. **Backbone.js makes writing a javascript application fun!**