├── .gitignore
├── Gruntfile.coffee
├── README.md
├── api
├── models
│ └── todo.coffee
└── routes
│ └── todos.coffee
├── bashrc
├── package.json
├── server.coffee
└── web
├── app.coffee
├── main.coffee
├── models
└── todo.coffee
├── public
├── css
│ ├── style.css
│ └── style.styl
├── index.html
└── js
│ ├── app.js
│ ├── lib
│ ├── backbone.babysitter.js
│ ├── backbone.js
│ ├── backbone.marionette.js
│ ├── backbone.relational.js
│ ├── backbone.wreqr.js
│ ├── jquery.js
│ ├── require.js
│ └── underscore.js
│ ├── main.js
│ ├── models
│ └── todo.js
│ ├── router.js
│ ├── templates
│ ├── jade.js
│ ├── template.js
│ └── todo.js
│ └── views
│ ├── template.js
│ ├── todo.js
│ └── todolist.js
├── router.coffee
├── templates
├── template.jade
└── todo.jade
└── views
├── .template.coffee.swp
├── template.coffee
├── todo.coffee
└── todolist.coffee
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 |
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | node_modules
16 |
17 | *.swp
18 |
--------------------------------------------------------------------------------
/Gruntfile.coffee:
--------------------------------------------------------------------------------
1 | path = require('path')
2 |
3 | module.exports = (grunt) ->
4 | grunt.initConfig({
5 | express: {
6 | server: {
7 | options: {
8 | bases: path.resolve('web/public')
9 | debug: true
10 | server: path.resolve('./server.coffee')
11 | monitor: {
12 | command: 'coffee'
13 | }
14 | }
15 | }
16 | }
17 |
18 | coffee: {
19 | compile: {
20 | files: [
21 | expand: true
22 | cwd: 'web/'
23 | src: '**/*.coffee'
24 | dest: 'web/public/js/'
25 | ext: '.js'
26 | ]
27 | }
28 | }
29 |
30 | jade: {
31 | compile: {
32 | options: {
33 | client: true
34 | namespace: false
35 | amd: true
36 | }
37 | files: [
38 | expand: true
39 | cwd: 'web/templates'
40 | src: '**/*.jade'
41 | dest: 'web/public/js/templates'
42 | ext: '.js'
43 | ]
44 | }
45 | }
46 |
47 | watch: {
48 | coffee: {
49 | files: 'web/**/*.coffee'
50 | tasks: ['coffee']
51 | }
52 | jade: {
53 | files: 'web/templates/*.jade'
54 | tasks: ['jade']
55 | }
56 | }
57 |
58 | })
59 |
60 | grunt.loadNpmTasks('grunt-contrib-coffee')
61 | grunt.loadNpmTasks('grunt-contrib-jade')
62 | grunt.loadNpmTasks('grunt-contrib-requirejs')
63 | grunt.loadNpmTasks('grunt-contrib-watch')
64 | grunt.loadNpmTasks('grunt-express')
65 |
66 | grunt.registerTask('build', 'coffee jade stylus requirejs')
67 | grunt.registerTask('build-dev', 'coffee jade')
68 | grunt.registerTask('default', ['express', 'watch'])
69 |
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node / Backbone Web App Boilerplate
2 |
3 | ## Description
4 | Node/Backbone forms a powerful combination for building single page web applications that scale. Because of these two project's popularity, there are several great boilerplate projects out there, but all for either Node or Backbone seperately. This project aims to integrate these two worlds (Backend and Frontend) making it easy to start building next generation web applications.
5 |
6 | The most prominent technologies are:
7 |
8 | #### Backend:
9 | - Node.js
10 | - Express.js
11 | - Mongoose.js / MongoDB
12 |
13 | #### Frontend:
14 | - Backbone.js
15 | - Marionette.js
16 | - Require.js
17 | - Jade Templates
18 | - Stylus CSS Preprocessor
19 |
20 | ### Other
21 | - CoffeeScript
22 | - Grunt based development environment
23 |
24 | Although it should already provide a solid starting point, this project is still a work in progress (See Todo's below).
25 |
26 | ## Getting started
27 |
28 | 1. In an Ubuntu 12.10 terminal, run the following command to install the required dependencies. (Or use your distribution's instructions to install Git, Node v0.8, NPM and MongoDB.)
29 |
30 | sudo apt-get install git nodejs npm mongodb-server
31 |
32 | 2. Get the project source code from GitHub
33 |
34 | git clone git@github.com:skaapgif/webapp-boilerplate.git webapp
35 | cd webapp
36 |
37 | 3. Optionally run these commands to set up the node package manager to install node packages in your home folder (useful if you don't have sudo rights, like at a university lab)
38 |
39 | cat npmrc >> ~/.npmrc
40 | cat bashrc >> ~/.bashrc
41 | source ~/.bashrc
42 |
43 | 4. Finally, install node package dependencies and start the server
44 |
45 | npm install
46 | npm install -g grunt-cli
47 | npm start
48 |
49 | 5. Point your browser to localhost:3000 and start hacking! Any changes to source files will cause grunt to recompile/reload.
50 |
51 | ## Todo
52 | - Mocha BDD api tests
53 | - Mocha BDD frontend tests using WebDriver
54 | - Introduction to folder layout & files, links to relevant tutorials
55 | - Example stylus css
56 | - MongoDB sessions
57 | - Production hardening (Cluster, error handling, monitoring)
58 | - Heroku Procfile and instructions for super easy heroku deployments
59 |
--------------------------------------------------------------------------------
/api/models/todo.coffee:
--------------------------------------------------------------------------------
1 | mongoose = require('mongoose')
2 | Schema = mongoose.Schema
3 |
4 | TodoSchema = new Schema({
5 | title: String
6 | done: Boolean
7 | })
8 |
9 | return mongoose.model('Todo', TodoSchema)
10 |
--------------------------------------------------------------------------------
/api/routes/todos.coffee:
--------------------------------------------------------------------------------
1 |
2 | Todo = require('../models/todo')
3 |
4 | # List todos
5 | exports.list = (req, res) ->
6 | res.json([{title: 'First todo'}])
7 |
8 | exports.post = (req, res) ->
9 | todo = new Todo({title: 'hello'})
10 | todo.save((err) ->
11 | if err? then next(err)
12 | res.json(todo)
13 | )
14 |
15 | exports.put = (req, res) ->
16 | res.send(404, 'Implement')
17 |
--------------------------------------------------------------------------------
/bashrc:
--------------------------------------------------------------------------------
1 | ### Add globally install node package binaries to path
2 | export PATH="$HOME/.npm/bin:$PATH"
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "application-name",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "start": "grunt"
7 | },
8 | "dependencies": {
9 | "express": "3.0.x",
10 | "jade": "0.28.x",
11 | "stylus": "0.32.x",
12 | "grunt": "0.4.x",
13 | "grunt-contrib-jade": "0.5.x",
14 | "grunt-contrib-coffee": "0.6.x",
15 | "grunt-contrib-watch": "~0.3.1",
16 | "grunt-express": "~0.3.0",
17 | "mongoose": "~3.5.9",
18 | "grunt-contrib-requirejs": "~0.4.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server.coffee:
--------------------------------------------------------------------------------
1 | http = require("http")
2 | express = require("express")
3 | todos = require("./api/routes/todos")
4 | mongoose = require('mongoose')
5 | #http = require("http")
6 | path = require("path")
7 |
8 | mongoose.connect('localhost', 'test')
9 |
10 | app = express()
11 | app.configure(() ->
12 | app.set("port", process.env.PORT or 3000)
13 | app.use(express.favicon())
14 | app.use(express.logger("dev"))
15 | app.use(express.bodyParser())
16 | app.use(app.router)
17 | app.use(express.static(path.join(__dirname, "/web/public")))
18 | )
19 |
20 | app.configure("development", () ->
21 | app.use(express.errorHandler())
22 | )
23 |
24 | #app.get("/", routes.index)
25 | app.get("/todos", todos.list)
26 | app.put("/todos", todos.post)
27 | app.put("/todos/:id", todos.put)
28 |
29 | module.exports = app
30 |
31 | ### Handled by grunt-express
32 | http.createServer(app).listen(app.get("port"), () ->
33 | console.log "Express server listening on port " + app.get("port")
34 | )
35 | ###
36 |
--------------------------------------------------------------------------------
/web/app.coffee:
--------------------------------------------------------------------------------
1 | define((require) ->
2 | $ = require('jquery')
3 | Backbone = require('backbone')
4 | Marionette = require('marionette')
5 | router = require('router')
6 |
7 | app = new Marionette.Application()
8 | app.router = router
9 |
10 | app.addInitializer((options) ->
11 | Backbone.history.start()
12 | )
13 |
14 | app.addRegions({
15 | content: '#content'
16 | })
17 |
18 | return app
19 | )
20 |
--------------------------------------------------------------------------------
/web/main.coffee:
--------------------------------------------------------------------------------
1 | requirejs.config({
2 | baseUrl: 'js'
3 | paths: {
4 | 'backbone': 'lib/backbone'
5 | 'backbone.babysitter': 'lib/backbone.babysitter'
6 | 'backbone.wreqr': 'lib/backbone.wreqr'
7 | 'jade': 'templates/jade'
8 | 'jquery': 'lib/jquery'
9 | 'marionette': 'lib/backbone.marionette'
10 | 'underscore': 'lib/underscore'
11 | }
12 | shim: {
13 | }
14 | })
15 |
16 | require(['app'], (app) ->
17 |
18 | app.start()
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/web/models/todo.coffee:
--------------------------------------------------------------------------------
1 | define((require) ->
2 |
3 | Backbone = require('backbone')
4 |
5 | return Backbone.Model.extend({
6 | urlRoot: '/todos/'
7 | })
8 | )
9 |
--------------------------------------------------------------------------------
/web/public/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 | a {
6 | color: #00b7ff;
7 | }
8 |
--------------------------------------------------------------------------------
/web/public/css/style.styl:
--------------------------------------------------------------------------------
1 | body
2 | padding: 50px
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
4 | a
5 | color: #00B7FF
--------------------------------------------------------------------------------
/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Todo List
7 |
8 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web/public/js/app.js:
--------------------------------------------------------------------------------
1 | (function() {
2 |
3 | define(function(require) {
4 | var $, Backbone, Marionette, app, router;
5 | $ = require('jquery');
6 | Backbone = require('backbone');
7 | Marionette = require('marionette');
8 | router = require('router');
9 | app = new Marionette.Application();
10 | app.router = router;
11 | app.addInitializer(function(options) {
12 | return Backbone.history.start();
13 | });
14 | app.addRegions({
15 | content: '#content'
16 | });
17 | return app;
18 | });
19 |
20 | }).call(this);
21 |
--------------------------------------------------------------------------------
/web/public/js/lib/backbone.babysitter.js:
--------------------------------------------------------------------------------
1 | // Backbone.BabySitter, v0.0.4
2 | // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
3 | // Distributed under MIT license
4 | // http://github.com/marionettejs/backbone.babysitter
5 | (function (root, factory) {
6 | if (typeof exports === 'object') {
7 |
8 | var underscore = require('underscore');
9 | var backbone = require('backbone');
10 |
11 | module.exports = factory(underscore, backbone);
12 |
13 | } else if (typeof define === 'function' && define.amd) {
14 |
15 | define(['underscore', 'backbone'], factory);
16 |
17 | }
18 | }(this, function (_, Backbone) {
19 | "option strict";
20 |
21 | // Backbone.ChildViewContainer
22 | // ---------------------------
23 | //
24 | // Provide a container to store, retrieve and
25 | // shut down child views.
26 |
27 | Backbone.ChildViewContainer = (function(Backbone, _){
28 |
29 | // Container Constructor
30 | // ---------------------
31 |
32 | var Container = function(initialViews){
33 | this._views = {};
34 | this._indexByModel = {};
35 | this._indexByCollection = {};
36 | this._indexByCustom = {};
37 | this._updateLength();
38 |
39 | this._addInitialViews(initialViews);
40 | };
41 |
42 | // Container Methods
43 | // -----------------
44 |
45 | _.extend(Container.prototype, {
46 |
47 | // Add a view to this container. Stores the view
48 | // by `cid` and makes it searchable by the model
49 | // and/or collection of the view. Optionally specify
50 | // a custom key to store an retrieve the view.
51 | add: function(view, customIndex){
52 | var viewCid = view.cid;
53 |
54 | // store the view
55 | this._views[viewCid] = view;
56 |
57 | // index it by model
58 | if (view.model){
59 | this._indexByModel[view.model.cid] = viewCid;
60 | }
61 |
62 | // index it by collection
63 | if (view.collection){
64 | this._indexByCollection[view.collection.cid] = viewCid;
65 | }
66 |
67 | // index by custom
68 | if (customIndex){
69 | this._indexByCustom[customIndex] = viewCid;
70 | }
71 |
72 | this._updateLength();
73 | },
74 |
75 | // Find a view by the model that was attached to
76 | // it. Uses the model's `cid` to find it, and
77 | // retrieves the view by it's `cid` from the result
78 | findByModel: function(model){
79 | var viewCid = this._indexByModel[model.cid];
80 | return this.findByCid(viewCid);
81 | },
82 |
83 | // Find a view by the collection that was attached to
84 | // it. Uses the collection's `cid` to find it, and
85 | // retrieves the view by it's `cid` from the result
86 | findByCollection: function(col){
87 | var viewCid = this._indexByCollection[col.cid];
88 | return this.findByCid(viewCid);
89 | },
90 |
91 | // Find a view by a custom indexer.
92 | findByCustom: function(index){
93 | var viewCid = this._indexByCustom[index];
94 | return this.findByCid(viewCid);
95 | },
96 |
97 | // Find by index. This is not guaranteed to be a
98 | // stable index.
99 | findByIndex: function(index){
100 | return _.values(this._views)[index];
101 | },
102 |
103 | // retrieve a view by it's `cid` directly
104 | findByCid: function(cid){
105 | return this._views[cid];
106 | },
107 |
108 | // Remove a view
109 | remove: function(view){
110 | var viewCid = view.cid;
111 |
112 | // delete model index
113 | if (view.model){
114 | delete this._indexByModel[view.model.cid];
115 | }
116 |
117 | // delete collection index
118 | if (view.collection){
119 | delete this._indexByCollection[view.collection.cid];
120 | }
121 |
122 | // delete custom index
123 | var cust;
124 |
125 | for (var key in this._indexByCustom){
126 | if (this._indexByCustom.hasOwnProperty(key)){
127 | if (this._indexByCustom[key] === viewCid){
128 | cust = key;
129 | break;
130 | }
131 | }
132 | }
133 |
134 | if (cust){
135 | delete this._indexByCustom[cust];
136 | }
137 |
138 | // remove the view from the container
139 | delete this._views[viewCid];
140 |
141 | // update the length
142 | this._updateLength();
143 | },
144 |
145 | // Call a method on every view in the container,
146 | // passing parameters to the call method one at a
147 | // time, like `function.call`.
148 | call: function(method, args){
149 | args = Array.prototype.slice.call(arguments, 1);
150 | this.apply(method, args);
151 | },
152 |
153 | // Apply a method on every view in the container,
154 | // passing parameters to the call method one at a
155 | // time, like `function.apply`.
156 | apply: function(method, args){
157 | var view;
158 |
159 | // fix for IE < 9
160 | args = args || [];
161 |
162 | _.each(this._views, function(view, key){
163 | if (_.isFunction(view[method])){
164 | view[method].apply(view, args);
165 | }
166 | });
167 |
168 | },
169 |
170 | // Update the `.length` attribute on this container
171 | _updateLength: function(){
172 | this.length = _.size(this._views);
173 | },
174 |
175 | // set up an initial list of views
176 | _addInitialViews: function(views){
177 | if (!views){ return; }
178 |
179 | var view, i,
180 | length = views.length;
181 |
182 | for (i=0; i').hide().appendTo('body')[0].contentWindow;
1093 | this.navigate(fragment);
1094 | }
1095 |
1096 | // Depending on whether we're using pushState or hashes, and whether
1097 | // 'onhashchange' is supported, determine how we check the URL state.
1098 | if (this._hasPushState) {
1099 | Backbone.$(window).on('popstate', this.checkUrl);
1100 | } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1101 | Backbone.$(window).on('hashchange', this.checkUrl);
1102 | } else if (this._wantsHashChange) {
1103 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1104 | }
1105 |
1106 | // Determine if we need to change the base url, for a pushState link
1107 | // opened by a non-pushState browser.
1108 | this.fragment = fragment;
1109 | var loc = this.location;
1110 | var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1111 |
1112 | // If we've started off with a route from a `pushState`-enabled browser,
1113 | // but we're currently in a browser that doesn't support it...
1114 | if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
1115 | this.fragment = this.getFragment(null, true);
1116 | this.location.replace(this.root + this.location.search + '#' + this.fragment);
1117 | // Return immediately as browser will do redirect to new url
1118 | return true;
1119 |
1120 | // Or if we've started out with a hash-based route, but we're currently
1121 | // in a browser where it could be `pushState`-based instead...
1122 | } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
1123 | this.fragment = this.getHash().replace(routeStripper, '');
1124 | this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1125 | }
1126 |
1127 | if (!this.options.silent) return this.loadUrl();
1128 | },
1129 |
1130 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1131 | // but possibly useful for unit testing Routers.
1132 | stop: function() {
1133 | Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1134 | clearInterval(this._checkUrlInterval);
1135 | History.started = false;
1136 | },
1137 |
1138 | // Add a route to be tested when the fragment changes. Routes added later
1139 | // may override previous routes.
1140 | route: function(route, callback) {
1141 | this.handlers.unshift({route: route, callback: callback});
1142 | },
1143 |
1144 | // Checks the current URL to see if it has changed, and if it has,
1145 | // calls `loadUrl`, normalizing across the hidden iframe.
1146 | checkUrl: function(e) {
1147 | var current = this.getFragment();
1148 | if (current === this.fragment && this.iframe) {
1149 | current = this.getFragment(this.getHash(this.iframe));
1150 | }
1151 | if (current === this.fragment) return false;
1152 | if (this.iframe) this.navigate(current);
1153 | this.loadUrl() || this.loadUrl(this.getHash());
1154 | },
1155 |
1156 | // Attempt to load the current URL fragment. If a route succeeds with a
1157 | // match, returns `true`. If no defined routes matches the fragment,
1158 | // returns `false`.
1159 | loadUrl: function(fragmentOverride) {
1160 | var fragment = this.fragment = this.getFragment(fragmentOverride);
1161 | var matched = _.any(this.handlers, function(handler) {
1162 | if (handler.route.test(fragment)) {
1163 | handler.callback(fragment);
1164 | return true;
1165 | }
1166 | });
1167 | return matched;
1168 | },
1169 |
1170 | // Save a fragment into the hash history, or replace the URL state if the
1171 | // 'replace' option is passed. You are responsible for properly URL-encoding
1172 | // the fragment in advance.
1173 | //
1174 | // The options object can contain `trigger: true` if you wish to have the
1175 | // route callback be fired (not usually desirable), or `replace: true`, if
1176 | // you wish to modify the current URL without adding an entry to the history.
1177 | navigate: function(fragment, options) {
1178 | if (!History.started) return false;
1179 | if (!options || options === true) options = {trigger: options};
1180 | fragment = this.getFragment(fragment || '');
1181 | if (this.fragment === fragment) return;
1182 | this.fragment = fragment;
1183 | var url = this.root + fragment;
1184 |
1185 | // If pushState is available, we use it to set the fragment as a real URL.
1186 | if (this._hasPushState) {
1187 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1188 |
1189 | // If hash changes haven't been explicitly disabled, update the hash
1190 | // fragment to store history.
1191 | } else if (this._wantsHashChange) {
1192 | this._updateHash(this.location, fragment, options.replace);
1193 | if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1194 | // Opening and closing the iframe tricks IE7 and earlier to push a
1195 | // history entry on hash-tag change. When replace is true, we don't
1196 | // want this.
1197 | if(!options.replace) this.iframe.document.open().close();
1198 | this._updateHash(this.iframe.location, fragment, options.replace);
1199 | }
1200 |
1201 | // If you've told us that you explicitly don't want fallback hashchange-
1202 | // based history, then `navigate` becomes a page refresh.
1203 | } else {
1204 | return this.location.assign(url);
1205 | }
1206 | if (options.trigger) this.loadUrl(fragment);
1207 | },
1208 |
1209 | // Update the hash location, either replacing the current entry, or adding
1210 | // a new one to the browser history.
1211 | _updateHash: function(location, fragment, replace) {
1212 | if (replace) {
1213 | var href = location.href.replace(/(javascript:|#).*$/, '');
1214 | location.replace(href + '#' + fragment);
1215 | } else {
1216 | // gh-1649: Some browsers require that `hash` contains a leading #.
1217 | location.hash = '#' + fragment;
1218 | }
1219 | }
1220 |
1221 | });
1222 |
1223 | // Create the default Backbone.history.
1224 | Backbone.history = new History;
1225 |
1226 | // Backbone.View
1227 | // -------------
1228 |
1229 | // Creating a Backbone.View creates its initial element outside of the DOM,
1230 | // if an existing element is not provided...
1231 | var View = Backbone.View = function(options) {
1232 | this.cid = _.uniqueId('view');
1233 | this._configure(options || {});
1234 | this._ensureElement();
1235 | this.initialize.apply(this, arguments);
1236 | this.delegateEvents();
1237 | };
1238 |
1239 | // Cached regex to split keys for `delegate`.
1240 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1241 |
1242 | // List of view options to be merged as properties.
1243 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1244 |
1245 | // Set up all inheritable **Backbone.View** properties and methods.
1246 | _.extend(View.prototype, Events, {
1247 |
1248 | // The default `tagName` of a View's element is `"div"`.
1249 | tagName: 'div',
1250 |
1251 | // jQuery delegate for element lookup, scoped to DOM elements within the
1252 | // current view. This should be prefered to global lookups where possible.
1253 | $: function(selector) {
1254 | return this.$el.find(selector);
1255 | },
1256 |
1257 | // Initialize is an empty function by default. Override it with your own
1258 | // initialization logic.
1259 | initialize: function(){},
1260 |
1261 | // **render** is the core function that your view should override, in order
1262 | // to populate its element (`this.el`), with the appropriate HTML. The
1263 | // convention is for **render** to always return `this`.
1264 | render: function() {
1265 | return this;
1266 | },
1267 |
1268 | // Remove this view by taking the element out of the DOM, and removing any
1269 | // applicable Backbone.Events listeners.
1270 | remove: function() {
1271 | this.$el.remove();
1272 | this.stopListening();
1273 | return this;
1274 | },
1275 |
1276 | // Change the view's element (`this.el` property), including event
1277 | // re-delegation.
1278 | setElement: function(element, delegate) {
1279 | if (this.$el) this.undelegateEvents();
1280 | this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1281 | this.el = this.$el[0];
1282 | if (delegate !== false) this.delegateEvents();
1283 | return this;
1284 | },
1285 |
1286 | // Set callbacks, where `this.events` is a hash of
1287 | //
1288 | // *{"event selector": "callback"}*
1289 | //
1290 | // {
1291 | // 'mousedown .title': 'edit',
1292 | // 'click .button': 'save'
1293 | // 'click .open': function(e) { ... }
1294 | // }
1295 | //
1296 | // pairs. Callbacks will be bound to the view, with `this` set properly.
1297 | // Uses event delegation for efficiency.
1298 | // Omitting the selector binds the event to `this.el`.
1299 | // This only works for delegate-able events: not `focus`, `blur`, and
1300 | // not `change`, `submit`, and `reset` in Internet Explorer.
1301 | delegateEvents: function(events) {
1302 | if (!(events || (events = _.result(this, 'events')))) return;
1303 | this.undelegateEvents();
1304 | for (var key in events) {
1305 | var method = events[key];
1306 | if (!_.isFunction(method)) method = this[events[key]];
1307 | if (!method) throw new Error('Method "' + events[key] + '" does not exist');
1308 | var match = key.match(delegateEventSplitter);
1309 | var eventName = match[1], selector = match[2];
1310 | method = _.bind(method, this);
1311 | eventName += '.delegateEvents' + this.cid;
1312 | if (selector === '') {
1313 | this.$el.on(eventName, method);
1314 | } else {
1315 | this.$el.on(eventName, selector, method);
1316 | }
1317 | }
1318 | },
1319 |
1320 | // Clears all callbacks previously bound to the view with `delegateEvents`.
1321 | // You usually don't need to use this, but may wish to if you have multiple
1322 | // Backbone views attached to the same DOM element.
1323 | undelegateEvents: function() {
1324 | this.$el.off('.delegateEvents' + this.cid);
1325 | },
1326 |
1327 | // Performs the initial configuration of a View with a set of options.
1328 | // Keys with special meaning *(model, collection, id, className)*, are
1329 | // attached directly to the view.
1330 | _configure: function(options) {
1331 | if (this.options) options = _.extend({}, _.result(this, 'options'), options);
1332 | _.extend(this, _.pick(options, viewOptions));
1333 | this.options = options;
1334 | },
1335 |
1336 | // Ensure that the View has a DOM element to render into.
1337 | // If `this.el` is a string, pass it through `$()`, take the first
1338 | // matching element, and re-assign it to `el`. Otherwise, create
1339 | // an element from the `id`, `className` and `tagName` properties.
1340 | _ensureElement: function() {
1341 | if (!this.el) {
1342 | var attrs = _.extend({}, _.result(this, 'attributes'));
1343 | if (this.id) attrs.id = _.result(this, 'id');
1344 | if (this.className) attrs['class'] = _.result(this, 'className');
1345 | var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1346 | this.setElement($el, false);
1347 | } else {
1348 | this.setElement(_.result(this, 'el'), false);
1349 | }
1350 | }
1351 |
1352 | });
1353 |
1354 | // Backbone.sync
1355 | // -------------
1356 |
1357 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1358 | var methodMap = {
1359 | 'create': 'POST',
1360 | 'update': 'PUT',
1361 | 'patch': 'PATCH',
1362 | 'delete': 'DELETE',
1363 | 'read': 'GET'
1364 | };
1365 |
1366 | // Override this function to change the manner in which Backbone persists
1367 | // models to the server. You will be passed the type of request, and the
1368 | // model in question. By default, makes a RESTful Ajax request
1369 | // to the model's `url()`. Some possible customizations could be:
1370 | //
1371 | // * Use `setTimeout` to batch rapid-fire updates into a single request.
1372 | // * Send up the models as XML instead of JSON.
1373 | // * Persist models via WebSockets instead of Ajax.
1374 | //
1375 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1376 | // as `POST`, with a `_method` parameter containing the true HTTP method,
1377 | // as well as all requests with the body as `application/x-www-form-urlencoded`
1378 | // instead of `application/json` with the model in a param named `model`.
1379 | // Useful when interfacing with server-side languages like **PHP** that make
1380 | // it difficult to read the body of `PUT` requests.
1381 | Backbone.sync = function(method, model, options) {
1382 | var type = methodMap[method];
1383 |
1384 | // Default options, unless specified.
1385 | _.defaults(options || (options = {}), {
1386 | emulateHTTP: Backbone.emulateHTTP,
1387 | emulateJSON: Backbone.emulateJSON
1388 | });
1389 |
1390 | // Default JSON-request options.
1391 | var params = {type: type, dataType: 'json'};
1392 |
1393 | // Ensure that we have a URL.
1394 | if (!options.url) {
1395 | params.url = _.result(model, 'url') || urlError();
1396 | }
1397 |
1398 | // Ensure that we have the appropriate request data.
1399 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1400 | params.contentType = 'application/json';
1401 | params.data = JSON.stringify(options.attrs || model.toJSON(options));
1402 | }
1403 |
1404 | // For older servers, emulate JSON by encoding the request into an HTML-form.
1405 | if (options.emulateJSON) {
1406 | params.contentType = 'application/x-www-form-urlencoded';
1407 | params.data = params.data ? {model: params.data} : {};
1408 | }
1409 |
1410 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1411 | // And an `X-HTTP-Method-Override` header.
1412 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1413 | params.type = 'POST';
1414 | if (options.emulateJSON) params.data._method = type;
1415 | var beforeSend = options.beforeSend;
1416 | options.beforeSend = function(xhr) {
1417 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
1418 | if (beforeSend) return beforeSend.apply(this, arguments);
1419 | };
1420 | }
1421 |
1422 | // Don't process data on a non-GET request.
1423 | if (params.type !== 'GET' && !options.emulateJSON) {
1424 | params.processData = false;
1425 | }
1426 |
1427 | var success = options.success;
1428 | options.success = function(resp) {
1429 | if (success) success(model, resp, options);
1430 | model.trigger('sync', model, resp, options);
1431 | };
1432 |
1433 | var error = options.error;
1434 | options.error = function(xhr) {
1435 | if (error) error(model, xhr, options);
1436 | model.trigger('error', model, xhr, options);
1437 | };
1438 |
1439 | // Make the request, allowing the user to override any Ajax options.
1440 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1441 | model.trigger('request', model, xhr, options);
1442 | return xhr;
1443 | };
1444 |
1445 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1446 | Backbone.ajax = function() {
1447 | return Backbone.$.ajax.apply(Backbone.$, arguments);
1448 | };
1449 |
1450 | // Helpers
1451 | // -------
1452 |
1453 | // Helper function to correctly set up the prototype chain, for subclasses.
1454 | // Similar to `goog.inherits`, but uses a hash of prototype properties and
1455 | // class properties to be extended.
1456 | var extend = function(protoProps, staticProps) {
1457 | var parent = this;
1458 | var child;
1459 |
1460 | // The constructor function for the new subclass is either defined by you
1461 | // (the "constructor" property in your `extend` definition), or defaulted
1462 | // by us to simply call the parent's constructor.
1463 | if (protoProps && _.has(protoProps, 'constructor')) {
1464 | child = protoProps.constructor;
1465 | } else {
1466 | child = function(){ return parent.apply(this, arguments); };
1467 | }
1468 |
1469 | // Add static properties to the constructor function, if supplied.
1470 | _.extend(child, parent, staticProps);
1471 |
1472 | // Set the prototype chain to inherit from `parent`, without calling
1473 | // `parent`'s constructor function.
1474 | var Surrogate = function(){ this.constructor = child; };
1475 | Surrogate.prototype = parent.prototype;
1476 | child.prototype = new Surrogate;
1477 |
1478 | // Add prototype properties (instance properties) to the subclass,
1479 | // if supplied.
1480 | if (protoProps) _.extend(child.prototype, protoProps);
1481 |
1482 | // Set a convenience property in case the parent's prototype is needed
1483 | // later.
1484 | child.__super__ = parent.prototype;
1485 |
1486 | return child;
1487 | };
1488 |
1489 | // Set up inheritance for the model, collection, router, view and history.
1490 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1491 |
1492 | // Throw an error when a URL is needed, and none is supplied.
1493 | var urlError = function() {
1494 | throw new Error('A "url" property or function must be specified');
1495 | };
1496 |
1497 | return Backbone;
1498 | }));
1499 |
--------------------------------------------------------------------------------
/web/public/js/lib/backbone.marionette.js:
--------------------------------------------------------------------------------
1 | // Backbone.Marionette, v1.0.0-rc5
2 | // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
3 | // Distributed under MIT license
4 | // http://github.com/marionettejs/backbone.marionette
5 |
6 | (function (root, factory) {
7 | if (typeof exports === 'object') {
8 |
9 | var jquery = require('jquery');
10 | var underscore = require('underscore');
11 | var backbone = require('backbone');
12 | var wreqr = require('backbone.wreqr');
13 | var babysitter = require('backbone.babysitter');
14 |
15 | module.exports = factory(jquery, underscore, backbone, wreqr, babysitter);
16 |
17 | } else if (typeof define === 'function' && define.amd) {
18 |
19 | define(['jquery', 'underscore', 'backbone', 'backbone.wreqr', 'backbone.babysitter'], factory);
20 |
21 | }
22 | }(this, function ($, _, Backbone) {
23 |
24 | var Marionette = (function(Backbone, _, $){
25 | "use strict";
26 |
27 | var Marionette = {};
28 | Backbone.Marionette = Marionette;
29 |
30 | // Helpers
31 | // -------
32 |
33 | // For slicing `arguments` in functions
34 | var slice = Array.prototype.slice;
35 |
36 | // Marionette.extend
37 | // -----------------
38 |
39 | // Borrow the Backbone `extend` method so we can use it as needed
40 | Marionette.extend = Backbone.Model.extend;
41 |
42 | // Marionette.getOption
43 | // --------------------
44 |
45 | // Retrieve an object, function or other value from a target
46 | // object or it's `options`, with `options` taking precedence.
47 | Marionette.getOption = function(target, optionName){
48 | if (!target || !optionName){ return; }
49 | var value;
50 |
51 | if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){
52 | value = target.options[optionName];
53 | } else {
54 | value = target[optionName];
55 | }
56 |
57 | return value;
58 | };
59 |
60 | // Mairionette.createObject
61 | // ------------------------
62 |
63 | // A wrapper / shim for `Object.create`. Uses native `Object.create`
64 | // if available, otherwise shims it in place for Marionette to use.
65 | Marionette.createObject = (function(){
66 | var createObject;
67 |
68 | // Define this once, and just replace the .prototype on it as needed,
69 | // to improve performance in older / less optimized JS engines
70 | function F() {}
71 |
72 |
73 | // Check for existing native / shimmed Object.create
74 | if (typeof Object.create === "function"){
75 |
76 | // found native/shim, so use it
77 | createObject = Object.create;
78 |
79 | } else {
80 |
81 | // An implementation of the Boodman/Crockford delegation
82 | // w/ Cornford optimization, as suggested by @unscriptable
83 | // https://gist.github.com/3959151
84 |
85 | // native/shim not found, so shim it ourself
86 | createObject = function (o) {
87 |
88 | // set the prototype of the function
89 | // so we will get `o` as the prototype
90 | // of the new object instance
91 | F.prototype = o;
92 |
93 | // create a new object that inherits from
94 | // the `o` parameter
95 | var child = new F();
96 |
97 | // clean up just in case o is really large
98 | F.prototype = null;
99 |
100 | // send it back
101 | return child;
102 | };
103 |
104 | }
105 |
106 | return createObject;
107 | })();
108 |
109 | // Trigger an event and a corresponding method name. Examples:
110 | //
111 | // `this.triggerMethod("foo")` will trigger the "foo" event and
112 | // call the "onFoo" method.
113 | //
114 | // `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and
115 | // call the "onFooBar" method.
116 | Marionette.triggerMethod = function(){
117 | var args = Array.prototype.slice.apply(arguments);
118 | var eventName = args[0];
119 | var segments = eventName.split(":");
120 | var segment, capLetter, methodName = "on";
121 |
122 | for (var i = 0; i < segments.length; i++){
123 | segment = segments[i];
124 | capLetter = segment.charAt(0).toUpperCase();
125 | methodName += capLetter + segment.slice(1);
126 | }
127 |
128 | this.trigger.apply(this, args);
129 |
130 | if (_.isFunction(this[methodName])){
131 | args.shift();
132 | return this[methodName].apply(this, args);
133 | }
134 | };
135 |
136 | // DOMRefresh
137 | // ----------
138 | //
139 | // Monitor a view's state, and after it has been rendered and shown
140 | // in the DOM, trigger a "dom:refresh" event every time it is
141 | // re-rendered.
142 |
143 | Marionette.MonitorDOMRefresh = (function(){
144 | // track when the view has been rendered
145 | function handleShow(view){
146 | view._isShown = true;
147 | triggerDOMRefresh(view);
148 | }
149 |
150 | // track when the view has been shown in the DOM,
151 | // using a Marionette.Region (or by other means of triggering "show")
152 | function handleRender(view){
153 | view._isRendered = true;
154 | triggerDOMRefresh(view);
155 | }
156 |
157 | // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
158 | function triggerDOMRefresh(view){
159 | if (view._isShown && view._isRendered){
160 | if (_.isFunction(view.triggerMethod)){
161 | view.triggerMethod("dom:refresh");
162 | }
163 | }
164 | }
165 |
166 | // Export public API
167 | return function(view){
168 | view.listenTo(view, "show", function(){
169 | handleShow(view);
170 | });
171 |
172 | view.listenTo(view, "render", function(){
173 | handleRender(view);
174 | });
175 | };
176 | })();
177 |
178 |
179 | // Marionette.bindEntityEvents & unbindEntityEvents
180 | // ---------------------------
181 | //
182 | // These methods are used to bind/unbind a backbone "entity" (collection/model)
183 | // to methods on a target object.
184 | //
185 | // The first paremter, `target`, must have a `listenTo` method from the
186 | // EventBinder object.
187 | //
188 | // The second parameter is the entity (Backbone.Model or Backbone.Collection)
189 | // to bind the events from.
190 | //
191 | // The third parameter is a hash of { "event:name": "eventHandler" }
192 | // configuration. Multiple handlers can be separated by a space. A
193 | // function can be supplied instead of a string handler name.
194 |
195 | (function(Marionette){
196 | "use strict";
197 |
198 | // Bind the event to handlers specified as a string of
199 | // handler names on the target object
200 | function bindFromStrings(target, entity, evt, methods){
201 | var methodNames = methods.split(/\s+/);
202 |
203 | _.each(methodNames,function(methodName) {
204 |
205 | var method = target[methodName];
206 | if(!method) {
207 | throw new Error("Method '"+ methodName +"' was configured as an event handler, but does not exist.");
208 | }
209 |
210 | target.listenTo(entity, evt, method, target);
211 | });
212 | }
213 |
214 | // Bind the event to a supplied callback function
215 | function bindToFunction(target, entity, evt, method){
216 | target.listenTo(entity, evt, method, target);
217 | }
218 |
219 | // Bind the event to handlers specified as a string of
220 | // handler names on the target object
221 | function unbindFromStrings(target, entity, evt, methods){
222 | var methodNames = methods.split(/\s+/);
223 |
224 | _.each(methodNames,function(methodName) {
225 | var method = target[method];
226 | target.stopListening(entity, evt, method, target);
227 | });
228 | }
229 |
230 | // Bind the event to a supplied callback function
231 | function unbindToFunction(target, entity, evt, method){
232 | target.stopListening(entity, evt, method, target);
233 | }
234 |
235 |
236 | // generic looping function
237 | function iterateEvents(target, entity, bindings, functionCallback, stringCallback){
238 | if (!entity || !bindings) { return; }
239 |
240 | // allow the bindings to be a function
241 | if (_.isFunction(bindings)){
242 | bindings = bindings.call(target);
243 | }
244 |
245 | // iterate the bindings and bind them
246 | _.each(bindings, function(methods, evt){
247 |
248 | // allow for a function as the handler,
249 | // or a list of event names as a string
250 | if (_.isFunction(methods)){
251 | functionCallback(target, entity, evt, methods);
252 | } else {
253 | stringCallback(target, entity, evt, methods);
254 | }
255 |
256 | });
257 | }
258 |
259 | // Export Public API
260 | Marionette.bindEntityEvents = function(target, entity, bindings){
261 | iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings);
262 | };
263 |
264 | Marionette.unbindEntityEvents = function(target, entity, bindings){
265 | iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings);
266 | };
267 |
268 | })(Marionette);
269 |
270 |
271 | // Callbacks
272 | // ---------
273 |
274 | // A simple way of managing a collection of callbacks
275 | // and executing them at a later point in time, using jQuery's
276 | // `Deferred` object.
277 | Marionette.Callbacks = function(){
278 | this._deferred = $.Deferred();
279 | this._callbacks = [];
280 | };
281 |
282 | _.extend(Marionette.Callbacks.prototype, {
283 |
284 | // Add a callback to be executed. Callbacks added here are
285 | // guaranteed to execute, even if they are added after the
286 | // `run` method is called.
287 | add: function(callback, contextOverride){
288 | this._callbacks.push({cb: callback, ctx: contextOverride});
289 |
290 | this._deferred.done(function(context, options){
291 | if (contextOverride){ context = contextOverride; }
292 | callback.call(context, options);
293 | });
294 | },
295 |
296 | // Run all registered callbacks with the context specified.
297 | // Additional callbacks can be added after this has been run
298 | // and they will still be executed.
299 | run: function(options, context){
300 | this._deferred.resolve(context, options);
301 | },
302 |
303 | // Resets the list of callbacks to be run, allowing the same list
304 | // to be run multiple times - whenever the `run` method is called.
305 | reset: function(){
306 | var that = this;
307 | var callbacks = this._callbacks;
308 | this._deferred = $.Deferred();
309 | this._callbacks = [];
310 | _.each(callbacks, function(cb){
311 | that.add(cb.cb, cb.ctx);
312 | });
313 | }
314 | });
315 |
316 |
317 | // Marionette Controller
318 | // ---------------------
319 | //
320 | // A multi-purpose object to use as a controller for
321 | // modules and routers, and as a mediator for workflow
322 | // and coordination of other objects, views, and more.
323 | Marionette.Controller = function(options){
324 | this.triggerMethod = Marionette.triggerMethod;
325 | this.options = options || {};
326 |
327 | if (_.isFunction(this.initialize)){
328 | this.initialize(this.options);
329 | }
330 | };
331 |
332 | Marionette.Controller.extend = Marionette.extend;
333 |
334 | // Controller Methods
335 | // --------------
336 |
337 | // Ensure it can trigger events with Backbone.Events
338 | _.extend(Marionette.Controller.prototype, Backbone.Events, {
339 | close: function(){
340 | this.stopListening();
341 | this.triggerMethod("close");
342 | this.unbind();
343 | }
344 | });
345 |
346 | // Region
347 | // ------
348 | //
349 | // Manage the visual regions of your composite application. See
350 | // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
351 |
352 | Marionette.Region = function(options){
353 | this.options = options || {};
354 |
355 | this.el = Marionette.getOption(this, "el");
356 |
357 | if (!this.el){
358 | var err = new Error("An 'el' must be specified for a region.");
359 | err.name = "NoElError";
360 | throw err;
361 | }
362 |
363 | if (this.initialize){
364 | var args = Array.prototype.slice.apply(arguments);
365 | this.initialize.apply(this, args);
366 | }
367 | };
368 |
369 |
370 | // Region Type methods
371 | // -------------------
372 |
373 | _.extend(Marionette.Region, {
374 |
375 | // Build an instance of a region by passing in a configuration object
376 | // and a default region type to use if none is specified in the config.
377 | //
378 | // The config object should either be a string as a jQuery DOM selector,
379 | // a Region type directly, or an object literal that specifies both
380 | // a selector and regionType:
381 | //
382 | // ```js
383 | // {
384 | // selector: "#foo",
385 | // regionType: MyCustomRegion
386 | // }
387 | // ```
388 | //
389 | buildRegion: function(regionConfig, defaultRegionType){
390 | var regionIsString = (typeof regionConfig === "string");
391 | var regionSelectorIsString = (typeof regionConfig.selector === "string");
392 | var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined");
393 | var regionIsType = (typeof regionConfig === "function");
394 |
395 | if (!regionIsType && !regionIsString && !regionSelectorIsString) {
396 | throw new Error("Region must be specified as a Region type, a selector string or an object with selector property");
397 | }
398 |
399 | var selector, RegionType;
400 |
401 | // get the selector for the region
402 |
403 | if (regionIsString) {
404 | selector = regionConfig;
405 | }
406 |
407 | if (regionConfig.selector) {
408 | selector = regionConfig.selector;
409 | }
410 |
411 | // get the type for the region
412 |
413 | if (regionIsType){
414 | RegionType = regionConfig;
415 | }
416 |
417 | if (!regionIsType && regionTypeIsUndefined) {
418 | RegionType = defaultRegionType;
419 | }
420 |
421 | if (regionConfig.regionType) {
422 | RegionType = regionConfig.regionType;
423 | }
424 |
425 | // build the region instance
426 |
427 | var regionManager = new RegionType({
428 | el: selector
429 | });
430 |
431 | return regionManager;
432 | }
433 |
434 | });
435 |
436 | // Region Instance Methods
437 | // -----------------------
438 |
439 | _.extend(Marionette.Region.prototype, Backbone.Events, {
440 |
441 | // Displays a backbone view instance inside of the region.
442 | // Handles calling the `render` method for you. Reads content
443 | // directly from the `el` attribute. Also calls an optional
444 | // `onShow` and `close` method on your view, just after showing
445 | // or just before closing the view, respectively.
446 | show: function(view){
447 |
448 | this.ensureEl();
449 | this.close();
450 |
451 | view.render();
452 | this.open(view);
453 |
454 | Marionette.triggerMethod.call(view, "show");
455 | Marionette.triggerMethod.call(this, "show", view);
456 |
457 | this.currentView = view;
458 | },
459 |
460 | ensureEl: function(){
461 | if (!this.$el || this.$el.length === 0){
462 | this.$el = this.getEl(this.el);
463 | }
464 | },
465 |
466 | // Override this method to change how the region finds the
467 | // DOM element that it manages. Return a jQuery selector object.
468 | getEl: function(selector){
469 | return $(selector);
470 | },
471 |
472 | // Override this method to change how the new view is
473 | // appended to the `$el` that the region is managing
474 | open: function(view){
475 | this.$el.empty().append(view.el);
476 | },
477 |
478 | // Close the current view, if there is one. If there is no
479 | // current view, it does nothing and returns immediately.
480 | close: function(){
481 | var view = this.currentView;
482 | if (!view || view.isClosed){ return; }
483 |
484 | if (view.close) { view.close(); }
485 | Marionette.triggerMethod.call(this, "close");
486 |
487 | delete this.currentView;
488 | },
489 |
490 | // Attach an existing view to the region. This
491 | // will not call `render` or `onShow` for the new view,
492 | // and will not replace the current HTML for the `el`
493 | // of the region.
494 | attachView: function(view){
495 | this.currentView = view;
496 | },
497 |
498 | // Reset the region by closing any existing view and
499 | // clearing out the cached `$el`. The next time a view
500 | // is shown via this region, the region will re-query the
501 | // DOM for the region's `el`.
502 | reset: function(){
503 | this.close();
504 | delete this.$el;
505 | }
506 | });
507 |
508 | // Copy the `extend` function used by Backbone's classes
509 | Marionette.Region.extend = Marionette.extend;
510 |
511 |
512 | // Template Cache
513 | // --------------
514 |
515 | // Manage templates stored in `