├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CHANGELOG.md ├── CNAME ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── MIT-LICENSE.txt ├── README.md ├── coffeelint.json ├── docs ├── .gitignore ├── CNAME ├── _config.yml ├── _includes │ └── modules.md ├── _layouts │ └── default.html ├── chaplin.application.md ├── chaplin.collection.md ├── chaplin.collection_view.md ├── chaplin.composer.md ├── chaplin.controller.md ├── chaplin.dispatcher.md ├── chaplin.event_broker.md ├── chaplin.layout.md ├── chaplin.mediator.md ├── chaplin.model.md ├── chaplin.router.md ├── chaplin.support.md ├── chaplin.sync_machine.md ├── chaplin.utils.md ├── chaplin.view.md ├── disposal.md ├── events.md ├── favicon.ico ├── getting_started.md ├── handling_async.md ├── images │ ├── chaplin.png │ ├── coffeescript-inverted.png │ ├── coffeescript.png │ └── javascript.png ├── index.md ├── javascripts │ └── main.js ├── no_deps.md ├── stylesheets │ ├── main.css │ └── pygments.css └── upgrading.md ├── package-lock.json ├── package.json ├── src ├── chaplin.coffee └── chaplin │ ├── application.coffee │ ├── composer.coffee │ ├── controllers │ └── controller.coffee │ ├── dispatcher.coffee │ ├── lib │ ├── composition.coffee │ ├── event_broker.coffee │ ├── history.coffee │ ├── route.coffee │ ├── router.coffee │ ├── support.coffee │ ├── sync_machine.coffee │ └── utils.coffee │ ├── mediator.coffee │ ├── models │ ├── collection.coffee │ └── model.coffee │ └── views │ ├── collection_view.coffee │ ├── layout.coffee │ └── view.coffee └── test ├── application_spec.coffee ├── bench.html ├── collection_spec.coffee ├── collection_view_spec.coffee ├── composer_spec.coffee ├── composition_spec.coffee ├── controller_spec.coffee ├── dispatcher_spec.coffee ├── event_broker_spec.coffee ├── layout_spec.coffee ├── mediator_spec.coffee ├── model_spec.coffee ├── router_spec.coffee ├── sync_machine_spec.coffee ├── utils_spec.coffee └── view_spec.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System-specific, temporary files 2 | .DS_Store 3 | 4 | # Generated folders from build process 5 | /build 6 | 7 | # Local package dependencies 8 | /node_modules 9 | 10 | # Coverage report 11 | coverage* 12 | 13 | # Logs 14 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ## The Cast 2 | 3 | Chaplin is currently maintained by: 4 | 5 | * Mathias Schäfer ([9elements](http://9elements.com/)) – [mathias.schaefer@9elements.com](mailto:mathias.schaefer@9elements.com) – [@molily](https://twitter.com/molily) – [molily.de](http://molily.de/) 6 | * Paul Miller – [@paulmillr](http://twitter.com/paulmillr) – [paulmillr.com](http://paulmillr.com/) 7 | * Karel Ledru-Mathé – [@karelledrumathe](http://twitter.com/karelledrumathe) – [ledrumathe.com](http://ledrumathe.com/) 8 | * Ryan Leckey – [@mehcode](https://github.com/mehcode) 9 | * Johannes Emerich – [@knuton](https://twitter.com/knuton) – [johannes.emerich.de](http://johannes.emerich.de/) 10 | 11 | With input and contributions from (in chronological order): 12 | 13 | * Nico Hagenburger – [@hagenburger](http://twitter.com/hagenburger) – [hagenburger.net](http://www.hagenburger.net/) 14 | * Rin Räuber (9elements) – [@rinpaku](http://twitter.com/rinpaku) – [rin-raeuber.com](http://rin-raeuber.com/) 15 | * Wojtek Gorecki (9elements) – [@newmetl](http://twitter.com/newmetl) 16 | * Jan Monschke (9elements) – [@thedeftone](http://twitter.com/thedeftone) 17 | * Jan Varwig (9elements) – [@agento](http://twitter.com/agento) – [jan.varwig.org](http://jan.varwig.org/) 18 | * Patrick Schneider (9elements) – [@padschneider](http://twitter.com/padschneider) – [padschneider.com](http://padschneider.com/) 19 | * Luis Merino (Moviepilot) – [@rendez](http://twitter.com/rendez) 20 | 21 | [See all Github contributors](https://github.com/chaplinjs/chaplin/contributors) 22 | 23 | ## The Producers 24 | 25 | The Chaplin architecture was derived from [moviepilot.com](http://moviepilot.com/), a project by **Moviepilot** with support from **9elements**. 26 | 27 | **Moviepilot** is an internet startup from Berlin, Germany, maintaining several movie news sites and communities. 28 | 29 | Find out more [about moviepilot.com](http://moviepilot.com/about) and [moviepilot.de](http://www.moviepilot.de/pages/about). 30 | 31 | **9elements** is a software and design company from Bochum & Berlin, Germany, focussed on web applications with Ruby on Rails and JavaScript, as well as iOS app development. 32 | 33 | Find out more [about 9elements](http://9elements.com/) or read our blog [IO 9elements](http://9elements.com/io/). 34 | 35 | Check out more open-source projects by Moviepilot and 9elements: [github.com/moviepilot](https://github.com/moviepilot) and [github.com/9elements](https://github.com/9elements). 36 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docs.chaplinjs.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Chaplin 2 | For non-technical issues (questions etc.), 3 | use [ost.io](http://ost.io/chaplinjs/chaplin) forum or our mailing list. 4 | 5 | See releasing guide at the end. 6 | 7 | If you submit changes to coffeescript code, make sure it conforms with the style guide. 8 | 9 | ## Chaplin code style guide 10 | 11 | Unless stated otherwise, follow the [CoffeeScript style guide](https://github.com/polarmobile/coffeescript-style-guide). 12 | 13 | ## Commenting and Whitespace 14 | 15 | * Use whitespace generously. 16 | * Use comments generously and wisely. Keep them short and helpful, use the simplest language possible. 17 | 18 | ## Code readability 19 | 20 | * Write simple and verbose code instead of complex and dense. Don’t minify the code manually, don’t use ugly tricks to reduce the resulting code size. 21 | * Explicit is better than implicit. When in doubt, use several statements and use additional variables instead of putting everything in a huge expression statement. 22 | * What can be done with pure CoffeeScript should be done by CoffeeScript, not Underscore or jQuery. 23 | * Check if the CoffeeScript code makes sense when compiled to JavaScript. The JavaScript code should be readable, brief and clear. Avoid CoffeeScript features that create overly complex and obscure Javascript code. 24 | 25 | ## CoffeeScript list operations 26 | 27 | Don’t use CoffeeScript if the compiled JavaScript code is extremely verbose or inefficient. For list comprehension and map operations, CoffeeScript creates an immediately-invoked function which leads to horrible verbose and inefficient code. Also, the CoffeeScript isn’t always readable. 28 | 29 | Avoid: 30 | 31 | ``` 32 | foo = (x for x in y) 33 | foo = for … 34 | … 35 | foo = while … 36 | … 37 | ``` 38 | 39 | Better: 40 | 41 | ``` 42 | foo = [] 43 | for … 44 | foo.push … 45 | // or 46 | foo[index] = … 47 | ``` 48 | 49 | This is more efficient and compiles to less JavaScript code. CoffeeScript’s syntactic sugar has little value here. 50 | 51 | Use simple CoffeeScript `for in` / `for of` loops when applicable. Use the semantic `_.map`, `_.filter` functions from Underscore if necessary. When using Underscore, use the canonical ECMAScript 5 names (for example, `_.reduce` instead of `_.inject`). 52 | 53 | Take care of the return value of functions. CoffeeScript adds implicit return statements. If a loop is at the end of a function, CoffeeScript creates a list comprehension which might be unnecessary: 54 | 55 | ``` 56 | method: -> 57 | for … 58 | … 59 | ``` 60 | 61 | Add a return statement to avoid this. 62 | 63 | ``` 64 | method: -> 65 | for … 66 | … 67 | return 68 | ``` 69 | 70 | ## Type checking 71 | 72 | Avoid using Underscore’s type checking functions. They aren’t needed in most cases. Use the simplest way which is appropriate: 73 | 74 | Use duck typing instead of requiring a specific type where applicable. 75 | 76 | When expecting objects (non-primitives), just check for truthiness. This is fast and easy to read. If the value is a truthy primitive, the code will fail when trying to use undefined properties. 77 | 78 | Use the CoffeeScript `?` operator to exclude `null` and `undefined` while allowing all other types (truthy or falsy). But use the operator sparingly, avoid chains like `foo?.bar?.quux` because they compile to unreadable and inefficient JavaScript code. For example, use `if foo and foo.bar` instead of `if foo?.bar` if truthiness is okay. 79 | 80 | - Check for `string.length`, `number > 0` etc. 81 | - Use `typeof` to detect `function`, `string`, `number` (if type detection is necessary) 82 | - Use `is/isnt null` to detect `null` 83 | - Use `obj.prop is/isnt undefined` to detect `undefined` 84 | - Use the `of` operator to check for properties that might be inherited 85 | - Use `_.has` for `hasOwnProperty` checks 86 | 87 | ## Function calls 88 | 89 | In general, don’t use parentheses when calling functions and methods with arguments: 90 | 91 | ``` 92 | func() 93 | func arg1 94 | func arg1, arg2, option1: 'foo', option2: 'bar' 95 | ``` 96 | 97 | When the arguments do not fit into one line, use parentheses to make the function call clear: 98 | 99 | ``` 100 | foo( 101 | longLongLongLongExpression1, 102 | longLongLongLongExpression2, 103 | longLongLongLongExpression3 104 | ) 105 | ``` 106 | 107 | Better avoid these problems by using more variables: 108 | 109 | ``` 110 | arg1 = longLongLongLongExpression 111 | arg2 = longLongLongLongExpression 112 | arg3 = longLongLongLongExpression 113 | foo arg1, arg2, arg3 114 | ``` 115 | 116 | If there is an object literal as last argument which spans multiple lines, 117 | use explicit curly braces: 118 | 119 | ``` 120 | func arg1, arg3, { 121 | prop1: 'val1' 122 | prop2: 'val2' 123 | } 124 | ``` 125 | 126 | When there are several object literals as arguments, use curly braces to 127 | make this obvious: 128 | 129 | ``` 130 | func { first: 'object' }, { second: 'object' } 131 | ``` 132 | 133 | ### Nesting function calls 134 | 135 | Omit parentheses only on the first level, use them on the subsequent levels: 136 | 137 | ``` 138 | foo bar quux # Don’t 139 | foo bar(quux) # Do 140 | ``` 141 | 142 | In general, don’t put too much logic on one line of code so heavy nesting isn’t needed. 143 | 144 | ### Chaining function calls 145 | 146 | Use this style of chaining function calls: 147 | 148 | ``` 149 | $('#selector').addClass 'class' 150 | foo(4).bar 8 151 | ``` 152 | 153 | Avoid the “function grouping style”, as described in the [CoffeeScript style guide](https://github.com/polarmobile/coffeescript-style-guide). 154 | 155 | ## Spec style 156 | 157 | Use `expect(…).to.be(…)` instead of `.to.equal()`. 158 | 159 | Use the bridge between Expect.js and Sinon.js for nice spy/stub/mock expectations, see [sinon-expect.js](https://github.com/lightsofapollo/sinon-expect/blob/master/lib/sinon-expect.js). 160 | 161 | ## Git style 162 | 163 | Follow [the git style guide](https://github.com/paulmillr/code-style-guides/blob/master/README.md#git). 164 | 165 | # Releasing Chaplin 166 | 167 | A reminder to maintainers what should be done before every release. 168 | 169 | 1. Update `package.json`, `bower.json`, `component.json` and `CHANGELOG.md`, 170 | `grunt release` 171 | 2. Update chaplinjs.org. 172 | 3. Tweet about new version. Template: 173 | 174 | Chaplin $RELEASE released! $CHANGES. Changelog: https://github.com/chaplinjs/chaplin/blob/master/CHANGELOG.md. Diff: https://github.com/chaplinjs/chaplin/compare/$PREV_RELEASE...$RELEASE 175 | 176 | 4. Update 177 | [brunch-with-chaplin](https://github.com/paulmillr/brunch-with-chaplin), 178 | [chaplin-boilerplate](https://github.com/chaplinjs/chaplin-boilerplate) and 179 | [chaplin-boilerplate-plain](https://github.com/chaplinjs/chaplin-boilerplate-plain) 180 | with new chaplin versions. 181 | 5. Update examples: 182 | [Ost.io](https://github.com/paulmillr/ostio), 183 | [composer-example](https://github.com/chaplinjs/composer-example), 184 | [todomvc](https://github.com/addyosmani/todomvc). 185 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Moviepilot GmbH 2 | http://moviepilot.com/contact 3 | With contributions by several individuals: 4 | https://github.com/chaplinjs/chaplin/graphs/contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Chaplin](http://s3.amazonaws.com/imgly_production/3401027/original.png) 2 | 3 | [![Build Status](https://travis-ci.org/chaplinjs/chaplin.svg?branch=master)](https://travis-ci.org/chaplinjs/chaplin) 4 | 5 | # An Application Architecture Using Backbone.js 6 | 7 | ## Introduction 8 | 9 | Chaplin is an architecture for JavaScript applications using the [Backbone.js](http://backbonejs.org/) library. 10 | 11 | All information, commercial support contacts and examples are available at [chaplinjs.org](http://chaplinjs.org), comprehensive documentation and class reference can be found at [docs.chaplinjs.org](http://docs.chaplinjs.org). 12 | 13 | [Download the latest release on chaplinjs.org](http://chaplinjs.org/#downloads). See below on how to compile from source manually. 14 | 15 | ## Building Chaplin 16 | 17 | The Chaplin source files are originally written in the [CoffeeScript](http://coffeescript.org/) meta-language. However, the Chaplin library file is a compiled JavaScript file which defines the `chaplin` module. 18 | 19 | Our build script compiles the CoffeeScripts and bundles them into one file. To run the script, follow these steps: 20 | 21 | 1. Download and install [Node.js](http://nodejs.org/). 22 | 2. Open a shell (aka terminal aka command prompt) and type in the commands in the following steps. 23 | 3. Change into the Chaplin root directory. 24 | 4. Install all dependencies 25 | 26 | ``` 27 | npm install 28 | ``` 29 | 30 | 5. Start the build 31 | 32 | ``` 33 | npm run build 34 | ``` 35 | 36 | 37 | This creates these files in `build` dir: 38 | 39 | * `chaplin.js` – The library as a compiled JavaScript file. 40 | * `chaplin.min.js` – Minified. For production use you should pick this. 41 | 42 | ## Running the Tests 43 | 44 | Chaplin aims to be fully unit-tested. At the moment most of the modules are covered by Mocha tests. 45 | 46 | How to run the tests: 47 | 48 | 1. Follow the steps for [building chaplin](#building-chaplin). 49 | 2. Open a shell (aka terminal aka command prompt) and type in the commands in the following steps. 50 | 3. Change into the Chaplin root directory. 51 | 4. Start the test runner. 52 | 53 | ``` 54 | npm test 55 | ``` 56 | 57 | or alternatively, if you want code coverage reports 58 | 59 | ``` 60 | npm run coverage 61 | ``` 62 | 63 | Generated code coverage reports may be viewed by opening `coverage/index.html` in your browser. 64 | 65 | ![Ending](http://s3.amazonaws.com/imgly_production/3362023/original.jpg) 66 | 67 | ## [The Cast](https://github.com/chaplinjs/chaplin/blob/master/AUTHORS.md#the-cast) 68 | 69 | ## [The Producers](https://github.com/chaplinjs/chaplin/blob/master/AUTHORS.md#the-producers) 70 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "coffeescript_error": { 3 | "level": "error" 4 | }, 5 | "arrow_spacing": { 6 | "name": "arrow_spacing", 7 | "level": "warn" 8 | }, 9 | "no_tabs": { 10 | "name": "no_tabs", 11 | "level": "error" 12 | }, 13 | "no_trailing_whitespace": { 14 | "name": "no_trailing_whitespace", 15 | "level": "warn", 16 | "allowed_in_comments": false, 17 | "allowed_in_empty_lines": true 18 | }, 19 | "max_line_length": { 20 | "name": "max_line_length", 21 | "value": 80, 22 | "level": "warn", 23 | "limitComments": true 24 | }, 25 | "line_endings": { 26 | "name": "line_endings", 27 | "level": "ignore", 28 | "value": "unix" 29 | }, 30 | "no_trailing_semicolons": { 31 | "name": "no_trailing_semicolons", 32 | "level": "error" 33 | }, 34 | "indentation": { 35 | "name": "indentation", 36 | "value": 2, 37 | "level": "error" 38 | }, 39 | "camel_case_classes": { 40 | "name": "camel_case_classes", 41 | "level": "error" 42 | }, 43 | "colon_assignment_spacing": { 44 | "name": "colon_assignment_spacing", 45 | "level": "ignore", 46 | "spacing": { 47 | "left": 0, 48 | "right": 1 49 | } 50 | }, 51 | "no_implicit_braces": { 52 | "name": "no_implicit_braces", 53 | "level": "ignore", 54 | "strict": true 55 | }, 56 | "no_plusplus": { 57 | "name": "no_plusplus", 58 | "level": "ignore" 59 | }, 60 | "no_throwing_strings": { 61 | "name": "no_throwing_strings", 62 | "level": "error" 63 | }, 64 | "no_backticks": { 65 | "name": "no_backticks", 66 | "level": "error" 67 | }, 68 | "no_implicit_parens": { 69 | "name": "no_implicit_parens", 70 | "level": "ignore" 71 | }, 72 | "no_empty_param_list": { 73 | "name": "no_empty_param_list", 74 | "level": "warn" 75 | }, 76 | "no_stand_alone_at": { 77 | "name": "no_stand_alone_at", 78 | "level": "warn" 79 | }, 80 | "space_operators": { 81 | "name": "space_operators", 82 | "level": "warn" 83 | }, 84 | "duplicate_key": { 85 | "name": "duplicate_key", 86 | "level": "error" 87 | }, 88 | "empty_constructor_needs_parens": { 89 | "name": "empty_constructor_needs_parens", 90 | "level": "ignore" 91 | }, 92 | "cyclomatic_complexity": { 93 | "name": "cyclomatic_complexity", 94 | "value": 10, 95 | "level": "ignore" 96 | }, 97 | "newlines_after_classes": { 98 | "name": "newlines_after_classes", 99 | "value": 3, 100 | "level": "ignore" 101 | }, 102 | "no_unnecessary_fat_arrows": { 103 | "name": "no_unnecessary_fat_arrows", 104 | "level": "warn" 105 | }, 106 | "missing_fat_arrows": { 107 | "name": "missing_fat_arrows", 108 | "level": "ignore" 109 | }, 110 | "non_empty_constructor_needs_parens": { 111 | "name": "non_empty_constructor_needs_parens", 112 | "level": "ignore" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.chaplinjs.org -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | markdown: redcarpet 2 | pygments: true 3 | redcarpet: 4 | extensions: [with_toc_data] 5 | -------------------------------------------------------------------------------- /docs/_includes/modules.md: -------------------------------------------------------------------------------- 1 | ### General 2 | 3 | * [Overview](./) 4 | * [Getting started](./getting_started.html) 5 | * [Event handling overview](./events.html) 6 | * [Disposal](./disposal.html) 7 | * [Using without jQuery and Underscore](./no_deps.html) 8 | * [Handling async deps](./handling_async.html) 9 | * [Upgrading Chaplin](./upgrading.html) 10 | 11 | ### Core 12 | * Chaplin.mediator 13 | * Chaplin.Dispatcher 14 | * Chaplin.Composer 15 | * Chaplin.Layout 16 | * Chaplin.Application 17 | 18 | ### MVC 19 | * Chaplin.Controller 20 | * Chaplin.Model 21 | * Chaplin.Collection 22 | * Chaplin.View 23 | * Chaplin.CollectionView 24 | 25 | ### Libs 26 | * Chaplin.EventBroker 27 | * Chaplin.SyncMachine 28 | * Chaplin.Router 29 | * Chaplin.support 30 | * Chaplin.utils 31 | 32 | ### More 33 | * [Best practices and cookbook](https://github.com/chaplinjs/chaplin/wiki/) 34 | * [Annotated source code](http://chaplinjs.org/annotated/chaplin.html) 35 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | --- 2 | source_base: https://github.com/chaplinjs/chaplin/blob/ 3 | version: 1.1.0 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | {% if page.title %} {{ page.title }} · {% endif %} Chaplin Documentation 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 |
26 | 27 | 59 |
60 | 61 | 80 | 81 | 82 |
83 |
84 | {% if page.title %} 85 |

86 | {{ page.title }} 87 | {% if page.module_path %} 88 | → Source 89 | {% endif %} 90 |

91 | {% endif %} 92 |
93 | {{ content }} 94 |
95 |
96 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/chaplin.application.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Application 4 | module_path: src/chaplin/application.coffee 5 | Chaplin: Application 6 | --- 7 | 8 | The **Chaplin.Application** object is a bootstrapper and a point of extension 9 | for the core modules of **Chaplin**: the **[Dispatcher](#initDispatcher)**, the 10 | **[Layout](#initLayout)**, the **[Router](#initRouter)**, and the 11 | **[Composer](#initComposer)**. The object can be extended by your application. 12 | 13 | The easiest way to get started is by extending `Chaplin.Application` with the 14 | bare essentials: 15 | 16 | ```coffeescript 17 | Chaplin = require 'chaplin' 18 | routes = require 'routes' 19 | 20 | module.exports = class MyApplication extends Chaplin.Application 21 | title: 'My Application' 22 | ``` 23 | ```javascript 24 | var Chaplin = require('chaplin'); 25 | var routes = require('routes'); 26 | 27 | var MyApplication = Chaplin.Application.extend({ 28 | title: 'My Application' 29 | }); 30 | 31 | module.exports = MyApplication; 32 | ``` 33 | 34 | Then you can pass options for router, dispatcher, etc. when starting the 35 | application: 36 | 37 | ```html 38 | 43 | ``` 44 | 45 | For a complete example of this approach, see [Chaplin 46 | Boilerplate](https://github.com/chaplinjs/chaplin-boilerplate-plain). 47 | 48 | If you want to have more fine-grained control over application startup, you can 49 | override the various methods of `Chaplin.Application`. When overriding the 50 | `initialize` method, you should not call `super`, but do all the 51 | required calls in place or you run risk of initializing modules twice. 52 | 53 | Instead, the `initialize` method of your derived class must initialize 54 | the core modules by calling the `initRouter`, `initDispatcher`, `initLayout` 55 | and `initMediator` methods and then initiating navigation with `start`. 56 | For more details on proper initialization, see the original implementation in 57 | `Chaplin.Application`. 58 | 59 |

Properties

60 | 61 |

title

62 | This is the top-level title, handed to the layout module in the options hash. 63 | When using the [layout module](./chaplin.layout.html)’s default title template, 64 | the value for `title` will be appended to the subtitle passed to the 65 | `!adjustTitle` event in order to construct the document’s title. 66 | 67 | ```coffeescript 68 | # [...] 69 | class Application extends Chaplin.Application 70 | # [...] 71 | title: "Fruit" 72 | 73 | mediator.publish '!adjustTitle', 'Apple' 74 | # Document title is now "Apple ­— Fruit". 75 | ``` 76 | 77 | ```javascript 78 | // [...] 79 | var Application = Chaplin.Application.extend({ 80 | // [...] 81 | title: 'Fruit' 82 | }); 83 | mediator.publish('!adjustTitle', 'Apple'); 84 | // Document title is now "Apple ­— Fruit". 85 | ``` 86 | 87 |

Methods

88 | 89 |

initDispatcher([options])

90 | Initializes the **dispatcher** module; forwards passed options to its constructor. See **[Chaplin.Dispatcher](./chaplin.dispatcher.html)** for more information. 91 | 92 | To replace the dispatcher with a derived class (possibly with various extensions), you’d override the `initDispatcher` method and construct the dispatcher class as follows: 93 | 94 | ```coffeescript 95 | # [...] 96 | Dispatcher = require 'dispatcher' 97 | class Application extends Chaplin.Application 98 | # [...] 99 | initDispatcher: (options) -> 100 | @dispatcher = new Dispatcher options 101 | ``` 102 | 103 | ```javascript 104 | // [...] 105 | var Dispatcher = require('dispatcher'); 106 | var Application = Chaplin.Application.extend({ 107 | // [...] 108 | initDispatcher: function(options) { 109 | this.dispatcher = new Dispatcher(options); 110 | } 111 | }); 112 | ``` 113 | 114 |

initRouter(routes, [options])

115 | Initializes the **router** module; forwards passed options to its constructor. This starts the routing off by checking the current URL against all defined routes and executing the matched handler. See **[Chaplin.Router](./chaplin.router.html)** for more information. 116 | 117 | * **routes** 118 | The routing function that contains the match invocations, normally located in `routes.coffee`. 119 | 120 | To replace the router with a derived class (possibly with various extensions), you’d override the `initRouter` method and construct the router class as follows (ensuring to start the routing process as well): 121 | 122 | ```coffeescript 123 | # [...] 124 | Router = require 'router' 125 | class Application extends Chaplin.Application 126 | # [...] 127 | initRouter: (routes, options) -> 128 | @router = new Router options 129 | 130 | # Register any provided routes. 131 | routes? @router.match 132 | ``` 133 | 134 | ```javascript 135 | // [...] 136 | var Router = require('router'); 137 | var Application = Chaplin.Application.extend({ 138 | // [...] 139 | initRouter: function(routes, options) { 140 | this.router = new Router(options); 141 | 142 | // Register any provided routes. 143 | if (routes != null) routes(this.router.match); 144 | } 145 | }); 146 | ``` 147 | 148 |

start()

149 | When all of the routes have been matched, call `start()` to begin monitoring routing events, and dispatching routes. Invoke this method after all of the components have been initialized as this will also match the current URL and dispatch the matched route. 150 | 151 | For example, if you want to fetch some data before application is started, you can do it like that: 152 | 153 | ```coffeescript 154 | # [...] 155 | class Application extends Chaplin.Application 156 | # [...] 157 | start: -> 158 | mediator.user.fetch().then => 159 | super 160 | ``` 161 | 162 | ```javascript 163 | // [...] 164 | var Application = Chaplin.Application.extend({ 165 | // [...] 166 | start: function() { 167 | mediator.user.fetch().then(function() { 168 | Chaplin.Application.prototype.call(this); 169 | }); 170 | } 171 | }); 172 | ``` 173 | 174 |

initComposer([options])

175 | Initializes the **composer** module; forwards passed options to its constructor. See **[Chaplin.Composer](./chaplin.composer.html)** for more information. 176 | 177 | To replace the layout with a derived class (possibly with various extensions), you'd override the `initComposer` method and construct the composer class as follows: 178 | 179 | ```coffeescript 180 | # [...] 181 | Composer = require 'composer' 182 | class Application extends Chaplin.Application 183 | # [...] 184 | initComposer: (options) -> 185 | @composer = new Composer options 186 | ``` 187 | 188 | ```javascript 189 | // [...] 190 | var Composer = require('composer'); 191 | var Application = Chaplin.Application.extend({ 192 | // [...] 193 | initComposer: function(options) { 194 | this.composer = new Composer(options); 195 | } 196 | }); 197 | ``` 198 | 199 |

initLayout([options])

200 | Initializes the **layout** module, forwarding the options hash to its constructor. See **[Chaplin.Layout](./chaplin.layout.html)** for more information. 201 | 202 | To replace the layout with a derived class (possibly with various extensions), you'd override the `initLayout` method and construct the layout class as follows: 203 | 204 | ```coffeescript 205 | # [...] 206 | _ = require 'underscore' 207 | Layout = require 'layout' 208 | class Application extends Chaplin.Application 209 | # [...] 210 | initLayout: (options) -> 211 | @layout = new Layout _.defaults options, {@title} 212 | ``` 213 | 214 | ```javascript 215 | // [...] 216 | var _ = require('underscore'); 217 | var Layout = require('layout'); 218 | var Application = Chaplin.Application.extend({ 219 | // [...] 220 | initLayout: function(options) { 221 | this.layout = new Layout(_.defaults(options, {title: this.title})); 222 | } 223 | }); 224 | ``` 225 | -------------------------------------------------------------------------------- /docs/chaplin.collection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Collection 4 | module_path: src/chaplin/models/collection.coffee 5 | Chaplin: Collection 6 | --- 7 | 8 | `Chaplin.Collection` is an extension of `Backbone.Collection`. Major additions are disposal for improved memory management and the inclusion of the pub/sub pattern via the `Chaplin.EventBroker` mixin. 9 | 10 |

Methods

11 | All [`Backbone.Collection` methods](http://backbonejs.org/#Collection). 12 | 13 |

serialize()

14 | Memory-saving model serialization. Maps models to their attributes recursively. Creates an object which delegates to the original attributes when a property needs to be overwritten. 15 | 16 |

dispose()

17 | Announces to all associated views that the model is being disposed. Unbinds all the global event handlers and also removes all the event handlers on the `Model` module. Removes internal attribute hashes and event handlers. If supported by the runtime, the `Collection` is [frozen](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze) to prevent any changes after disposal. 18 | 19 | ## Usage 20 | To make use of Chaplin’s automatic memory management, use `subscribeEvent` instead of registering methods directly as pub/sub listeners. This forces the handler context so the handler might be removed again on model/collection disposal. It’s crucial to remove all references to model/collection methods to allow them to be garbage collected. 21 | -------------------------------------------------------------------------------- /docs/chaplin.composer.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Composer 4 | module_path: src/chaplin/composer.coffee 5 | Chaplin: Composer 6 | --- 7 | 8 | ## Overview 9 | 10 | Grants the ability for views (and related data) to be persisted beyond one controller action. 11 | 12 | If a view is reused in a controller action method it will be instantiated and rendered if the view has not been reused in the current or previous action methods. 13 | 14 | If a view was reused in the previous action method and is not reused in the current action method, it will be disposed and removed from the DOM. 15 | 16 | ## Example 17 | 18 | A common use case is a login page. This login page is a simple centered form. However, the main application needs both header and footer controllers. 19 | 20 | The following is a sketch of this use case put into code: 21 | 22 | ```coffeescript 23 | # routes.coffee 24 | (match) -> 25 | match 'login', 'login#show' 26 | match '', 'index#show' 27 | match 'about', 'about#show' 28 | 29 | 30 | # controllers/login_controller.coffee 31 | Login = require 'views/login' 32 | class LoginController extends Chaplin.Controller 33 | show: -> 34 | # Simple view, just want to show the login screen 35 | @view = new Login() 36 | 37 | 38 | # controllers/site_controller.coffee 39 | Site = require 'views/site' 40 | Header = require 'views/header' 41 | Footer = require 'views/footer' 42 | class SiteController extends Chaplin.Controller 43 | beforeAction: -> 44 | # Reuse the Site view, which is a simple 3-row stacked layout that 45 | # provides the header, footer, and body regions 46 | @reuse 'site', Site 47 | 48 | # Reuse the Header view, which binds itself to whatever container 49 | # is exposed in Site under the header region 50 | @reuse 'header', Header, region: 'header' 51 | 52 | # Likewise for the footer region 53 | @reuse 'footer', Footer, region: 'footer' 54 | 55 | 56 | # controllers/index_controller.coffee 57 | Index = require 'views/index' 58 | SiteController = require 'controllers/site_controller' 59 | class IndexController extends SiteController 60 | show: -> 61 | # Instantiate this simple index view at the body region 62 | @view = new Index region: 'body' 63 | 64 | 65 | # controllers/about_me_controller.coffee 66 | AboutMe = require 'views/aboutme' 67 | SiteController = require 'controllers/site_controller' 68 | class AboutMeController extends SiteController 69 | show: -> 70 | # Instantiate this simple about me view at the body region 71 | @view = new AboutMe region: 'body' 72 | ``` 73 | 74 | ```javascript 75 | // routes.js 76 | function(match) { 77 | match('login', 'login#show'); 78 | match('', 'index#show'); 79 | match('about', 'about#show'); 80 | } 81 | 82 | // controllers/login_controller.js 83 | var Login = require('views/login'); 84 | var LoginController = Chaplin.Controller.extend({ 85 | show: function() { 86 | // Simple view, just want to show the login screen. 87 | this.view = new Login(); 88 | } 89 | }); 90 | 91 | // controllers/site_controller.js 92 | var Site = require('views/site'); 93 | var Header = require('views/header'); 94 | var Footer = require('views/footer'); 95 | var SiteController = Chaplin.Controller.extend({ 96 | beforeAction: function() { 97 | // Reuse the Site view, which is a simple 3-row stacked layout that 98 | // provides the header, footer, and body regions 99 | this.reuse('site', Site); 100 | 101 | // Reuse the Header view, which binds itself to whatever container 102 | // is exposed in Site under the header region 103 | this.reuse('header', Header, {region: 'header'}); 104 | 105 | // Likewise for the footer region 106 | this.reuse('footer', Footer, {region: 'footer'}); 107 | } 108 | }); 109 | 110 | // controllers/index_controller.js 111 | var Index = require('views/index'); 112 | var SiteController = require('controllers/site_controller'); 113 | var IndexController = SiteController.extend({ 114 | show: function() { 115 | // Instantiate this simple index view at the body region. 116 | this.view = new Index({region: 'body'}); 117 | } 118 | }); 119 | 120 | // controllers/about_me_controller.js 121 | var AboutMe = require('views/aboutme'); 122 | var SiteController = require('controllers/site_controller'); 123 | var AboutMeController = SiteController.extend({ 124 | show: function() { 125 | // Instantiate this simple about me view at the body region. 126 | this.view = new AboutMe({region: 'body'}); 127 | } 128 | }); 129 | ``` 130 | 131 | Given the controllers above here is what would happen each time the URL is routed: 132 | 133 | ```coffeescript 134 | route('login') 135 | # 'views/login' is initialized and rendered 136 | 137 | route('') 138 | # 'views/site' is initialized and rendered 139 | # 'views/header' is initialized and rendered 140 | # 'views/footer' is initialized and rendered 141 | # 'views/index' is initialized and rendered 142 | # 'views/login' is disposed 143 | 144 | route('about') 145 | # 'views/aboutme' is initialized and rendered 146 | # 'views/index' is disposed 147 | 148 | route('login') 149 | # 'views/login' is initialized and rendered 150 | # 'views/index' is disposed 151 | # 'views/footer' is disposed 152 | # 'views/header' is disposed 153 | # 'views/site' is disposed 154 | ``` 155 | 156 | 157 | ## Long form 158 | 159 | By default, when a controller requests a view to be reused, the composer checks if the view instance exists and the new options are the same as before. If that is true the view is destroyed and reused. 160 | 161 | The following example shows another way to use the `compose` method to allow for just about anything. The check method should return true when it wishes the composition to be disposed and the `compose` method to be called. The composer will track and ensure proper disposal of whatever is returned from the compose method (be it a view or an object with properties that have dispose methods). 162 | 163 | ```coffeescript 164 | @reuse 'main-post', 165 | compose: -> 166 | @model = new Post {id: 42} 167 | @view = new PostView {@model} 168 | @model.fetch() 169 | 170 | check: -> @model.id isnt 42 171 | ``` 172 | 173 | ```javascript 174 | this.reuse('main-post', { 175 | compose: function() { 176 | this.model = new Post({id: 42}); 177 | this.view = new PostView({model: this.model}); 178 | this.model.fetch(); 179 | }, 180 | 181 | check: function() {return this.model.id !== 42;} 182 | }); 183 | ``` 184 | -------------------------------------------------------------------------------- /docs/chaplin.controller.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Controller 4 | module_path: src/chaplin/controllers/controller.coffee 5 | Chaplin: Controller 6 | --- 7 | 8 | Controllers are in charge of handling the lifecycle of specific models and their associated views. That is, they are responsible for both instantiating and connecting models/collections and their views, as well as disposing of them, before handing control over to another controller. There can be only one *current* controller, which provides the main view and represents the current URL. In addition, there can be several persistent controllers for overarching tasks, like for example a `SessionController`. 9 | 10 |

Methods

11 | 12 |

adjustTitle(subtitle)

13 | Adjusts document title to `subtitle - title`. A title template can be set when initializing the `Dispatcher`. 14 | 15 |

redirectTo(params, options)

16 | 17 | Simple proxy to `Chaplin.utils.redirectTo` that also does `this.redirected = true;`. See [`Chaplin.utils.redirectTo`](./chaplin.utils.html#redirectTo) for details. 18 | 19 |

dispose()

20 | 21 | Disposes all models and views on current `Controller` instance. 22 | 23 | ## Usage 24 | 25 | ### Structure 26 | 27 | By convention, there is one controller for each application module. A controller may provide methods for several actions like `index`, `show`, `edit`, etc. These action methods are called by the [Chaplin.Dispatcher](./chaplin.dispatcher.html) when an associated route matches. 28 | 29 | A controller is usually started following a route match. Each route entry points to one controller action, for example `likes#show`, which is the `show` action of the `LikesController`. 30 | 31 | 32 | ### Naming convention 33 | 34 | By default, all controllers must be placed in the `/controllers/` folder (the / stands for the root of the `baseURL` you have defined for your loader) and be suffixed with `_controller`. So for instance, the `LikesController` needs to be defined in the file `/controllers/likes_controller.js`. 35 | 36 | If you want to overwrite this behaviour, you can edit the `controller_path` and `controller_suffix` options in the options hash you pass to `Chaplin.Application.initDispatcher` or `Chaplin.Dispatcher.initialize`. See details in the `Chaplin.Dispatcher` [documentation](./chaplin.dispatcher.html#initialize). 37 | 38 | 39 | ### Before actions 40 | 41 | To execute code before the controller action is called, you can define a handler as the `beforeAction` property (e.g. to add access control checks). 42 | 43 | 44 | ### Example 45 | 46 | ```coffeescript 47 | define [ 48 | 'controllers/controller', 49 | 'models/likes', # the collection 50 | 'models/like', # the model 51 | 'views/likes-view', # the collection view 52 | 'views/full-like-view' # the view 53 | ], (Controller, Likes, Like, LikesView, FullLikeView) -> 54 | 'use strict' 55 | 56 | class LikesController extends Controller 57 | beforeAction: (params, route) -> 58 | if route.action is 'show' 59 | @redirectUnlessLoggedIn() 60 | 61 | # Initialize method is empty here. 62 | index: (params) -> 63 | @collection = new Likes() 64 | @view = new LikesView {@collection} 65 | 66 | show: (params) -> 67 | @model = new Like id: params.id 68 | @view = new FullLikeView {@model} 69 | ``` 70 | 71 | ```javascript 72 | define([ 73 | 'controllers/controller', 74 | 'models/likes', // the collection 75 | 'models/like', // the model 76 | 'views/likes-view', // the collection view 77 | 'views/full-like-view' // the view 78 | ], function(Controller, Likes, Like, LikesView, FullLikeView) { 79 | 'use strict' 80 | 81 | var LikesController = Controller.extend({ 82 | beforeAction: function() { 83 | this.redirectUnlessLoggedIn(); 84 | }, 85 | 86 | // Initialize method is empty here. 87 | index: function(params) { 88 | this.collection = new Likes(); 89 | this.view = new LikesView({collection: this.collection}); 90 | }, 91 | 92 | show: function(params) { 93 | this.model = new Like({id: params.id}); 94 | this.view = new FullLikeView({model: this.model}); 95 | } 96 | }); 97 | return LikesController; 98 | }); 99 | ``` 100 | 101 | ### Creating models and views 102 | 103 | A controller action should create a main view and save it as an instance property named `view`: `this.view = new SomeView(…)`. 104 | 105 | Normal models and collections should also be saved as instance properties so Chaplin can reach them. 106 | 107 | ### Controller disposal and object persistence 108 | 109 | By default a new controller is instantiated with every route match. That means models and views are disposed by default, even if the new controller is the same as the old controller. 110 | 111 | To persist models and views in a controlled way, it is recommended to use the [Chaplin.Composer](./chaplin.composer.html). 112 | 113 | Chaplin will automatically dispose all models and views that are properties of the controller instance. If you’re using the Composer to reuse models and views, you need to use local variables instead of controller properties. Otherwise Chaplin will dispose them with the controller. 114 | 115 | ### Including Controllers in the production build 116 | 117 | In your production environment, you may want to package your files together using a build tool like [r.js](http://requirejs.org/docs/optimization.html). 118 | 119 | Controllers are dynamically loaded from the `Chaplin.Dispatcher` using the `require()` method. Build tools like r.js can’t know about files that are lazy-loaded using `require()`. They only consider the static dependencies specified by `define()`. 120 | 121 | This means that build tools will ignore your controllers and won’t include them in your package. You need to include them manually, for instance with r.js: 122 | 123 | ```yaml 124 | paths: 125 | # ... 126 | modules: 127 | - name: 'application' 128 | - name: 'controllers/one_controller' # included manually into the build 129 | - name: 'controllers/another_controller' # same 130 | ``` 131 | -------------------------------------------------------------------------------- /docs/chaplin.dispatcher.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Dispatcher 4 | module_path: src/chaplin/dispatcher.coffee 5 | Chaplin: Dispatcher 6 | --- 7 | 8 | The `Dispatcher` sits between the router and the various controllers of your application. It listens for a routing event to occur and then: 9 | 10 | * disposes the previously active controller, 11 | * loads the target controller module, 12 | * instantiates the new controller, and 13 | * calls the target action. 14 | 15 |

Methods

16 | 17 |

initialize([options={}])

18 | 19 | * **options**: 20 | * **controllerPath** (default `'/controllers'`): the path to the folder for the controllers. 21 | * **controllerSuffix** (default `'_controller':`) the suffix used for controller files. 22 | Both of these options serve to generate path names for autoloading controller modules. 23 | -------------------------------------------------------------------------------- /docs/chaplin.event_broker.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.EventBroker 4 | module_path: src/chaplin/lib/event_broker.coffee 5 | Chaplin: EventBroker 6 | --- 7 | 8 | The `EventBroker` offers an interface to interact with [Chaplin.mediator](./chaplin.mediator.html), meant to be used as a mixin. 9 | 10 |

Methods

11 | 12 |

publishEvent(event, arguments...)

13 | Publishes `event` globally, passing `arguments` along for interested subscribers. 14 | 15 |

subscribeEvent(event, handler)

16 | Subscribes the `handler` to the given `event`. If `handler` already subscribed to `event`, it will be removed as a subscriber and added afresh. This function is like `Chaplin.mediator.subscribe` except it cannot subscribe twice. 17 | 18 |

unsubscribeEvent(event, handler)

19 | Unsubcribe the `handler` from the `event`. This functions like `Chaplin.mediator.unsubscribe`. 20 | 21 |

unsubscribeAllEvents()

22 | Unsubcribe from any subscriptions made through this objects `subscribeEvent` method. 23 | 24 | ## Usage 25 | 26 | To give a class these pub/sub capabilities, you just need to make it extend `Chaplin.EventBroker`: `_.extend @prototype, EventBroker` `_.extend(this.prototype, EventBroker)`. 27 | -------------------------------------------------------------------------------- /docs/chaplin.layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Layout 4 | module_path: src/chaplin/views/layout.coffee 5 | Chaplin: Layout 6 | --- 7 | 8 | `Chaplin.Layout` is the top-level application “view”. It doesn't inherit from `Chaplin.View` but borrows some of its functionalities. It is tied to the `document` DOM element and handles app-wide events, such as clicks on application-internal links. Most importantly, when a new controller is activated, `Chaplin.Layout` is responsible for changing the main view to the view of the new controller. 9 | 10 |

Methods

11 | 12 |

initialize([options={}])

13 | 14 | * **options**: 15 | * **routeLinks** (default `'a, .go-to'`): the selector of elements you want to apply internal routing to. Set to false to deactivate internal routing. If `false`y, chaplin won’t route links at all. 16 | * **skipRouting** (default `'.noscript'`): if you want to skip the internal routing in some situation. Can take the following value: 17 | * selector: check if the activated link matches the selector. The default value is a selector and will prevent routing for any links with class `noscript`. 18 | * function: check the return value. Return `true` to continue routing, return `false` to stop routing. The path and the elements are passed as parameters. Example: `function(href, el) { return href == 'bla'; }` 19 | * false: never skip routing 20 | * **openExternalToBlank** (default `false`): whether or not links to external domains should open in a new window/tab. 21 | * **scrollTo** (default `[0, 0]`): the coordinates (x, y) you want to scroll to on view replacement. Set to *false* to deactivate it. 22 | * **titleTemplate** (default `_.template("<%= subtitle %> - <%= title %>")`): a function which returns the document title. Per default, it receives an object with the properties `title` and `subtitle`. 23 | 24 | 25 |

delegateEvents([events])

26 | 27 | A wrapper for `Backbone.View.delegateEvents`. See Backbone [documentation](http://backbonejs.org/#View-delegateEvents) for more details. 28 | 29 | 30 |

undelegateEvents()

31 | 32 | A wrapper for `Backbone.View.undelegateEvents`. See Backbone [documentation](http://backbonejs.org/#View-undelegateEvents) for more details. 33 | 34 | 35 |

hideOldView(controller)

36 | 37 | Hide the active (old) view on the `beforeControllerDispose` event sent by the dispatcher on route change and scroll to the coordinates specified by the initialize `scrollTo` option. 38 | 39 | 40 |

showNewView(context)

41 | 42 | Show the new view on the `dispatcher:dispatch` event sent by the dispatcher on route change. 43 | 44 | 45 |

adjustTitle(context)

46 | 47 | Adjust the title of the page based on the `titleTemplate` option. The `title` variable is the one defined at application level, the `subtitle` the one defined at controller level. 48 | 49 | 50 | 51 | Open the `href` or `data-href` URL of a DOM element. When `openLink` is called it checks if the `href` is valid and runs the `skipRouting` function if set by the user. If the `href` is valid, it checks if it is an external link and depending on the `openExternalToBlank` option, opens it in a new window. Finally, if it is an internal link, it starts routing the URL. 52 | 53 | ## Usage 54 | 55 | ### App-wide events 56 | 57 | To register app-wide events, you can define them in the `events` hash. It works like `Backbone.View.delegateEvent` on the `document` DOM element. 58 | 59 | 60 | ### Route links internally 61 | 62 | If you want to route links internally, you can use the `events` hash with the `openLink` function like so: 63 | 64 | ```coffeescript 65 | events: 66 | 'click a': 'openLink' 67 | ``` 68 | 69 | ```javascript 70 | events: { 71 | 'click a': 'openLink' 72 | } 73 | ``` 74 | 75 | To open all external links (different hostname) in a new window, you can set `openExternalToBlank` to true when initializing `Chaplin.Layout` in your `Application`: 76 | 77 | ```coffeescript 78 | class MyApplication extends Chaplin.Application 79 | initialize: -> 80 | # ... 81 | @initLayout openExternalToBlank: true 82 | ``` 83 | 84 | ```javascript 85 | var MyApplication = Chaplin.Application.extend({ 86 | initialize: function() { 87 | // ... 88 | this.initLayout({openExternalToBlank: true}); 89 | } 90 | }); 91 | ``` 92 | 93 | To add a custom check whether or not a link should be open internally, you can override the `isExternalLink` method: 94 | 95 | ```coffeescript 96 | class Layout extends Chaplin.Layout 97 | isExternalLink: (href) -> # some test on the href variable 98 | ``` 99 | 100 | ```javascript 101 | var Layout = Chaplin.Layout.extend({ 102 | isExternalLink: function(href) {} // some test on the href variable 103 | }); 104 | ``` 105 | 106 | ### View loading 107 | 108 | There is nothing to do, the Layout is listening to the `beforeControllerDispose` and `dispatcher:dispatch` and will trigger the function when a new route is called. If you are not happy with the site scrolling to the top of the page on each view load, you can set the `scrollTo` option when initializing `Chaplin.Layout` in your `Application`: 109 | 110 | ```coffeescript 111 | class MyApplication extends Chaplin.Application 112 | 113 | initialize: -> 114 | # ... 115 | @initLayout 116 | scrollTo: [10, 30] # will scroll to x=10px and y=30px. 117 | # OR 118 | scrollTo: false # deactivate the scroll 119 | ``` 120 | 121 | ```javascript 122 | var MyApplication = Chaplin.Application.extend({ 123 | initialize: function() { 124 | // ... 125 | this.initLayout({ 126 | scrollTo: [10, 30] // will scroll to x=10px and y=30px. 127 | // OR 128 | scrollTo: false // deactivate the scroll 129 | }); 130 | } 131 | }); 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/chaplin.mediator.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.mediator 4 | module_path: src/chaplin/mediator.coffee 5 | Chaplin: mediator 6 | --- 7 | 8 | It is one of the basic goals of Chaplin to enforce module encapsulation and independence and to direct communication through controlled channels. Chaplin’s `mediator` object is the enabler of this controlled communication. It implements the [Publish/Subscribe](http://en.wikipedia.org/wiki/Publish/subscribe) (pub/sub) pattern to ensure loose coupling of application modules, while still allowing for ease of information exchange. Instead of making direct use of other parts of the application, modules communicate by events, similar to how changes in the DOM are communicated to client-side code. Modules can listen for and react to events, but also publish events of their own to give other modules the chance to react. There are only three basic methods for this application-wide communication: `subscribe`, `unsubscribe` and `publish`. 9 | 10 | **Note:** If you want to give local pub/sub functionality to a class, take a look at the [EventBroker](./chaplin.event_broker.html). 11 | 12 |

Methods

13 | 14 |

subscribe(event, handler, [context])

15 | 16 | A wrapper for `Backbone.Events.on`. See Backbone [documentation](http://backbonejs.org/#Events-on) for more details. 17 | 18 |

unsubscribe([event], [handler], [context])

19 | 20 | A wrapper for `Backbone.Events.off`. See Backbone [documentation](http://backbonejs.org/#Events-off) for more details. 21 | 22 |

publish(event, [*args])

23 | 24 | A wrapper for `Backbone.Events.trigger`. See Backbone [documentation](http://backbonejs.org/#Events-trigger) for more details. 25 | 26 | ## Request-response methods 27 | 28 | Since Chaplin 0.11, Chaplin uses 29 | [request-response](http://en.wikipedia.org/wiki/Request-response) 30 | strategy for communication between application parts. 31 | 32 | Think of it as events, but with only one allowed handler which is at the 33 | same time required. 34 | 35 |

setHandler(handlerName, handler)

36 | 37 | Sets a handler function for particular `handlerName`. 38 | 39 |

execute(handlerName, [*args])

40 | 41 | Executes a handler function from particular `handlerName`. If the handler 42 | is not present, an error will be thrown. 43 | 44 | ## Usage 45 | 46 | In any module that needs to communicate with other modules, access to the application-wide pub/sub system can be gained by requiring `Chaplin` as a dependency. The mediator object is then available as `Chaplin.mediator`. 47 | 48 | ```coffeescript 49 | define ['chaplin', 'otherdependency'], (Chaplin, OtherDependency) -> 50 | ``` 51 | 52 | ```javascript 53 | define(['chaplin', 'otherdependency'], function(Chaplin, OtherDependency) {}) 54 | ``` 55 | 56 | For example, if you have a session controller for logging in users, it will tell the mediator that the login occurred: 57 | 58 | ```coffeescript 59 | Chaplin.mediator.publish 'login', user 60 | ``` 61 | 62 | ```javascript 63 | Chaplin.mediator.publish('login', user); 64 | ``` 65 | 66 | The mediator will propagate this event to any module that was subscribed to the `'login'` event, as in this example: 67 | 68 | ```coffeescript 69 | Chaplin.mediator.subscribe 'login', @doSomething 70 | ``` 71 | 72 | ```javascript 73 | Chaplin.mediator.subscribe('login', this.doSomething); 74 | ``` 75 | 76 | Finally, if this module needs to stop listening for the login event, it can simply unsubscribe at any time: 77 | 78 | ```coffeescript 79 | Chaplin.mediator.unsubscribe 'login', @doSomething 80 | ``` 81 | 82 | ```javascript 83 | Chaplin.mediator.unsubscribe('login', this.doSomething); 84 | ``` 85 | 86 | To add some property on mediator, it is suggested to do it in `Application#initMediator`, when mediator is getting sealed: 87 | 88 | ```coffeescript 89 | class Application extends Chaplin.Application 90 | initMediator: -> 91 | Chaplin.mediator.prop = {hello: 'world'} 92 | super 93 | ``` 94 | 95 | ```javascript 96 | var Application = Chaplin.Application.extend({ 97 | initMediator: function() { 98 | Chaplin.mediator.prop = {hello: 'world'}; 99 | this.constructor.__super__.initMediator.call(this); 100 | } 101 | }) 102 | ``` 103 | -------------------------------------------------------------------------------- /docs/chaplin.model.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Model 4 | module_path: src/chaplin/models/model.coffee 5 | Chaplin: Model 6 | --- 7 | 8 | `Chaplin.Model` is an extension of `Backbone.Model`. Major additions are disposal for improved memory management and the inclusion of the pub/sub pattern via the `Chaplin.EventBroker` mixin. 9 | 10 |

Methods

11 | All `Backbone.Model` [methods](http://backbonejs.org/#Model). 12 | 13 |

getAttributes()

14 | An accessor for the model’s `attributes` property. The accessor can be overwritten by decorators to optionally perform any kind of processing of the data. 15 | 16 | **Note:** Pay attention to the fact that this returns the actual `attributes` object, not a serialization. 17 | 18 |

serialize()

19 | Memory-saving serializing of model attributes. Maps models to their attributes recursively. Creates an object which delegates to the original attributes when a property needs to be overwritten. 20 | 21 |

dispose()

22 | Sends a `'dispose'` event to all associated collections and views to announce that the model is being disposed. Unsubscribes all global and local event handlers and also removes all the event handlers that were subscribed to this model’s events. Removes the collection reference, internal attribute hashes and event handlers. On compliant runtimes the model is frozen to prevent any further changes to it, see [Object.freeze](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze). 23 | 24 | ## Usage 25 | To take advantage of the built in memory management, use `subscribeEvent` instead of registering model methods directly as handlers for global events. This ensures that the handler is added in a way that allows for automatic removal on model/collection disposal. It’s crucial to remove all references to model/collection methods to allow them to be garbage collected. 26 | 27 | The `SyncMachine` mixin for models simplifies the handling of asynchronous data fetching. Its functionality can be included with a simple `_.extend`: `_.extend @prototype, Chaplin.SyncMachine` `_.extend(this.prototype, Chaplin.SyncMachine);`. To learn more about `SyncMachine`, see the [SyncMachine docs](./chaplin.sync_machine.html). 28 | -------------------------------------------------------------------------------- /docs/chaplin.router.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.Router 4 | module_path: src/chaplin/lib/router.coffee 5 | Chaplin: Router 6 | --- 7 | 8 | This module is responsible for observing URL changes and matching them against a list of declared routes. If a declared route matches the current URL, a `router:match` event is triggered. 9 | 10 | `Chaplin.Router` is a replacement for [Backbone.Router](http://documentcloud.github.com/backbone/#Router) and does not inherit from it. It is a stand-alone implementation with several advantages over Backbone’s default. Why change the router implementation completely? 11 | 12 | In Backbone there are no controllers. Instead, Backbone’s `Router` maps routes to *its own methods*, serving two purposes and being more than just a router. Chaplin on the other hand delegates the handling of actions related to a specific route to controllers. Consequently, the router is really just a router. While the router has been rewritten for this purpose, Chaplin is using `Backbone.History` in the background. That is, Chaplin relies upon Backbone for handling hash URLs and interacting with the HTML5 History API (`pushState`). 13 | 14 | ## Declaring routes in the `routes` file 15 | 16 | By convention, all application routes should be declared in a separate file, the `routes` module. This is a simple module in which a list of `match` statements serve to declare corresponding routes. For example: 17 | 18 | ```coffeescript 19 | match '', 'home#index' 20 | match 'likes/:id', controller: 'controllers/likes', action: 'show' 21 | ``` 22 | 23 | ```javascript 24 | match('', 'home#index'); 25 | match('likes/:id', {controller: 'controllers/likes', action: 'show'}); 26 | ``` 27 | 28 | Ruby on Rails developers may find `match` intuitively familiar. For more information on its usage, [see below](#match). Internally, route objects representing each entry are created. If a route matches, a `router:match` event is published, passing the route object and a `params` hash which contains name-value pairs for named placeholder parts of the path description (like `id` in the example above), as well as additional GET parameters. 29 | 30 |

Methods

31 | 32 |

createHistory()

33 | Creates the `Backbone.History` instance. 34 | 35 |

startHistory()

36 | Starts `Backbone.History` instance. This method should be called only after all routes have been registered. 37 | 38 |

stopHistory()

39 | Stops the `Backbone.History` instance from observing URL changes. 40 | 41 |

match([pattern], [target], [options={}])

42 | 43 | Connects a path with a controller action. 44 | 45 | * **pattern** (String): A pattern to match against the current path. 46 | * **target** (String): Specifies the controller action which is called if this route matches. Optionally, replaced by an equivalent description through the `options` hash. 47 | * **options** (Object): optional 48 | 49 | The `pattern` argument may contain named placeholders starting with a colon (`:`) followed by an identifier. For example, `'products/:product_id/ratings/:id'` will match the paths 50 | `/products/vacuum-cleaner/ratings/jane-doe` as well as `/products/8426/ratings/72`. The controller action will be passed the parameter hash `{product_id: "vacuum-cleaner", id: "jane-doe"}` or `{product_id: "8426", id: "72"}`, respectively. 51 | 52 | The `target` argument is a string with the controller name and the action name separated by the `#` character. For example, `'likes#show'` denotes the `show` action of the `LikesController`. 53 | 54 | You can also equivalently specify the target via the `action` and `controller` properties of the `options` hash. 55 | 56 | The following properties of the `options` hash are recognized: 57 | 58 | * **params** (Object): Constant parameters that will be added to the params passed to the action and overwrite any values coming from a named placeholder 59 | 60 | ```coffeescript 61 | match 'likes/:id', 'likes#show', params: {foo: 'bar'} 62 | ``` 63 | 64 | ```javascript 65 | match('likes/:id', 'likes#show', {params: {foo: 'bar'}}); 66 | ``` 67 | 68 | In this example, the `LikesController` will receive a `params` hash which has a `foo` property. 69 | 70 | * **constraints** (Object): For each placeholder you would like to put constraints on, pass a regular expression of the same name: 71 | 72 | ```coffeescript 73 | match 'likes/:id', 'likes#show', constraints: {id: /^\d+$/} 74 | ``` 75 | 76 | ```javascript 77 | match('likes/:id', 'likes#show', {constraints: {id: /^\d+$/}}); 78 | ``` 79 | 80 | The `id` regular expression enforces the corresponding part of the path to be numeric. This route will match the path `/likes/5636`, but not `/likes/5636-icecream`. 81 | 82 | For every constraint in the constraints object, there must be a corresponding named placeholder, and it must satisfy the constraint in order for the route to match. 83 | For example, if you have a constraints object with three constraints: x, y, and z, then the route will match if and only if it has named parameters :x, :y, and :z and they all satisfy their respective regex. 84 | 85 | * **name** (String): Named routes can be used when reverse-generating paths using `Chaplin.utils.reverse` helper: 86 | 87 | ```coffeescript 88 | match 'likes/:id', 'likes#show', name: 'like' 89 | Chaplin.utils.reverse 'like', id: 581 # => likes/581 90 | ``` 91 | 92 | ```javascript 93 | match('likes/:id', 'likes#show', {name: 'like'}); 94 | Chaplin.utils.reverse('like', {id: 581}); // => likes/581 95 | ``` 96 | If no name is provided, the entry will automatically be named by the scheme `controller#action`, e.g. `likes#show`. 97 | 98 |

route([path])

99 | 100 | Route a given path manually. Returns a boolean after it has been matched against the registered routes, corresponding to whether or not a match occurred. Updates the URL in the browser. 101 | 102 | * **path** can be an object describing a route by 103 | * **controller**: name of the controller, 104 | * **action**: name of the action, 105 | * **name**: name of a [named route](#match), can replace **controller** and **action**, 106 | * **params**: params hash. 107 | 108 | For routing from other modules, `Chaplin.utils.redirectTo` can be used. All of the following would be valid use cases. 109 | 110 | ```coffeescript 111 | Chaplin.utils.redirectTo 'messages#show', id: 80 112 | Chaplin.utils.redirectTo controller: 'messages', action: 'show', params: {id: 80} 113 | Chaplin.utils.redirectTo url: '/messages/80' 114 | ``` 115 | ```javascript 116 | Chaplin.utils.redirectTo('messages#show', {id: 80}); 117 | Chaplin.utils.redirectTo({controller: 'messages', action: 'show', params: {id: 80}}); 118 | Chaplin.utils.redirectTo({url: '/messages/80'}); 119 | ``` 120 | 121 |

changeURL([url])

122 | 123 | Changes the current URL and adds a history entry without triggering any route actions. 124 | 125 | Handler for the globalized `router:changeURL` request-response handler. 126 | 127 | * **url**: string that is going to be pushed as the page’s URL 128 | 129 |

dispose()

130 | 131 | Stops the Backbone.history instance and removes it from the router object. Also unsubscribes any events attached to the Router. On compliant runtimes, the router object is frozen, see [Object.freeze](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze). 132 | 133 | ## Request-response handlers of `Chaplin.Router` 134 | 135 | `Chaplin.Router` sets these global request-response: 136 | 137 | * `router:route path[, options]` 138 | * `router:reverse name, params[, options], callback` 139 | * `router:changeURL url[, options]` 140 | 141 | ## Usage 142 | `Chaplin.Router` is a dependency of [Chaplin.Application](./chaplin.application.html) which should be extended by your main application class. Within your application class you should initialize the `Router` by calling `initRouter` (passing your routes module as an argument) followed by `start`. 143 | 144 | 145 | ```coffeescript 146 | define [ 147 | 'chaplin', 148 | 'routes' 149 | ], (Chaplin, routes) -> 150 | 'use strict' 151 | 152 | class MyApplication extends Chaplin.Application 153 | title: 'The title for your application' 154 | 155 | initialize: -> 156 | super 157 | @initRouter routes 158 | @start() 159 | ``` 160 | 161 | ```javascript 162 | define([ 163 | 'chaplin', 164 | 'routes' 165 | ], function(Chaplin, routes) { 166 | 'use strict'; 167 | 168 | var MyApplication = Chaplin.Application.extend({ 169 | title: 'The title for your application', 170 | 171 | initialize: function() { 172 | Chaplin.Application.prototype.initialize.apply(this, arguments); 173 | this.initRouter(routes); 174 | this.start(); 175 | } 176 | }); 177 | 178 | return MyApplication; 179 | }); 180 | ``` 181 | -------------------------------------------------------------------------------- /docs/chaplin.support.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.support 4 | module_path: src/chaplin/lib/support.coffee 5 | Chaplin: support 6 | --- 7 | 8 | Provides feature detection that is used internally to determine the code path so that ECMAScript 5 features can be used if possible, without breaking compatibility with non-compliant engines. 9 | 10 |

propertyDescriptors

11 | 12 | Indicates if **[Object.defineProperty](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/defineProperty)** is supported. It’s important to note that while Internet Explorer 8 has an implementation of `Object.defineProperty`, the method can only be used on DOM objects. This implementation takes this fact into account when determining support. 13 | -------------------------------------------------------------------------------- /docs/chaplin.sync_machine.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.SyncMachine 4 | module_path: src/chaplin/lib/sync_machine.coffee 5 | Chaplin: SyncMachine 6 | --- 7 | 8 | The `Chaplin.SyncMachine` is a [finite-state machine](http://en.wikipedia.org/wiki/Finite-state_machine) for synchronization of models/collections. There are three states in which a model or collection can be in; unsynced, syncing, and synced. When a state transition (unsynced, syncing, synced, and syncStateChange) occurs, Backbone events are called on the model or collection. 9 | 10 |

Methods

11 | 12 |

syncState

13 | 14 | Returns the current synchronization state of the machine. 15 | 16 |

isUnsynced

17 | 18 | Returns a boolean to help determine if the model or collection is unsynced. 19 | 20 |

isSynced

21 | 22 | Returns a boolean to help determine if the model or collection is synced. 23 | 24 |

isSyncing

25 | 26 | Returns a boolean to help determine if the model or collection is currently syncing. 27 | 28 |

unsync

29 | 30 | Sets the state machine’s state to `unsynced`, then triggers any events listening for the `unsynced` and `syncStateChange` events. 31 | 32 |

beginSync

33 | 34 | Sets the state machine’s state to `syncing`, then triggers any events listening for the `syncing` and `syncStateChange` events. 35 | 36 |

finishSync

37 | 38 | Sets the state machine’s state to `synced`, then triggers any events listening for the `synced` and `syncStateChange` events. 39 | 40 |

abortSync

41 | 42 | Sets the state machine’s state back to the previous state if the state machine is in the `syncing` state. Then triggers any events listening for the previous state and `syncStateChange` events. 43 | 44 |

unsynced([callback], [context=this])

45 | 46 | `unsynced` is a convenience method which will execute a callback in a specified context whenever the state machine enters into the `unsynced` state. 47 | 48 | * **callback**: a function to be called when the `unsynced` event occurs 49 | * **context**: the context in which the callback should execute in. Defaults to `this`. 50 | 51 |

syncing([callback], [context=this])

52 | 53 | `syncing` is a convenience method which will execute a callback in a specified context whenever the state machine enters into the `syncing` state. 54 | 55 | * **callback**: a function to be called when the `syncing` event occurs 56 | * **context**: the context in which the callback should execute in. Defaults to `this`. 57 | 58 |

synced([callback], [context=this])

59 | 60 | `synced` is a convenience method which will execute a callback in a specified context whenever the state machine enters into the `synced` state. 61 | 62 | * **callback**: a function to be called when the `synced` event occurs 63 | * **context**: the context in which the callback should execute in. Defaults to `this`. 64 | 65 |

syncStateChange([callback], [context=this])

66 | 67 | `syncStateChange` is a convenience method which will execute a callback in a specified context whenever the state machine changes state. 68 | 69 | * **callback**: a function to be called when a state change occurs. 70 | * **context**: the context in which the callback should execute in. Defaults to `this`. 71 | 72 | ## Usage 73 | 74 | The `Chaplin.SyncMachine` is a dependency of `Chaplin.Model` and `Chaplin.Collection` and should be used for complex synchronization of models and collections. As an example, think of making requests to you own REST API or some third party web service. 75 | 76 | ```coffeescript 77 | class Model extends Chaplin.Model 78 | _.extend @prototype, Chaplin.SyncMachine 79 | 80 | initialize: -> 81 | super 82 | @on 'request', @beginSync 83 | @on 'sync', @finishSync 84 | @on 'error', @unsync 85 | 86 | ... 87 | 88 | # Will render view when model data will arrive from server. 89 | model = new Model 90 | view = new Chaplin.View {model} 91 | model.fetch().then view.render 92 | ``` 93 | 94 | ```javascript 95 | var Model = Chaplin.Model.extend({ 96 | initialize: function() { 97 | Chaplin.Model.prototype.initialize.apply(this, arguments); 98 | this.on('request', this.beginSync); 99 | this.on('sync', this.finishSync); 100 | this.on('error', this.unsync); 101 | } 102 | }); 103 | 104 | _.extend(Model.prototype, Chaplin.SyncMachine); 105 | 106 | ... 107 | 108 | // Will render view when model data will arrive from server. 109 | var model = new Model; 110 | var view = new Chaplin.View({model: model}); 111 | model.fetch().then(view.render); 112 | ``` 113 | 114 | You can do the same to `Collection`. 115 | 116 | More complex example involving `Collection`: 117 | 118 | ```coffeescript 119 | define [ 120 | 'chaplin' 121 | 'models/post' # Post model 122 | ], (Chaplin.Collection, Post) -> 123 | 124 | class Posts extends Chaplin.Collection 125 | # Initialize the SyncMachine 126 | _.extend @prototype, Chaplin.SyncMachine 127 | 128 | model: Post 129 | 130 | initialize: -> 131 | super 132 | 133 | # Will be called on every state change 134 | @syncStateChange announce 135 | 136 | @fetch() 137 | 138 | # Custom fetch method which warrents 139 | # the sync machine 140 | fetch: => 141 | 142 | #Set the machine into `syncing` state 143 | @beginSync() 144 | 145 | # Do something interesting like calling 146 | # a 3rd party service 147 | $.get 'http://some-service.com/posts', @processPosts 148 | 149 | processPosts: (response) => 150 | # Exit if for some reason this collection was 151 | # disposed prior to the response 152 | return if @disposed 153 | 154 | # Update the collection 155 | @reset(if response and response.data then response.data else []) 156 | 157 | # Set the machine into `synced` state 158 | @finishSync() 159 | 160 | announce: => 161 | console.debug 'state changed' 162 | ``` 163 | 164 | ```javascript 165 | define([ 166 | 'chaplin', 167 | 'models/post' // Post model 168 | ], function(Chaplin.Collection, Post) { 169 | 170 | var Posts = Chaplin.Collection.extend({ 171 | model: Post, 172 | 173 | initialize: function() { 174 | Chaplin.Collection.prototype.initialize.apply(this, arguments); 175 | 176 | // Initialize the SyncMachine 177 | _.extend(this, Chaplin.SyncMachine); 178 | 179 | // Will be called on every state change 180 | this.syncStateChange(this.announce.bind(this)); 181 | 182 | this.fetch(); 183 | }, 184 | 185 | // Custom fetch method which warrents 186 | // the sync machine 187 | fetch: function() { 188 | // Set the machine into `syncing` state 189 | this.beginSync() 190 | 191 | // Do something interesting like calling 192 | // a 3rd party service 193 | $.get('http://some-service.com/posts', this.processPosts.bind(this)) 194 | }, 195 | 196 | processPosts: function(response) { 197 | // Exit if for some reason this collection was 198 | // disposed prior to the response 199 | if (this.disposed) return; 200 | 201 | // Update the collection 202 | this.reset((response && response.data) ? response.data : []); 203 | 204 | // Set the machine into `synced` state 205 | this.finishSync(); 206 | }, 207 | 208 | announce: function() { 209 | console.debug('state changed'); 210 | } 211 | }); 212 | 213 | return Posts; 214 | ``` 215 | -------------------------------------------------------------------------------- /docs/chaplin.utils.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin.utils 4 | module_path: src/chaplin/lib/utils.coffee 5 | Chaplin: utils 6 | --- 7 | 8 | Chaplin’s utils provide common functions for use throughout the project. 9 | 10 |

redirectTo([...params])

11 | Does a in-app redirect: 12 | 13 | 1. `redirectTo('messages#show', {id: 2})` — to a named route. 14 | 2. `redirectTo({url: 'messages/2'})` — to an URL. 15 | 3. `redirectTo({controller: 'messages', action: 'show', params: {id: 2}})` — etc. 16 | 17 | In the past, `!route:route[byName]` event was used for this purpose. 18 | 19 | To use replaceState and overwrite the current URL in the history you can use the 20 | third options arg that is forwarded to Backbone's `router.navigate`, e.g. 21 | `redirectTo('messages#show', {id: 2}, {replace: true})` 22 | 23 |

reverse(routeName[,...params])

24 | Returns the URL for a named route, appropriately filling in values given as `params`. 25 | 26 | For example, if you have declared the route 27 | 28 | ```coffeescript 29 | match '/users/:login/profile', 'users#show' 30 | ``` 31 | 32 | ```javascript 33 | match('/users/:login/profile', 'users#show'); 34 | ``` 35 | 36 | you can use 37 | 38 | ```coffeescript 39 | Chaplin.utils.reverse 'users#show', login: 'paulmillr' 40 | # or 41 | Chaplin.utils.reverse 'users#show', ['paulmillr'] 42 | ``` 43 | 44 | ```javascript 45 | Chaplin.utils.reverse('users#show', {login: 'paulmillr'}); 46 | // or 47 | Chaplin.utils.reverse('users#show', ['paulmillr']); 48 | ``` 49 | 50 | to yield `'/users/paulmillr/profile'`. 51 | 52 |

beget(parent)

53 | * **returns a new object with `parent` as its prototype** 54 | 55 | A standard Javascript helper function that creates an object which delegates to another object. (see Douglas Crockford's *Javascript: The Good Parts* for more details). Uses [Object.create](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create) when available, and falls back to a polyfill if not present. 56 | 57 |

readonly(object, [*properties])

58 | * **returns true if successful, false if unsupported by the browser’s runtime** 59 | 60 | Makes properties of **object** read-only so they cannot be overwritten. The success of this operation depends on the current environment’s support. 61 | 62 |

getPrototypeChain(object)

63 | * **Object object** 64 | 65 | Gets the whole chain of prototypes for `object`. 66 | 67 |

getAllPropertyVersions(object, property)

68 | * **Object object** 69 | * **String property** 70 | 71 | Get all different value versions for `property` from `object`’s prototype chain. Usage: 72 | 73 | ```coffeescript 74 | class A 75 | prop: 1 76 | class B extends A 77 | prop: 2 78 | 79 | b = new B 80 | getAllPropertyVersions b, 'prop' # => [1, 2] 81 | ``` 82 | 83 | ```javascript 84 | function A() {} 85 | A.prototype.prop = 1; 86 | 87 | function B() {} 88 | B.prototype = Object.create(A); 89 | 90 | var b = new B; 91 | getAllPropertyVersions(b, 'prop'); // => [1, 2] 92 | ``` 93 | 94 |

upcase(str)

95 | * **String `str`** 96 | * **returns upcased version of `str`** 97 | 98 | Ensure the first character of `str` is capitalized 99 | 100 | ```coffeescript 101 | utils.upcase 'larry bird' # 'Larry bird' 102 | utils.upcase 'AIR' # 'AIR' 103 | ``` 104 | 105 | ```javascript 106 | utils.upcase('larry bird'); // 'Larry bird' 107 | utils.upcase('AIR'); // 'AIR' 108 | ``` 109 | 110 |

modifierKeyPressed(event)

111 | * **jQuery normalized event object `event`** 112 | * **returns boolean** 113 | 114 | Looks at an event object `event` to determine if the **shift**, **alt**, **ctrl**, or **meta** keys were pressed. Useful in link click handling (i.e. if you need ctrl-click or shift-click to open the link in a new window). 115 | 116 |

querystring.stringify(object)

117 | * **Object object** 118 | 119 | Returns a query string from a hash. 120 | 121 |

querystring.parse(string)

122 | * **String string** 123 | 124 | Returns a hash with query parameters from a query string. 125 | -------------------------------------------------------------------------------- /docs/disposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Memory Management and Object Disposal 4 | Chaplin: Memory Management and Object Disposal 5 | --- 6 | 7 | A core concern of the Chaplin architecture is proper memory management. While there isn’t a broad discussion about garbage collection in JavaScript applications, it is an important topic. Since Backbone provides little out of the box to help manage memory, Chaplin extends Backbone’s `Model`, `Collection` and `View` classes to implement a powerful disposal process which ensures that each controller, model, collection and view cleans up after itself. 8 | 9 | Event handling creates references between objects. If a view listens for model changes, then that model has a reference to a view method in its internal `_callbacks` list. View methods are often bound to the view instance using `Function.prototype.bind`, `_.bind()`, CoffeeScript’s fat arrow `=>` or alike. When a `change` handler is bound to the view, the view will remain in memory even if it was already detached from the DOM. The garbage collector can’t free its memory because of this reference. 10 | 11 | In Chaplin, before a new controller takes over and the user interface changes, the `dispose` method of the current controller is invoked: 12 | 13 | * The controller calls the `dispose` method on its models/collections and then removes its references to them. 14 | * On disposal, each model clears all of its attributes and disposes all associated views. 15 | * A view’s `dispose` method removes all of its DOM elements, unsubscribes from DOM or model/collection events and calls `dispose` on its subviews. 16 | * Models/collections and views unsubscribe from global publish/subscribe events. 17 | 18 | This disposal process is quite complex and many objects needs a custom `dispose` method. But this is just the least Chaplin can do. 19 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Event Handling 4 | Chaplin: Event Handling 5 | --- 6 | 7 | For models and views, there are several wrapper methods for event handler registration. In contrast to the direct methods, they help prevent memory leakage, because the handlers will be removed correctly once the model or view is disposed. The methods will also be bound to the caller for ease of registration. 8 | 9 | ## Mediator 10 | 11 | Global events use the `mediator` as an event channel. On most objects in chaplin (including models, views, and controllers), there are shortcuts for manipulating global events. These methods are mixed into eventable objects by way of the [EventBroker][]. 12 | 13 | [EventBroker]: chaplin.event_broker.html 14 | 15 | ```coffeescript 16 | @subscribeEvent 'dispatcher:dispatch', @dispatch 17 | @subscribeEvent '!router:route', -> console.log arguments... 18 | ``` 19 | 20 | ```javascript 21 | this.subscribeEvent('dispatcher:dispatch', this.dispatch); 22 | this.subscribeEvent('!router:route', console.log.bind(console)); 23 | ``` 24 | 25 | These are aliased to `Chaplin.mediator.*` with the additional benefit of automatically invoking `Chaplin.mediator.unsubscribe` in the `dispose` method of the eventable and providing some small type checking. 26 | 27 | ## Eventable 28 | 29 | In views, the standard `model.on` way to register a handler for a model event should not be used. Use the memory-saving wrapper `listenTo` instead: 30 | 31 | ```coffeescript 32 | @listenTo @model, 'add', @doSomething 33 | ``` 34 | 35 | ```javascript 36 | this.listenTo(this.model, 'add', this.doSomething); 37 | ``` 38 | 39 | In a model, it’s fine to use `on` directly as long as the handler is a method of the model itself. 40 | 41 | ## User Input 42 | 43 | Most views handle user input by listening to DOM events. Backbone provides the `events` property to register event handlers declaratively. But this does not work nicely when views inherit from each other and a specific view needs to handle additional events. 44 | 45 | Chaplin’s `View` class provides the `delegate` method as a shortcut for `this.$el.on`. It has the same signature as the jQuery 1.7 `on` method. Some examples: 46 | 47 | ```coffeescript 48 | @delegate 'click', '.like-button', @like 49 | @delegate 'click', '.close-button', @skip 50 | ``` 51 | 52 | ```javascript 53 | this.delegate('click', '.like-button', this.like); 54 | this.delegate('click', '.close-button', this.skip); 55 | ``` 56 | 57 | `delegate` registers the handler at the topmost DOM element of the view (`this.el`) and catches events from nested elements using event bubbling. You can specify an optional selector to target nested elements. 58 | 59 | In addition, `delegate` automatically binds the handler to the view object, so `this` points to the view. This means `delegate` creates a wrapper function which acts as the handler. As a consequence, it’s currently impossible to unbind a specific handler. Please use `this.$el.off` directly to unbind all handlers of an event type for a selector: 60 | 61 | ```coffeescript 62 | @$el.off 'click', '.like-button' 63 | @$el.off 'click', '.close' 64 | ``` 65 | 66 | ```javascript 67 | this.$el.off('click', '.like-button'); 68 | this.$el.off('click', '.close'); 69 | ``` 70 | 71 | ## Events catalog 72 | 73 | Events that start with `!` immediately do something. 74 | 75 | * `beforeControllerDispose` — emitted before current controller is disposed. 76 | * `dispatcher:dispatch` — emitted after controller action has been started. 77 | * `adjustTitle` — adjusts window title. 78 | * `router:match` — tries to match URL with routes 79 | 80 | ![Dance](http://s3.amazonaws.com/imgly_production/3362020/original.jpg) 81 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaplinjs/chaplin/0eff0e23ec2a40cb7be4ed82ea7a4f1eb545d769/docs/favicon.ico -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Getting started 4 | Chaplin: Getting started 5 | --- 6 | 7 | ## Download the boilerplate 8 | 9 | The easiest way to start a Chaplin application is to download the boilerplate. It contains a recent build of Chaplin as well as all JavaScript libraries Chaplin depends upon: 10 | 11 | * [Underscore](http://underscorejs.org/) 12 | * [Backbone](http://backbonejs.org/) 13 | * [jQuery](http://jquery.com/) 14 | * [RequireJS](http://requirejs.org/) as AMD module loader 15 | 16 | This is just the standard setup. You may substitute Underscore with [Lodash](http://lodash.com/docs), jQuery with [Zepto](http://zeptojs.com/) and RequireJS with different other AMD module loaders like [Curl](https://github.com/cujojs/curl). 17 | 18 | The boilerplate comes in four flavours: 19 | 20 | * [CoffeeScript code](https://github.com/chaplinjs/chaplin-boilerplate), if you develop your application in CoffeeScript 21 | * [Plain JavaScript code](https://github.com/chaplinjs/chaplin-boilerplate-plain), if you develop your application in normal JavaScript 22 | * [Brunch skeleton](https://github.com/paulmillr/brunch-with-chaplin), if you prefer using [Brunch](http://brunch.io) and synchronous common.js modules. 23 | * [Yo generator](https://github.com/ButuzGOL/generator-chaplinjs), that provides a functional boilerplate Chaplin app out of the box using JavaScript, Stylus and Ejs. You also get access to a number of sub-generators which can be used to easily create individual models, views, collections and so on. 24 | 25 | ## Hello World! 26 | 27 | The boilerplate contains the necessary files which inherit from the core Chaplin class. 28 | 29 | ## Integrating Chaplin into Rails 3 30 | 31 | Use `requirejs-rails` gem. 32 | -------------------------------------------------------------------------------- /docs/handling_async.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Handling Asynchronous Dependencies 4 | Chaplin: Handling Asynchronous Dependencies 5 | --- 6 | 7 | Most processes in a client-side JavaScript application run asynchronously. It’s quite common for an application to communicate with multiple different external APIs. API bridges are established on demand and of course all API calls are asynchronous. Lazy-loading code and content is a key to performance. Therefore, handling asynchronous dependencies is a big challenge for JavaScript web applications. We’re using the following techniques to handle dependencies, from bottom-level to top-level. 8 | 9 | ## Backbone Events 10 | 11 | Model-view-binding, Backbone’s key feature, is still a building block in Chaplin: A view can listen to model changes by subscribing to a `change` event or other custom model events. In addition, collections and collection views can listen for events which occur on their items. This works because model events bubble up to the associated collection. 12 | 13 | ## State Machines for Synchronization: Deferreds and SyncMachine 14 | 15 | Models, collections and third-party scripts typically have a loaded state. But they’re often not ready for use initially because they rely upon asynchronous input such as waiting for data to be fetched from the server or a successful user login. 16 | 17 | For this purpose, [jQuery Deferreds](http://api.jquery.com/category/deferred-object/) (or [standalone-deferreds](https://github.com/Mumakil/Standalone-Deferred) if you’re using Zepto) could be utilized. They allow registering of load handlers using the [done](http://api.jquery.com/deferred.done/) method. The handlers will be called once the Deferred is resolved. 18 | 19 | Deferreds are a versatile pattern which can be used on different levels in an application, but in terms of the amount of states they can handle, they are rather simple because they only have three states (pending, resolved, rejected) and two one-way transitions (resolve, reject). For more complex synchronization tasks, Chaplin offers the `SyncMachine` which is a more complex and long-lived state machine. 20 | 21 | ## Wrapping Methods to Wait for a Deferred 22 | 23 | On moviepilot.com, for example, methods of several Deferreds are called everywhere throughout the application. It wouldn’t be feasible for every caller to check the resolved state and register a callback if necessary. Instead, these methods are wrapped so they can be called safely before the Deferred is resolved. In this case, the calls are automatically saved as `done` callbacks, once the Deferred is resolved, they are passed through immediately. Of course this wrapping is only possible for asynchronous methods which don’t have a return value but expect a callback function. 24 | 25 | The helper method `utils.deferMethods` in [the Facebook example repository](https://github.com/chaplinjs/facebook-example/blob/master/coffee/lib/utils.coffee) wraps methods so calls are postponed until a given Deferred object is resolved. The method is quite flexible and we’re using it in several situations. 26 | 27 | ## Publish/Subscribe 28 | 29 | The publish/subscribe pattern is the most important glue in Chaplin applications because it’s used for most of the cross-module interaction. It’s a powerful pattern to promote loose coupling of application modules. Chaplin’s implementation using `Backbone.Events` is simple but highly beneficial. 30 | -------------------------------------------------------------------------------- /docs/images/chaplin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaplinjs/chaplin/0eff0e23ec2a40cb7be4ed82ea7a4f1eb545d769/docs/images/chaplin.png -------------------------------------------------------------------------------- /docs/images/coffeescript-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaplinjs/chaplin/0eff0e23ec2a40cb7be4ed82ea7a4f1eb545d769/docs/images/coffeescript-inverted.png -------------------------------------------------------------------------------- /docs/images/coffeescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaplinjs/chaplin/0eff0e23ec2a40cb7be4ed82ea7a4f1eb545d769/docs/images/coffeescript.png -------------------------------------------------------------------------------- /docs/images/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chaplinjs/chaplin/0eff0e23ec2a40cb7be4ed82ea7a4f1eb545d769/docs/images/javascript.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Chaplin Documentation 4 | Chaplin: Documentation 5 | --- 6 | 7 | Welcome to the documentation of the Chaplin framework. Chaplin is an architecture for complex single-page web applications. To learn more about Chaplin go to [chaplinjs.org](http://chaplinjs.org) or dive right into the docs! 8 | 9 | Chaplin empowers you to **quickly** develop **scalable** **single-page** web applications; allowing you to focus on designing and developing the underlying functionality in your web application. 10 | 11 | ## Architecture 12 | Chaplin is an architecture for JavaScript web applications based on the [Backbone.js](http://backbonejs.org) library. The code is originally derived from [moviepilot.com](http://moviepilot.com), a large single-page application. 13 | 14 | While Backbone is an easy starting point, it provides only basic, low-level patterns. Backbone provides little structure above simple routing, individual models, views and their binding. Chaplin addresses these limitations by providing a light-weight but flexible structure which leverages well-proven design patterns and best practises. 15 | 16 | ## Framework 17 | 18 | [![](http://chaplinjs.org/images/chaplin-lifecycle.png)](http://chaplinjs.org/images/chaplin-lifecycle.png) 19 | 20 | ### [Application](./chaplin.application.html) 21 | The bootstrapper of the application; an extension point for key parts of the architecture. 22 | 23 | ### [Router](./chaplin.router.html) 24 | Facilitates mapping URLs to controller actions based on a user-defined configuration file. It is responsible for observing and acting upon URL changes. It does no direct action apart from notifying the dispatcher of such a change however. 25 | 26 | #### Routes 27 | By convention, routes should be declared in a separate module (typically `routes.coffee`). For example: 28 | 29 | ```coffeescript 30 | match 'likes/:id', 'likes#show' 31 | ``` 32 | 33 | ```javascript 34 | match('likes/:id', 'likes#show'); 35 | ``` 36 | 37 | This works much like the [Ruby on Rails counterpart][]. If a route matches, a `router:match` event is published passing a `params` hash which contains pattern matches (like `id` in the example above) and additional GET parameters parsed from the query string. This hands control over to the **Dispatcher**. 38 | 39 | [Ruby on Rails counterpart]: http://guides.rubyonrails.org/routing.html 40 | [Router]: ./chaplin.router.html 41 | 42 | ### [Dispatcher](./chaplin.dispatcher.html) 43 | Between the router and the controllers, there is the **Dispatcher** listening for routing events. On such events, it loads the target controller, creates an instance of it and calls the target action. The action is actually a method of the controller. The previously active controller is automatically disposed. 44 | 45 | ### [Layout](./chaplin.layout.html) 46 | The `Layout` is the top-level application view. When a new controller is activated, the `Layout` is responsible for changing the main view to the view of the new controller. 47 | 48 | In addition, the `Layout` handles the activation of internal links. That is, you can use a normal `` element to link to another controller module. 49 | 50 | Furthermore, top-level DOM events on `document` or `body`, should be registered here. 51 | 52 | ### [mediator](./chaplin.mediator.html) 53 | The mediator is an event broker that implements the [Publish/Subscribe](http://en.wikipedia.org/wiki/Publish/subscribe) design pattern. It should be used for most of the inter-module communication in Chaplin applications. Modules can emit events using `this.publishEvent` in order to notify other modules, and listen for such events using `this.subscribeEvent`. The mediator can also be used to easily share data between several modules, like a user model or other persistent and globally accessible data. 54 | 55 | ### [Controller](./chaplin.controller.html) 56 | A controller is the place where a model and associated views are instantiated. Typically, a controller represents one screen of the application. There can be one current controller which provides the main view and represents the current URL. 57 | 58 | By convention, there is a controller for each application module. A controller may provide several action methods like `index`, `show`, `edit` and so on. These actions are called by the `Dispatcher` when a route matches. 59 | 60 | ### [Model](./chaplin.model.html) 61 | Holds reference to the data and contains any logic necessary to retrieve the data from its source and optionally send it back. 62 | 63 | ### [Collection](./chaplin.collection.html) 64 | A collection of models. Contains logic to provide client-side filtering and sorting of them. 65 | 66 | ### [View](./chaplin.view.html) 67 | Provides the logic that drives the user interface such as responding to DOM events and mapping data from the model to a template. 68 | 69 | ### [Collection View](./chaplin.collection_view.html) 70 | Maps to a collection to generate a list of item views that are bound to the models in the collection. 71 | 72 | ## Flow 73 | Every Chaplin application starts with a class that inherits from `Application`. This is merely a bootstrapper which instantiates and configures the four core modules: **Dispatcher**, **Layout**, **mediator**, and **Router** (in that order). 74 | 75 | After creating the **Router**, the routes are registered. Usually they are read from a configuration file called `routes.{coffee,js}`. A route maps a URL pattern to a controller action. For example, the path `/` can be mapped to the `index` action of the `HomeController`. 76 | 77 | After the **Application** invokes `startRouting`; the **Router** starts to observe the current URL. If a route matches, it notifies the other modules. 78 | 79 | This is where the **Dispatcher** takes over. It loads the target controller and its dependencies (e.g. `HomeController`). Then, the controller is instantiated and the controller action is called (e.g. `index`). An *action* is a method of the controller. The **Dispatcher** also keeps track of the currently active controller, and disposes the previously active controller. 80 | 81 | Typically, a controller creates a **Model** or **Collection** and a corresponding **View**. The model or collection may fetch some data from the server which is then rendered by the view. By convention, the models, collection and views are saved as properties on the controller instance. 82 | 83 | ## [Memory Management](./disposal.html) 84 | A core concern of the Chaplin architecture is proper memory management. While there isn’t a broad discussion about garbage collection in JavaScript applications, it is an important topic, especially in single-page applications, where the lifetime and multitude of objects increases compared to earlier architectures. In event-driven systems, registering events creates references between objects. If these references aren’t removed when a module is no longer in use, the garbage collector can’t free the memory. 85 | 86 | Since Backbone provides little out of the box to manage memory, Chaplin extends Backbone’s `Model`, `Collection` and `View` classes to implement a powerful disposal process which ensures that each controller, model, collection and view cleans up after itself. 87 | 88 | ![Ending](http://s3.amazonaws.com/imgly_production/3362023/original.jpg) 89 | -------------------------------------------------------------------------------- /docs/javascripts/main.js: -------------------------------------------------------------------------------- 1 | // Polyfill localStorage 2 | // https://developer.mozilla.org/nl/docs/DOM/Storage 3 | if (!window.localStorage) { window.localStorage = { getItem: function (sKey) { if (!sKey || !this.hasOwnProperty(sKey)) { return null; } return unescape(document.cookie.replace(new RegExp("(?:^|.*;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*"), "$1")); }, key: function (nKeyId) { return unescape(document.cookie.replace(/\s*\=(?:.(?!;))*$/, "").split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]); }, setItem: function (sKey, sValue) { if(!sKey) { return; } document.cookie = escape(sKey) + "=" + escape(sValue) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/"; this.length = document.cookie.match(/\=/g).length; }, length: 0, removeItem: function (sKey) { if (!sKey || !this.hasOwnProperty(sKey)) { return; } document.cookie = escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; this.length--; }, hasOwnProperty: function (sKey) { return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); } }; window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length; } 4 | 5 | // Encapsulates handling of preferred language (CoffeeScript or 6 | // JavaScript) 7 | CD = (function () { 8 | var language = localStorage.getItem('language') || 'coffeescript'; 9 | return { 10 | // Get/set preferred language 11 | language: function (value) { 12 | if (value) { 13 | language = value; 14 | localStorage.setItem('language', language); 15 | } 16 | return language; 17 | }, 18 | // Show code examples for given language 19 | show: function (language) { 20 | var language = language.toLowerCase(); 21 | var other = language === "coffeescript" ? "javascript" : "coffeescript"; 22 | // Update UI 23 | $('form.language li').removeClass('active').find(':radio[value=' + language + ']') 24 | .prop('checked', true) 25 | .closest('li').addClass('active'); 26 | // Add class for inline code 27 | $('body').removeClass('show-' + other).addClass('show-' + language); 28 | // Toggle code blocks 29 | $('.highlight:has(.coffeescript), .highlight:has(.javascript)') 30 | .hide() 31 | .filter(':has(.' + language + ')').show(); 32 | } 33 | }; 34 | })(); 35 | 36 | $(document).ready(function () { 37 | // Set up handling of language toggling 38 | $(':radio[name=language]') 39 | .click(function () { 40 | var language = $('form.language input:checked').val(); 41 | CD.show(CD.language(language)); 42 | }); 43 | // Show code examples according to user prefs 44 | CD.show(CD.language()); 45 | 46 | $('.sidebar .toggle-navigation').click(function(event) { 47 | $('.sidebar .navigation').slideToggle(); 48 | }); 49 | 50 | var queryAll = function(selector) { 51 | return [].slice.call(document.querySelectorAll(selector)); 52 | }; 53 | 54 | document.querySelector('.page-nav .toggle-navigation').addEventListener('click', function() { 55 | queryAll('.page-nav .nav-toggle-content').forEach(function(item) { 56 | var existing = window.getComputedStyle(item).display; 57 | var newStyle = (existing === 'none') ? 'list-item' : 'none'; 58 | console.log(item, newStyle) 59 | item.style.display = newStyle; 60 | }); 61 | }, true); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/no_deps.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Using without jQuery and Underscore 4 | Chaplin: Using without jQuery and Underscore 5 | --- 6 | 7 | Thanks to [Exoskeleton](http://exosjs.com), Chaplin can be used without any dependencies other than Exoskeleton (Backbone) itself. 8 | 9 | Exoskeleton is a faster and leaner Backbone for your HTML5 apps. It targets newer (IE9+) browsers, incorporates great new features and speed updates and plays great with Chaplin. 10 | 11 | Instead of including 40K of gzipped javascript before Chaplin, you just need to include 8K — that's almost five times less! 12 | 13 | To use Chaplin with Exoskeleton without dependencies on Underscore and jQuery: 14 | 15 | * If you are using **AMD** (not Brunch): 16 | Define dummy underscore and jQuery modules before application start: 17 | 18 | ```javascript 19 | define('jquery', function(){}); 20 | define('underscore', ['backbone'], function(Backbone){ 21 | return Backbone.utils; 22 | }); 23 | ``` 24 | * If you are using **Brunch**: 25 | 1. Install exoskeleton: `bower install -s exoskeleton` 26 | 2. Add override of chaplin dependencies to `bower.json`: 27 | 28 | ``` 29 | "overrides": { 30 | "chaplin": {"dependencies": {"exoskeleton": "*"}} 31 | } 32 | ``` 33 | 34 | Example commit of switching Backbone app (Brunch) to Exoskeleton shown here: [paulmillr/ostio@514ba8](https://github.com/paulmillr/ostio/commit/514ba86d32ae174d144871c25f58825ea093de33) 35 | -------------------------------------------------------------------------------- /docs/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | /* LAYOUT */ 2 | body { 3 | text-align: left; 4 | } 5 | .site-header { 6 | padding: 1em; 7 | text-align: center; 8 | } 9 | .site-header h1 a { 10 | display: block; 11 | text-indent: -10000px; 12 | overflow: hidden; 13 | width: 100%; 14 | height: 120px; 15 | background-image: url('../images/chaplin.png'); 16 | background-repeat: no-repeat; 17 | background-size: contain; 18 | background-position: center center; 19 | } 20 | 21 | .sidebar ul { 22 | list-style-image: url("../images/bullet.png"); 23 | font-size: 14px; 24 | line-height: 24px; 25 | } 26 | 27 | .sidebar ul li { 28 | padding: 5px 0px; 29 | margin-left: 1em; 30 | line-height: 16px; 31 | } 32 | 33 | .sidebar ul li.tag-h1 { 34 | font-size: 1.2em; 35 | list-style: none; 36 | margin-left: 0; 37 | } 38 | 39 | .sidebar ul li.tag-h1 a { 40 | font-weight: bold; 41 | color: #333; 42 | } 43 | 44 | .sidebar ul li.tag-h2 + .tag-h1 { 45 | margin-top: 10px; 46 | } 47 | 48 | .sidebar ul a { 49 | color: #666; 50 | } 51 | 52 | .sidebar ul a:hover { 53 | color: #666; 54 | text-decoration: underline; 55 | } 56 | 57 | .sidebar nav { 58 | padding: 0 1em; 59 | } 60 | .sidebar ul { 61 | line-height: 1.25; 62 | } 63 | 64 | .wrapper { 65 | width: 753px; 66 | margin: 0 auto; 67 | position: relative; 68 | height: 100%; 69 | } 70 | .actual-content { 71 | padding: 56px 0 0; 72 | } 73 | .module-info a { 74 | font-size: 14px; 75 | vertical-align: middle; 76 | } 77 | 78 | .toggle-navigation { 79 | margin: 0; 80 | text-transform: none; 81 | top: 9px; 82 | right: 10px; 83 | width: 48px; 84 | height: 32px; 85 | padding: 8px 12px; 86 | background-color: rgba(188, 122, 152, 0.95);; 87 | } 88 | 89 | .toggle-navigation > .icon-bar { 90 | display: block; 91 | width: 22px; 92 | height: 2px; 93 | background-color: #fff; 94 | border-radius: 1px; 95 | } 96 | 97 | .toggle-navigation > .icon-bar + .icon-bar { 98 | margin-top: 4px; 99 | } 100 | 101 | /* LANGUAGE SWITCHER */ 102 | form.language { 103 | text-align: left; 104 | } 105 | form.language legend { 106 | margin: 0; 107 | line-height: 160%; 108 | border: none; 109 | font-size: 100%; 110 | } 111 | form.language ul { 112 | list-style-type: none; 113 | list-style-image: none; 114 | padding: 0; 115 | } 116 | form.language ul:after { 117 | content: "."; 118 | display: block; 119 | width: 0; 120 | height: 0; 121 | overflow: hidden; 122 | clear: both; 123 | } 124 | form.language li { 125 | display: block; 126 | position: relative; 127 | width: 100%; 128 | height: 32px; 129 | padding: 0; 130 | margin: 0 0 2px; 131 | background: white; 132 | background-color: rgba(255,255,255,0.75); 133 | background-repeat: no-repeat; 134 | background-position: top right; 135 | cursor: pointer; 136 | color: #BBB; 137 | } 138 | form.language li:active { 139 | top: 1px; 140 | left: 1px; 141 | } 142 | form.language .active { 143 | background-color: #666; 144 | color: white; 145 | } 146 | form.language .active:before { 147 | content: ""; 148 | display: block; 149 | width: 0; 150 | height: 0; 151 | position: absolute; 152 | top: 50%; 153 | left: 8px; 154 | margin-top: -5px; 155 | border: 5px solid transparent; 156 | border-left-color: white; 157 | } 158 | form.language input { 159 | position: relative; 160 | left: -10000px; 161 | } 162 | form.language label { 163 | overflow: hidden; 164 | line-height: 32px; 165 | text-indent: 4px; 166 | cursor: pointer; 167 | } 168 | form.language .coffeescript { 169 | background-image: url('../images/coffeescript.png'); 170 | } 171 | form.language .coffeescript.active { 172 | background-image: url('../images/coffeescript-inverted.png'); 173 | } 174 | form.language .javascript { 175 | background-image: url('../images/javascript.png'); 176 | } 177 | form.language > fieldset { 178 | border: 0; 179 | } 180 | /* INLINE CODE */ 181 | .show-coffeescript span.coffeescript { 182 | display: inline; 183 | } 184 | .show-coffeescript span.javascript { 185 | display: none; 186 | } 187 | .show-javascript span.coffeescript { 188 | display: none; 189 | } 190 | .show-javascript span.javascript { 191 | display: inline; 192 | } 193 | 194 | img[src$="chaplin-lifecycle.png"] { 195 | width: 100%; 196 | } 197 | 198 | #content { 199 | width: 750px; 200 | padding: 0; 201 | margin-bottom: 60px; 202 | margin-top: 0; 203 | /* margin: 40px 0 20px 0; */ 204 | position: relative; 205 | background: #fbfbfb; 206 | border-radius: 3px; 207 | border: 1px solid #cbcbcb; 208 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); 209 | height: auto; 210 | } 211 | 212 | .actual-content { 213 | padding: 30px; 214 | padding-top: 45px; 215 | top: 60px; 216 | } 217 | 218 | .module-info { 219 | margin-left: -40px; 220 | padding-top: 7px; 221 | padding-left: 40px; 222 | } 223 | .module-info > h1 { 224 | /*background-color: #fff;*/ 225 | /*max-width: 500px;*/ 226 | } 227 | 228 | /* Device Dependent */ 229 | @media (min-width: 750px) { 230 | #content { 231 | /*margin-left: 25.64%;*/ 232 | position: absolute; 233 | /*right: 103px;*/ 234 | } 235 | } 236 | @media (min-width: 1150px) { /* Desktop */ 237 | .module-info { 238 | /*position: fixed;*/ 239 | top: 0; 240 | /*padding: 7px 0;*/ 241 | width: 100%; 242 | /*background-color: #F5F1E8;*/ 243 | } 244 | 245 | .sidebar { 246 | position: fixed; 247 | width: 185px; 248 | top: 10px; 249 | left: 50%; 250 | margin-left: -570px; 251 | height: 100%; 252 | overflow: scroll; 253 | } 254 | } 255 | 256 | @media (min-width: 750px) and (max-width: 1150px) { 257 | .sidebar { 258 | margin: 45px auto 0 auto; 259 | text-align: center; 260 | } 261 | } 262 | 263 | @media (max-width: 1150px) { 264 | .sidebar { 265 | display: block; 266 | text-align: center; 267 | } 268 | .sidebar ul { 269 | list-style: none; 270 | padding-left: 0; 271 | } 272 | 273 | .sidebar ul li { 274 | margin: 0; 275 | } 276 | 277 | .site-header h1 a { 278 | height: 60px; 279 | } 280 | 281 | .navigation { 282 | display: none; 283 | } 284 | .sidebar .toggle-navigation { 285 | display: inline-block; 286 | } 287 | 288 | form.language > fieldset { 289 | margin: 0; 290 | padding: 10px 0 0 0; 291 | } 292 | } 293 | 294 | @media (max-width: 750px) { 295 | .wrapper { 296 | width: auto; 297 | } 298 | #content { 299 | width: auto; 300 | margin-top: 0; 301 | margin-bottom: 0; 302 | top: 0; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /docs/stylesheets/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #408080; font-style: italic } /* Comment */ 3 | .err { border: 1px solid #FF0000 } /* Error */ 4 | .k { color: #008000; font-weight: bold } /* Keyword */ 5 | .o { color: #666666 } /* Operator */ 6 | .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #BC7A00 } /* Comment.Preproc */ 8 | .c1 { color: #408080; font-style: italic } /* Comment.Single */ 9 | .cs { color: #408080; font-style: italic } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #888888 } /* Generic.Output */ 16 | .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0044DD } /* Generic.Traceback */ 20 | .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #008000 } /* Keyword.Pseudo */ 24 | .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #B00040 } /* Keyword.Type */ 26 | .m { color: #666666 } /* Literal.Number */ 27 | .s { color: #BA2121 } /* Literal.String */ 28 | .na { color: #7D9029 } /* Name.Attribute */ 29 | .nb { color: #008000 } /* Name.Builtin */ 30 | .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 31 | .no { color: #880000 } /* Name.Constant */ 32 | .nd { color: #AA22FF } /* Name.Decorator */ 33 | .ni { color: #999999; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 35 | .nf { color: #0000FF } /* Name.Function */ 36 | .nl { color: #A0A000 } /* Name.Label */ 37 | .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #008000; font-weight: bold } /* Name.Tag */ 39 | .nv { color: #19177C } /* Name.Variable */ 40 | .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #666666 } /* Literal.Number.Float */ 43 | .mh { color: #666666 } /* Literal.Number.Hex */ 44 | .mi { color: #666666 } /* Literal.Number.Integer */ 45 | .mo { color: #666666 } /* Literal.Number.Oct */ 46 | .sb { color: #BA2121 } /* Literal.String.Backtick */ 47 | .sc { color: #BA2121 } /* Literal.String.Char */ 48 | .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #BA2121 } /* Literal.String.Double */ 50 | .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 51 | .sh { color: #BA2121 } /* Literal.String.Heredoc */ 52 | .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 53 | .sx { color: #008000 } /* Literal.String.Other */ 54 | .sr { color: #BB6688 } /* Literal.String.Regex */ 55 | .s1 { color: #BA2121 } /* Literal.String.Single */ 56 | .ss { color: #19177C } /* Literal.String.Symbol */ 57 | .bp { color: #008000 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #19177C } /* Name.Variable.Class */ 59 | .vg { color: #19177C } /* Name.Variable.Global */ 60 | .vi { color: #19177C } /* Name.Variable.Instance */ 61 | .il { color: #666666 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Upgrading guide 4 | Chaplin: Upgrading guide 5 | --- 6 | 7 | ### Upgrading to 1.0 (0.13) 8 | * Replace `this.compose` in Controllers with `this.reuse`. 9 | 10 | ### Upgrading to 0.12 11 | * Replace all references to `Chaplin.helpers` with `Chaplin.utils`. 12 | * If you are using Delayer, make sure to include [separate Delayer package](https://github.com/chaplinjs/delayer) — it has been removed from Chaplin core. 13 | * If you are using Exoskeleton, make sure to upgrade to v0.6. 14 | 15 | ### Upgrading to 0.11 16 | * Change controller action param order: from (`params, route, options`), query string URL will no longer reside in `params`. 17 | `/users/paulmillr?popup=1` (assuming `/users/:name` route) will pass: 18 | * Before: `params={name: 'paulmillr', popup: '1'}` 19 | * Now: `params={name: 'paulmillr'}, options={query=popup: '1'}` 20 | * Rename `Application#startRouting` to `Application#start` 21 | (the following method also does freezing of app object). 22 | * Since `!event` pattern was replaced with *Request / Response*, 23 | stop publishing various global events: 24 | * Instead of `!router:route` and `!router:routeByName`, 25 | use `Chaplin.helpers.redirectTo`. 26 | * Instead of `!router:changeURL` event, 27 | execute mediator handler: `mediator.execute('router:changeURL', args...)` 28 | * Instead of `!adjustTitle` event, 29 | execute mediator handler: `mediator.execute('adjustTitle', name)` 30 | * Instead of `!composer:compose` / `!composer:retrieve` events, 31 | use `mediator.execute('composer:compose')` / `mediator.execute('composer:retrieve')` 32 | * Instead of `!region:register` / `!region:unregister` events, 33 | use `mediator.execute('region:register')` / `mediator.execute('region:unregister')` 34 | * Replace `Controller#redirectToRoute(name)` with 35 | `Controller#redirectTo(name)` 36 | * Replace `Controller#redirectTo(url)` with 37 | `Controller#redirectTo({url: url})`. 38 | * Keep in mind that `Controller#compose` now: 39 | * by default, returns the composition itself 40 | * if composition body returned a promise, it returns a promise too 41 | 42 | ### Upgrading to 0.10 43 | * Replace `application = new Chaplin.Application(); application.initialize()` with `new Chaplin.Application`: `initialize` is now called by default. 44 | * `Application#initialize` now has default functionality, 45 | make sure to adjust `super` calls. 46 | * Swap view regions syntax to more logical: 47 | * Before: `regions: {'.selector': 'region'}`. 48 | * Now: `regions: {'region': '.selector'}`. 49 | * Make sure to remove `callback` argument from `!router:route` and 50 | `!router:routeByName` event calls — it is synchronous now. 51 | 52 | ### Upgrading to 0.9 53 | * `Controller#beforeAction` must now be a function instead of 54 | an object. 55 | * Remove `initDeferred` method calls from Models and Collections 56 | (or provide your own). 57 | * Make sure to adjust your routes: `deleted_users#show` won’t longer be rewritten to `deletedUsers#show` 58 | * Rename methods in your `Layout` subclass (if you're subclassing it): 59 | * `_registeredRegions` to `globalRegions` 60 | * `registerRegion` to `registerGlobalRegion` 61 | * `registerRegion` to `registerGlobalRegions` 62 | * `unregisterRegion` to `unregisterGlobalRegion` 63 | * `unregisterRegions` to `unregisterGlobalRegions` 64 | * Provide your own `utils.underscorize`. 65 | 66 | ### Upgrading to 0.8 67 | *`Application#initRouter`. 68 | `Application#startRouting` 69 | * Adjust your controller actions params to 70 | `params, route, options` instead of `params, options`. 71 | * Remove RegExp routes. Use `constraints` route param and strings instead. 72 | * Rename `matchRoute` global event to `router:match` 73 | * Rename `startupController` global event to `dispatcher:dispatch` 74 | * If you are subclassing `Dispatcher`, many methods now 75 | receive `route` too. 76 | 77 | ### Upgrading to 0.7 78 | * Change your controller action params: instea dof 79 | `params, previousControllerName`, use 80 | `params, options={previousControllerName, path...}` 81 | * Change `View`: 82 | * Rename `View#afterRender` to `View#attach`. 83 | * Remove `View#afterInitialize`. 84 | * Remove `View#pass`. 85 | * Change `CollectionView`: 86 | * Rename `CollectionView#itemsResetted` to `CollectionView#itemsReset`. 87 | * Rename `CollectionView#getView` to `CollectionView#initItemView`. 88 | * Rename `CollectionView#showHideFallback` to `CollectionView#toggleFallback`. 89 | * Rename `CollectionView#showHideLoadingIndicator` to `CollectionView#toggleLoadingIndicator`. 90 | * Remove `CollectionView#renderAndInsertItem`. 91 | * Item views will now emit `addedToParent` event instead of `addedToDOM` 92 | when they are appended to collection view. 93 | * Don't use `utils.wrapMethod` (or provide your own). 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chaplin", 3 | "version": "1.2.0", 4 | "description": "An Application Architecture Using Backbone.js", 5 | "repository": "git://github.com/chaplinjs/chaplin.git", 6 | "dependencies": { 7 | "backbone": "~1.3.3", 8 | "underscore": "~1.8.3" 9 | }, 10 | "devDependencies": { 11 | "backbone.nativeview": "~0.3.4", 12 | "chai": "~4.2.0", 13 | "coffee-coverage": "~2.0.0", 14 | "coffeescript": "~1.12.7", 15 | "coffeeify": "~3.0.1", 16 | "grunt": "~1.0.4", 17 | "grunt-browserify": "~5.3.0", 18 | "grunt-cli": "~1.3.2", 19 | "grunt-coffeelint": "~0.0.16", 20 | "grunt-contrib-compress": "1.4.3", 21 | "grunt-contrib-uglify": "~4.0.1", 22 | "grunt-contrib-watch": "~1.1.0", 23 | "grunt-istanbul": "0.8.0", 24 | "grunt-mocha-test": "~0.13.3", 25 | "grunt-transbrute": "1.0.1", 26 | "jquery": "~3.3.1", 27 | "jsdom": "~14.0.0", 28 | "jsdom-global": "~3.0.2", 29 | "mocha": "~6.0.2", 30 | "prompt": "~1.0.0", 31 | "sinon": "~7.3.1", 32 | "sinon-chai": "~3.3.0" 33 | }, 34 | "main": "build/chaplin.js", 35 | "scripts": { 36 | "test": "grunt test && grunt test:jquery", 37 | "coverage": "grunt coverage", 38 | "build": "grunt build" 39 | }, 40 | "license": "SEE LICENSE IN MIT-LICENSE.txt" 41 | } 42 | -------------------------------------------------------------------------------- /src/chaplin.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Main entry point into Chaplin module. 4 | # Load all components and expose them. 5 | module.exports = 6 | Application: require './chaplin/application' 7 | Composer: require './chaplin/composer' 8 | Controller: require './chaplin/controllers/controller' 9 | Dispatcher: require './chaplin/dispatcher' 10 | Composition: require './chaplin/lib/composition' 11 | EventBroker: require './chaplin/lib/event_broker' 12 | History: require './chaplin/lib/history' 13 | Route: require './chaplin/lib/route' 14 | Router: require './chaplin/lib/router' 15 | support: require './chaplin/lib/support' 16 | SyncMachine: require './chaplin/lib/sync_machine' 17 | utils: require './chaplin/lib/utils' 18 | mediator: require './chaplin/mediator' 19 | Collection: require './chaplin/models/collection' 20 | Model: require './chaplin/models/model' 21 | CollectionView: require './chaplin/views/collection_view' 22 | Layout: require './chaplin/views/layout' 23 | View: require './chaplin/views/view' 24 | -------------------------------------------------------------------------------- /src/chaplin/application.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Third-party libraries. 4 | _ = require 'underscore' 5 | Backbone = require 'backbone' 6 | 7 | # CoffeeScript classes which are instantiated with `new` 8 | Composer = require './composer' 9 | Dispatcher = require './dispatcher' 10 | Router = require './lib/router' 11 | Layout = require './views/layout' 12 | 13 | # A mix-in that should be mixed to class. 14 | EventBroker = require './lib/event_broker' 15 | 16 | # Independent global event bus that is used by itself, so lowercased. 17 | mediator = require './mediator' 18 | 19 | # The bootstrapper is the entry point for Chaplin apps. 20 | module.exports = class Application 21 | # Borrow the `extend` method from a dear friend. 22 | @extend = Backbone.Model.extend 23 | 24 | # Mixin an `EventBroker` for **publish/subscribe** functionality. 25 | _.extend @prototype, EventBroker 26 | 27 | # Site-wide title that is mapped to HTML `title` tag. 28 | title: '' 29 | 30 | # Core Object Instantiation 31 | # ------------------------- 32 | 33 | # The application instantiates three **core modules**: 34 | dispatcher: null 35 | layout: null 36 | router: null 37 | composer: null 38 | started: false 39 | 40 | constructor: (options = {}) -> 41 | @initialize options 42 | 43 | initialize: (options = {}) -> 44 | # Check if app is already started. 45 | if @started 46 | throw new Error 'Application#initialize: App was already started' 47 | 48 | # Initialize core components. 49 | # --------------------------- 50 | 51 | # Register all routes. 52 | # You might pass Router/History options as the second parameter. 53 | # Chaplin enables pushState per default and Backbone uses / as 54 | # the root per default. You might change that in the options 55 | # if necessary: 56 | # @initRouter routes, pushState: false, root: '/subdir/' 57 | @initRouter options.routes, options 58 | 59 | # Dispatcher listens for routing events and initialises controllers. 60 | @initDispatcher options 61 | 62 | # Layout listens for click events & delegates internal links to router. 63 | @initLayout options 64 | 65 | # Composer grants the ability for views and stuff to be persisted. 66 | @initComposer options 67 | 68 | # Mediator is a global message broker which implements pub / sub pattern. 69 | @initMediator() 70 | 71 | # Start the application. 72 | @start() 73 | 74 | # **Chaplin.Dispatcher** sits between the router and controllers to listen 75 | # for routing events. When they occur, Chaplin.Dispatcher loads the target 76 | # controller module and instantiates it before invoking the target action. 77 | # Any previously active controller is automatically disposed. 78 | 79 | initDispatcher: (options) -> 80 | @dispatcher = new Dispatcher options 81 | 82 | # **Chaplin.Layout** is the top-level application view. It *does not 83 | # inherit* from Chaplin.View but borrows some of its functionalities. It 84 | # is tied to the document dom element and registers application-wide 85 | # events, such as internal links. And mainly, when a new controller is 86 | # activated, Chaplin.Layout is responsible for changing the main view to 87 | # the view of the new controller. 88 | 89 | initLayout: (options = {}) -> 90 | options.title ?= @title 91 | @layout = new Layout options 92 | 93 | initComposer: (options = {}) -> 94 | @composer = new Composer options 95 | 96 | # **Chaplin.mediator** is a singleton that serves as the sole communication 97 | # channel for all parts of the application. It should be sealed so that its 98 | # misuse as a kitchen sink is prohibited. If you do want to give modules 99 | # access to some shared resource, however, add it here before sealing the 100 | # mediator. 101 | 102 | initMediator: -> 103 | Object.seal mediator 104 | 105 | # **Chaplin.Router** is responsible for observing URL changes. The router 106 | # is a replacement for Backbone.Router and *does not inherit from it* 107 | # directly. It's a different implementation with several advantages over 108 | # the standard router provided by Backbone. The router is typically 109 | # initialized by passing the function returned by **routes.coffee**. 110 | 111 | initRouter: (routes, options) -> 112 | # Save the reference for testing introspection only. 113 | # Modules should communicate with each other via **publish/subscribe**. 114 | @router = new Router options 115 | 116 | # Register any provided routes. 117 | routes? @router.match 118 | 119 | # Can be customized when overridden. 120 | start: -> 121 | # After registering the routes, start **Backbone.history**. 122 | @router.startHistory() 123 | 124 | # Mark app as initialized. 125 | @started = true 126 | 127 | # Disposal should be own property because of `Object.seal` 128 | @disposed = false 129 | 130 | # Seal the application instance to prevent further changes. 131 | Object.seal this 132 | 133 | dispose: -> 134 | # Am I already disposed? 135 | return if @disposed 136 | 137 | properties = ['dispatcher', 'layout', 'router', 'composer'] 138 | for prop in properties when this[prop]? 139 | this[prop].dispose() 140 | 141 | @disposed = true 142 | 143 | # You're frozen when your heart's not open. 144 | Object.freeze this 145 | -------------------------------------------------------------------------------- /src/chaplin/composer.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | Composition = require './lib/composition' 7 | EventBroker = require './lib/event_broker' 8 | mediator = require './mediator' 9 | 10 | # Composer 11 | # -------- 12 | 13 | # The sole job of the composer is to allow views to be 'composed'. 14 | # 15 | # If the view has already been composed by a previous action then nothing 16 | # apart from registering the view as in use happens. Else, the view 17 | # is instantiated and passed the options that were passed in. If an action 18 | # is routed to where a view that was composed is not re-composed, the 19 | # composed view is disposed. 20 | 21 | module.exports = class Composer 22 | # Borrow the static extend method from Backbone 23 | @extend = Backbone.Model.extend 24 | 25 | # Mixin an EventBroker 26 | _.extend @prototype, EventBroker 27 | 28 | # The collection of composed compositions 29 | compositions: null 30 | 31 | constructor: -> 32 | @initialize arguments... 33 | 34 | initialize: (options = {}) -> 35 | # Initialize collections. 36 | @compositions = {} 37 | 38 | # Subscribe to events. 39 | mediator.setHandler 'composer:compose', @compose, this 40 | mediator.setHandler 'composer:retrieve', @retrieve, this 41 | @subscribeEvent 'dispatcher:dispatch', @cleanup 42 | 43 | # Constructs a composition and composes into the active compositions. 44 | # This function has several forms as described below: 45 | # 46 | # 1. compose('name', Class[, options]) 47 | # Composes a class object. The options are passed to the class when 48 | # an instance is contructed and are further used to test if the 49 | # composition should be re-composed. 50 | # 51 | # 2. compose('name', function) 52 | # Composes a function that executes in the context of the controller; 53 | # do NOT bind the function context. 54 | # 55 | # 3. compose('name', options, function) 56 | # Composes a function that executes in the context of the controller; 57 | # do NOT bind the function context and is passed the options as a 58 | # parameter. The options are further used to test if the composition 59 | # should be recomposed. 60 | # 61 | # 4. compose('name', options) 62 | # Gives control over the composition process; the compose method of 63 | # the options hash is executed in place of the function of form (3) and 64 | # the check method is called (if present) to determine re-composition ( 65 | # otherwise this is the same as form [3]). 66 | # 67 | # 5. compose('name', CompositionClass[, options]) 68 | # Gives complete control over the composition process. 69 | # 70 | compose: (name, second, third) -> 71 | # Normalize the arguments 72 | # If the second parameter is a function we know it is (1) or (2). 73 | if typeof second is 'function' 74 | # This is form (1) or (5) with the optional options hash if the third 75 | # is an obj or the second parameter's prototype has a dispose method 76 | if third or second::dispose 77 | # If the class is a Composition class then it is form (5). 78 | if second.prototype instanceof Composition 79 | return @_compose name, composition: second, options: third 80 | else 81 | return @_compose name, options: third, compose: -> 82 | # The compose method here just constructs the class. 83 | # Model and Collection both take `options` as the second argument. 84 | if second.prototype instanceof Backbone.Model or 85 | second.prototype instanceof Backbone.Collection 86 | @item = new second null, @options 87 | else 88 | @item = new second @options 89 | 90 | # Render this item if it has a render method and it either 91 | # doesn't have an autoRender property or that autoRender 92 | # property is false 93 | autoRender = @item.autoRender 94 | disabledAutoRender = autoRender is undefined or not autoRender 95 | if disabledAutoRender and typeof @item.render is 'function' 96 | @item.render() 97 | 98 | # This is form (2). 99 | return @_compose name, compose: second 100 | 101 | # If the third parameter exists and is a function this is (3). 102 | if typeof third is 'function' 103 | return @_compose name, compose: third, options: second 104 | 105 | # This must be form (4). 106 | return @_compose name, second 107 | 108 | _compose: (name, options) -> 109 | # Assert for programmer errors 110 | if typeof options.compose isnt 'function' and not options.composition? 111 | throw new Error 'Composer#compose was used incorrectly' 112 | 113 | if options.composition? 114 | # Use the passed composition directly 115 | composition = new options.composition options.options 116 | else 117 | # Create the composition and apply the methods (if available) 118 | composition = new Composition options.options 119 | composition.compose = options.compose 120 | composition.check = options.check if options.check 121 | 122 | # Check for an existing composition 123 | current = @compositions[name] 124 | 125 | # Apply the check method 126 | if current and current.check composition.options 127 | # Mark the current composition as not stale 128 | current.stale false 129 | else 130 | # Remove the current composition and apply this one 131 | current.dispose() if current 132 | returned = composition.compose composition.options 133 | isPromise = typeof returned?.then is 'function' 134 | composition.stale false 135 | @compositions[name] = composition 136 | 137 | # Return the active composition 138 | if isPromise 139 | returned 140 | else 141 | @compositions[name].item 142 | 143 | # Retrieves an active composition using the compose method. 144 | retrieve: (name) -> 145 | active = @compositions[name] 146 | if active and not active.stale() then active.item 147 | 148 | # Declare all compositions as stale and remove all that were previously 149 | # marked stale without being re-composed. 150 | cleanup: -> 151 | # Action method is done; perform post-action clean up 152 | # Dispose and delete all no-longer-active compositions. 153 | # Declare all active compositions as de-activated (eg. to be removed 154 | # on the next controller startup unless they are re-composed). 155 | for key in Object.keys @compositions 156 | composition = @compositions[key] 157 | if composition.stale() 158 | composition.dispose() 159 | delete @compositions[key] 160 | else 161 | composition.stale true 162 | 163 | # Return nothing. 164 | return 165 | 166 | disposed: false 167 | 168 | dispose: -> 169 | return if @disposed 170 | 171 | # Unbind handlers of global events 172 | @unsubscribeAllEvents() 173 | 174 | mediator.removeHandlers this 175 | 176 | # Dispose of all compositions and their items (that can be) 177 | for key in Object.keys @compositions 178 | @compositions[key].dispose() 179 | 180 | # Remove properties 181 | delete @compositions 182 | 183 | # Finished 184 | @disposed = true 185 | 186 | # You’re frozen when your heart’s not open 187 | Object.freeze this 188 | -------------------------------------------------------------------------------- /src/chaplin/controllers/controller.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | mediator = require '../mediator' 7 | EventBroker = require '../lib/event_broker' 8 | utils = require '../lib/utils' 9 | 10 | module.exports = class Controller 11 | # Borrow the static extend method from Backbone. 12 | @extend = Backbone.Model.extend 13 | 14 | # Mixin Backbone events and EventBroker. 15 | _.extend @prototype, Backbone.Events 16 | _.extend @prototype, EventBroker 17 | 18 | view: null 19 | 20 | # Internal flag which stores whether `redirectTo` 21 | # was called in the current action. 22 | redirected: false 23 | 24 | constructor: -> 25 | @initialize arguments... 26 | 27 | initialize: -> 28 | # Empty per default. 29 | 30 | beforeAction: -> 31 | # Empty per default. 32 | 33 | # Change document title. 34 | adjustTitle: (subtitle) -> 35 | mediator.execute 'adjustTitle', subtitle 36 | 37 | # Composer 38 | # -------- 39 | 40 | # Convenience method to publish the `!composer:compose` event. See the 41 | # composer for information on parameters, etc. 42 | reuse: -> 43 | method = if arguments.length is 1 then 'retrieve' else 'compose' 44 | mediator.execute "composer:#{method}", arguments... 45 | 46 | # Deprecated method. 47 | compose: -> 48 | throw new Error 'Controller#compose was moved to Controller#reuse' 49 | 50 | # Redirection 51 | # ----------- 52 | 53 | # Redirect to URL. 54 | redirectTo: -> 55 | @redirected = true 56 | utils.redirectTo arguments... 57 | 58 | # Disposal 59 | # -------- 60 | 61 | disposed: false 62 | 63 | dispose: -> 64 | return if @disposed 65 | 66 | # Dispose and delete all members which are disposable. 67 | for key in Object.keys this 68 | member = @[key] 69 | if typeof member?.dispose is 'function' 70 | member.dispose() 71 | delete @[key] 72 | 73 | # Unbind handlers of global events. 74 | @unsubscribeAllEvents() 75 | 76 | # Unbind all referenced handlers. 77 | @stopListening() 78 | 79 | # Finished. 80 | @disposed = true 81 | 82 | # You're frozen when your heart’s not open. 83 | Object.freeze this 84 | -------------------------------------------------------------------------------- /src/chaplin/dispatcher.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | EventBroker = require './lib/event_broker' 7 | utils = require './lib/utils' 8 | mediator = require './mediator' 9 | 10 | module.exports = class Dispatcher 11 | # Borrow the static extend method from Backbone. 12 | @extend = Backbone.Model.extend 13 | 14 | # Mixin an EventBroker. 15 | _.extend @prototype, EventBroker 16 | 17 | # The previous route information. 18 | # This object contains the controller name, action, path, and name (if any). 19 | previousRoute: null 20 | 21 | # The current controller, route information, and parameters. 22 | # The current route object contains the same information as previous. 23 | currentController: null 24 | currentRoute: null 25 | currentParams: null 26 | currentQuery: null 27 | 28 | constructor: -> 29 | @initialize arguments... 30 | 31 | initialize: (options = {}) -> 32 | # Merge the options. 33 | @settings = _.defaults options, 34 | controllerPath: 'controllers/' 35 | controllerSuffix: '_controller' 36 | 37 | # Listen to global events. 38 | @subscribeEvent 'router:match', @dispatch 39 | 40 | # Controller management. 41 | # Starting and disposing controllers. 42 | # ---------------------------------- 43 | 44 | # The standard flow is: 45 | # 46 | # 1. Test if it’s a new controller/action with new params 47 | # 1. Hide the previous view 48 | # 2. Dispose the previous controller 49 | # 3. Instantiate the new controller, call the controller action 50 | # 4. Show the new view 51 | # 52 | dispatch: (route, params, options) -> 53 | # Clone params and options so the original objects remain untouched. 54 | params = _.extend {}, params 55 | options = _.extend {}, options 56 | 57 | # null or undefined query parameters are equivalent to an empty hash 58 | options.query = {} if not options.query? 59 | 60 | # Whether to force the controller startup even 61 | # if current and new controllers and params match 62 | # Default to false unless explicitly set to true. 63 | options.forceStartup = false unless options.forceStartup is true 64 | 65 | # Stop if the desired controller/action is already active 66 | # with the same params. 67 | return if not options.forceStartup and 68 | @currentRoute?.controller is route.controller and 69 | @currentRoute?.action is route.action and 70 | _.isEqual(@currentParams, params) and 71 | _.isEqual(@currentQuery, options.query) 72 | 73 | # Fetch the new controller, then go on. 74 | @loadController route.controller, (Controller) => 75 | @controllerLoaded route, params, options, Controller 76 | 77 | # Load the constructor for a given controller name. 78 | # The default implementation uses require() from a AMD module loader 79 | # like RequireJS to fetch the constructor. 80 | loadController: (name, handler) -> 81 | return handler name if name is Object name 82 | 83 | fileName = name + @settings.controllerSuffix 84 | moduleName = @settings.controllerPath + fileName 85 | utils.loadModule moduleName, handler 86 | 87 | # Handler for the controller lazy-loading. 88 | controllerLoaded: (route, params, options, Controller) -> 89 | if @nextPreviousRoute = @currentRoute 90 | previous = _.extend {}, @nextPreviousRoute 91 | previous.params = @currentParams if @currentParams? 92 | delete previous.previous if previous.previous 93 | prev = {previous} 94 | @nextCurrentRoute = _.extend {}, route, prev 95 | 96 | controller = new Controller params, @nextCurrentRoute, options 97 | @executeBeforeAction controller, @nextCurrentRoute, params, options 98 | 99 | # Executes controller action. 100 | executeAction: (controller, route, params, options) -> 101 | # Dispose the previous controller. 102 | if @currentController 103 | # Notify the rest of the world beforehand. 104 | @publishEvent 'beforeControllerDispose', @currentController 105 | 106 | # Passing new parameters that the action method will receive. 107 | @currentController.dispose params, route, options 108 | 109 | # Save the new controller and its parameters. 110 | @currentController = controller 111 | @currentParams = _.extend {}, params 112 | @currentQuery = _.extend {}, options.query 113 | 114 | # Call the controller action with params and options. 115 | controller[route.action] params, route, options 116 | 117 | # Stop if the action triggered a redirect. 118 | return if controller.redirected 119 | 120 | # We're done! Spread the word! 121 | @publishEvent 'dispatcher:dispatch', @currentController, 122 | params, route, options 123 | 124 | # Executes before action filterer. 125 | executeBeforeAction: (controller, route, params, options) -> 126 | before = controller.beforeAction 127 | 128 | executeAction = => 129 | if controller.redirected or @currentRoute and route is @currentRoute 130 | @nextPreviousRoute = @nextCurrentRoute = null 131 | controller.dispose() 132 | return 133 | @previousRoute = @nextPreviousRoute 134 | @currentRoute = @nextCurrentRoute 135 | @nextPreviousRoute = @nextCurrentRoute = null 136 | @executeAction controller, route, params, options 137 | 138 | unless before 139 | executeAction() 140 | return 141 | 142 | # Throw deprecation warning. 143 | if typeof before isnt 'function' 144 | throw new TypeError 'Controller#beforeAction: function expected. ' + 145 | 'Old object-like form is not supported.' 146 | 147 | # Execute action in controller context. 148 | promise = controller.beforeAction params, route, options 149 | if typeof promise?.then is 'function' 150 | promise.then executeAction 151 | else 152 | executeAction() 153 | 154 | # Disposal 155 | # -------- 156 | 157 | disposed: false 158 | 159 | dispose: -> 160 | return if @disposed 161 | 162 | @unsubscribeAllEvents() 163 | 164 | @disposed = true 165 | 166 | # You’re frozen when your heart’s not open. 167 | Object.freeze this 168 | -------------------------------------------------------------------------------- /src/chaplin/lib/composition.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | EventBroker = require './event_broker' 6 | 7 | # Composition 8 | # ----------- 9 | 10 | # A utility class that is meant as a simple proxied version of a 11 | # controller that is used internally to inflate simple 12 | # calls to !composer:compose and may be extended and used to have complete 13 | # control over the composition process. 14 | module.exports = class Composition 15 | # Borrow the static extend method from Backbone. 16 | @extend = Backbone.Model.extend 17 | 18 | # Mixin Backbone events and EventBroker. 19 | _.extend @prototype, Backbone.Events 20 | _.extend @prototype, EventBroker 21 | 22 | # The item that is composed; this is by default a reference to this. 23 | item: null 24 | 25 | # The options that this composition was constructed with. 26 | options: null 27 | 28 | # Whether this composition is currently stale. 29 | _stale: false 30 | 31 | constructor: (options) -> 32 | @options = _.extend {}, options 33 | @item = this 34 | @initialize @options 35 | 36 | initialize: -> 37 | # Empty per default. 38 | 39 | # The compose method is called when this composition is to be composed. 40 | compose: -> 41 | # Empty per default. 42 | 43 | # The check method is called when this composition is asked to be 44 | # composed again. The passed options are the newly passed options. 45 | # If this returns false then the composition is re-composed. 46 | check: (options) -> 47 | _.isEqual @options, options 48 | 49 | # Marks all applicable items as stale. 50 | stale: (value) -> 51 | # Return the current property if not requesting a change. 52 | return @_stale unless value? 53 | 54 | # Sets the stale property for every item in the composition that has it. 55 | @_stale = value 56 | for name, item of this when ( 57 | item and item isnt this and 58 | typeof item is 'object' and item.hasOwnProperty 'stale' 59 | ) 60 | item.stale = value 61 | 62 | # Return nothing. 63 | return 64 | 65 | # Disposal 66 | # -------- 67 | 68 | disposed: false 69 | 70 | dispose: -> 71 | return if @disposed 72 | 73 | # Dispose and delete all members which are disposable. 74 | for key in Object.keys this 75 | member = @[key] 76 | if member and member isnt this and 77 | typeof member.dispose is 'function' 78 | member.dispose() 79 | delete @[key] 80 | 81 | # Unbind handlers of global events. 82 | @unsubscribeAllEvents() 83 | 84 | # Unbind all referenced handlers. 85 | @stopListening() 86 | 87 | # Remove properties which are not disposable. 88 | delete this.redirected 89 | 90 | # Finished. 91 | @disposed = true 92 | 93 | # You're frozen when your heart’s not open. 94 | Object.freeze this 95 | -------------------------------------------------------------------------------- /src/chaplin/lib/event_broker.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | mediator = require '../mediator' 4 | 5 | # Add functionality to subscribe and publish to global 6 | # Publish/Subscribe events so they can be removed afterwards 7 | # when disposing the object. 8 | # 9 | # Mixin this object to add the subscriber capability to any object: 10 | # _.extend object, EventBroker 11 | # Or to a prototype of a class: 12 | # _.extend @prototype, EventBroker 13 | # 14 | # Since Backbone 0.9.2 this abstraction just serves the purpose 15 | # that a handler cannot be registered twice for the same event. 16 | 17 | EventBroker = 18 | subscribeEvent: (type, handler) -> 19 | if typeof type isnt 'string' 20 | throw new TypeError 'EventBroker#subscribeEvent: ' + 21 | 'type argument must be a string' 22 | if typeof handler isnt 'function' 23 | throw new TypeError 'EventBroker#subscribeEvent: ' + 24 | 'handler argument must be a function' 25 | 26 | # Ensure that a handler isn’t registered twice. 27 | mediator.unsubscribe type, handler, this 28 | 29 | # Register global handler, force context to the subscriber. 30 | mediator.subscribe type, handler, this 31 | 32 | subscribeEventOnce: (type, handler) -> 33 | if typeof type isnt 'string' 34 | throw new TypeError 'EventBroker#subscribeEventOnce: ' + 35 | 'type argument must be a string' 36 | if typeof handler isnt 'function' 37 | throw new TypeError 'EventBroker#subscribeEventOnce: ' + 38 | 'handler argument must be a function' 39 | 40 | # Ensure that a handler isn’t registered twice. 41 | mediator.unsubscribe type, handler, this 42 | 43 | # Register global handler, force context to the subscriber. 44 | mediator.subscribeOnce type, handler, this 45 | 46 | unsubscribeEvent: (type, handler) -> 47 | if typeof type isnt 'string' 48 | throw new TypeError 'EventBroker#unsubscribeEvent: ' + 49 | 'type argument must be a string' 50 | if typeof handler isnt 'function' 51 | throw new TypeError 'EventBroker#unsubscribeEvent: ' + 52 | 'handler argument must be a function' 53 | 54 | # Remove global handler. 55 | mediator.unsubscribe type, handler 56 | 57 | # Unbind all global handlers. 58 | unsubscribeAllEvents: -> 59 | # Remove all handlers with a context of this subscriber. 60 | mediator.unsubscribe null, null, this 61 | 62 | publishEvent: (type, args...) -> 63 | if typeof type isnt 'string' 64 | throw new TypeError 'EventBroker#publishEvent: ' + 65 | 'type argument must be a string' 66 | 67 | # Publish global handler. 68 | mediator.publish type, args... 69 | 70 | # You’re frozen when your heart’s not open. 71 | Object.freeze EventBroker 72 | 73 | # Return our creation. 74 | module.exports = EventBroker 75 | -------------------------------------------------------------------------------- /src/chaplin/lib/history.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | # Cached regex for stripping a leading hash/slash and trailing space. 7 | routeStripper = /^[#\/]|\s+$/g 8 | 9 | # Cached regex for stripping leading and trailing slashes. 10 | rootStripper = /^\/+|\/+$/g 11 | 12 | # Patched Backbone.History with a basic query strings support 13 | class History extends Backbone.History 14 | 15 | # Get the cross-browser normalized URL fragment, either from the URL, 16 | # the hash, or the override. 17 | getFragment: (fragment, forcePushState) -> 18 | if not fragment? 19 | if @_hasPushState or not @_wantsHashChange or forcePushState 20 | # CHANGED: Make fragment include query string. 21 | fragment = @location.pathname + @location.search 22 | # Remove trailing slash. 23 | root = @root.replace /\/$/, '' 24 | fragment = fragment.slice root.length unless fragment.indexOf root 25 | else 26 | fragment = @getHash() 27 | 28 | fragment.replace routeStripper, '' 29 | 30 | # Start the hash change handling, returning `true` if the current URL matches 31 | # an existing route, and `false` otherwise. 32 | start: (options) -> 33 | if Backbone.History.started 34 | throw new Error 'Backbone.history has already been started' 35 | Backbone.History.started = true 36 | 37 | # Figure out the initial configuration. Is pushState desired? 38 | # Is it available? Are custom strippers provided? 39 | @options = _.extend {}, {root: '/'}, @options, options 40 | @root = @options.root 41 | @_wantsHashChange = @options.hashChange isnt false 42 | @_wantsPushState = Boolean @options.pushState 43 | @_hasPushState = Boolean @options.pushState and @history?.pushState 44 | fragment = @getFragment() 45 | routeStripper = @options.routeStripper ? routeStripper 46 | rootStripper = @options.rootStripper ? rootStripper 47 | 48 | # Normalize root to always include a leading and trailing slash. 49 | @root = ('/' + @root + '/').replace rootStripper, '/' 50 | 51 | # Depending on whether we're using pushState or hashes, 52 | # determine how we check the URL state. 53 | if @_hasPushState 54 | Backbone.$(window).on 'popstate', @checkUrl 55 | else if @_wantsHashChange 56 | Backbone.$(window).on 'hashchange', @checkUrl 57 | 58 | # Determine if we need to change the base url, for a pushState link 59 | # opened by a non-pushState browser. 60 | @fragment = fragment 61 | loc = @location 62 | atRoot = loc.pathname.replace(/[^\/]$/, '$&/') is @root 63 | 64 | # If we've started off with a route from a `pushState`-enabled browser, 65 | # but we're currently in a browser that doesn't support it... 66 | if @_wantsHashChange and @_wantsPushState and 67 | not @_hasPushState and not atRoot 68 | # CHANGED: Prevent query string from being added before hash. 69 | # So, it will appear only after #, as it has been already included 70 | # into @fragment 71 | @fragment = @getFragment null, true 72 | @location.replace @root + '#' + @fragment 73 | # Return immediately as browser will do redirect to new url 74 | return true 75 | 76 | # Or if we've started out with a hash-based route, but we're currently 77 | # in a browser where it could be `pushState`-based instead... 78 | else if @_wantsPushState and @_hasPushState and atRoot and loc.hash 79 | @fragment = @getHash().replace routeStripper, '' 80 | # CHANGED: It's no longer needed to add loc.search at the end, 81 | # as query params have been already included into @fragment 82 | @history.replaceState {}, document.title, @root + @fragment 83 | 84 | @loadUrl() if not @options.silent 85 | 86 | navigate: (fragment = '', options) -> 87 | return false unless Backbone.History.started 88 | 89 | options = {trigger: options} if not options or options is true 90 | 91 | fragment = @getFragment fragment 92 | url = @root + fragment 93 | 94 | # Remove fragment replace, coz query string different mean difference page 95 | # Strip the fragment of the query and hash for matching. 96 | # fragment = fragment.replace(pathStripper, '') 97 | 98 | return false if @fragment is fragment 99 | @fragment = fragment 100 | 101 | # Don't include a trailing slash on the root. 102 | if fragment.length is 0 and url isnt @root 103 | url = url.slice 0, -1 104 | 105 | # If pushState is available, we use it to set the fragment as a real URL. 106 | if @_hasPushState 107 | historyMethod = if options.replace then 'replaceState' else 'pushState' 108 | @history[historyMethod] {}, document.title, url 109 | 110 | # If hash changes haven't been explicitly disabled, update the hash 111 | # fragment to store history. 112 | else if @_wantsHashChange 113 | @_updateHash @location, fragment, options.replace 114 | 115 | # If you've told us that you explicitly don't want fallback hashchange- 116 | # based history, then `navigate` becomes a page refresh. 117 | else 118 | return @location.assign url 119 | 120 | if options.trigger 121 | @loadUrl fragment 122 | 123 | module.exports = if Backbone.$ then History else Backbone.History 124 | -------------------------------------------------------------------------------- /src/chaplin/lib/router.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | EventBroker = require './event_broker' 7 | History = require './history' 8 | Route = require './route' 9 | utils = require './utils' 10 | mediator = require '../mediator' 11 | 12 | # The router which is a replacement for Backbone.Router. 13 | # Like the standard router, it creates a Backbone.History 14 | # instance and registers routes on it. 15 | module.exports = class Router # This class does not extend Backbone.Router. 16 | # Borrow the static extend method from Backbone. 17 | @extend = Backbone.Model.extend 18 | 19 | # Mixin an EventBroker. 20 | _.extend @prototype, EventBroker 21 | 22 | constructor: (@options = {}) -> 23 | # Enable pushState by default for HTTP(s). 24 | # Disable it for file:// schema. 25 | isWebFile = window.location.protocol isnt 'file:' 26 | _.defaults @options, 27 | pushState: isWebFile 28 | root: '/' 29 | trailing: no 30 | 31 | # Cached regex for stripping a leading subdir and hash/slash. 32 | @removeRoot = new RegExp '^' + utils.escapeRegExp(@options.root) + '(#)?' 33 | 34 | @subscribeEvent '!router:route', @oldEventError 35 | @subscribeEvent '!router:routeByName', @oldEventError 36 | @subscribeEvent '!router:changeURL', @oldURLEventError 37 | 38 | @subscribeEvent 'dispatcher:dispatch', @changeURL 39 | 40 | mediator.setHandler 'router:route', @route, this 41 | mediator.setHandler 'router:reverse', @reverse, this 42 | 43 | @createHistory() 44 | 45 | oldEventError: -> 46 | throw new Error '!router:route and !router:routeByName events were removed. 47 | Use `Chaplin.utils.redirectTo`' 48 | 49 | oldURLEventError: -> 50 | throw new Error '!router:changeURL event was removed.' 51 | 52 | # Create a Backbone.History instance. 53 | createHistory: -> 54 | Backbone.history = new History() 55 | 56 | startHistory: -> 57 | # Start the Backbone.History instance to start routing. 58 | # This should be called after all routes have been registered. 59 | Backbone.history.start @options 60 | 61 | # Stop the current Backbone.History instance from observing URL changes. 62 | stopHistory: -> 63 | Backbone.history.stop() if Backbone.History.started 64 | 65 | # Search through backbone history handlers. 66 | findHandler: (predicate) -> 67 | for handler in Backbone.history.handlers when predicate handler 68 | return handler 69 | 70 | # Connect an address with a controller action. 71 | # Creates a route on the Backbone.History instance. 72 | match: (pattern, target, options = {}) => 73 | if arguments.length is 2 and target and typeof target is 'object' 74 | # Handles cases like `match 'url', controller: 'c', action: 'a'`. 75 | {controller, action} = options = target 76 | unless controller and action 77 | throw new Error 'Router#match must receive either target or ' + 78 | 'options.controller & options.action' 79 | else 80 | # Handles `match 'url', 'c#a'`. 81 | {controller, action} = options 82 | if controller or action 83 | throw new Error 'Router#match cannot use both target and ' + 84 | 'options.controller / options.action' 85 | # Separate target into controller and controller action. 86 | [controller, action] = target.split '#' 87 | 88 | # Let each match call provide its own trailing option to appropriate Route. 89 | # Pass trailing value from the Router by default. 90 | _.defaults options, trailing: @options.trailing 91 | 92 | # Create the route. 93 | route = new Route pattern, controller, action, options 94 | # Register the route at the Backbone.History instance. 95 | # Don’t use Backbone.history.route here because it calls 96 | # handlers.unshift, inserting the handler at the top of the list. 97 | # Since we want routes to match in the order they were specified, 98 | # we’re appending the route at the end. 99 | Backbone.history.handlers.push {route, callback: route.handler} 100 | route 101 | 102 | # Route a given URL path manually. Returns whether a route matched. 103 | # This looks quite like Backbone.History::loadUrl but it 104 | # accepts an absolute URL with a leading slash (e.g. /foo) 105 | # and passes the routing options to the callback function. 106 | route: (pathDesc, params, options) -> 107 | # Try to extract an URL from the pathDesc if it's a hash. 108 | if pathDesc and typeof pathDesc is 'object' 109 | path = pathDesc.url 110 | params = pathDesc.params if not params and pathDesc.params 111 | 112 | params = if Array.isArray params 113 | params.slice() 114 | else 115 | _.extend {}, params 116 | 117 | # Accept path to be given via URL wrapped in object, 118 | # or implicitly via route name, or explicitly via object. 119 | if path? 120 | # Remove leading subdir and hash or slash. 121 | path = path.replace @removeRoot, '' 122 | 123 | # Find a matching route. 124 | handler = @findHandler (handler) -> handler.route.test path 125 | 126 | # Options is the second argument in this case. 127 | options = params 128 | params = null 129 | else 130 | options = _.extend {}, options 131 | 132 | # Find a route using a passed via pathDesc string route name. 133 | handler = @findHandler (handler) -> 134 | if handler.route.matches pathDesc 135 | params = handler.route.normalizeParams params 136 | return true if params 137 | false 138 | 139 | if handler 140 | # Update the URL programmatically after routing. 141 | _.defaults options, changeURL: true 142 | 143 | pathParams = if path? then path else params 144 | handler.callback pathParams, options 145 | true 146 | else 147 | throw new Error 'Router#route: request was not routed' 148 | 149 | # Find the URL for given criteria using the registered routes and 150 | # provided parameters. The criteria may be just the name of a route 151 | # or an object containing the name, controller, and/or action. 152 | # Warning: this is usually **hot** code in terms of performance. 153 | # Returns the URL string or false. 154 | reverse: (criteria, params, query) -> 155 | root = @options.root 156 | 157 | if params? and typeof params isnt 'object' 158 | throw new TypeError 'Router#reverse: params must be an array or an ' + 159 | 'object' 160 | 161 | # First filter the route handlers to those that are of the same name. 162 | handlers = Backbone.history.handlers 163 | for handler in handlers when handler.route.matches criteria 164 | # Attempt to reverse using the provided parameter hash. 165 | reversed = handler.route.reverse params, query 166 | 167 | # Return the url if we got a valid one; else we continue on. 168 | if reversed isnt false 169 | url = if root then root + reversed else reversed 170 | return url 171 | 172 | # We didn't get anything. 173 | throw new Error 'Router#reverse: invalid route criteria specified: ' + 174 | "#{JSON.stringify criteria}" 175 | 176 | # Change the current URL, add a history entry. 177 | changeURL: (controller, params, route, options) -> 178 | return unless route.path? and options?.changeURL 179 | 180 | url = route.path + if route.query then "?#{route.query}" else '' 181 | 182 | navigateOptions = 183 | # Do not trigger or replace per default. 184 | trigger: options.trigger is true 185 | replace: options.replace is true 186 | 187 | # Navigate to the passed URL and forward options to Backbone. 188 | Backbone.history.navigate url, navigateOptions 189 | 190 | # Disposal 191 | # -------- 192 | 193 | disposed: false 194 | 195 | dispose: -> 196 | return if @disposed 197 | 198 | # Stop Backbone.History instance and remove it. 199 | @stopHistory() 200 | delete Backbone.history 201 | 202 | @unsubscribeAllEvents() 203 | 204 | mediator.removeHandlers this 205 | 206 | # Finished. 207 | @disposed = true 208 | 209 | # You’re frozen when your heart’s not open. 210 | Object.freeze this 211 | -------------------------------------------------------------------------------- /src/chaplin/lib/support.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Backwards-compatibility module 4 | # ------------------------------ 5 | 6 | module.exports = 7 | propertyDescriptors: yes -------------------------------------------------------------------------------- /src/chaplin/lib/sync_machine.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Simple finite state machine for synchronization of models/collections 4 | # Three states: unsynced, syncing and synced 5 | # Several transitions between them 6 | # Fires Backbone events on every transition 7 | # (unsynced, syncing, synced; syncStateChange) 8 | # Provides shortcut methods to call handlers when a given state is reached 9 | # (named after the events above) 10 | 11 | UNSYNCED = 'unsynced' 12 | SYNCING = 'syncing' 13 | SYNCED = 'synced' 14 | 15 | STATE_CHANGE = 'syncStateChange' 16 | 17 | SyncMachine = 18 | _syncState: UNSYNCED 19 | _previousSyncState: null 20 | 21 | # Get the current state 22 | # --------------------- 23 | 24 | syncState: -> 25 | @_syncState 26 | 27 | isUnsynced: -> 28 | @_syncState is UNSYNCED 29 | 30 | isSynced: -> 31 | @_syncState is SYNCED 32 | 33 | isSyncing: -> 34 | @_syncState is SYNCING 35 | 36 | # Transitions 37 | # ----------- 38 | 39 | unsync: -> 40 | if @_syncState in [SYNCING, SYNCED] 41 | @_previousSync = @_syncState 42 | @_syncState = UNSYNCED 43 | @trigger @_syncState, this, @_syncState 44 | @trigger STATE_CHANGE, this, @_syncState 45 | # when UNSYNCED do nothing 46 | return 47 | 48 | beginSync: -> 49 | if @_syncState in [UNSYNCED, SYNCED] 50 | @_previousSync = @_syncState 51 | @_syncState = SYNCING 52 | @trigger @_syncState, this, @_syncState 53 | @trigger STATE_CHANGE, this, @_syncState 54 | # when SYNCING do nothing 55 | return 56 | 57 | finishSync: -> 58 | if @_syncState is SYNCING 59 | @_previousSync = @_syncState 60 | @_syncState = SYNCED 61 | @trigger @_syncState, this, @_syncState 62 | @trigger STATE_CHANGE, this, @_syncState 63 | # when SYNCED, UNSYNCED do nothing 64 | return 65 | 66 | abortSync: -> 67 | if @_syncState is SYNCING 68 | @_syncState = @_previousSync 69 | @_previousSync = @_syncState 70 | @trigger @_syncState, this, @_syncState 71 | @trigger STATE_CHANGE, this, @_syncState 72 | # when UNSYNCED, SYNCED do nothing 73 | return 74 | 75 | # Create shortcut methods to bind a handler to a state change 76 | # ----------------------------------------------------------- 77 | 78 | for event in [UNSYNCED, SYNCING, SYNCED, STATE_CHANGE] 79 | do (event) -> 80 | SyncMachine[event] = (callback, context = this) -> 81 | @on event, callback, context 82 | callback.call(context) if @_syncState is event 83 | 84 | # You’re frozen when your heart’s not open. 85 | Object.freeze SyncMachine 86 | 87 | # Return our creation. 88 | module.exports = SyncMachine 89 | -------------------------------------------------------------------------------- /src/chaplin/lib/utils.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | # Utilities 4 | # --------- 5 | 6 | utils = 7 | isEmpty: (object) -> 8 | not Object.getOwnPropertyNames(object).length 9 | 10 | # Simple duck-typing serializer for models and collections. 11 | serialize: (data) -> 12 | if typeof data.serialize is 'function' 13 | data.serialize() 14 | else if typeof data.toJSON is 'function' 15 | data.toJSON() 16 | else 17 | throw new TypeError 'utils.serialize: Unknown data was passed' 18 | 19 | # Make properties readonly and not configurable 20 | # using ECMAScript 5 property descriptors. 21 | readonly: (object, keys...) -> 22 | for key in keys 23 | Object.defineProperty object, key, 24 | value: object[key] 25 | writable: false 26 | configurable: false 27 | # Always return `true` for compatibility reasons. 28 | true 29 | 30 | # Get the whole chain of object prototypes. 31 | getPrototypeChain: (object) -> 32 | chain = [] 33 | while object = Object.getPrototypeOf object 34 | chain.unshift object 35 | chain 36 | 37 | # Get all property versions from object’s prototype chain. 38 | # E.g. if object1 & object2 have `key` and object2 inherits from 39 | # object1, it will get [object1prop, object2prop]. 40 | getAllPropertyVersions: (object, key) -> 41 | result = [] 42 | for proto in utils.getPrototypeChain object 43 | value = proto[key] 44 | if value and value not in result 45 | result.push value 46 | result 47 | 48 | # String Helpers 49 | # -------------- 50 | 51 | # Upcase the first character. 52 | upcase: (str) -> 53 | str.charAt(0).toUpperCase() + str.slice 1 54 | 55 | # Escapes a string to use in a regex. 56 | escapeRegExp: (str) -> 57 | return String(str or '').replace /([.*+?^=!:${}()|[\]\/\\])/g, '\\$1' 58 | 59 | 60 | # Event handling helpers 61 | # ---------------------- 62 | 63 | # Returns whether a modifier key is pressed during a keypress or mouse click. 64 | modifierKeyPressed: (event) -> 65 | event.shiftKey or event.altKey or event.ctrlKey or event.metaKey 66 | 67 | # Routing Helpers 68 | # --------------- 69 | 70 | # Returns the url for a named route and any params. 71 | reverse: (criteria, params, query) -> 72 | require('../mediator').execute 'router:reverse', 73 | criteria, params, query 74 | 75 | # Redirects to URL, route name or controller and action pair. 76 | redirectTo: (pathDesc, params, options) -> 77 | require('../mediator').execute 'router:route', 78 | pathDesc, params, options 79 | 80 | # Determines module system and returns module loader function. 81 | loadModule: do -> 82 | {define, require} = window 83 | 84 | if typeof define is 'function' and define.amd 85 | (moduleName, handler) -> 86 | require [moduleName], handler 87 | else 88 | enqueue = setImmediate ? setTimeout 89 | 90 | (moduleName, handler) -> 91 | enqueue -> handler require moduleName 92 | 93 | # DOM helpers 94 | # ----------- 95 | 96 | matchesSelector: do -> 97 | el = document.documentElement 98 | matches = el.matches or 99 | el.msMatchesSelector or 100 | el.mozMatchesSelector or 101 | el.webkitMatchesSelector 102 | 103 | -> matches.call arguments... 104 | 105 | # Query parameters Helpers 106 | # ------------------------ 107 | 108 | querystring: 109 | 110 | # Returns a query string from a hash. 111 | stringify: (params = {}, replacer) -> 112 | if typeof replacer isnt 'function' 113 | replacer = (key, value) -> 114 | if Array.isArray value 115 | value.map (value) -> {key, value} 116 | else if value? 117 | {key, value} 118 | 119 | Object.keys(params).reduce (pairs, key) -> 120 | pair = replacer key, params[key] 121 | pairs.concat pair or [] 122 | , [] 123 | .map ({key, value}) -> 124 | [key, value].map(encodeURIComponent).join '=' 125 | .join '&' 126 | 127 | # Returns a hash with query parameters from a query string. 128 | parse: (string = '', reviver) -> 129 | if typeof reviver isnt 'function' 130 | reviver = (key, value) -> {key, value} 131 | 132 | string = string.slice 1 + string.indexOf '?' 133 | string.split('&').reduce (params, pair) -> 134 | parts = pair.split('=').map decodeURIComponent 135 | {key, value} = reviver(parts...) or {} 136 | 137 | if value? then params[key] = 138 | if params.hasOwnProperty key 139 | [].concat params[key], value 140 | else 141 | value 142 | 143 | params 144 | , {} 145 | 146 | 147 | # Backwards-compatibility methods 148 | # ------------------------------- 149 | 150 | utils.beget = Object.create 151 | utils.indexOf = (array, item) -> array.indexOf item 152 | utils.isArray = Array.isArray 153 | utils.queryParams = utils.querystring 154 | 155 | # Finish 156 | # ------ 157 | 158 | # Seal the utils object. 159 | Object.seal utils 160 | 161 | # Return our creation. 162 | module.exports = utils 163 | -------------------------------------------------------------------------------- /src/chaplin/mediator.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | Backbone = require 'backbone' 4 | utils = require './lib/utils' 5 | 6 | # Mediator 7 | # -------- 8 | 9 | # The mediator is a simple object all other modules use to communicate 10 | # with each other. It implements the Publish/Subscribe pattern. 11 | # 12 | # Additionally, it holds objects which need to be shared between modules. 13 | # In this case, a `user` property is created for getting the user object 14 | # and a `setUser` method for setting the user. 15 | # 16 | # This module returns the singleton object. This is the 17 | # application-wide mediator you might load into modules 18 | # which need to talk to other modules using Publish/Subscribe. 19 | 20 | # Start with a simple object 21 | mediator = {} 22 | 23 | # Publish / Subscribe 24 | # ------------------- 25 | 26 | # Mixin event methods from Backbone.Events, 27 | # create Publish/Subscribe aliases. 28 | mediator.subscribe = mediator.on = Backbone.Events.on 29 | mediator.subscribeOnce = mediator.once = Backbone.Events.once 30 | mediator.unsubscribe = mediator.off = Backbone.Events.off 31 | mediator.publish = mediator.trigger = Backbone.Events.trigger 32 | 33 | # Initialize an empty callback list so we might seal the mediator later. 34 | mediator._callbacks = null 35 | 36 | # Request / Response 37 | # --–--------------- 38 | 39 | # Like pub / sub, but with one handler. Similar to OOP message passing. 40 | 41 | handlers = mediator._handlers = {} 42 | 43 | # Sets a handler function for requests. 44 | mediator.setHandler = (name, method, instance) -> 45 | handlers[name] = {instance, method} 46 | 47 | # Retrieves a handler function and executes it. 48 | mediator.execute = (options, args...) -> 49 | if options and typeof options is 'object' 50 | {name, silent} = options 51 | else 52 | name = options 53 | handler = handlers[name] 54 | if handler 55 | handler.method.apply handler.instance, args 56 | else if not silent 57 | throw new Error "mediator.execute: #{name} handler is not defined" 58 | 59 | # Removes handlers from storage. 60 | # Can take no args, list of handler names or instance which had bound handlers. 61 | mediator.removeHandlers = (instanceOrNames) -> 62 | unless instanceOrNames 63 | mediator._handlers = {} 64 | 65 | if Array.isArray instanceOrNames 66 | for name in instanceOrNames 67 | delete handlers[name] 68 | else 69 | for name, handler of handlers when handler.instance is instanceOrNames 70 | delete handlers[name] 71 | return 72 | 73 | # Sealing the mediator 74 | # -------------------- 75 | 76 | # After adding all needed properties, you should seal the mediator 77 | # using this method. 78 | mediator.seal = -> 79 | # Prevent extensions and make all properties non-configurable. 80 | Object.seal mediator 81 | 82 | # Make properties readonly. 83 | utils.readonly mediator, 84 | 'subscribe', 'subscribeOnce', 'unsubscribe', 'publish', 85 | 'setHandler', 'execute', 'removeHandlers', 'seal' 86 | 87 | # Return our creation. 88 | module.exports = mediator 89 | -------------------------------------------------------------------------------- /src/chaplin/models/collection.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | Model = require './model' 7 | EventBroker = require '../lib/event_broker' 8 | utils = require '../lib/utils' 9 | 10 | # Abstract class which extends the standard Backbone collection 11 | # in order to add some functionality. 12 | module.exports = class Collection extends Backbone.Collection 13 | # Mixin an EventBroker. 14 | _.extend @prototype, EventBroker 15 | 16 | # Use the Chaplin model per default, not Backbone.Model. 17 | model: Model 18 | 19 | # Serializes collection. 20 | serialize: -> 21 | @map utils.serialize 22 | 23 | # Disposal 24 | # -------- 25 | 26 | disposed: false 27 | 28 | dispose: -> 29 | return if @disposed 30 | 31 | # Fire an event to notify associated views. 32 | @trigger 'dispose', this 33 | 34 | # Empty the list silently, but do not dispose all models since 35 | # they might be referenced elsewhere. 36 | @reset [], silent: true 37 | 38 | # Unbind all global event handlers. 39 | @unsubscribeAllEvents() 40 | 41 | # Unbind all referenced handlers. 42 | @stopListening() 43 | 44 | # Remove all event handlers on this module. 45 | @off() 46 | 47 | # Remove model constructor reference, internal model lists 48 | # and event handlers. 49 | delete this[prop] for prop in [ 50 | 'model', 51 | 'models', '_byCid', 52 | '_callbacks' 53 | ] 54 | 55 | @_byId = {} 56 | 57 | # Finished. 58 | @disposed = true 59 | 60 | # You’re frozen when your heart’s not open. 61 | Object.freeze this 62 | -------------------------------------------------------------------------------- /src/chaplin/models/model.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | EventBroker = require '../lib/event_broker' 6 | 7 | # Private helper function for serializing attributes recursively, 8 | # creating objects which delegate to the original attributes 9 | # in order to protect them from changes. 10 | serializeAttributes = (model, attributes, modelStack) -> 11 | # Create a delegator object. 12 | delegator = Object.create attributes 13 | 14 | # Add model to stack. 15 | modelStack ?= {} 16 | modelStack[model.cid] = true 17 | 18 | # Map model/collection to their attributes. Create a property 19 | # on the delegator that shadows the original attribute. 20 | for key, value of attributes 21 | 22 | # Handle models. 23 | if value instanceof Backbone.Model 24 | delegator[key] = serializeModelAttributes value, model, modelStack 25 | 26 | # Handle collections. 27 | else if value instanceof Backbone.Collection 28 | serializedModels = [] 29 | for otherModel in value.models 30 | serializedModels.push( 31 | serializeModelAttributes(otherModel, model, modelStack) 32 | ) 33 | delegator[key] = serializedModels 34 | 35 | # Remove model from stack. 36 | delete modelStack[model.cid] 37 | 38 | # Return the delegator. 39 | delegator 40 | 41 | # Serialize the attributes of a given model 42 | # in the context of a given tree. 43 | serializeModelAttributes = (model, currentModel, modelStack) -> 44 | # Nullify circular references. 45 | return null if model is currentModel or model.cid of modelStack 46 | # Serialize recursively. 47 | attributes = if typeof model.getAttributes is 'function' 48 | # Chaplin models. 49 | model.getAttributes() 50 | else 51 | # Backbone models. 52 | model.attributes 53 | serializeAttributes model, attributes, modelStack 54 | 55 | 56 | # Abstraction that adds some useful functionality to backbone model. 57 | module.exports = class Model extends Backbone.Model 58 | # Mixin an EventBroker. 59 | _.extend @prototype, EventBroker 60 | 61 | # This method is used to get the attributes for the view template 62 | # and might be overwritten by decorators which cannot create a 63 | # proper `attributes` getter due to ECMAScript 3 limits. 64 | getAttributes: -> 65 | @attributes 66 | 67 | # Return an object which delegates to the attributes 68 | # (i.e. an object which has the attributes as prototype) 69 | # so primitive values might be added and altered safely. 70 | # Map models to their attributes, recursively. 71 | serialize: -> 72 | serializeAttributes this, @getAttributes() 73 | 74 | # Disposal 75 | # -------- 76 | 77 | disposed: false 78 | 79 | dispose: -> 80 | return if @disposed 81 | 82 | # Fire an event to notify associated collections and views. 83 | @trigger 'dispose', this 84 | 85 | @collection?.remove? this, silent: true 86 | 87 | # Unbind all global event handlers. 88 | @unsubscribeAllEvents() 89 | 90 | # Unbind all referenced handlers. 91 | @stopListening() 92 | 93 | # Remove all event handlers on this module. 94 | @off() 95 | 96 | # Remove the collection reference, internal attribute hashes 97 | # and event handlers. 98 | delete this[prop] for prop in [ 99 | 'collection', 100 | 'attributes', 'changed', 'defaults', 101 | '_escapedAttributes', '_previousAttributes', 102 | '_silent', '_pending', 103 | '_callbacks' 104 | ] 105 | 106 | # Finished. 107 | @disposed = true 108 | 109 | # You’re frozen when your heart’s not open. 110 | Object.freeze this 111 | -------------------------------------------------------------------------------- /src/chaplin/views/layout.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | _ = require 'underscore' 4 | Backbone = require 'backbone' 5 | 6 | View = require './view' 7 | EventBroker = require '../lib/event_broker' 8 | utils = require '../lib/utils' 9 | mediator = require '../mediator' 10 | 11 | # Shortcut to access the DOM manipulation library. 12 | {$} = Backbone 13 | 14 | module.exports = class Layout extends View 15 | # Bind to document body by default. 16 | el: 'body' 17 | 18 | # Override default view behavior, we don’t want document.body to be removed. 19 | keepElement: true 20 | 21 | # The site title used in the document title. 22 | # This should be set in your app-specific Application class 23 | # and passed as an option. 24 | title: '' 25 | 26 | # Regions 27 | # ------- 28 | 29 | # Collection of registered regions; all view regions are collected here. 30 | globalRegions: null 31 | 32 | listen: 33 | 'beforeControllerDispose mediator': 'scroll' 34 | 35 | constructor: (options = {}) -> 36 | @globalRegions = [] 37 | @title = options.title 38 | @regions = options.regions if options.regions 39 | @settings = _.defaults options, 40 | titleTemplate: (data) -> 41 | st = if data.subtitle then "#{data.subtitle} \u2013 " else '' 42 | st + data.title 43 | openExternalToBlank: false 44 | routeLinks: 'a, .go-to' 45 | skipRouting: '.noscript' 46 | # Per default, jump to the top of the page. 47 | scrollTo: [0, 0] 48 | 49 | mediator.setHandler 'region:show', @showRegion, this 50 | mediator.setHandler 'region:register', @registerRegionHandler, this 51 | mediator.setHandler 'region:unregister', @unregisterRegionHandler, this 52 | mediator.setHandler 'region:find', @regionByName, this 53 | mediator.setHandler 'adjustTitle', @adjustTitle, this 54 | 55 | super 56 | 57 | # Set the app link routing. 58 | @startLinkRouting() if @settings.routeLinks 59 | 60 | # Controller startup and disposal 61 | # ------------------------------- 62 | 63 | # Handler for the global beforeControllerDispose event. 64 | scroll: -> 65 | # Reset the scroll position. 66 | to = @settings.scrollTo 67 | if to and typeof to is 'object' 68 | [x, y] = to 69 | window.scrollTo x, y 70 | 71 | # Handler for the global dispatcher:dispatch event. 72 | # Change the document title to match the new controller. 73 | # Get the title from the title property of the current controller. 74 | adjustTitle: (subtitle = '') -> 75 | title = @settings.titleTemplate {@title, subtitle} 76 | document.title = title 77 | @publishEvent 'adjustTitle', subtitle, title 78 | title 79 | 80 | # Automatic routing of internal links 81 | # ----------------------------------- 82 | 83 | startLinkRouting: -> 84 | route = @settings.routeLinks 85 | @delegate 'click', route, @openLink if route 86 | 87 | stopLinkRouting: -> 88 | route = @settings.routeLinks 89 | @undelegate 'click', route if route 90 | 91 | isExternalLink: (link) -> 92 | return false unless utils.matchesSelector link, 'a, area' 93 | return true if link.hasAttribute 'download' 94 | 95 | # IE 9-11 resolve href but do not populate protocol, host etc. 96 | # Reassigning href helps. See #878 issue for details. 97 | link.href += '' unless link.host 98 | 99 | {target} = link 100 | 101 | target is '_blank' or 102 | link.rel is 'external' or 103 | link.origin isnt location.origin or 104 | (target is '_parent' and parent isnt self) or 105 | (target is '_top' and top isnt self) 106 | 107 | # Handle all clicks on A elements and try to route them internally. 108 | openLink: (event) => 109 | return if utils.modifierKeyPressed event 110 | 111 | el = if $ then event.currentTarget else event.delegateTarget 112 | 113 | # Get the href and perform checks on it. 114 | href = el.getAttribute('href') or el.getAttribute('data-href') 115 | 116 | # Basic href checks. 117 | # Technically an empty string is a valid relative URL 118 | # but it doesn’t make sense to route it. 119 | return if not href or 120 | # Exclude fragment links. 121 | href[0] is '#' 122 | 123 | # Apply skipRouting option. 124 | {skipRouting} = @settings 125 | switch typeof skipRouting 126 | when 'function' 127 | return unless skipRouting href, el 128 | when 'string' 129 | return if utils.matchesSelector el, skipRouting 130 | 131 | # Handle external links. 132 | if @isExternalLink el 133 | if @settings.openExternalToBlank 134 | # Open external links normally in a new tab. 135 | event.preventDefault() 136 | @openWindow href 137 | return 138 | 139 | # Pass to the router, try to route the path internally. 140 | utils.redirectTo url: href 141 | 142 | # Prevent default handling if the URL could be routed. 143 | event.preventDefault() 144 | 145 | # Handle all browsing context resources 146 | openWindow: (href) -> 147 | window.open href 148 | 149 | # Region management 150 | # ----------------- 151 | 152 | # Handler for `!region:register`. 153 | # Register a single view region or all regions exposed. 154 | registerRegionHandler: (instance, name, selector) -> 155 | if name? 156 | @registerGlobalRegion instance, name, selector 157 | else 158 | @registerGlobalRegions instance 159 | 160 | # Registering one region bound to a view. 161 | registerGlobalRegion: (instance, name, selector) -> 162 | # Remove the region if there was already one registered perhaps by 163 | # a base class. 164 | @unregisterGlobalRegion instance, name 165 | 166 | # Place this region registration into the regions array. 167 | @globalRegions.unshift {instance, name, selector} 168 | 169 | # Triggered by view; passed in the regions hash. 170 | # Simply register all regions exposed by it. 171 | registerGlobalRegions: (instance) -> 172 | # Regions can be be extended by subclasses, so we need to check the 173 | # whole prototype chain for matching regions. Regions registered by the 174 | # more-derived class overwrites the region registered by the less-derived 175 | # class. 176 | for version in utils.getAllPropertyVersions instance, 'regions' 177 | for name, selector of version 178 | @registerGlobalRegion instance, name, selector 179 | # Return nothing. 180 | return 181 | 182 | # Handler for `!region:unregister`. 183 | # Unregisters single named region or all view regions. 184 | unregisterRegionHandler: (instance, name) -> 185 | if name? 186 | @unregisterGlobalRegion instance, name 187 | else 188 | @unregisterGlobalRegions instance 189 | 190 | # Unregisters a specific named region from a view. 191 | unregisterGlobalRegion: (instance, name) -> 192 | cid = instance.cid 193 | @globalRegions = (region for region in @globalRegions when ( 194 | region.instance.cid isnt cid or region.name isnt name 195 | )) 196 | 197 | # When views are disposed; remove all their registered regions. 198 | unregisterGlobalRegions: (instance) -> 199 | @globalRegions = (region for region in @globalRegions when ( 200 | region.instance.cid isnt instance.cid 201 | )) 202 | 203 | # Returns the region by its name, if found. 204 | regionByName: (name) -> 205 | for reg in @globalRegions when reg.name is name and not reg.instance.stale 206 | return reg 207 | 208 | # When views are instantiated and request for a region assignment; 209 | # attempt to fulfill it. 210 | showRegion: (name, instance) -> 211 | # Find an appropriate region. 212 | region = @regionByName name 213 | 214 | # Assert that we got a valid region. 215 | throw new Error "No region registered under #{name}" unless region 216 | 217 | # Apply the region selector. 218 | instance.container = if region.selector is '' 219 | if $ 220 | region.instance.$el 221 | else 222 | region.instance.el 223 | else 224 | if region.instance.noWrap 225 | region.instance.container.find region.selector 226 | else 227 | region.instance.find region.selector 228 | 229 | # Disposal 230 | # -------- 231 | 232 | dispose: -> 233 | return if @disposed 234 | 235 | # Stop routing links. 236 | @stopLinkRouting() 237 | 238 | # Remove all regions and document title setting. 239 | delete this[prop] for prop in ['globalRegions', 'title', 'route'] 240 | 241 | mediator.removeHandlers this 242 | 243 | super 244 | -------------------------------------------------------------------------------- /test/application_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | {Application, Composer, Dispatcher} = require '../src/chaplin' 4 | {EventBroker, Router, mediator, Layout} = require '../src/chaplin' 5 | 6 | describe 'Application', -> 7 | app = null 8 | 9 | getApp = (dontInit) -> 10 | if dontInit 11 | class extends Application then initialize: -> 12 | else 13 | Application 14 | 15 | beforeEach -> 16 | app = new (getApp yes) 17 | 18 | afterEach -> 19 | app.dispose() 20 | 21 | it 'should be a simple object', -> 22 | expect(app).to.be.an 'object' 23 | expect(app).to.be.an.instanceof Application 24 | 25 | it 'should mixin a EventBroker', -> 26 | prototype = Application.prototype 27 | expect(prototype).to.contain.all.keys EventBroker 28 | 29 | it 'should have initialize function', -> 30 | expect(app.initialize).to.be.a 'function' 31 | app.initialize() 32 | 33 | it 'should create a dispatcher', -> 34 | expect(app.initDispatcher).to.be.a 'function' 35 | app.initDispatcher() 36 | expect(app.dispatcher).to.be.an.instanceof Dispatcher 37 | 38 | it 'should create a layout', -> 39 | expect(app.initLayout).to.be.a 'function' 40 | app.initLayout() 41 | expect(app.layout).to.be.an.instanceof Layout 42 | 43 | it 'should create a composer', -> 44 | expect(app.initComposer).to.be.a 'function' 45 | app.initComposer() 46 | expect(app.composer).to.be.an.instanceof Composer 47 | 48 | it 'should seal mediator', -> 49 | expect(mediator).not.to.be.sealed 50 | app.initMediator() 51 | expect(mediator).to.be.sealed 52 | 53 | it 'should create a router', -> 54 | passedMatch = null 55 | routesCalled = no 56 | routes = (match) -> 57 | routesCalled = yes 58 | passedMatch = match 59 | 60 | expect(app.initRouter).to.be.a 'function' 61 | expect(app.initRouter.length).to.equal 2 62 | app.initRouter routes, root: '/', pushState: false 63 | 64 | expect(app.router).to.be.an.instanceof Router 65 | expect(routesCalled).to.be.true 66 | expect(passedMatch).to.be.a 'function' 67 | expect(Backbone.History.started).to.be.false 68 | 69 | it 'should start Backbone.history with start()', -> 70 | app.initRouter (->), root: '/', pushState: false 71 | app.start() 72 | expect(Backbone.History.started).to.be.true 73 | Backbone.history.stop() 74 | 75 | it 'should seal itself with start()', -> 76 | app.initRouter() 77 | app.start() 78 | expect(app).to.be.sealed 79 | 80 | it 'should throw an error on double-init', -> 81 | app = new (getApp no) 82 | expect(-> app.initialize()).to.throw Error 83 | 84 | it 'should dispose itself correctly', -> 85 | expect(app.disposed).not.to.be.ok 86 | expect(app.dispose).to.be.a 'function' 87 | app.dispose() 88 | 89 | for key in ['dispatcher', 'layout', 'router', 'composer'] 90 | expect(app).not.to.have.ownProperty key 91 | 92 | expect(app.disposed).to.be.true 93 | expect(app).to.be.frozen 94 | 95 | it 'should be extendable', -> 96 | expect(Application.extend).to.be.a 'function' 97 | 98 | DerivedApplication = Application.extend() 99 | derivedApp = new DerivedApplication() 100 | derivedApp.dispose() 101 | 102 | expect(derivedApp).to.be.an.instanceof Application 103 | -------------------------------------------------------------------------------- /test/bench.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chaplin Test Runner 6 | 7 | 8 | 137 | 138 | 139 |
140 |
141 | 142 | 143 | -------------------------------------------------------------------------------- /test/collection_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | sinon = require 'sinon' 4 | {EventBroker, mediator, Collection, Model} = require '../src/chaplin' 5 | 6 | describe 'Collection', -> 7 | collection = null 8 | 9 | beforeEach -> 10 | collection = new Collection() 11 | 12 | afterEach -> 13 | collection.dispose() 14 | 15 | it 'should mixin a EventBroker', -> 16 | prototype = Collection.prototype 17 | expect(prototype).to.contain.all.keys EventBroker 18 | 19 | it 'should serialize the models', -> 20 | model1 = new Model id: 1, foo: 'foo' 21 | model2 = new Backbone.Model id: 2, bar: 'bar' 22 | collection = new Collection [model1, model2] 23 | 24 | expect(collection.serialize).to.be.a 'function' 25 | expect(collection.serialize collection).to.deep.equal [ 26 | {id: 1, foo: 'foo'} 27 | {id: 2, bar: 'bar'} 28 | ] 29 | 30 | describe 'Disposal', -> 31 | it 'should dispose itself correctly', -> 32 | expect(collection.disposed).to.be.false 33 | expect(collection.dispose).to.be.a 'function' 34 | collection.dispose() 35 | 36 | expect(collection.length).to.equal 0 37 | expect(collection.disposed).to.be.true 38 | expect(collection).to.be.frozen 39 | 40 | it 'should fire a dispose event', -> 41 | disposeSpy = sinon.spy() 42 | collection.on 'dispose', disposeSpy 43 | collection.dispose() 44 | 45 | expect(disposeSpy).to.have.been.calledOnce 46 | 47 | it 'should unsubscribe from Pub/Sub events', -> 48 | pubSubSpy = sinon.spy() 49 | collection.subscribeEvent 'foo', pubSubSpy 50 | collection.dispose() 51 | 52 | mediator.publish 'foo' 53 | expect(pubSubSpy).to.not.have.been.called 54 | 55 | it 'should remove all event handlers from itself', -> 56 | collectionBindSpy = sinon.spy() 57 | collection.on 'foo', collectionBindSpy 58 | collection.dispose() 59 | 60 | collection.trigger 'foo' 61 | expect(collectionBindSpy).to.not.have.been.called 62 | 63 | it 'should unsubscribe from other events', -> 64 | spy = sinon.spy() 65 | model = new Model() 66 | 67 | collection.listenTo model, 'foo', spy 68 | collection.dispose() 69 | 70 | model.trigger 'foo' 71 | expect(spy).to.not.have.been.called 72 | 73 | it 'should remove instance properties', -> 74 | collection.dispose() 75 | 76 | for key in ['model', 'models', '_byCid'] 77 | expect(collection).not.to.have.ownProperty key 78 | 79 | expect(collection._byId).to.deep.equal {} 80 | -------------------------------------------------------------------------------- /test/composition_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | {Composition, EventBroker, mediator} = require '../src/chaplin' 3 | 4 | describe 'Composition', -> 5 | composition = null 6 | 7 | beforeEach -> 8 | # Instantiate 9 | composition = new Composition() 10 | 11 | afterEach -> 12 | # Dispose 13 | composition.dispose() 14 | composition = null 15 | 16 | # Mixin 17 | # ----- 18 | 19 | it 'should mixin a EventBroker', -> 20 | prototype = Composition.prototype 21 | expect(prototype).to.contain.all.keys EventBroker 22 | 23 | # Initialize 24 | # ---------- 25 | 26 | it 'should initialize', -> 27 | expect(composition.initialize).to.be.a 'function' 28 | composition.initialize() 29 | 30 | expect(composition.stale()).to.be.false 31 | expect(composition.item).to.equal composition 32 | 33 | # Disposal 34 | # -------- 35 | 36 | it 'should dispose itself correctly', -> 37 | expect(composition.disposed).to.be.false 38 | expect(composition.dispose).to.be.a 'function' 39 | composition.dispose() 40 | 41 | expect(composition).not.to.have.property 'compositions' 42 | expect(composition.disposed).to.be.true 43 | expect(composition).to.be.frozen 44 | 45 | # Extensible 46 | # ---------- 47 | 48 | it 'should be extendable', -> 49 | expect(Composition.extend).to.be.a 'function' 50 | 51 | DerivedComposition = Composition.extend() 52 | derivedComposition = new DerivedComposition() 53 | derivedComposition.dispose() 54 | 55 | expect(derivedComposition).to.be.an.instanceof Composition 56 | -------------------------------------------------------------------------------- /test/controller_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | sinon = require 'sinon' 4 | {Controller, EventBroker, mediator, Model, View} = require '../src/chaplin' 5 | 6 | describe 'Controller', -> 7 | controller = null 8 | 9 | beforeEach -> 10 | controller = new Controller() 11 | 12 | afterEach -> 13 | controller.dispose() 14 | mediator.removeHandlers ['router:route'] 15 | 16 | it 'should mixin a Backbone.Events', -> 17 | prototype = Controller.prototype 18 | expect(prototype).to.contain.all.keys Backbone.Events 19 | 20 | it 'should mixin an EventBroker', -> 21 | prototype = Controller.prototype 22 | expect(prototype).to.contain.all.keys EventBroker 23 | 24 | it 'should be extendable', -> 25 | expect(Controller.extend).to.be.a 'function' 26 | 27 | DerivedController = Controller.extend() 28 | derivedController = new DerivedController() 29 | expect(derivedController).to.be.an.instanceof Controller 30 | 31 | derivedController.dispose() 32 | 33 | it 'should redirect to a URL', -> 34 | expect(controller.redirectTo).to.be.a 'function' 35 | 36 | routerRoute = sinon.spy() 37 | mediator.setHandler 'router:route', routerRoute 38 | url = 'redirect-target/123' 39 | controller.redirectTo url 40 | 41 | expect(controller.redirected).to.be.true 42 | expect(routerRoute).to.have.been.calledWith url 43 | 44 | it 'should redirect to a URL with routing options', -> 45 | routerRoute = sinon.spy() 46 | mediator.setHandler 'router:route', routerRoute 47 | 48 | url = 'redirect-target/123' 49 | options = replace: true 50 | controller.redirectTo url, options 51 | 52 | expect(controller.redirected).to.be.true 53 | expect(routerRoute).to.been.calledWith url, options 54 | 55 | it 'should redirect to a named route', -> 56 | routerRoute = sinon.spy() 57 | mediator.setHandler 'router:route', routerRoute 58 | 59 | name = 'params' 60 | params = one: '21' 61 | pathDesc = name: name, params: params 62 | controller.redirectTo pathDesc 63 | 64 | expect(controller.redirected).to.be.true 65 | expect(routerRoute).to.have.been.calledWith pathDesc 66 | 67 | it 'should redirect to a named route with options', -> 68 | routerRoute = sinon.spy() 69 | mediator.setHandler 'router:route', routerRoute 70 | 71 | name = 'params' 72 | params = one: '21' 73 | pathDesc = name: name, params: params 74 | options = replace: true 75 | controller.redirectTo pathDesc, options 76 | 77 | expect(controller.redirected).to.be.true 78 | expect(routerRoute).to.have.been.calledWith pathDesc, options 79 | 80 | it 'should adjust page title', -> 81 | spy = sinon.spy() 82 | mediator.setHandler 'adjustTitle', spy 83 | controller.adjustTitle 'meh' 84 | 85 | expect(spy).to.have.been.calledOnce 86 | expect(spy).to.have.been.calledWith 'meh' 87 | 88 | describe 'Disposal', -> 89 | mediator.setHandler 'region:unregister', -> 90 | 91 | it 'should dispose itself correctly', -> 92 | expect(controller.disposed).to.be.false 93 | expect(controller.dispose).to.be.a 'function' 94 | controller.dispose() 95 | 96 | expect(controller.disposed).to.be.true 97 | expect(controller).to.be.frozen 98 | 99 | it 'should dispose disposable properties', -> 100 | model = controller.model = new Model() 101 | view = controller.view = new View model: model 102 | 103 | controller.dispose() 104 | 105 | expect(controller).not.to.have.ownProperty 'model' 106 | expect(controller).not.to.have.ownProperty 'view' 107 | 108 | expect(model.disposed).to.be.true 109 | expect(view.disposed).to.be.true 110 | 111 | it 'should unsubscribe from Pub/Sub events', -> 112 | pubSubSpy = sinon.spy() 113 | controller.subscribeEvent 'foo', pubSubSpy 114 | controller.dispose() 115 | 116 | mediator.publish 'foo' 117 | expect(pubSubSpy).to.not.have.been.called 118 | 119 | it 'should unsubscribe from other events', -> 120 | spy = sinon.spy() 121 | model = new Model() 122 | 123 | controller.listenTo model, 'foo', spy 124 | controller.dispose() 125 | 126 | model.trigger 'foo' 127 | expect(spy).to.not.have.been.called 128 | -------------------------------------------------------------------------------- /test/event_broker_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | sinon = require 'sinon' 3 | {EventBroker, mediator} = require '../src/chaplin' 4 | 5 | describe 'EventBroker', -> 6 | # Create a simple object which mixes in the EventBroker 7 | eventBroker = Object.assign {}, EventBroker 8 | 9 | it 'should subscribe to events', -> 10 | expect(eventBroker.subscribeEvent).to.be.a 'function' 11 | 12 | # We could mock mediator.publish here and test if it was called, 13 | # well, better testing the outcome. 14 | type = 'eventBrokerTest' 15 | spy = sinon.spy() 16 | eventBroker.subscribeEvent type, spy 17 | 18 | mediator.publish type, 1, 2, 3, 4 19 | expect(spy).to.have.been.calledOnce 20 | expect(spy).to.have.been.calledWith 1, 2, 3, 4 21 | expect(spy).to.have.been.calledOn eventBroker 22 | 23 | mediator.unsubscribe type, spy 24 | 25 | it 'should not subscribe the same handler twice', -> 26 | type = 'eventBrokerTest' 27 | spy = sinon.spy() 28 | eventBroker.subscribeEvent type, spy 29 | eventBroker.subscribeEvent type, spy 30 | 31 | mediator.publish type, 1, 2, 3, 4 32 | expect(spy).to.have.been.calledOnce 33 | expect(spy).to.have.been.calledWith 1, 2, 3, 4 34 | expect(spy).to.have.been.calledOn eventBroker 35 | 36 | mediator.unsubscribe type, spy 37 | 38 | it 'should not call `once` handler twice', -> 39 | type = 'eventBrokerTest' 40 | spy = sinon.spy() 41 | eventBroker.subscribeEventOnce type, spy 42 | eventBroker.subscribeEventOnce type, spy 43 | 44 | mediator.publish type, 1, 2, 3, 4 45 | mediator.publish type, 5, 6, 7, 8 46 | 47 | expect(spy).to.have.been.calledOnce 48 | expect(spy).to.have.been.calledWith 1, 2, 3, 4 49 | expect(spy).to.have.been.calledOn eventBroker 50 | 51 | it 'should unsubscribe from events', -> 52 | expect(eventBroker.unsubscribeEvent).to.be.a 'function' 53 | 54 | type = 'eventBrokerTest' 55 | spy = sinon.spy() 56 | eventBroker.subscribeEvent type, spy 57 | eventBroker.unsubscribeEvent type, spy 58 | 59 | mediator.publish type 60 | expect(spy).to.not.have.been.called 61 | 62 | it 'should unsubscribe from all events', -> 63 | expect(eventBroker.unsubscribeAllEvents).to.be.a 'function' 64 | 65 | spy = sinon.spy() 66 | unrelatedHandler = sinon.spy() 67 | context = {} 68 | 69 | eventBroker.subscribeEvent 'one', spy 70 | eventBroker.subscribeEvent 'two', spy 71 | eventBroker.subscribeEvent 'three', spy 72 | mediator.subscribe 'four', unrelatedHandler 73 | mediator.subscribe 'four', unrelatedHandler, context 74 | 75 | eventBroker.unsubscribeAllEvents() 76 | mediator.publish 'one' 77 | mediator.publish 'two' 78 | mediator.publish 'three' 79 | mediator.publish 'four' 80 | 81 | expect(spy).to.not.have.been.called 82 | # Ensure other handlers remain untouched 83 | expect(unrelatedHandler).to.have.been.calledTwice 84 | mediator.unsubscribe 'four', unrelatedHandler 85 | 86 | it 'should publish events', -> 87 | expect(eventBroker.publishEvent).to.be.a 'function' 88 | 89 | type = 'eventBrokerTest' 90 | spy = sinon.spy() 91 | mediator.subscribe type, spy 92 | 93 | eventBroker.publishEvent type, 1, 2, 3, 4 94 | expect(spy).to.have.been.calledOnce 95 | expect(spy).to.have.been.calledWith 1, 2, 3, 4 96 | 97 | mediator.unsubscribe type, spy 98 | -------------------------------------------------------------------------------- /test/mediator_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | sinon = require 'sinon' 3 | {mediator, Model} = require '../src/chaplin' 4 | 5 | describe 'mediator', -> 6 | it 'should be a simple object', -> 7 | expect(mediator).to.be.an 'object' 8 | 9 | it 'should have seal method and be sealed', -> 10 | expect(mediator.seal).to.be.a 'function' 11 | expect(mediator).to.be.sealed 12 | 13 | it 'should have Pub/Sub methods', -> 14 | expect(mediator.subscribe).to.be.a 'function' 15 | expect(mediator.subscribeOnce).to.be.a 'function' 16 | expect(mediator.unsubscribe).to.be.a 'function' 17 | expect(mediator.publish).to.be.a 'function' 18 | 19 | it 'should have readonly Pub/Sub and Resp/Req methods', -> 20 | methods = [ 21 | 'subscribe', 'subscribeOnce', 'unsubscribe', 'publish', 22 | 'setHandler', 'execute', 'removeHandlers' 23 | ] 24 | 25 | for method in methods 26 | expect(mediator).to.have.ownPropertyDescriptor method, 27 | value: mediator[method] 28 | writable: false 29 | enumerable: true 30 | configurable: false 31 | 32 | it 'should publish messages to subscribers', -> 33 | spy = sinon.spy() 34 | eventName = 'foo' 35 | payload = 'payload' 36 | 37 | mediator.subscribe eventName, spy 38 | mediator.publish eventName, payload 39 | 40 | expect(spy).to.have.been.calledOnce 41 | mediator.unsubscribe eventName, spy 42 | 43 | it 'should publish messages to subscribers once', -> 44 | spy = sinon.spy() 45 | eventName = 'foo' 46 | payload = 'payload' 47 | 48 | mediator.subscribeOnce eventName, spy 49 | mediator.publish eventName, payload 50 | mediator.publish eventName, 'second' 51 | 52 | expect(spy).to.have.been.calledOnce 53 | expect(spy).to.have.been.calledWith payload 54 | 55 | it 'should allow to unsubscribe to events', -> 56 | spy = sinon.spy() 57 | eventName = 'foo' 58 | payload = 'payload' 59 | 60 | mediator.subscribe eventName, spy 61 | mediator.unsubscribe eventName, spy 62 | mediator.publish eventName, payload 63 | 64 | expect(spy).to.not.have.been.calledWith payload 65 | 66 | it 'should have response / request methods', -> 67 | expect(mediator.setHandler).to.be.a 'function' 68 | expect(mediator.execute).to.be.a 'function' 69 | expect(mediator.removeHandlers).to.be.a 'function' 70 | 71 | it 'should allow to set and execute handlers', -> 72 | response = 'austrian' 73 | spy = sinon.stub().returns response 74 | name = 'ancap' 75 | 76 | mediator.setHandler name, spy 77 | expect(mediator.execute name).to.equal response 78 | -------------------------------------------------------------------------------- /test/model_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | sinon = require 'sinon' 4 | {EventBroker, mediator, Model} = require '../src/chaplin' 5 | 6 | describe 'Model', -> 7 | model = null 8 | 9 | beforeEach -> 10 | model = new Model id: 1 11 | 12 | afterEach -> 13 | model.dispose() 14 | 15 | it 'should mixin a EventBroker', -> 16 | prototype = Model.prototype 17 | expect(prototype).to.contain.all.keys EventBroker 18 | 19 | it 'should return the attributes per default', -> 20 | expect(model.getAttributes()).to.deep.equal model.attributes 21 | 22 | it 'should serialize the attributes', -> 23 | model1 = model.set number: 'one' 24 | model2 = new Model id: 2, number: 'two' 25 | model3 = new Model id: 3, number: 'three' 26 | model4 = new Model id: 4, number: 'four' 27 | model5 = new Model id: 5, number: 'five' 28 | collection = new Backbone.Collection [model4, model5] 29 | model1.set {model2} 30 | model2.set {model3} 31 | model2.set {collection} 32 | model2.set {model2} # Circular fun! 33 | model3.set {model2} # Even more fun! 34 | model4.set {model2} # Even more fun! 35 | 36 | # Reference tree: 37 | # model1 38 | # model2 39 | # model3 40 | # model2 41 | # collection 42 | # model4 43 | # model2 44 | # model5 45 | # model2 46 | 47 | expect(model.serialize()).to.deep.equal 48 | id: 1 49 | number: 'one' 50 | model2: 51 | id: 2 52 | number: 'two' 53 | # Circular references are nullified 54 | model2: null 55 | model3: 56 | id: 3 57 | number: 'three' 58 | # Circular references are nullified 59 | model2: null 60 | collection: [ 61 | { 62 | id: 4 63 | number: 'four' 64 | # Circular references are nullified 65 | model2: null 66 | }, 67 | { 68 | id: 5 69 | number: 'five' 70 | } 71 | ] 72 | 73 | it 'should protect the original attributes when serializing', -> 74 | model1 = model.set number: 'one' 75 | model2 = new Model id: 2, number: 'two' 76 | model3 = new Backbone.Model id: 3, number: 'three' 77 | model1.set {model2} 78 | model1.set {model3} 79 | 80 | serialized = model1.serialize() 81 | 82 | # Try to tamper with the model attributes 83 | serialized.number = 84 | serialized.model2.number = 85 | serialized.model3.number = 'new' 86 | 87 | actual = [ model1, model2, model3 ].map (model) -> 88 | model.get 'number' 89 | 90 | # Original attributes remain unchanged 91 | expect(actual).to.deep.equal ['one', 'two', 'three'] 92 | 93 | it 'should serialize nested Backbone models and collections', -> 94 | model1 = model.set number: 'one' 95 | model2 = new Model id: 2, number: 'two' 96 | model3 = new Backbone.Model id: 3, number: 'three' 97 | collection = new Backbone.Collection [ 98 | new Model id: 4, number: 'four' 99 | new Backbone.Model id: 5, number: 'five' 100 | ] 101 | 102 | model1.set {model2} 103 | model1.set {model3} 104 | model1.set {collection} 105 | 106 | actual = model1.serialize() 107 | 108 | # expect(model1.serialize()).to.deep.equal 109 | # number: 'one' 110 | # model2: 111 | # number: 'two' 112 | # model3: 113 | # number: 'three' 114 | # collection: [ 115 | # number: 'four' 116 | # number: 'five' 117 | # ] 118 | 119 | expect(actual.number).to.equal 'one' 120 | expect(actual.model2).to.be.an 'object' 121 | expect(actual.model2.number).to.equal 'two' 122 | expect(actual.model3).to.be.an 'object' 123 | expect(actual.model3.number).to.equal 'three' 124 | 125 | expect(actual.collection).to.be.an 'array' 126 | expect(actual.collection.length).to.equal 2 127 | expect(actual.collection[0].number).to.equal 'four' 128 | expect(actual.collection[1].number).to.equal 'five' 129 | 130 | describe 'Disposal', -> 131 | it 'should dispose itself correctly', -> 132 | expect(model.disposed).to.be.false 133 | expect(model.dispose).to.be.a 'function' 134 | model.dispose() 135 | 136 | expect(model.disposed).to.be.true 137 | expect(model).to.be.frozen 138 | 139 | it 'should fire a dispose event', -> 140 | disposeSpy = sinon.spy() 141 | 142 | model.on 'dispose', disposeSpy 143 | model.dispose() 144 | 145 | expect(disposeSpy).to.have.been.called 146 | 147 | it 'should unsubscribe from Pub/Sub events', -> 148 | pubSubSpy = sinon.spy() 149 | 150 | model.subscribeEvent 'foo', pubSubSpy 151 | model.dispose() 152 | mediator.publish 'foo' 153 | 154 | expect(pubSubSpy).to.not.have.been.called 155 | 156 | it 'should remove all event handlers from itself', -> 157 | modelBindSpy = sinon.spy() 158 | 159 | model.on 'foo', modelBindSpy 160 | model.dispose() 161 | model.trigger 'foo' 162 | 163 | expect(modelBindSpy).to.not.have.been.called 164 | 165 | it 'should unsubscribe from other events', -> 166 | spy = sinon.spy() 167 | model2 = new Model() 168 | model.listenTo model2, 'foo', spy 169 | model.dispose() 170 | 171 | model2.trigger 'foo' 172 | expect(spy).to.not.have.been.called 173 | 174 | it 'should remove instance properties', -> 175 | model.dispose() 176 | 177 | keys = [ 178 | 'collection', 179 | 'attributes', 'changed' 180 | '_escapedAttributes', '_previousAttributes', 181 | '_silent', '_pending', 182 | '_callbacks' 183 | ] 184 | 185 | for key in keys 186 | expect(model).not.to.have.ownProperty key 187 | -------------------------------------------------------------------------------- /test/sync_machine_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | sinon = require 'sinon' 4 | {SyncMachine} = require '../src/chaplin' 5 | 6 | describe 'SyncMachine', -> 7 | machine = null 8 | 9 | beforeEach -> 10 | machine = {} 11 | Object.assign machine, Backbone.Events 12 | Object.assign machine, SyncMachine 13 | 14 | it 'should change its state', -> 15 | expect(machine.syncState()).to.equal 'unsynced' 16 | 17 | machine.beginSync() 18 | expect(machine.syncState()).to.equal 'syncing' 19 | 20 | machine.finishSync() 21 | expect(machine.syncState()).to.equal 'synced' 22 | 23 | machine.unsync() 24 | expect(machine.syncState()).to.equal 'unsynced' 25 | 26 | it 'should emit sync events', -> 27 | stateChange = sinon.spy() 28 | syncing = sinon.spy() 29 | synced = sinon.spy() 30 | 31 | machine.on 'syncStateChange', stateChange 32 | machine.on 'syncing', syncing 33 | machine.on 'synced', synced 34 | 35 | machine.beginSync() 36 | expect(stateChange).to.have.been.calledOnce 37 | expect(stateChange).to.have.been.calledWith machine, 'syncing' 38 | expect(syncing).to.have.been.calledOnce 39 | 40 | machine.finishSync() 41 | expect(stateChange).to.have.been.calledTwice 42 | expect(stateChange).to.have.been.calledWith machine, 'synced' 43 | expect(synced).to.have.been.calledOnce 44 | 45 | it 'should has shortcuts for checking sync state', -> 46 | expect(machine.isUnsynced()).to.be.true 47 | expect(machine.isSyncing()).to.be.false 48 | expect(machine.isSynced()).to.be.false 49 | 50 | machine.beginSync() 51 | expect(machine.isUnsynced()).to.be.false 52 | expect(machine.isSyncing()).to.be.true 53 | expect(machine.isSynced()).to.be.false 54 | 55 | machine.finishSync() 56 | expect(machine.isUnsynced()).to.be.false 57 | expect(machine.isSyncing()).to.be.false 58 | expect(machine.isSynced()).to.be.true 59 | 60 | it 'should be able to abort sync', -> 61 | machine.beginSync() 62 | machine.abortSync() 63 | expect(machine.syncState()).to.equal 'unsynced' 64 | 65 | it 'should has sync callbacks', -> 66 | syncing = sinon.spy() 67 | synced = sinon.spy() 68 | unsynced = sinon.spy() 69 | 70 | machine.syncing syncing 71 | machine.synced synced 72 | machine.unsynced unsynced 73 | 74 | machine.beginSync() 75 | expect(syncing).to.have.been.calledOnce 76 | 77 | machine.finishSync() 78 | expect(synced).to.have.been.calledOnce 79 | 80 | machine.unsync() 81 | expect(unsynced).to.have.been.calledTwice # Including initial call. 82 | -------------------------------------------------------------------------------- /test/utils_spec.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | Backbone = require 'backbone' 3 | {utils, mediator} = require '../src/chaplin' 4 | 5 | describe 'utils', -> 6 | class A 7 | prop: 1 8 | class B extends A 9 | prop: null 10 | class C extends B 11 | prop: 2 12 | class D extends C 13 | prop: 3 14 | 15 | describe 'isEmpty', -> 16 | it 'should check whether an object does not have own properties', -> 17 | object = {} 18 | expect(utils.isEmpty object).to.be.true 19 | object.a = 1 20 | expect(utils.isEmpty object).to.be.false 21 | 22 | it 'should check only own properties', -> 23 | object = Object.create b: 2 24 | expect(utils.isEmpty object).to.be.true 25 | 26 | it 'should check non-enumerable properties too', -> 27 | object = {} 28 | Object.defineProperty object, 'c', value: 3 29 | expect(utils.isEmpty object).to.be.false 30 | 31 | describe 'serialize', -> 32 | it 'should serialize objects and incorporate duck-typing', -> 33 | date = new Date() 34 | expect(utils.serialize date).to.equal date.toISOString() 35 | model = new Backbone.Model a: 1 36 | expect(utils.serialize model).to.deep.equal a: 1 37 | expect(utils.serialize serialize: -> 2).to.equal 2 38 | expect(-> utils.serialize {}).to.throw TypeError 39 | 40 | describe 'readonly', -> 41 | it 'should make property read-only', -> 42 | object = {a: 228} 43 | expect(utils.readonly object, 'a').to.be.true 44 | expect(-> object.a = 666).to.throw TypeError 45 | expect(object).to.have.ownPropertyDescriptor 'a', 46 | value: 228 47 | writable: false 48 | enumerable: true 49 | configurable: false 50 | 51 | describe 'getPrototypeChain', -> 52 | it 'should get prototype chain of instance', -> 53 | object = new D 54 | expect(utils.getPrototypeChain object).to.deep.equal [ 55 | Object.prototype, 56 | A.prototype, 57 | B.prototype, 58 | C.prototype, 59 | D.prototype 60 | ] 61 | 62 | describe 'getAllPropertyVersions', -> 63 | it 'should get property from all prototypes', -> 64 | object = new D 65 | expect(utils.getAllPropertyVersions object, 'prop').to.deep.equal [ 66 | 1, 67 | 2, 68 | 3 69 | ] 70 | 71 | describe 'upcase', -> 72 | it 'should make the first character in string upper-cased', -> 73 | expect(utils.upcase 'stuff').to.equal 'Stuff' 74 | expect(utils.upcase 'стафф').to.equal 'Стафф' 75 | expect(utils.upcase '123456').to.equal '123456' 76 | 77 | describe 'reverse', -> 78 | beforeEach -> 79 | mediator.unsubscribe() 80 | afterEach -> 81 | mediator.unsubscribe() 82 | 83 | it 'should throw exception if no route found', -> 84 | expect(-> utils.reverse 'foo', id: 3, d: 'data').to.throw Error 85 | 86 | it 'should return the url for a named route', -> 87 | stubbedRouteHandler = (routeName, params) -> 88 | expect(routeName).to.equal 'foo' 89 | expect(params).to.deep.equal id: 3, d: 'data' 90 | '/foo/bar' 91 | 92 | mediator.setHandler 'router:reverse', stubbedRouteHandler 93 | url = utils.reverse 'foo', id: 3, d: 'data' 94 | expect(url).to.equal '/foo/bar' 95 | 96 | it 'should return the url for a named route with empty path', -> 97 | stubbedRouteHandler = (routeName, params) -> 98 | expect(routeName).to.equal 'home' 99 | expect(params).to.be.undefined 100 | '/' 101 | 102 | mediator.setHandler 'router:reverse', stubbedRouteHandler 103 | url = utils.reverse 'home' 104 | expect(url).to.equal '/' 105 | 106 | # it 'should return null if router does not respond', -> 107 | # url = utils.reverse 'foo', id: 3, d: 'data' 108 | # expect(url).to.be.null 109 | 110 | describe 'matchesSelector', -> 111 | {matchesSelector} = utils 112 | 113 | el = document.createElement 'input' 114 | el.type = 'email' 115 | el.required = true 116 | el.className = 'cls' 117 | el.setAttribute 'data-a', 'b' 118 | 119 | it 'should check whether the element matches CSS selector', -> 120 | expect(matchesSelector el, 'input[type=email].cls').to.be.true 121 | expect(matchesSelector el, '[data-a][required]').to.be.true 122 | expect(matchesSelector el, ':not([optional])').to.be.true 123 | expect(matchesSelector el, '[data-a=b]').to.be.true 124 | 125 | describe 'queryParams', -> 126 | falsy = ['', 0, NaN, false, null, undefined] 127 | {stringify, parse} = utils.querystring 128 | 129 | queryParams = p1: 'With space', p2_empty: '', 'p 3': ['999', 'a&b'] 130 | queryString = 'p1=With%20space&p2_empty=&p%203=999&p%203=a%26b' 131 | 132 | it 'should serialize query parameters from object into string', -> 133 | expect(stringify queryParams).to.equal queryString 134 | 135 | it 'should ignore undefined and null when serializing query parameters', -> 136 | params = p1: null, p2: undefined, p3: 'third' 137 | expect(stringify params).to.equal 'p3=third' 138 | 139 | it 'should ignore first parameter == null', -> 140 | expect(stringify {}).to.equal '' 141 | expect(parse '').to.deep.equal {} 142 | 143 | it 'should ignore non-callable second parameter', -> 144 | expect(stringify queryParams, 1).to.equal queryString 145 | expect(parse queryString, []).to.deep.equal queryParams 146 | 147 | it 'should serialize with replacer when provided', -> 148 | replacer = (key, value) -> 149 | value += '_' 150 | {key, value} 151 | 152 | expect(stringify queryParams, replacer).to.equal( 153 | 'p1=With%20space_&p2_empty=_&p%203=999%2Ca%26b_') 154 | 155 | it 'should skip pair if replacer returns falsy value', -> 156 | for value in falsy 157 | expect(stringify queryParams, -> value).to.equal '' 158 | 159 | it 'should deserialize query parameters from query string into object', -> 160 | expect(parse queryString).to.deep.equal queryParams 161 | 162 | it 'should take a full url and only return params object', -> 163 | url = "http://foo.com/app/path?#{queryString}" 164 | expect(parse url).to.deep.equal queryParams 165 | 166 | it 'should deserialize with reviver when provided', -> 167 | reviver = (key, value) -> 168 | if ',' in value 169 | value = value.split(',').map (value) -> 170 | if isNaN value then value else +value 171 | {key, value} 172 | 173 | expect(parse 'a=1%2C2%2C3&b=c%2Cd&e=4', reviver).to.deep.equal 174 | a: [1, 2, 3] 175 | b: ['c', 'd'] 176 | e: '4' 177 | 178 | it 'should skip pair if reviver returns falsy value', -> 179 | for value in falsy 180 | expect(parse queryString, -> value).to.deep.equal {} 181 | 182 | it 'should skip pair if reviver returns value == null', -> 183 | expect(parse 'a&').to.deep.equal {} 184 | expect(parse 'b=1', -> {value: null}).to.deep.equal {} 185 | expect(parse 'c=2', -> []).to.deep.equal {} 186 | 187 | it 'should have old methods', -> 188 | expect(utils.queryParams.stringify).to.be.a 'function' 189 | expect(utils.queryParams.parse).to.be.a 'function' 190 | --------------------------------------------------------------------------------