├── .gitignore ├── Rakefile ├── style.css ├── doc └── style.css └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | repo = ENV['GITHUB_REPO'] || 'rstacruz/backbone-patterns' 2 | 3 | desc "Builds documentation" 4 | task :build do 5 | # github.com/rstacruz/reacco 6 | analytics = "--analytics #{ENV['ANALYTICS_ID']}" if ENV['ANALYTICS_ID'] 7 | system "reacco --literate --toc --api lib --github #{repo} #{analytics} -o output --css style.css" 8 | end 9 | 10 | desc "Uploads documentation" 11 | task :deploy => :build do 12 | # github.com/rstacruz/git-update-ghpages 13 | system "git update-ghpages -i output #{repo}" 14 | end 15 | 16 | task :read => :build do 17 | system "open output/index.html" 18 | end 19 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | aside#toc { 2 | overflow-x: hidden; } 3 | 4 | aside#toc a { 5 | font-size: 0.9em; } 6 | 7 | aside#toc h2 a { 8 | text-transform: uppercase; 9 | font-size: 0.9em; 10 | padding: 2px 30px; 11 | margin: 0 -30px; 12 | background: rgba(0, 0, 0, 0.1); } 13 | 14 | aside#toc h2, 15 | aside#toc h3 { 16 | padding-top: 1px; 17 | border-top: 0; } 18 | 19 | aside#toc .level-2 ul { 20 | display: none; } 21 | 22 | section.h1 { 23 | padding-top: 0; 24 | border-top: 0; } 25 | 26 | section.h1.anti-patterns h2 { 27 | margin-bottom: 5px; 28 | font-size: 1.3em; } 29 | 30 | section.h2 { 31 | @include box-shadow(0 0 5px rgba(black, 0.2)); 32 | background: white; } 33 | 34 | section.h2 + section.h2 { 35 | margin-top: 10px; } 36 | -------------------------------------------------------------------------------- /doc/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; } 4 | 5 | body { 6 | background: #fcfcfa; 7 | font-family: 'pt sans', sans-serif; 8 | color: #444; 9 | font-size: 11pt; 10 | line-height: 1.5; } 11 | 12 | ::-moz-selection { background: #88a; color: #fff; text-shadow: none; } 13 | ::selection { background: #88a; color: #fff; text-shadow: none; } 14 | 15 | h1, h2, h3, h4, h5, h6, p, ol, ul, pre, ol>li { 16 | max-width: 500px; 17 | margin: 20px 0; } 18 | 19 | ol, ul { 20 | padding-left: 0; 21 | margin-left: 30px; } 22 | 23 | #all { 24 | min-width: 960px; 25 | box-sizing: border-box; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | width: 100%; 29 | overflow-x: hidden; 30 | padding: 40px; } 31 | 32 | a, a:visited { 33 | text-decoration: none; 34 | border-bottom: solid 1px #ccf; 35 | color: #46b; } 36 | 37 | a:hover { 38 | background: #fafae0; } 39 | 40 | /* List */ 41 | ul li > p > strong:first-child { 42 | display: block; 43 | font-size: 1.1em; } 44 | 45 | /* Headings */ 46 | h1 { 47 | font-family: palatino, serif; } 48 | 49 | h1, h2, h3, h4, h5, h6 { 50 | text-shadow: 2px 2px 0 rgba(55, 55, 55, 0.1); 51 | color: #568; 52 | font-weight: normal; 53 | font-family: shanti, palatino, serif; } 54 | 55 | h3 { 56 | color: #77a; } 57 | 58 | h4, h5, h6 { 59 | color: #88a; } 60 | 61 | /* The "(class method)" thing */ 62 | h1 .type, h2 .type, h3 .type, h4 .type, h5 .type, h6 .type { 63 | font-size: 9pt; 64 | color: #aaa; 65 | text-transform: uppercase; 66 | margin-left: 10px; } 67 | 68 | h2 { 69 | max-width: none; 70 | padding-top: 30px; 71 | margin-top: 40px; } 72 | 73 | h3 { 74 | max-width: none; 75 | padding-top: 10px; 76 | margin-top: 40px; } 77 | 78 | h1 { font-size: 30pt; } 79 | h2 { font-size: 23pt; } 80 | h3 { font-size: 14pt; } 81 | h4 { font-size: 14pt; } 82 | h5 { font-size: 10pt; } 83 | h6 { font-size: 10pt; } 84 | 85 | h2+p { margin-top: 0; } 86 | h2+pre.right+p { margin-top: 0; } 87 | 88 | /* Code */ 89 | pre, code { 90 | font-family: Monaco, monospace; } 91 | 92 | code { 93 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.99); 94 | font-family: 'shanti', sans-serif; 95 | color: #9090aa; } 96 | 97 | pre > code { 98 | color: #60606f; 99 | text-shadow: none; 100 | font-family: Monaco, monospace; 101 | background: 0; 102 | padding: 0; 103 | border: 0; } 104 | 105 | pre { 106 | font-size: 9pt; 107 | padding-top: 5px; 108 | background: transparent; } 109 | 110 | /* The header */ 111 | .header h1, .header h2, .header h3, 112 | .header h4, .header h5, .header h6 { 113 | text-shadow: none; 114 | line-height: 1.35; 115 | margin: 0; 116 | padding: 0; } 117 | 118 | .header h1 a, 119 | .header h1 { 120 | font-family: 'pt sans', sans-serif; 121 | font-weight: bold; 122 | color: #779; } 123 | 124 | .header h1 a { 125 | border-bottom: 0; } 126 | 127 | .header h2, .header h3, 128 | .header h4, .header h5, .header h6 { 129 | color: #aab; } /* color: #78b; */ 130 | 131 | .header h4 { 132 | font-size: 1.4em; 133 | font-family: palatino, serif; 134 | font-style: italic; 135 | font-weight: normal; } 136 | 137 | /* Literate */ 138 | body.literate h4, body.literate h5, body.literate h6, body.literate p, 139 | body.literate ol, body.literate ul, body.literate pre { 140 | max-width: 400px; } 141 | 142 | body.literate pre.right { 143 | clear: both; 144 | max-width: none; 145 | width: 490px; 146 | margin-left: 50px; 147 | float: right; } 148 | 149 | body.literate pre.right code { 150 | color: #78a; } 151 | 152 | body.literate h1 + pre.right, 153 | body.literate h2 + pre.right, 154 | body.literate h3 + pre.right { 155 | margin-top: 0; } 156 | 157 | h1, h2, h3 { 158 | clear: both; } 159 | 160 | .clear { 161 | clear: both; } 162 | 163 | br.post-pre { 164 | clear: both; } 165 | 166 | h4 { margin-bottom: 5px; } 167 | h4+p, 168 | h4+pre.right, 169 | h4+pre.right+p { margin-top: 0; } 170 | 171 | /* Section-wrap */ 172 | section { 173 | width: 960px; 174 | margin-top: 0; 175 | margin-left: 0; 176 | padding-left: 40px; 177 | margin-left: -40px; 178 | padding-right: 9000px; } 179 | 180 | section::after { 181 | content: ''; 182 | display: table; 183 | clear: both; } 184 | 185 | section.h2:nth-child(even) { 186 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 187 | background: rgba(255, 255, 255, 0.8); } 188 | 189 | section:first-of-type { 190 | margin-top: 40px; } 191 | 192 | section.h1, section.h2, section.h3 { 193 | padding-top: 30px; 194 | padding-bottom: 40px; 195 | border-top: solid 1px #eee; } 196 | 197 | section.h1 { 198 | border-top: solid 10px #ddd; } 199 | 200 | section.h3 { 201 | padding-top: 20px; 202 | margin-top: 40px; 203 | padding-left: 30px; 204 | border-left: solid 10px #e7e7f4; 205 | border-top: dotted 1px #ddd; } 206 | 207 | section.h3:nth-child(odd) { 208 | border-left-color: #d0d0e8; } 209 | 210 | section.h3 + section.h3 { 211 | margin-top: -20px; } 212 | 213 | section.h1>:first-child, section.h2>:first-child, section.h3>:first-child { 214 | padding-top: 0; 215 | margin-top: 0; } 216 | 217 | /* API */ 218 | section.api h2, 219 | section.api h3 { 220 | margin-bottom: 0; } 221 | 222 | section.api h2 + p, 223 | section.api h2 + pre + p, 224 | section.api h3 + p, 225 | section.api h3 + pre + p { 226 | margin-top: 0; } 227 | 228 | h2 .args, 229 | h3 .args { 230 | margin-left: 3px; 231 | font-size: 0.9em; 232 | margin-right: 15px; 233 | color: #aab; } 234 | 235 | /* Pretty printing styles. Used with prettify.js. */ 236 | .str { color: #080; } 237 | .kwd { color: #008; } 238 | .com { color: #607077; background: rgba(0, 0, 0, 0.05); padding: 1px 3px; } 239 | .typ { color: #606; } 240 | .lit { color: #066; } 241 | .pun { color: #660; } 242 | .pln { color: #000; } 243 | .tag { color: #008; } 244 | .atn { color: #606; } 245 | .atv { color: #080; } 246 | .dec { color: #606; } 247 | 248 | /* Specify class=linenums on a pre to get line numbering */ 249 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 250 | li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { list-style-type: none } 251 | /* Alternate shading for lines */ 252 | li.L1, li.L3, li.L5, li.L7, li.L9 { background: #eee } 253 | 254 | @media print { 255 | .str { color: #060; } 256 | .kwd { color: #006; font-weight: bold; } 257 | .com { color: #600; font-style: italic; } 258 | .typ { color: #404; font-weight: bold; } 259 | .lit { color: #044; } 260 | .pun { color: #440; } 261 | .pln { color: #000; } 262 | .tag { color: #006; font-weight: bold; } 263 | .atn { color: #404; } 264 | .atv { color: #060; } 265 | } 266 | 267 | /* TOC */ 268 | body.toc { 269 | margin-left: 200px; } 270 | 271 | aside#toc { 272 | font-size: 0.9em; 273 | line-height: 1.7; 274 | position: fixed; 275 | top: 0; 276 | left: 0; 277 | bottom: 0; 278 | padding: 20px; 279 | width: 180px; 280 | overflow-y: auto; 281 | box-shadow: inset -2px 0 2px rgba(0, 0, 0, 0.1); 282 | border-right: solid 1px #ccc; 283 | background: #f4f4fa; } 284 | 285 | aside#toc h1, 286 | aside#toc h2, 287 | aside#toc h3 { 288 | margin: 10px 0; 289 | padding: 0; 290 | font-size: 1.1em; } 291 | 292 | aside#toc h1 { 293 | color: #aaa; 294 | text-shadow: none; 295 | font-size: 1.3em; } 296 | 297 | aside#toc h2, 298 | aside#toc h3 { 299 | border-top: solid 1px #ddd; 300 | padding-top: 10px; } 301 | 302 | aside#toc h2 .type, 303 | aside#toc h3 .type { 304 | margin-top: 2px; 305 | margin-left: 0; } 306 | 307 | aside#toc ul, 308 | aside#toc li { 309 | margin: 0; 310 | padding: 0; 311 | list-style-type: none; } 312 | 313 | aside#toc h1 a, 314 | aside#toc h2 a, 315 | aside#toc h3 a { 316 | color: #333; } 317 | 318 | aside#toc a { 319 | height: 1.6em; 320 | overflow: hidden; 321 | line-height: 1.6em; 322 | position: relative; 323 | display: block; 324 | border-bottom: 0; 325 | color: #555; } 326 | 327 | aside#toc a span.args { 328 | display: none; } 329 | 330 | aside#toc a span.type { 331 | font-weight: normal; 332 | color: #999; 333 | text-transform: uppercase; 334 | font-size: 8pt; 335 | float: right; } 336 | 337 | aside#toc::-webkit-scrollbar { 338 | height: 15px; width: 15px; 339 | } 340 | 341 | aside#toc::-webkit-scrollbar-thumb { 342 | border-width: 7px 7px 7px 7px; 343 | -webkit-border-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDZEMERCMUZGRUI2MTFERkIwQ0RGMkFBNEE3NDlCOUMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDZEMERCMjBGRUI2MTFERkIwQ0RGMkFBNEE3NDlCOUMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NkQwREIxREZFQjYxMURGQjBDREYyQUE0QTc0OUI5QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NkQwREIxRUZFQjYxMURGQjBDREYyQUE0QTc0OUI5QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpHo8kwAAACBSURBVHjaYvz//z8DJYCJgUJAsQEsyBxGRkYwBcS6QKwBxLJQqcdAfAOILwPxf2RvM6JwICbYA7EDDgsPAPFBZBPQvaCLRzMDVE4XXxhoEOFtDXwGyBJhgCxNo/ExEXoe4zPgBhEG3MBnwGVoVDHgicbLKGkHLR1QlpCGZmYCCDAAxuMoE4WhhWUAAAAASUVORK5CYII=) 7 7 7 7 round round; 344 | } 345 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Backbone patterns](http://ricostacruz.com/backbone-patterns) 2 | 3 | Here, I try to document the good practices that our team has learned along the 4 | way building [Backbone][bb] applications. 5 | 6 | ### Assumptions 7 | 8 | This document assumes that you already have some knowledge of [Backbone.js][bb], 9 | [jQuery][jq], and of course, JavaScript itself. 10 | 11 | [rsc]: http://ricostacruz.com/ 12 | [bb]: http://documentcloud.github.com/backbone/ 13 | [jq]: http://jquery.com/ 14 | 15 | Model patterns 16 | ============== 17 | 18 | Bootstrapping data 19 | ------------------ 20 | 21 | __The problem:__ Your application needs models to be available on page load. 22 | 23 | __Solution:__ Bootstrap collections and models by creating collections in an 24 | inline ` 47 | 48 | ``` 49 | 50 | ### Accessing instances 51 | 52 | To get a single `Photo`, instead of creating a `Photo` instance and using 53 | `fetch()`, simply pluck it from the giant collection. 54 | 55 | ``` javascript 56 | // Gets by ID 57 | var photo = App.photos.get(2); 58 | 59 | // Gets a bunch of photos based on criteria 60 | var photo = App.photos.select(function(photo) { 61 | return photo.filename.match(/^IMG/); 62 | }); 63 | ``` 64 | 65 | ### In Ruby (ERB) 66 | 67 | In your server-side templates, you will probably be using `to_json` on a 68 | collection of your server-side models. 69 | 70 | ``` html 71 | 74 | ``` 75 | 76 | ### In Ruby (HAML) 77 | 78 | If you use HAML, you will need use a syntax similar to this. 79 | 80 | ``` haml 81 | :javascript 82 | App.photos = new Photos(#{@photos.to_json}); 83 | ``` 84 | 85 | ### In PHP 86 | 87 | In your server-side templates, you will probably be using `json_encode()` on a 88 | collection of your server-side models. 89 | 90 | ``` php 91 | 94 | ``` 95 | 96 | [bb.bootstrap]: http://documentcloud.github.com/backbone/#FAQ-bootstrap 97 | 98 | View patterns 99 | ============= 100 | 101 | Inline templates 102 | ---------------- 103 | 104 | __The problem:__ if you need to use view templates in a small Backbone 105 | application, defining your templates in JavaScript code will be unwieldy and 106 | difficult to maintain. 107 | 108 | __Solution:__ You may need some view templates to be inline in the HTML page. 109 | 110 | This solution has been outlined by John Resig in his blog post about [JavaScript 111 | micro templating](http://ejohn.org/blog/javascript-micro-templating/). 112 | 113 | ### Defining inline templates 114 | You can put templates in an HTML ` 128 | ``` 129 | 130 | ### Using inline templates 131 | In JavaScript, you can get the `innerHTML` (or jQuery's [.html()][html]) of that 132 | HTML element to fetch the raw template data. You can pass this onto Underscore's 133 | `_.template` to create a template function. 134 | 135 | [html]: http://api.jquery.com/html 136 | 137 | ``` javascript 138 | $("#template-contact").html(); 139 | //=> "
\n<%= name %> function() { ... } 143 | ``` 144 | 145 | ### Integrating into Backbone 146 | 147 | In practice, you will most likely be using this in the `render()` method of a 148 | view like so. 149 | 150 | ``` javascript 151 | var ContactView = Backbone.View.extend({ 152 | template: _.template($("#template-contact").html()), 153 | 154 | render: function() { 155 | // This is a dictionary object of the attributes of the models. 156 | // => { name: "Jason", email: "j.smith@gmail.com" } 157 | var dict = this.model.toJSON(); 158 | 159 | // Pass this object onto the template function. 160 | // This returns an HTML string. 161 | var html = this.template(dict); 162 | 163 | // Append the result to the view's element. 164 | $(this.el).append(html); 165 | 166 | // ... 167 | } 168 | }); 169 | ``` 170 | 171 | ### Limitations 172 | 173 | __Single-page apps only.__ 174 | This assumes that your Backbone application is all contained in one HTML page. 175 | If your app spans across multiple HTML pages, and each page will be needing the 176 | same templates, you may be redundantly streaming the template data to the 177 | browser unnecessarily. Consider using JST templates instead. 178 | 179 | Note that the given example assumes that the `#template-contact` element appears 180 | before you include JavaScript files, as it requires the template element to be 181 | accessible before the class is defined. 182 | 183 | JST templates 184 | ------------- 185 | 186 | __The problem:__ if you need to use view templates in a small-to-large Backbone 187 | application, defining your templates in JavaScript code will be unwieldy and 188 | difficult to maintain. 189 | 190 | __Solution:__ You may need put the templates in a JavaScript file. 191 | 192 | ### The structure 193 | 194 | Your app will need to serve a _dynamically-created_ JavaScript file that 195 | compiles your files. 196 | 197 | A common JST file will create the `JST` object (in the window namespace), with 198 | each of its members defined as template functions. In this example, we'll use 199 | Underscore's `_.template`, which returns functions. 200 | 201 | ``` javascript 202 | // http://myapp.com/javascripts/jst.js 203 | window.JST = {}; 204 | 205 | window.JST['person/contact'] = _.template( 206 | "
<%= name %> ..." 207 | ); 208 | 209 | window.JST['person/edit'] = _.template( 210 | "
218 | ``` 219 | 220 | ### Using JST templates 221 | 222 | In your JavaScript code, simply access the JST object's members to access the 223 | views. 224 | 225 | ``` javascript 226 | var html = JST['person/edit'](); 227 | 228 | var dict = { name: "Jason", email: "j.smith@gmail.com" }; 229 | var html = JST['person/contact'](dict); 230 | ``` 231 | 232 | ### Integration notes 233 | 234 | * __Rails 3.1 and above__: The Rails Asset pipeline already comes with support 235 | for JST pages. 236 | * __Rails 3.0 and below__: consider using [Sprockets][sprockets] or 237 | [Jammit][jammit]. 238 | * __In Sinatra__: The [sinatra-backbone][sinatra-backbone] gem can take care of 239 | dynamically serving JST templates. 240 | 241 | [jammit]: http://documentcloud.github.com/jammit 242 | [sprockets]: http://getsprockets.org 243 | [sinatra-backbone]: http://ricostacruz.com/sinatra-backbone 244 | 245 | Partials 246 | -------- 247 | 248 | __The problem:__ there may be parts of HTML templates that can be reused in many 249 | parts of the application. Defining them more than once is not DRY, which may 250 | make your application less maintainable. 251 | 252 | __Solution:__ separating these snippets into partials. 253 | 254 | Partials are templates that are meant to be used *inside* other templates. 255 | 256 | One typical use of partials is for lists where the template for list items may 257 | be defined as a separate template from the list itself. 258 | 259 | ### Solution 260 | 261 | You can pass the template function for the partial as a parameter to the first 262 | template. 263 | 264 | In this example, the function `itemTemplate` is passed onto the parameters for 265 | `template()`. 266 | 267 | ``` javascript 268 | TasksList = Backbone.View.extend({ 269 | template: _.template([ 270 | "
    ", 271 | "<% items.each(function(item) { %>", 272 | "<%= itemTemplate(item) %>", 273 | "<% }); %>", 274 | "
" 275 | ].join('')), 276 | 277 | itemTemplate: _.template( 278 | "
  • <%= name %>
  • " 279 | ), 280 | 281 | render: function() { 282 | var html = this.template({ 283 | items: tasks /* a collection */, 284 | itemTemplate: this.itemTemplate 285 | }); 286 | 287 | $(this.el).append(html); 288 | } 289 | }); 290 | ``` 291 | 292 | Animation buffer 293 | ---------------- 294 | 295 | __The problem:__ When you have events that trigger animations, they can mess up 296 | when the user clicks too fast. 297 | 298 | __The solution:__ Make a buffering system to ensure that animations are fired 299 | serially (one after the other) and never parallel (at the same time). 300 | 301 | ### The situation 302 | 303 | Let's say you have this innocent code that performs an animation. 304 | 305 | One fundamental flaw here is that it assumes that `.showNext()` will only be 306 | called when it is not animating. When the user clicks "Next" while the animation 307 | is working, unexpected results will occur. 308 | 309 | ``` javascript 310 | PicturesView = Backbone.View.extend({ 311 | events: { 312 | 'click .next': 'showNext' 313 | }, 314 | 315 | showNext: function() { 316 | var current = this.$(".current"); 317 | var nextDiv = this.$(".current + div"); 318 | 319 | if (nextDiv.length == 0) { return; } 320 | 321 | // Make the current one move to the left via jQuery. 322 | // This uses jQuery.fn.animate() that changes CSS values, then fires 323 | // the function supplied when it's done. 324 | current.animate({ left: -300, opacity: 0 }, function() { 325 | current.removeClass('.current'); 326 | nextDiv.addClass('.current'); 327 | }); 328 | } 329 | }); 330 | ``` 331 | 332 | ### The solution 333 | 334 | Here's a simple buffering solution. It provides two commands: 335 | 336 | * `add(fn)` which adds a given function to the buffer, and 337 | * `next()` which moves onto the next command. This is passed onto the functions 338 | when they are called. 339 | 340 | To use this, put your animations inside an anonymous function to be passed onto 341 | `add()`. Be sure to trigger `next()` when the animations are done. 342 | 343 | ``` javascript 344 | Buffer = { 345 | commands: [], 346 | 347 | add: function(fn) { 348 | // Adds a command to the buffer, and executes it if it's 349 | // the only command to be ran. 350 | var commands = this.commands; 351 | commands.push(fn); 352 | if (this.commands.length == 1) fn(next); 353 | 354 | // Moves onto the next command in the buffer. 355 | function next() { 356 | commands.shift(); 357 | if (commands.length) commands[0](next); 358 | } 359 | } 360 | }; 361 | ``` 362 | 363 | ### Example 364 | 365 | This is our example from a while ago that has been modified to use the bufferer. 366 | 367 | ``` javascript 368 | showNext: function() { 369 | var current = this.$(".current"); 370 | var nextDiv = this.$(".current + div"); 371 | 372 | if (nextDiv.length == 0) { return; } 373 | 374 | // Ensure that the animation will not happen while another 375 | // animation is ongoing. 376 | Buffer.add(function(next) { 377 | current.animate({ left: -300, opacity: 0 }, function() { 378 | current.removeClass('.current'); 379 | nextDiv.addClass('.current'); 380 | 381 | // Trigger the next animation. 382 | next(); 383 | }); 384 | }); 385 | } 386 | ``` 387 | 388 | ### Variations 389 | 390 | You can make the `Buffer` object into a class that you can instantiate. This 391 | lets you have multiple buffers as you need. This way, you can have a buffer for 392 | each view instance. 393 | 394 | jQuery also provides a very similar function, [jQuery.fn.queue()][queue]. This 395 | may be adequate for most simple animations. 396 | 397 | [queue]: http://api.jquery.com/queue/ 398 | 399 | Sub views 400 | --------- 401 | 402 | __The problem:__ Your view code is starting to bloat as it tries to do too many 403 | things in one class. 404 | 405 | __The solution:__ Break it apart into smaller sub-views. 406 | 407 | ### The situation 408 | 409 | This is a common occurrence if you have one _giant_ view that takes care of the 410 | entire page. View classes may become unwieldy once they get up to 200 lines. 411 | 412 | ### Solution 1: Sub views 413 | 414 | It may be wise to delegate some areas of the view to be the responsibility of 415 | another view. 416 | 417 | In this example, we have a view that handles the entire application "chrome." 418 | Let's break apart some of its parts on its `render()` function. Notice that 419 | we're using `this.$()` to select elements inside the `ChromeView`'s element 420 | itself. 421 | 422 | ``` javascript 423 | App.ChromeView = Backbone.View.extend({ 424 | render: function() { 425 | // Instantiate some "sub" views to handle the responsibilities of 426 | // their respective elements. 427 | this.sidebar = new App.SidebarView({ el: this.$(".sidebar") }); 428 | this.menu = new App.NavigationView({ el: this.$("nav") }); 429 | } 430 | }); 431 | 432 | $(function() { 433 | App.chrome = new App.ChromeView({ el: $("#chrome") }); 434 | }); 435 | ``` 436 | 437 | We will then be able to access the sub-views like so: 438 | 439 | ``` javascript 440 | App.chrome.sidebar.toggle(); 441 | 442 | App.chrome.menu.expand(); 443 | ``` 444 | 445 | ### Events 446 | 447 | All Backbone objects can emit [events][events]. To maintain the separation of 448 | responsibilities of the view classes, you may have the sub-views trigger 449 | events that the parent view would need (and vice versa). 450 | 451 | [events]: http://documentcloud.github.com/backbone/#Events 452 | 453 | For instance, we may implement `SidebarView` to trigger events when the sidebar 454 | is collapsed or expanded: 455 | 456 | ``` javascript 457 | App.SidebarView = Backbone.View.extend({ 458 | toggle: function() { 459 | if ($(this.el).is(':visible')) { 460 | $(this.el).hide(); 461 | this.trigger('collapse'); // <== 462 | } else { 463 | $(this.el).show(); 464 | this.trigger('expand'); // <== 465 | } 466 | }, 467 | }); 468 | ``` 469 | 470 | And the parent view (`ChromeView`) may listen to them like so: 471 | 472 | ``` javascript 473 | App.ChromeView = Backbone.View.extend({ 474 | render: function() { 475 | this.sidebar = new App.SidebarView({ el: this.$(".sidebar") }); 476 | 477 | this.sidebar 478 | .bind('collapse', this.onSidebarCollapse) 479 | .bind('expand', this.onSidebarExpand); 480 | 481 | // ... 482 | } 483 | }); 484 | ``` 485 | 486 | Delegate views 487 | -------------- 488 | 489 | __The problem:__ Your view code is starting to bloat as it tries to do too many 490 | things in one class, and making sub-views with its child elements is not an 491 | option. 492 | 493 | __The solution:__ Make a sub-view with the same element. This will allow you to 494 | [delegate][delegate] certain responsibilities to another view class. 495 | 496 | [delegate]: http://en.wikipedia.org/wiki/Delegation_pattern 497 | 498 | ### Solution 499 | 500 | You can make 2 or more views that target the same element. This is useful when 501 | there are many controls in a view, but creating sub-views (with their scopes 502 | limited to a set of elements in the bigger view) may be too messy, or just not 503 | possible. 504 | 505 | In this example, `ChromeView` will make a sub-view that shares the same element 506 | as it does. 507 | 508 | ``` javascript 509 | App.ChromeView = Backbone.View.extend({ 510 | events: { 511 | 'click button': 'onButtonClick' 512 | }, 513 | render: function() { 514 | // Pass our own element to the other view. 515 | this.tabs = new App.TabView({ el: this.el }); 516 | } 517 | }); 518 | 519 | App.TabView = Backbone.View.extend({ 520 | // Notice this view has its own events. They will not 521 | // interfere with ChromeView's events. 522 | events: { 523 | 'click nav.tabs a': 'switchTab' 524 | }, 525 | 526 | switchTo: function(tab) { 527 | // ... 528 | }, 529 | 530 | hide: function() { 531 | // ... 532 | } 533 | }); 534 | ``` 535 | 536 | ### Using delegate views 537 | 538 | You can delegate some functionality to the sub-view. In this example, we can 539 | write the (potentially long) code for hiding tabs in the `TabView`, making 540 | `ChromeView` easier to maintain and manage. 541 | 542 | ``` javascript 543 | App.ChromeView = Backbone.View.extend({ 544 | // ... 545 | 546 | goFullscreen: function() { 547 | this.tabs.hide(); 548 | } 549 | }); 550 | ``` 551 | 552 | You may also provide publicly-accessible methods to `TabView` that will be meant 553 | to be accessed outside of `ChromeView`. 554 | 555 | ``` javascript 556 | var chrome = new App.ChromeView; 557 | chrome.tabs.switchTo('home'); 558 | ``` 559 | 560 | ### Variation: private delegate views 561 | 562 | You can also make delegate views *private* by design: that is, it shouldn't be 563 | used outside the parent view (`ChromeView` in our example). 564 | 565 | As JavaScript lacks true private attributes, you can set prefix it with an 566 | underscore to signify that it's private and is not part of it's public 567 | interface. (This is a practice taken from Python's [official style 568 | guide][pep8].) 569 | 570 | ``` javascript 571 | App.ChromeView = Backbone.View.extend({ 572 | render: function() { 573 | this._tabs = new App.TabView({ el: this.el }); 574 | } 575 | }); 576 | ``` 577 | 578 | [pep8]: http://www.python.org/dev/peps/pep-0008/ 579 | 580 | General patterns 581 | ================ 582 | 583 | Mixins 584 | ------ 585 | 586 | __The problem:__ Sometimes you have the same functionality for multiple objects and it doesn't 587 | make sense to wrap your objects in a parent object. For example, if you have 588 | two views that share methods but don't -- and shouldn't -- have a shared 589 | parent view. 590 | 591 | __The solution:__ For this scenario, it's appropriate to use a mixin. 592 | 593 | ### Defining mixins 594 | 595 | You can define an object that has attributes and methods that can be shared 596 | across different classes. This is called a [mixin][mixin]. 597 | 598 | You can define a mixin as a regular object literal with functions in it. 599 | 600 | ``` javascript 601 | App.Mixins.Navigation = { 602 | 603 | toggle: function() { /* ... */ }, 604 | 605 | open: function() { /*... */ }, 606 | 607 | close: function() { /* ... */ } 608 | 609 | }; 610 | ``` 611 | 612 | ### Using mixins 613 | 614 | You may then extend your classes with these mixins. You can use Underscore's 615 | [_.extend][extend] function to attach these to your class prototypes. 616 | 617 | [mixin]: http://en.wikipedia.org/wiki/Mixin 618 | [extend]: http://documentcloud.github.com/underscore/#extend 619 | 620 | ``` javascript 621 | App.Menu = Backbone.View.extend({ 622 | // I need to know how to toggle, open, and close! 623 | }); 624 | 625 | _.extend(App.Views.Menu.prototype, App.Mixins.Navigation); 626 | 627 | App.Tabs = Backbone.View.extend({ 628 | // I too need to know how to toggle, open, and close! 629 | }); 630 | 631 | _.extend(App.Views.Tabs.prototype, App.Mixins.Navigation); 632 | 633 | ``` 634 | 635 | ### Alternative syntax 636 | 637 | The above presents two caveats, which can be problematic in some situations: 638 | 639 | * Your attributes and methods in your mixin will *override* the methods you 640 | define in the class itself (via `Backbone.View.extend`). Ideally, it should be 641 | the other way around. 642 | 643 | * The `_.extend(...)` line is after all the methods you've defined in the 644 | class, and can easily be neglected by developers new to your project. 645 | 646 | To remedy this, you can use this alterative syntax. This will let you write 647 | methods and attributes in your class that will override the mixin's default 648 | behavior. 649 | 650 | ``` javascript 651 | App.Views.Menu = Backbone.View.extend( 652 | _.extend({}, App.Mixins.Navigation, { 653 | 654 | // (Methods and attributes here) 655 | 656 | })); 657 | 658 | App.Views.Tabs = Backbone.View.extend( 659 | _.extend({}, App.Mixins.Navigation, { 660 | 661 | // (Methods and attributes here) 662 | 663 | })); 664 | ``` 665 | 666 | ### Result 667 | 668 | The prototypes for your views now both have the methods defined in your mixin. 669 | New `App.Views.Tabs` and `App.Views.Menu` instances will now be able to respond 670 | to `.toggle()`, `.open()` and `.close()`. 671 | 672 | ``` javascript 673 | var tabs = new App.Views.Tabs; 674 | 675 | // These will call the methods you've defined 676 | // in App.Mixins.Navigation. 677 | tabs.toggle(); 678 | tabs.open(); 679 | tabs.close(); 680 | ``` 681 | 682 | ### Models and routers 683 | 684 | You can also use mixins in Models and Routers as well. 685 | 686 | ``` javascript 687 | // Router 688 | App.PageRouter = Backbone.Router.extend( 689 | _.extend({}, App.Mixins.HasSettings, { 690 | 691 | // (Methods and attributes here) 692 | 693 | })); 694 | 695 | // Model 696 | App.Widget = Backbone.Model.extend( 697 | _.extend({}, App.Mixins.IsDeletable, { 698 | 699 | // (Methods and attributes here) 700 | 701 | })); 702 | ``` 703 | 704 | Conventions 705 | =========== 706 | 707 | Naming convention 708 | ----------------- 709 | 710 | Classes often start in uppercase letters, while instances start with lowercase 711 | letters. This is a throwback of the general Python and Ruby practice of having 712 | constant names start with uppercase letters. 713 | 714 | ``` javascript 715 | // Classes: 716 | Photo 717 | Album 718 | Author 719 | 720 | // Instances: 721 | photo 722 | myAlbum 723 | ``` 724 | 725 | For names with multiple words, JavaScript often calls for CamelCase. Using 726 | underscores are discouraged in JavaScript. 727 | 728 | ``` javascript 729 | // Good (CamelCase): 730 | PhotoAlbum 731 | albumCover 732 | 733 | // Avoid (under_scores): 734 | photo_album 735 | album_cover 736 | ``` 737 | 738 | Namespace convention 739 | -------------------- 740 | 741 | The convention we use puts everything in one `App` namespace to keep things 742 | organized properly. 743 | 744 | ``` javascript 745 | window.App = { 746 | ... 747 | }; 748 | ``` 749 | 750 | Subsequent models, views, and other classes will be made in this namespace. 751 | 752 | ``` javascript 753 | App.Photo = Backbone.Model.extend({ 754 | ... 755 | }; 756 | ``` 757 | 758 | Some people prefer to use namespaces based on their app's name. Consider, say, 759 | `BF.Photo` (instead of `App.Photo`) if your application name is "Bacefook." 760 | 761 | Models: App.Photo 762 | Collections: App.Photos 763 | Views: App.PhotoView 764 | Main router: App.Router 765 | Custom routers: App.SpecialRouter 766 | 767 | Router instance: App.router 768 | View instances: App.photoView 769 | Singleton model instances: App.photo 770 | Collection instances: App.photos 771 | 772 | ### Variation: two-level namespace 773 | 774 | Some people prefer a verbose two-level version where the classes are divided 775 | into their own namespaces as well. 776 | 777 | This is often done to make it easy to iterate over all available models, 778 | collections, and views. 779 | 780 | Models: App.Models.Photo 781 | Collections: App.Collections.Photos 782 | Views: App.Views.Photo 783 | 784 | RequireJS and AMD 785 | ----------------- 786 | 787 | You may adopt a [Asynchronous Module Definition][amd]-style method of 788 | organization using a library like [RequireJS][require.js]. This will allow you 789 | to organize your modules in the `require(...)` way familiar to those who use 790 | NodeJS. 791 | 792 | If you adopt an AMD library, there will be no need to use namespaces for your 793 | JavaScript classes. 794 | 795 | ``` javascript 796 | define(function() { 797 | var Photo = require('models/photo'); 798 | var Photos = require('collections/photos'); 799 | var MenuView = require('views/menu'); 800 | var MainRouter = require('router/main'); 801 | 802 | // ... 803 | }); 804 | ``` 805 | 806 | For more information on RequireJS, AMD, and using it on your Backbone project, 807 | see: 808 | 809 | * [Organizing Backbone using Modules][bbt.modules] (via Backbonetutorials.com) 810 | * [RequireJS][require.js]'s official site 811 | 812 | [require.js]: http://requirejs.org 813 | [bbt.modules]: http://backbonetutorials.com/organizing-backbone-using-modules/ 814 | 815 | File naming 816 | ----------- 817 | 818 | For applications that do _not_ use [Asynchronous Module Definition][amd]-style 819 | organization, there always seem to have 3 basic JavaScript files. 820 | 821 | [amd]: http://requirejs.org/docs/whyamd.html 822 | 823 | ### The main namespace 824 | 825 | This is often `app.js`, which defines the basic namespace. 826 | 827 | ``` javascript 828 | // app.js 829 | window.App = { 830 | ... 831 | }; 832 | ``` 833 | 834 | ### The individual classes 835 | 836 | If you use the namespacing method outlined earlier in this document, there are 2 837 | common naming conventions for individual classes: 838 | 839 | * Name the files as the exact class name they contain. For instance, 840 | `App.PhotoView` should be stored as `app/photoview.js`. 841 | 842 | * Place each of the class types in their own folders. For instance, 843 | the `PhotoView` may be defined as `app/views/photoview.js`, or 844 | `views/photoview.js`. 845 | 846 | In this approach, **avoid** putting code in the files other than the actual 847 | class it defines. This makes your convention predictable for the benefit of 848 | those new to your project. 849 | 850 | ``` javascript 851 | // app/photoview.js 852 | App.PhotoView = Backbone.View.extend({ 853 | ... 854 | }); 855 | ``` 856 | 857 | ### The setup/glue code file 858 | 859 | This is the file where you do miscellaneous things that do not belong in any of 860 | the Backbone classes: 861 | 862 | * Instantiate the default view 863 | * Initialize the Backbone Router 864 | * Provide options for jQuery and its plugins 865 | 866 | This is often named `application.js` or `setup.js`. 867 | 868 | In larger projects, this can span multiple files. Don't be afraid to refactor it 869 | to multiple files. 870 | 871 | This is often the only place you will want to put the onload hook 872 | `$(function() { ... })`. 873 | 874 | ``` javascript 875 | $(function() { 876 | // Set up some options for jQuery and plugins. 877 | $(document).ajaxError(function() { 878 | alert("There was an error."); 879 | }); 880 | 881 | // Provide options for your plugins. 882 | $("a[rel~=lightbox]").click(function() { 883 | $(this).openAsLightbox(); 884 | }); 885 | 886 | Backbone.emulateJSON = true; 887 | 888 | // Initialize Backbone views. 889 | App.chromeView = new App.ChromeView({ el: $("body") }); 890 | App.router = new App.Router; 891 | 892 | // Initialize the Backbone router. 893 | Backbone.history.start(); 894 | }); 895 | ``` 896 | 897 | ### Load order 898 | 899 | Consider loading them in this order: 900 | 901 | * `app.js` (the namespace) 902 | * `app/*.js` (individual classes) 903 | * `setup.js` (the glue) 904 | 905 | ``` html 906 | 907 | 908 | 909 | 910 | 911 | ``` 912 | 913 | Anti-patterns 914 | ============= 915 | 916 | Here are a few common practices that I believe should generally be avoided. 917 | 918 | $() abuse 919 | --------- 920 | 921 | jQuery allows you to defer execution of code until when the DOM is fully-loaded 922 | with [$(document).ready(...)][jquery.ready], or its short form, `$(...)`. This 923 | is useful for getting everything set up once your HTML document is ready. 924 | 925 | [jquery.ready]: http://api.jquery.com/ready/ 926 | 927 | ``` javascript 928 | $(document).ready(function() { 929 | // Initialize the router. 930 | App.router = new App.MainRouter; 931 | 932 | // Initialize the main view. 933 | App.dashboard = new App.Dashboard({ ... }); 934 | 935 | // and so on... 936 | }); 937 | 938 | // Or its shorter form: 939 | $(function() { 940 | // ... 941 | }); 942 | ``` 943 | 944 | A common anti-pattern is to put class definitions (for views, models, and such) 945 | inside these blocks. They are not necessary. 946 | 947 | ``` javascript 948 | // AVOID this: 949 | $(function() { 950 | App.PhotoView = Backbone.View.extend({ 951 | ... 952 | }); 953 | }); 954 | ``` 955 | 956 | Your classes should be ready before the HTML DOM is. This will save you from 957 | running into problems later where certain classes may not be available at 958 | certain parts of your application. 959 | 960 | ``` javascript 961 | // Consider instead: 962 | App.PhotoView = Backbone.View.extend({ 963 | ... 964 | }); 965 | ``` 966 | 967 | Things outside views 968 | -------------------- 969 | 970 | Put things in your view class code as much as possible. 971 | 972 | Event handlers outside views 973 | ---------------------------- 974 | 975 | Every time you make an event handler outside a view class, consider making a new 976 | view class. 977 | 978 | ``` javascript 979 | App.PhotoView = Backbone.View.extend({ 980 | ... 981 | }); 982 | 983 | // AVOID this! 984 | $("a.photo").click(function() { ... }); 985 | ``` 986 | 987 | Other links 988 | =========== 989 | 990 | Other links of interest: 991 | 992 | * [Backbone][backbone] official documentation 993 | * [Backbonetutorials.com][bbtutorials] by Thomas Davis covers the basics of 994 | Backbone.js in more detail than the official docs. 995 | * [Backbone Fundamentals][bbfund] by Addy Osmani is a Creative Commons book for 996 | beginners and advanced users alike. 997 | 998 | [backbone]: http://documentcloud.github.com/backbone/ 999 | [bbtutorials]: http://backbonetutorials.com 1000 | [bbfund]: https://github.com/addyosmani/backbone-fundamentals 1001 | 1002 | Acknowledgements 1003 | ================ 1004 | 1005 | © 2011-2012, Rico Sta. Cruz. Released under the [MIT 1006 | License](http://www.opensource.org/licenses/mit-license.php). 1007 | 1008 | This document is authored and maintained by [Rico Sta. Cruz][rsc] with help from 1009 | its [contributors][c]. It is sponsored by my startup, [Sinefunc, Inc][sf]. 1010 | 1011 | * [My website](http://ricostacruz.com) (ricostacruz.com) 1012 | * [Sinefunc, Inc.](http://sinefunc.com) (sinefunc.com) 1013 | * [Github](http://github.com/rstacruz) (@rstacruz) 1014 | * [Twitter](http://twitter.com/rstacruz) (@rstacruz) 1015 | 1016 | [rsc]: http://ricostacruz.com 1017 | [c]: http://github.com/rstacruz/backbone-patterns/contributors 1018 | [sf]: http://sinefunc.com 1019 | 1020 | ### To do list 1021 | 1022 | - Model associations 1023 | - Adding events to subclasses 1024 | - View modes 1025 | - Nested views 1026 | - Router entry/exit 1027 | - View helpers 1028 | --------------------------------------------------------------------------------