PageRenderTime 16ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

Markdown | 337 lines | 236 code | 101 blank | 0 comment | 0 complexity | c29b1a14b06e5bc41d3d94e8d8477afb MD5 | raw file
  1. **What They Said**
  2. > "Using Wiring, you can create very complex inter-relationships with the elements on the screen. When one is updated, all the dependent elements update automatically. "--[Lift and its wiring.][wiring]
  3. **What We Say**
  4. Challenge Accepted!
  5. Fibonacci Numbers with Wiring
  6. ---
  7. [<font size ="+2"><b>Try Me!</b></font>][kt]
  8. Try this sample [browser application][kt] written with the [Faux][f] framework for building Javascript applications and [Backbone.js][bb]'s model, view, and controller classes. This demonstrates how to wire views to models, such that a change to a model causes any dependent views to re-render themselves in the DOM automatically. You will see a table with three columns, a number "n" in the first column,and its Fibonacci number, calculated two different ways, in the second and third columns.
  9. Click on any of the numbers in the first column. You will see the number you clicked on get "swapped" with one other randomly selected number. You will see the Fibonacci numbers recalculate themselves accordingly. This effect is achieved entirely with Backbone.js's support for wiring dependent views to models, and in this brief overview we'll see how Faux makes it easy to structure this kind of application.
  10. (This application builds on another example demonstrating [lazy and parallel page rendering][np]. You may want to review that example first.)
  11. [wiki]:
  12. **understanding how it works**
  13. To understand this application, have a look at Faux's [read me][f], follow the links to the introductory example application [Misadventure][play], and especially its [four][pi]-[part][pii] [code][piii] [review][piv]. The four-part code review will walk you through how a Faux application is structured. Then have a look at another example demonstrating [lazy and parallel page rendering][np].
  14. Now we can walk through this example and see how it makes wiring dependencies trivial.
  15. All Faux applications start with a controller. It's in `controller.js`:
  16. var controller = new Faux.Controller({
  17. save_location: true,
  18. element_selector: '.content',
  19. partial: 'haml',
  20. partial_suffix: '.haml',
  21. javascript: 'javascripts',
  22. clazz: true,
  23. title: 'Fibonacci Numbers with Wiring'
  24. });
  25. controller
  26. .method('values', {
  27. 'values=': function () { return new ValueCollection(); }
  28. })
  29. .begin({
  30. route: false,
  31. 'model=': {
  32. 'values, value_id': function (values, value_id) { return values.get(value_id); }
  33. }
  34. })
  35. .method('value', {
  36. renders: 'td.value.value_id_:value_id'
  37. })
  38. .method('naive', {
  39. renders: 'td.naive.value_id_:value_id'
  40. })
  41. .method('matrix', {
  42. renders: 'td.matrix.value_id_:value_id'
  43. })
  44. .end()
  45. ;
  46. // Kick off the application, and invoke `controller.values()` if no fragment is provided.
  47. $(function() {
  48. controller.define_all(function () {
  49. Backbone.history.start();
  50. globals.location.hash || controller.values();
  51. });
  52. });
  53. **about "`controller.values()`":**
  54. Faux application have methods, and those methods are usually associated with routes that are represented by URL fragments. This example application has a `.values()` method:
  55. .method('values', {
  56. 'values=': function () { return new ValueCollection(); }
  57. })
  58. By default, it is associated with the route fragment `#/values`, and we've set it up such that it's the default method when you open the application page. We've also added a default calculation that assigns a new `ValueCollection` to `values`. Looking in the project, we see that there's a `values.js` file that defines a Backbone Collection:
  59. globals.ValueCollection = Backbone.Collection.extend({
  60. initialize: function () {
  61. this.refresh(
  62. _.range(1, 26).map(function (n) {
  63. return new Backbone.Model({ id: n, n: n });
  64. })
  65. );
  66. }
  67. });
  68. Because we don't define our own `ValuesView`, Faux defines a default view class for us (`Backbone.View.extend({})`). It renders the page with `values.haml` by default:
  69. %table
  70. %thead
  71. %th n
  72. %th matrix
  73. %th naive
  74. %tbody
  75. :each value in this.model.models
  76. %tr
  77. %td{ class: 'value value_id_' + }
  78. %td{ class: 'matrix value_id_' + }
  79. %img{ src: './images/ajax-loader.gif' }
  80. %td{ class: 'naive value_id_' + }
  81. %img{ src: './images/ajax-loader.gif' }
  82. This sets up a table with twenty-five rows:
  83. %table
  84. %thead
  85. %th n
  86. %th matrix
  87. %th naive
  88. %tbody
  89. %tr.fibonacci.n_1
  90. %td.value.value_id_1
  91. %td.matrix.value_id_1
  92. %img{ src: './images/ajax-loader.gif' }
  93. %td.naive.value_id_1
  94. %img{ src: './images/ajax-loader.gif' }
  95. %tr
  96. %td.value.value_id_2
  97. %td.matrix.value_id_2
  98. %img{ src: './images/ajax-loader.gif' }
  99. %td.naive.value_id_2
  100. %img{ src: './images/ajax-loader.gif' }
  101. %tr
  102. %td.value.value_id_3
  103. %td.matrix.value_id_3
  104. %img{ src: './images/ajax-loader.gif' }
  105. %td.naive.value_id_3
  106. %img{ src: './images/ajax-loader.gif' }
  107. ...
  108. %tr
  109. %td.value.value_id_25
  110. %td.matrix.value_id_25
  111. %img{ src: './images/ajax-loader.gif' }
  112. %td.naive.value_id_25
  113. %img{ src: './images/ajax-loader.gif' }
  114. As rendered by `values.haml`, each row of the table has three columns. The first contains a number, *n*, and the other two are reserved for n's fibonacci number, calculated in two different ways. One calculation uses [matrix math][m], the other is the naive recursive procedure`fibd(n) = fib(n-1) + fib(n-2)`. These columns are filled with a progress `.gif`. Something else fills in actual values. But what?
  115. [m]: "A program to compute the nth Fibonacci number"
  116. **about "`controller.value()`," "`controller.naive()`" and "`controller.matrix()`":**
  117. The example application has three other methods, `controller.value()`, `controller.naive()` and `controller.matrix()`:
  118. controller
  119. .begin({
  120. route: false,
  121. 'model=': {
  122. 'values, value_id': function (values, value_id) { return values.get(value_id); }
  123. }
  124. })
  125. .method('value', {
  126. renders: 'td.value.value_id_:value_id'
  127. })
  128. .method('naive', {
  129. renders: 'td.naive.value_id_:value_id'
  130. })
  131. .method('matrix', {
  132. renders: 'td.matrix.value_id_:value_id'
  133. })
  134. .end()
  135. Faux associates a default view class with `controller.value()`, but we've defined two of our own view classes, `NaiveView`, and `MatrixView`. We'll discuss the exact code a little later. Just like `controller.values()`, these methods use haml templates to render their content. `value.haml` is remarkably simple:
  136. %span= this.n()
  137. `naive.haml` and `matrix.haml` are equally simple:
  138. %span= this.fibonacci()
  139. As you can see, the templates are remarkably simple! This is fine, because `controller.value()`, `controller.naive()` and `controller.matrix()` are not used by routes to render content on the page, they're used to render the contents of the individual table cells. Let's see how.
  140. **about "`route: false`":**
  141. Faux normally assigns a route to each method. If you don't supply one, Faux assumes there is one with the same name as the methods. This can be disabled with `route:false`, so we are declaring that `controller.value()`, `controller.naive()` and `controller.matrix()` cannot be invoked with routes. Likewise, there are not `route_to_matrix` or `route_to_naive` helper methods. We're disabling routes because these methods are only used to render portions of `controller.values()`'s content.
  142. **about "`renders: 'td.value.value_id_:value_id'`," "`renders: 'td.naive.value_id_:value_id'`" and "`renders: 'td.matrix.value_id_:value_id'`":**
  143. The `renders` configuration option is a vastly simplified jQuery selector with some special sauce. You are safe with an optional tag type ('td'), an optional id (not shown here, but something like `#42`), and zero or more optional classes (like `.matrix`). Faux extracts any id or class selector that has a parameter embedded in it, so Faux reads `'td.value.value_id_:value_id'` as:
  144. td <- tag selector
  145. .value <- class selector
  146. .value_id_:value_id <- parameter inference
  147. As we saw above, `controller.values()` renders table rows such as (in HTML):
  148. <tr>
  149. <td class='value value_id_42'></td>
  150. <td class='matrix value_id_42'><img src='./images/ajax-loader.gif'/></td>
  151. <td class='naive value_id_42'><img src='./images/ajax-loader.gif'/></td>
  152. </tr>
  153. Table cells such as `<td class='matrix value_id_42'>...</td>` match the jQuery selector 'td.matrix' and table cells such as `<td class='naive value_id_42'>...</td>` match the jQuery selector 'td.naive'. After `controller.values()` has finished rendering its content, Faux invokes `controller.value()` for each of the cells with class `value`, `controller.naive()` for each of the cells with class `naive` and `controller.matrix()` for each of the cells with class `matrix`. No routes are involved, and whatever these methods render replaces the content of their table cells.
  154. **inferences**
  155. Since `controller.value()`, `controller.naive()` and `controller.matrix()` are invoked by DOM elements matching their `renders` configuration and not by a route, they cannot accept a parameter embedded in the fragment (such as `#/naive/42`). Instead, Faux is able to infer parameters from their class(es) and/or ids. In this application, `.value_id_:value_id` instructs Faux that when invoking `controller.naive()` and `controller.matrix()` to render content for an element, it can infer that the parameter `value_id` is `42` from a class such as `value_id_42`. Each individual cell will be rendered by its own method being invoked, and each will get its own parameter.
  156. Note that an inference like `.value_id_:value_id` is not a selector. If there was an element such as `<td class='matrix frobbish_42'>...</td>` in the DOM, it would still trigger `controller.matrix()` even though there is no class in the DOM to "match" `.value_id_:value_id` and provide a way to infer the parameter `value_id`.
  157. (note: Inferences are always of the form `/(\.|#)\w+:[a-z_]\w*/i`).
  158. **about "`model=`":**
  159. Faux lets us define calculations for parameters. This one:
  160. 'model=': {
  161. 'values, value_id': function (values, value_id) { return values.get(value_id); }
  162. }
  163. Tells Faux how to determine the parameter `model` given `values` and `value_id`. `model` is a useful parameter: Unless you instruct Faux otherwise, the `model` parameter will be passed on to the method's view. We saw above how Faux will inter the `view_id` from a class, and now we see how it can combine this with `values` to get at a model instance that Faux will wire to the view.
  164. **more about view classes**
  165. Backbone.js view classes excel at providing a view of a model class that can handle events. In Faux, the structure of the HTML is placed in the template, and the Javascript for helpers, handling interaction, or anything else is handled in the view class. In `NaiveView` and `MatrixView`, we define helpers for calculating the Fibonacci number given a model's value of `n`:
  166. globals.NaiveView = Backbone.View.extend({
  167. initialize: function () {
  168. this.model.bind('change:n', this.render);
  169. },
  170. n: function () {
  171. return parseInt(this.model.attributes.n);
  172. },
  173. fibonacci: function (n) {
  174. // ...
  175. }
  176. });
  177. globals.MatrixView = Backbone.View.extend({
  178. initialize: function () {
  179. this.model.bind('change:n', this.render);
  180. },
  181. n: function () {
  182. return parseInt(this.model.attributes.n);
  183. },
  184. fibonacci: (function () {
  185. return function (n) {
  186. // ...
  187. }
  188. })()
  189. });
  190. You can read the exact code for calculating Fibonacci numbers in the sources. The interesting thing is the "wiring" that connects the model to the view. Since we provided a calculation for the `model` parameter, Faux automatically initializes each instance of our views with a value model. The value model is initialized with an `id` and a value for `n`. By itself, this would be enough for our views to display the Fibonacci numbers when the page is first displayed.
  191. But what if the model were to change? How would the page update itself? have a look at this line of code from the `initialize` method:
  192. this.model.bind('change:n', this.render);
  193. That binds the view's `render` method to the model's `change` event, with the consequence that that any change to `n` will cause the view to re-render. Faux has already bound the view to the element it renders, and it simply renders itself again with the help of its template. So what might cause a model to change? Let's look at `ValueView`:
  194. globals.ValueView = Backbone.View.extend({
  195. initialize: function () {
  196. this.model.bind('change:n', this.render);
  197. },
  198. n: function () {
  199. return parseInt(this.model.attributes.n);
  200. },
  201. events: {
  202. 'click': 'swap'
  203. },
  204. swap: function () {
  205. var this_n = this.n();
  206. var that_model = this.model.collection.get(1 + Math.floor(Math.random() * this.model.collection.size()));
  207. var that_n = that_model.attributes.n;
  208. this.model.set({ n: that_n });
  209. that_model.set({ n: this_n });
  210. }
  211. });
  212. Like `NaiveView` and `MatrixView`, `ValueView` is wired to its model so that changes to the model's `n` attribute will cause the view to re-render. But it has two other interesting definitions: `events` and a `swap` function. In Backbone.js, `events` defines a mapping between DOM events and view instance methods. `'click': 'swap'` instructs Backbone.js to bind the `swap` function to click events on the DOM element rendered by `ValueView`. Or in simpler terms, "When the user clicks on the cell representing the value, call the swap function."
  213. The swap function is interesting. What it does is swap the `n` attributes for the view's model and another, randomly selected model. (Once in a while it will swap with itself, viewers playing at home can review the commit history and see a longer version of the code that eliminates this edge case). Notice it doesn't swap models: If it swapped models, nothing would change, since the views are listening for model changes! Instead, it leaves the models as is but changes the `n` attributes with the `.set` method. A side-effect of using `.set` is that it fires a `change` event on the model. Thus, both affected models fire change events. Each one of those has three views listening for change events, so the views update accordingly when you click on the value in the first column.
  214. [Try it][kt]: click on any of the numbers in the first column. You will see the values swap and the Fibonacci numbers swill be recalculated automatically. The swap function doesn't know anything about what needs to be recalculated ore re-rendered, it doesn't even tell its own view to re-render, the "wiring" of views to change events takes care of that.
  215. **comparison to the previous example**
  216. This application is a modification of an example application that showed [lazy and parallel rendering][np]. What's different?
  217. 1. Instead of calculating `n` with an `:each` loop in haml, we built a collection of models.
  218. 2. Instead of passing `n` around directly, we wrapped it in a vanilla Backbone model. We don't declare the model, since we aren't interested in writing any methods on it, but we can still take advantage of change propagation. Essentially, we are working with `{ attributes: { n: ... }}` instead of `n`.
  219. 3. We declared a view for each of the three columns instead of just the second two. Adding a view for the first column ensures that its number updates when we perform a swap, and also gives us a plac eto put the `swap` function.
  220. 4. We added an `initialize` method to each view that wired it to the model.
  221. **summary**
  222. Lift is a web development framework based on the Scala programming language. It does have some interesting and useful features. One of these features is automatically updating dependents when precedent objects change. Automatically updating dependent content is handled easily by Faux's underlying Backbone.js models and views, and Faux provides a structure for declaring those relationships with each other.
  223. p.s. Faux is actually smart enough to wire _any_ view's render method to its single model's change event by convention, but we wanted to show you how easy it is to do it by hand, because you may want to have more complex dependencies such as a single view depending on multiple models.
  224. [f]:
  225. [kt]:
  226. [np]:
  227. [lazy_lift]: "Lift has built-in Lazy Loading"
  228. [parallel_lift]: "Lift can parallelize page rendering"
  229. [mis]:
  230. [play]:
  231. [pi]:
  232. [pii]:
  233. [piii]:
  234. [piv]:
  235. [bb]:
  236. [wiring]: