Commit 2233e565 authored by Ben McCormick's avatar Ben McCormick

Removed deprecated code and concepts from Backbone.Marionette example

- Removed modules, replacing them with a common global namespace
- Added Backbone.Radio as a new dependency
- Removed usage of Wreqr, replacing it with Radio
- Renamed TodoList.js to Router.js to more accurately reflect its
  function
- Moved all application initialization into TodoMVC.Application
- Moved FilterState code into its own file
- Removed inline script in index.html and moved inititialization code
  into TodoMVC.js
parent 987bc94c
...@@ -63,20 +63,14 @@ ...@@ -63,20 +63,14 @@
<script src="node_modules/backbone/backbone.js"></script> <script src="node_modules/backbone/backbone.js"></script>
<script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script> <script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<script src="node_modules/backbone.marionette/lib/backbone.marionette.js"></script> <script src="node_modules/backbone.marionette/lib/backbone.marionette.js"></script>
<script src="node_modules/backbone.radio/build/backbone.radio.js"></script>
<!-- application --> <!-- application -->
<script src="js/TodoMVC.js"></script> <script src="js/TodoMVC.Application.js"></script>
<script src="js/TodoMVC.Todos.js"></script> <script src="js/TodoMVC.Todos.js"></script>
<script src="js/TodoMVC.Layout.js"></script> <script src="js/TodoMVC.Layout.js"></script>
<script src="js/TodoMVC.TodoList.Views.js"></script> <script src="js/TodoMVC.TodoList.Views.js"></script>
<script src="js/TodoMVC.TodoList.js"></script> <script src="js/TodoMVC.Router.js"></script>
<script> <script src="js/TodoMVC.FilterState.js"></script>
$(function () { <script src="js/TodoMVC.js"></script>
TodoMVC.on('start', function () {
Backbone.history.start();
});
// start the TodoMVC app (defined in js/TodoMVC.js)
TodoMVC.start();
});
</script>
</body> </body>
</html> </html>
/*global Backbone, TodoMVC:true */
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
var TodoApp = Backbone.Marionette.Application.extend({
setRootLayout: function () {
this.root = new TodoMVC.RootLayout();
}
});
// The Application Object is responsible for kicking off
// a Marionette application when its start function is called
//
// This application has a singler root Layout that is attached
// before it is started. Other system components can listen
// for the application start event, and perform initialization
// on that event
TodoMVC.App = new TodoApp();
TodoMVC.App.on('before:start', function () {
TodoMVC.App.setRootLayout();
});
})();
/*global Backbone */
// This file acts as a Service, providing
// the rest of the app access to the filter state
// as needed, without them needing to know the implementation
// details
(function () {
'use strict';
var filterState = new Backbone.Model({
filter: 'all'
});
var filterChannel = Backbone.Radio.channel('filter');
filterChannel.reply('filterState', function () {
return filterState;
});
})();
/*global TodoMVC */ /*global TodoMVC:true, Backbone */
'use strict';
TodoMVC.module('Layout', function (Layout, App, Backbone) { var TodoMVC = TodoMVC || {};
(function () {
'use strict';
var filterChannel = Backbone.Radio.channel('filter');
TodoMVC.RootLayout = Backbone.Marionette.LayoutView.extend({
Layout.Root = Backbone.Marionette.LayoutView.extend({
el: '#todoapp', el: '#todoapp',
regions: { regions: {
header: '#header', header: '#header',
main: '#main', main: '#main',
...@@ -14,7 +20,8 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) { ...@@ -14,7 +20,8 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Header View // Layout Header View
// ------------------ // ------------------
Layout.Header = Backbone.Marionette.ItemView.extend({ TodoMVC.HeaderLayout = Backbone.Marionette.ItemView.extend({
template: '#template-header', template: '#template-header',
// UI bindings create cached attributes that // UI bindings create cached attributes that
...@@ -53,7 +60,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) { ...@@ -53,7 +60,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Footer View // Layout Footer View
// ------------------ // ------------------
Layout.Footer = Backbone.Marionette.ItemView.extend({ TodoMVC.FooterLayout = Backbone.Marionette.ItemView.extend({
template: '#template-footer', template: '#template-footer',
// UI bindings create cached attributes that // UI bindings create cached attributes that
...@@ -82,7 +89,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) { ...@@ -82,7 +89,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
}, },
initialize: function () { initialize: function () {
this.listenTo(App.request('filterState'), 'change:filter', this.updateFilterSelection, this); this.listenTo(filterChannel.request('filterState'), 'change:filter', this.updateFilterSelection, this);
}, },
serializeData: function () { serializeData: function () {
...@@ -103,7 +110,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) { ...@@ -103,7 +110,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
updateFilterSelection: function () { updateFilterSelection: function () {
this.ui.filters.removeClass('selected'); this.ui.filters.removeClass('selected');
this.ui[App.request('filterState').get('filter')] this.ui[filterChannel.request('filterState').get('filter')]
.addClass('selected'); .addClass('selected');
}, },
...@@ -114,4 +121,4 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) { ...@@ -114,4 +121,4 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
}); });
} }
}); });
}); })();
/*global TodoMVC */ /*global TodoMVC:true, Backbone, $ */
'use strict';
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
var filterChannel = Backbone.Radio.channel('filter');
TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
// TodoList Router // TodoList Router
// --------------- // ---------------
// //
// Handle routes to show the active vs complete todo items // Handles a single dynamic route to show
TodoList.Router = Marionette.AppRouter.extend({ // the active vs complete todo items
TodoMVC.Router = Backbone.Marionette.AppRouter.extend({
appRoutes: { appRoutes: {
'*filter': 'filterItems' '*filter': 'filterItems'
} }
...@@ -17,10 +23,12 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) { ...@@ -17,10 +23,12 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
// //
// Control the workflow and logic that exists at the application // Control the workflow and logic that exists at the application
// level, above the implementation detail of views and models // level, above the implementation detail of views and models
TodoList.Controller = Marionette.Object.extend({ TodoMVC.Controller = Backbone.Marionette.Object.extend({
initialize: function () { initialize: function () {
this.todoList = new App.Todos.TodoList(); this.todoList = new TodoMVC.TodoList();
}, },
// Start the app by showing the appropriate views // Start the app by showing the appropriate views
// and fetching the list of todo items, if there are any // and fetching the list of todo items, if there are any
start: function () { start: function () {
...@@ -36,21 +44,21 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) { ...@@ -36,21 +44,21 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
}, },
showHeader: function (todoList) { showHeader: function (todoList) {
var header = new App.Layout.Header({ var header = new TodoMVC.HeaderLayout({
collection: todoList collection: todoList
}); });
App.root.showChildView('header', header); TodoMVC.App.root.showChildView('header', header);
}, },
showFooter: function (todoList) { showFooter: function (todoList) {
var footer = new App.Layout.Footer({ var footer = new TodoMVC.FooterLayout({
collection: todoList collection: todoList
}); });
App.root.showChildView('footer', footer); TodoMVC.App.root.showChildView('footer', footer);
}, },
showTodoList: function (todoList) { showTodoList: function (todoList) {
App.root.showChildView('main', new TodoList.Views.ListView({ TodoMVC.App.root.showChildView('main', new TodoMVC.ListView({
collection: todoList collection: todoList
})); }));
}, },
...@@ -58,22 +66,7 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) { ...@@ -58,22 +66,7 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
// Set the filter to show complete or all items // Set the filter to show complete or all items
filterItems: function (filter) { filterItems: function (filter) {
var newFilter = filter && filter.trim() || 'all'; var newFilter = filter && filter.trim() || 'all';
App.request('filterState').set('filter', newFilter); filterChannel.request('filterState').set('filter', newFilter);
} }
}); });
})();
// On App start
// --------------------
//
// Get the TodoList up and running by initializing the mediator
// when the the application is started, pulling in all of the
// existing Todo items and displaying them.
App.on('start', function () {
var controller = new TodoList.Controller();
controller.router = new TodoList.Router({
controller: controller
});
controller.start();
});
});
/*global TodoMVC */ /*global TodoMVC: true, Backbone */
'use strict';
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
var filterChannel = Backbone.Radio.channel('filter');
TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
// Todo List Item View // Todo List Item View
// ------------------- // -------------------
// //
// Display an individual todo item, and respond to changes // Display an individual todo item, and respond to changes
// that are made to the item, including marking completed. // that are made to the item, including marking completed.
Views.ItemView = Marionette.ItemView.extend({ TodoMVC.TodoView = Backbone.Marionette.ItemView.extend({
tagName: 'li', tagName: 'li',
template: '#template-todoItemView', template: '#template-todoItemView',
className: function () { className: function () {
return this.model.get('completed') ? 'completed' : 'active'; return this.model.get('completed') ? 'completed' : 'active';
}, },
...@@ -78,9 +86,12 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) { ...@@ -78,9 +86,12 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
// //
// Controls the rendering of the list of items, including the // Controls the rendering of the list of items, including the
// filtering of activs vs completed items for display. // filtering of activs vs completed items for display.
Views.ListView = Backbone.Marionette.CompositeView.extend({ TodoMVC.ListView = Backbone.Marionette.CompositeView.extend({
template: '#template-todoListCompositeView', template: '#template-todoListCompositeView',
childView: Views.ItemView,
childView: TodoMVC.TodoView,
childViewContainer: '#todo-list', childViewContainer: '#todo-list',
ui: { ui: {
...@@ -97,11 +108,11 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) { ...@@ -97,11 +108,11 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
}, },
initialize: function () { initialize: function () {
this.listenTo(App.request('filterState'), 'change:filter', this.render, this); this.listenTo(filterChannel.request('filterState'), 'change:filter', this.render, this);
}, },
filter: function (child) { filter: function (child) {
var filteredOn = App.request('filterState').get('filter'); var filteredOn = filterChannel.request('filterState').get('filter');
return child.matchesFilter(filteredOn); return child.matchesFilter(filteredOn);
}, },
...@@ -123,4 +134,4 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) { ...@@ -123,4 +134,4 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
}); });
} }
}); });
}); })();
/*global TodoMVC */ /*global Backbone, TodoMVC:true */
'use strict';
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
TodoMVC.module('Todos', function (Todos, App, Backbone) {
// Todo Model // Todo Model
// ---------- // ----------
Todos.Todo = Backbone.Model.extend({ TodoMVC.Todo = Backbone.Model.extend({
defaults: { defaults: {
title: '', title: '',
completed: false, completed: false,
...@@ -40,8 +43,8 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) { ...@@ -40,8 +43,8 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) {
// Todo Collection // Todo Collection
// --------------- // ---------------
Todos.TodoList = Backbone.Collection.extend({ TodoMVC.TodoList = Backbone.Collection.extend({
model: Todos.Todo, model: TodoMVC.Todo,
localStorage: new Backbone.LocalStorage('todos-backbone-marionette'), localStorage: new Backbone.LocalStorage('todos-backbone-marionette'),
...@@ -59,4 +62,4 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) { ...@@ -59,4 +62,4 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) {
return todo.isCompleted(); return todo.isCompleted();
} }
}); });
}); })();
/*global Backbone */ /*global Backbone, TodoMVC:true, $ */
'use strict';
// TodoMVC is global for developing in the console var TodoMVC = TodoMVC || {};
// and functional testing.
var App = Backbone.Marionette.Application.extend({
setRootLayout: function () {
this.root = new TodoMVC.Layout.Root();
}
});
window.TodoMVC = new App(); $(function () {
'use strict';
(function () { // After we initialize the app, we want to kick off the router
var filterState = new Backbone.Model({ // and controller, which will handle initializing our Views
filter: 'all' TodoMVC.App.on('start', function () {
}); Backbone.history.start();
var controller = new TodoMVC.Controller();
controller.router = new TodoMVC.Router({
controller: controller
});
TodoMVC.reqres.setHandler('filterState', function () { controller.start();
return filterState;
}); });
})();
TodoMVC.on('before:start', function () { // start the TodoMVC app (defined in js/TodoMVC.js)
TodoMVC.setRootLayout(); TodoMVC.App.start();
}); });
// Backbone.Radio v1.0.2
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory(require("underscore"), require("backbone")) : typeof define === "function" && define.amd ? define(["underscore", "backbone"], factory) : global.Backbone.Radio = factory(global._, global.Backbone);
})(this, function (_, Backbone) {
"use strict";
var previousRadio = Backbone.Radio;
var Radio = Backbone.Radio = {};
Radio.VERSION = "1.0.2";
// This allows you to run multiple instances of Radio on the same
// webapp. After loading the new version, call `noConflict()` to
// get a reference to it. At the same time the old version will be
// returned to Backbone.Radio.
Radio.noConflict = function () {
Backbone.Radio = previousRadio;
return this;
};
// Whether or not we're in DEBUG mode or not. DEBUG mode helps you
// get around the issues of lack of warnings when events are mis-typed.
Radio.DEBUG = false;
// Format debug text.
Radio._debugText = function (warning, eventName, channelName) {
return warning + (channelName ? " on the " + channelName + " channel" : "") + ": \"" + eventName + "\"";
};
// This is the method that's called when an unregistered event was called.
// By default, it logs warning to the console. By overriding this you could
// make it throw an Error, for instance. This would make firing a nonexistent event
// have the same consequence as firing a nonexistent method on an Object.
Radio.debugLog = function (warning, eventName, channelName) {
if (Radio.DEBUG && console && console.warn) {
console.warn(Radio._debugText(warning, eventName, channelName));
}
};
var eventSplitter = /\s+/;
// An internal method used to handle Radio's method overloading for Requests.
// It's borrowed from Backbone.Events. It differs from Backbone's overload
// API (which is used in Backbone.Events) in that it doesn't support space-separated
// event names.
Radio._eventsApi = function (obj, action, name, rest) {
if (!name) {
return false;
}
var results = {};
// Handle event maps.
if (typeof name === "object") {
for (var key in name) {
var result = obj[action].apply(obj, [key, name[key]].concat(rest));
eventSplitter.test(key) ? _.extend(results, result) : results[key] = result;
}
return results;
}
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
results[names[i]] = obj[action].apply(obj, [names[i]].concat(rest));
}
return results;
}
return false;
};
// An optimized way to execute callbacks.
Radio._callHandler = function (callback, context, args) {
var a1 = args[0],
a2 = args[1],
a3 = args[2];
switch (args.length) {
case 0:
return callback.call(context);
case 1:
return callback.call(context, a1);
case 2:
return callback.call(context, a1, a2);
case 3:
return callback.call(context, a1, a2, a3);
default:
return callback.apply(context, args);
}
};
// A helper used by `off` methods to the handler from the store
function removeHandler(store, name, callback, context) {
var event = store[name];
if ((!callback || (callback === event.callback || callback === event.callback._callback)) && (!context || context === event.context)) {
delete store[name];
return true;
}
}
function removeHandlers(store, name, callback, context) {
store || (store = {});
var names = name ? [name] : _.keys(store);
var matched = false;
for (var i = 0, length = names.length; i < length; i++) {
name = names[i];
// If there's no event by this name, log it and continue
// with the loop
if (!store[name]) {
continue;
}
if (removeHandler(store, name, callback, context)) {
matched = true;
}
}
return matched;
}
/*
* tune-in
* -------
* Get console logs of a channel's activity
*
*/
var _logs = {};
// This is to produce an identical function in both tuneIn and tuneOut,
// so that Backbone.Events unregisters it.
function _partial(channelName) {
return _logs[channelName] || (_logs[channelName] = _.partial(Radio.log, channelName));
}
_.extend(Radio, {
// Log information about the channel and event
log: function log(channelName, eventName) {
var args = _.rest(arguments, 2);
console.log("[" + channelName + "] \"" + eventName + "\"", args);
},
// Logs all events on this channel to the console. It sets an
// internal value on the channel telling it we're listening,
// then sets a listener on the Backbone.Events
tuneIn: function tuneIn(channelName) {
var channel = Radio.channel(channelName);
channel._tunedIn = true;
channel.on("all", _partial(channelName));
return this;
},
// Stop logging all of the activities on this channel to the console
tuneOut: function tuneOut(channelName) {
var channel = Radio.channel(channelName);
channel._tunedIn = false;
channel.off("all", _partial(channelName));
delete _logs[channelName];
return this;
}
});
/*
* Backbone.Radio.Requests
* -----------------------
* A messaging system for requesting data.
*
*/
function makeCallback(callback) {
return _.isFunction(callback) ? callback : function () {
return callback;
};
}
Radio.Requests = {
// Make a request
request: function request(name) {
var args = _.rest(arguments);
var results = Radio._eventsApi(this, "request", name, args);
if (results) {
return results;
}
var channelName = this.channelName;
var requests = this._requests;
// Check if we should log the request, and if so, do it
if (channelName && this._tunedIn) {
Radio.log.apply(this, [channelName, name].concat(args));
}
// If the request isn't handled, log it in DEBUG mode and exit
if (requests && (requests[name] || requests["default"])) {
var handler = requests[name] || requests["default"];
args = requests[name] ? args : arguments;
return Radio._callHandler(handler.callback, handler.context, args);
} else {
Radio.debugLog("An unhandled request was fired", name, channelName);
}
},
// Set up a handler for a request
reply: function reply(name, callback, context) {
if (Radio._eventsApi(this, "reply", name, [callback, context])) {
return this;
}
this._requests || (this._requests = {});
if (this._requests[name]) {
Radio.debugLog("A request was overwritten", name, this.channelName);
}
this._requests[name] = {
callback: makeCallback(callback),
context: context || this
};
return this;
},
// Set up a handler that can only be requested once
replyOnce: function replyOnce(name, callback, context) {
if (Radio._eventsApi(this, "replyOnce", name, [callback, context])) {
return this;
}
var self = this;
var once = _.once(function () {
self.stopReplying(name);
return makeCallback(callback).apply(this, arguments);
});
return this.reply(name, once, context);
},
// Remove handler(s)
stopReplying: function stopReplying(name, callback, context) {
if (Radio._eventsApi(this, "stopReplying", name)) {
return this;
}
// Remove everything if there are no arguments passed
if (!name && !callback && !context) {
delete this._requests;
} else if (!removeHandlers(this._requests, name, callback, context)) {
Radio.debugLog("Attempted to remove the unregistered request", name, this.channelName);
}
return this;
}
};
/*
* Backbone.Radio.channel
* ----------------------
* Get a reference to a channel by name.
*
*/
Radio._channels = {};
Radio.channel = function (channelName) {
if (!channelName) {
throw new Error("You must provide a name for the channel.");
}
if (Radio._channels[channelName]) {
return Radio._channels[channelName];
} else {
return Radio._channels[channelName] = new Radio.Channel(channelName);
}
};
/*
* Backbone.Radio.Channel
* ----------------------
* A Channel is an object that extends from Backbone.Events,
* and Radio.Requests.
*
*/
Radio.Channel = function (channelName) {
this.channelName = channelName;
};
_.extend(Radio.Channel.prototype, Backbone.Events, Radio.Requests, {
// Remove all handlers from the messaging systems of this channel
reset: function reset() {
this.off();
this.stopListening();
this.stopReplying();
return this;
}
});
/*
* Top-level API
* -------------
* Supplies the 'top-level API' for working with Channels directly
* from Backbone.Radio.
*
*/
var channel,
args,
systems = [Backbone.Events, Radio.Commands, Radio.Requests];
_.each(systems, function (system) {
_.each(system, function (method, methodName) {
Radio[methodName] = function (channelName) {
args = _.rest(arguments);
channel = this.channel(channelName);
return channel[methodName].apply(channel, args);
};
});
});
Radio.reset = function (channelName) {
var channels = !channelName ? this._channels : [this._channels[channelName]];
_.invoke(channels, "reset");
};
var backbone_radio = Radio;
return backbone_radio;
});
//# sourceMappingURL=./backbone.radio.js.map
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"backbone": "^1.1.2", "backbone": "^1.1.2",
"backbone.localstorage": "^1.1.6", "backbone.localstorage": "^1.1.6",
"backbone.marionette": "^2.4.1", "backbone.marionette": "^2.4.1",
"backbone.radio": "^1.0.1",
"jquery": "^1.11.2", "jquery": "^1.11.2",
"todomvc-app-css": "^1.0.1", "todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1", "todomvc-common": "^1.0.1",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment