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 @@
<script src="node_modules/backbone/backbone.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.radio/build/backbone.radio.js"></script>
<!-- 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.Layout.js"></script>
<script src="js/TodoMVC.TodoList.Views.js"></script>
<script src="js/TodoMVC.TodoList.js"></script>
<script>
$(function () {
TodoMVC.on('start', function () {
Backbone.history.start();
});
// start the TodoMVC app (defined in js/TodoMVC.js)
TodoMVC.start();
});
</script>
<script src="js/TodoMVC.Router.js"></script>
<script src="js/TodoMVC.FilterState.js"></script>
<script src="js/TodoMVC.js"></script>
</body>
</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 */
'use strict';
/*global TodoMVC:true, Backbone */
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',
regions: {
header: '#header',
main: '#main',
......@@ -14,7 +20,8 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Header View
// ------------------
Layout.Header = Backbone.Marionette.ItemView.extend({
TodoMVC.HeaderLayout = Backbone.Marionette.ItemView.extend({
template: '#template-header',
// UI bindings create cached attributes that
......@@ -53,7 +60,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Footer View
// ------------------
Layout.Footer = Backbone.Marionette.ItemView.extend({
TodoMVC.FooterLayout = Backbone.Marionette.ItemView.extend({
template: '#template-footer',
// UI bindings create cached attributes that
......@@ -82,7 +89,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
},
initialize: function () {
this.listenTo(App.request('filterState'), 'change:filter', this.updateFilterSelection, this);
this.listenTo(filterChannel.request('filterState'), 'change:filter', this.updateFilterSelection, this);
},
serializeData: function () {
......@@ -103,7 +110,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
updateFilterSelection: function () {
this.ui.filters.removeClass('selected');
this.ui[App.request('filterState').get('filter')]
this.ui[filterChannel.request('filterState').get('filter')]
.addClass('selected');
},
......@@ -114,4 +121,4 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
});
}
});
});
})();
/*global TodoMVC */
'use strict';
/*global TodoMVC:true, Backbone, $ */
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
var filterChannel = Backbone.Radio.channel('filter');
TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
// TodoList Router
// ---------------
//
// Handle routes to show the active vs complete todo items
TodoList.Router = Marionette.AppRouter.extend({
// Handles a single dynamic route to show
// the active vs complete todo items
TodoMVC.Router = Backbone.Marionette.AppRouter.extend({
appRoutes: {
'*filter': 'filterItems'
}
......@@ -17,10 +23,12 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
//
// Control the workflow and logic that exists at the application
// level, above the implementation detail of views and models
TodoList.Controller = Marionette.Object.extend({
TodoMVC.Controller = Backbone.Marionette.Object.extend({
initialize: function () {
this.todoList = new App.Todos.TodoList();
this.todoList = new TodoMVC.TodoList();
},
// Start the app by showing the appropriate views
// and fetching the list of todo items, if there are any
start: function () {
......@@ -36,21 +44,21 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
},
showHeader: function (todoList) {
var header = new App.Layout.Header({
var header = new TodoMVC.HeaderLayout({
collection: todoList
});
App.root.showChildView('header', header);
TodoMVC.App.root.showChildView('header', header);
},
showFooter: function (todoList) {
var footer = new App.Layout.Footer({
var footer = new TodoMVC.FooterLayout({
collection: todoList
});
App.root.showChildView('footer', footer);
TodoMVC.App.root.showChildView('footer', footer);
},
showTodoList: function (todoList) {
App.root.showChildView('main', new TodoList.Views.ListView({
TodoMVC.App.root.showChildView('main', new TodoMVC.ListView({
collection: todoList
}));
},
......@@ -58,22 +66,7 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette) {
// Set the filter to show complete or all items
filterItems: function (filter) {
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 */
'use strict';
/*global TodoMVC: true, Backbone */
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
// -------------------
//
// Display an individual todo item, and respond to changes
// that are made to the item, including marking completed.
Views.ItemView = Marionette.ItemView.extend({
TodoMVC.TodoView = Backbone.Marionette.ItemView.extend({
tagName: 'li',
template: '#template-todoItemView',
className: function () {
return this.model.get('completed') ? 'completed' : 'active';
},
......@@ -78,9 +86,12 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
//
// Controls the rendering of the list of items, including the
// filtering of activs vs completed items for display.
Views.ListView = Backbone.Marionette.CompositeView.extend({
TodoMVC.ListView = Backbone.Marionette.CompositeView.extend({
template: '#template-todoListCompositeView',
childView: Views.ItemView,
childView: TodoMVC.TodoView,
childViewContainer: '#todo-list',
ui: {
......@@ -97,11 +108,11 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
},
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) {
var filteredOn = App.request('filterState').get('filter');
var filteredOn = filterChannel.request('filterState').get('filter');
return child.matchesFilter(filteredOn);
},
......@@ -123,4 +134,4 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette) {
});
}
});
});
})();
/*global TodoMVC */
'use strict';
/*global Backbone, TodoMVC:true */
var TodoMVC = TodoMVC || {};
(function () {
'use strict';
TodoMVC.module('Todos', function (Todos, App, Backbone) {
// Todo Model
// ----------
Todos.Todo = Backbone.Model.extend({
TodoMVC.Todo = Backbone.Model.extend({
defaults: {
title: '',
completed: false,
......@@ -40,8 +43,8 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) {
// Todo Collection
// ---------------
Todos.TodoList = Backbone.Collection.extend({
model: Todos.Todo,
TodoMVC.TodoList = Backbone.Collection.extend({
model: TodoMVC.Todo,
localStorage: new Backbone.LocalStorage('todos-backbone-marionette'),
......@@ -59,4 +62,4 @@ TodoMVC.module('Todos', function (Todos, App, Backbone) {
return todo.isCompleted();
}
});
});
})();
/*global Backbone */
'use strict';
/*global Backbone, TodoMVC:true, $ */
// TodoMVC is global for developing in the console
// and functional testing.
var App = Backbone.Marionette.Application.extend({
setRootLayout: function () {
this.root = new TodoMVC.Layout.Root();
}
});
var TodoMVC = TodoMVC || {};
window.TodoMVC = new App();
$(function () {
'use strict';
(function () {
var filterState = new Backbone.Model({
filter: 'all'
});
// After we initialize the app, we want to kick off the router
// and controller, which will handle initializing our Views
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 () {
return filterState;
controller.start();
});
})();
TodoMVC.on('before:start', function () {
TodoMVC.setRootLayout();
// start the TodoMVC app (defined in js/TodoMVC.js)
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 @@
"backbone": "^1.1.2",
"backbone.localstorage": "^1.1.6",
"backbone.marionette": "^2.4.1",
"backbone.radio": "^1.0.1",
"jquery": "^1.11.2",
"todomvc-app-css": "^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