Commit 339af9f9 authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #391 from BorisKozo/fix_marionette

Fixed the remaining bugs in Marionette implementation
parents fb7f0094 f9fab087
# Backbone.Marionette TodoMVC app
[Backbone.Marionette](http://marionettejs.com) is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications. It is a collection of common design and implementation patterns found in the applications that Derick Bailey has been building with Backbone, and includes various pieces inspired by composite application architectures, such as Microsoft's "Prism" framework.
[Backbone.Marionette](http://marionettejs.com) is a composite
application library for Backbone.js that aims to simplify
the construction of large scale JavaScript applications.
It is a collection of common design and implementation patterns
found in the applications that I (Derick Bailey) have been building
with Backbone, and includes various pieces inspired by composite
application architectures, such as Microsoft's "Prism" framework.
This implementation of the application uses Marionette's module system. Variations using RequireJS and a more classic approach to JavaScript modules are also [available](https://github.com/marionettejs/backbone.marionette/wiki/Projects-and-websites-using-marionette).
## Key Benefits
* Scale applications out with modular, event driven architecture
* Sensible defaults, such as using Underscore templates for view rendering
* Easy to modify to make it work with your applicaton's specific needs
* Reduce boilerplate for views, with specialized view types
* Build on a modular architecture with an Application and modules that attach to it
* Compose your application's visuals at runtime, with Region and Layout
* Nested views and layouts within visual regions
* Built-in memory management and zombie killing in views, regions and layouts
* Built-in event clean up with the EventBinder
* Event-driven architecture with the EventAggregator
* Flexible, "as-needed" architecture allowing you to pick and choose what you need
* And much, much more
This implementation of the application uses Marionette's module system.
Variations using RequireJS and a more classic approach to JavaScript modules
[are also available here](https://github.com/marionettejs/backbone.marionette/wiki/Projects-and-websites-using-marionette).
\ No newline at end of file
......@@ -10,7 +10,7 @@
<script src="../../../assets/ie.js"></script>
<![endif]-->
<script type="text/html" id="template-footer">
<span id="todo-count"><strong></strong> items left</span>
<span id="todo-count"><strong></strong><span></span></span>
<ul id="filters">
<li>
<a href="#">All</a>
......@@ -22,7 +22,7 @@
<a href="#completed">Completed</a>
</li>
</ul>
<button id="clear-completed">Clear completed</button>
<button id="clear-completed"></button>
</script>
<script type="text/html" id="template-header">
<h1>todos</h1>
......@@ -62,7 +62,7 @@
<script src="../../../assets/jquery.min.js"></script>
<script src="../../../assets/lodash.min.js"></script>
<script src="js/lib/backbone.js"></script>
<script src="js/lib/backbone-localStorage.js"></script>
<script src="js/lib/backbone.localStorage.js"></script>
<script src="js/lib/backbone.marionette.js"></script>
<!-- application -->
<script src="js/TodoMVC.js"></script>
......
TodoMVC.module('Layout', function(Layout, App, Backbone, Marionette, $, _) {
/*global TodoMVC */
'use strict';
TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Header View
// ------------------
......@@ -12,15 +15,15 @@ TodoMVC.module('Layout', function(Layout, App, Backbone, Marionette, $, _) {
input: '#new-todo'
},
events : {
events: {
'keypress #new-todo': 'onInputKeypress'
},
onInputKeypress: function(e) {
var ENTER_KEY = 13;
var todoText = this.ui.input.val().trim();
onInputKeypress: function (e) {
var ENTER_KEY = 13,
todoText = this.ui.input.val().trim();
if ( e.which === ENTER_KEY && todoText ) {
if (e.which === ENTER_KEY && todoText) {
this.collection.create({
title: todoText
});
......@@ -39,36 +42,50 @@ TodoMVC.module('Layout', function(Layout, App, Backbone, Marionette, $, _) {
// point to jQuery selected objects
ui: {
count: '#todo-count strong',
filters: '#filters a'
itemsString: '#todo-count span',
filters: '#filters a',
clearCompleted: '#clear-completed'
},
events: {
'click #clear-completed': 'onClearClick'
},
initialize : function() {
this.bindTo(App.vent, 'todoList:filter', this.updateFilterSelection, this);
this.bindTo(this.collection, 'all', this.updateCount, this);
initialize: function () {
this.listenTo(App.vent, 'todoList:filter', this.updateFilterSelection, this);
this.listenTo(this.collection, 'all', this.updateCount, this);
},
onRender: function() {
onRender: function () {
this.updateCount();
},
updateCount: function() {
var count = this.collection.getActive().length;
updateCount: function () {
var count = this.collection.getActive().length,
length = this.collection.length,
completed = length - count;
this.ui.count.html(count);
this.$el.parent().toggle(count > 0);
this.ui.itemsString.html(' ' + (count === 1 ? 'item' : 'items') + ' left');
this.$el.parent().toggle(length > 0);
if (completed > 0) {
this.ui.clearCompleted.show();
this.ui.clearCompleted.html('Clear completed (' + completed + ')');
} else {
this.ui.clearCompleted.hide();
}
},
updateFilterSelection : function(filter) {
updateFilterSelection: function (filter) {
this.ui.filters
.removeClass('selected')
.filter('[href="#' + filter + '"]')
.addClass('selected');
},
onClearClick: function() {
onClearClick: function () {
var completed = this.collection.getCompleted();
completed.forEach(function destroy(todo) {
todo.destroy();
......
TodoMVC.module('TodoList.Views', function(Views, App, Backbone, Marionette, $, _) {
/*global TodoMVC */
'use strict';
TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette, $) {
// Todo List Item View
// -------------------
......@@ -8,55 +11,70 @@ TodoMVC.module('TodoList.Views', function(Views, App, Backbone, Marionette, $, _
Views.ItemView = Marionette.ItemView.extend({
tagName: 'li',
template: '#template-todoItemView',
ui: {
edit: '.edit'
},
events : {
'click .destroy': 'destroy',
'dblclick label': 'onEditClick',
'keypress .edit': 'onEditKeypress',
'click .toggle' : 'toggle'
},
initialize: function() {
this.bindTo(this.model, 'change', this.render, this);
},
onRender: function() {
this.$el.removeClass('active completed');
if (this.model.get('completed')) {
this.$el.addClass('completed');
} else {
this.$el.addClass('active');
}
},
destroy: function() {
this.model.destroy();
},
toggle: function() {
this.model.toggle().save();
},
onEditClick: function() {
this.$el.addClass('editing');
this.ui.edit.focus();
},
onEditKeypress: function(e) {
var ENTER_KEY = 13;
var todoText = this.ui.edit.val().trim();
if ( e.which === ENTER_KEY && todoText ) {
this.model.set('title', todoText).save();
this.$el.removeClass('editing');
}
template: '#template-todoItemView',
ui: {
edit: '.edit'
},
events: {
'click .destroy': 'destroy',
'dblclick label': 'onEditClick',
'keydown .edit': 'onEditKeypress',
'focusout .edit': 'onEditFocusout',
'click .toggle': 'toggle'
},
initialize: function () {
this.listenTo(this.model, 'change', this.render, this);
},
onRender: function () {
this.$el.removeClass('active completed');
if (this.model.get('completed')) {
this.$el.addClass('completed');
} else {
this.$el.addClass('active');
}
},
destroy: function () {
this.model.destroy();
},
toggle: function () {
this.model.toggle().save();
},
onEditClick: function () {
this.$el.addClass('editing');
this.ui.edit.focus();
this.ui.edit.val(this.ui.edit.val());
},
onEditFocusout: function () {
var todoText = this.ui.edit.val().trim();
if (todoText) {
this.model.set('title', todoText).save();
this.$el.removeClass('editing');
} else {
this.destroy();
}
},
onEditKeypress: function (e) {
var ENTER_KEY = 13, ESC_KEY = 27;
if (e.which === ENTER_KEY) {
this.onEditFocusout();
return;
}
if (e.which === ESC_KEY) {
this.$el.removeClass('editing');
}
}
});
// Item List View
......@@ -67,43 +85,43 @@ TodoMVC.module('TodoList.Views', function(Views, App, Backbone, Marionette, $, _
Views.ListView = Backbone.Marionette.CompositeView.extend({
template: '#template-todoListCompositeView',
itemView: Views.ItemView,
itemViewContainer: '#todo-list',
itemView: Views.ItemView,
itemViewContainer: '#todo-list',
ui: {
toggle: '#toggle-all'
},
ui: {
toggle: '#toggle-all'
},
events : {
'click #toggle-all': 'onToggleAllClick'
},
events: {
'click #toggle-all': 'onToggleAllClick'
},
initialize: function() {
this.bindTo(this.collection, 'all', this.update, this);
},
initialize: function () {
this.listenTo(this.collection, 'all', this.update, this);
},
onRender: function() {
this.update();
},
onRender: function () {
this.update();
},
update: function() {
function reduceCompleted(left, right) {
return left && right.get('completed');
}
update: function () {
function reduceCompleted(left, right) {
return left && right.get('completed');
}
var allCompleted = this.collection.reduce(reduceCompleted,true);
var allCompleted = this.collection.reduce(reduceCompleted, true);
this.ui.toggle.prop('checked', allCompleted);
this.$el.parent().toggle(!!this.collection.length);
},
this.ui.toggle.prop('checked', allCompleted);
this.$el.parent().toggle(!!this.collection.length);
},
onToggleAllClick: function(e) {
var isChecked = e.currentTarget.checked;
onToggleAllClick: function (e) {
var isChecked = e.currentTarget.checked;
this.collection.each(function(todo){
todo.save({'completed': isChecked});
});
}
this.collection.each(function (todo) {
todo.save({ 'completed': isChecked });
});
}
});
// Application Event Handlers
......@@ -112,7 +130,7 @@ TodoMVC.module('TodoList.Views', function(Views, App, Backbone, Marionette, $, _
// Handler for filtering the list of items by showing and
// hiding through the use of various CSS classes
App.vent.on('todoList:filter', function(filter) {
App.vent.on('todoList:filter', function (filter) {
filter = filter || 'all';
$('#todoapp').attr('class', 'filter-' + filter);
});
......
TodoMVC.module('TodoList', function(TodoList, App, Backbone, Marionette, $, _) {
/*global TodoMVC */
'use strict';
TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette, $, _) {
// TodoList Router
// ---------------
......@@ -17,7 +20,7 @@ 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 = function() {
TodoList.Controller = function () {
this.todoList = new App.Todos.TodoList();
};
......@@ -25,7 +28,7 @@ TodoMVC.module('TodoList', function(TodoList, App, Backbone, Marionette, $, _) {
// Start the app by showing the appropriate views
// and fetching the list of todo items, if there are any
start: function(){
start: function () {
this.showHeader(this.todoList);
this.showFooter(this.todoList);
this.showTodoList(this.todoList);
......@@ -33,28 +36,28 @@ TodoMVC.module('TodoList', function(TodoList, App, Backbone, Marionette, $, _) {
this.todoList.fetch();
},
showHeader: function(todoList) {
showHeader: function (todoList) {
var header = new App.Layout.Header({
collection: todoList
});
App.header.show(header);
},
showFooter: function(todoList) {
showFooter: function (todoList) {
var footer = new App.Layout.Footer({
collection: todoList
});
App.footer.show(footer);
},
showTodoList: function(todoList) {
showTodoList: function (todoList) {
App.main.show(new TodoList.Views.ListView({
collection : todoList
collection: todoList
}));
},
// Set the filter to show complete or all items
filterItems: function(filter) {
filterItems: function (filter) {
App.vent.trigger('todoList:filter', filter.trim() || '');
}
});
......@@ -66,9 +69,9 @@ TodoMVC.module('TodoList', function(TodoList, App, Backbone, Marionette, $, _) {
// when the the application is started, pulling in all of the
// existing Todo items and displaying them.
TodoList.addInitializer(function() {
TodoList.addInitializer(function () {
var controller = new TodoList.Controller();
new TodoList.Router({
controller.router = new TodoList.Router({
controller: controller
});
......
TodoMVC.module('Todos', function(Todos, App, Backbone, Marionette, $, _) {
/*global TodoMVC */
'use strict';
TodoMVC.module('Todos', function (Todos, App, Backbone) {
// Todo Model
// ----------
Todos.Todo = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage('todos-backbone-marionettejs'),
defaults: {
title: '',
......@@ -12,17 +14,17 @@ TodoMVC.module('Todos', function(Todos, App, Backbone, Marionette, $, _) {
created: 0
},
initialize: function() {
initialize: function () {
if (this.isNew()) {
this.set('created', Date.now());
}
},
toggle: function() {
toggle: function () {
return this.set('completed', !this.isCompleted());
},
isCompleted: function() {
isCompleted: function () {
return this.get('completed');
}
});
......@@ -33,21 +35,21 @@ TodoMVC.module('Todos', function(Todos, App, Backbone, Marionette, $, _) {
Todos.TodoList = Backbone.Collection.extend({
model: Todos.Todo,
localStorage: new Backbone.LocalStorage('todos-backbone'),
localStorage: new Backbone.LocalStorage('todos-backbone-marionette'),
getCompleted: function() {
getCompleted: function () {
return this.filter(this._isCompleted);
},
getActive: function() {
getActive: function () {
return this.reject(this._isCompleted);
},
comparator: function(todo) {
comparator: function (todo) {
return todo.get('created');
},
_isCompleted: function(todo){
_isCompleted: function (todo) {
return todo.isCompleted();
}
});
......
/*global Backbone */
'use strict';
var TodoMVC = new Backbone.Marionette.Application();
TodoMVC.addRegions({
......@@ -6,6 +9,6 @@ TodoMVC.addRegions({
footer: '#footer'
});
TodoMVC.on('initialize:after', function() {
TodoMVC.on('initialize:after', function () {
Backbone.history.start();
});
/**
* Backbone localStorage Adapter
* https://github.com/jeromegn/Backbone.localStorage
*/
(function() {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
var _ = this._;
var Backbone = this.Backbone;
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
this.name = name;
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
this.localStorage().setItem(this.name, this.records.join(","));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return model.toJSON();
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save();
return model.toJSON();
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return JSON.parse(this.localStorage().getItem(this.name+"-"+model.id));
},
// Return the array of all models currently in storage.
findAll: function() {
return _(this.records).chain()
.map(function(id){return JSON.parse(this.localStorage().getItem(this.name+"-"+id));}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
this.localStorage().removeItem(this.name+"-"+model.id);
this.records = _.reject(this.records, function(record_id){return record_id == model.id.toString();});
this.save();
return model;
},
localStorage: function() {
return localStorage;
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options, error) {
var store = model.localStorage || model.collection.localStorage;
// Backwards compatibility with Backbone <= 0.3.3
if (typeof options == 'function') {
options = {
success: options,
error: error
};
}
var resp;
switch (method) {
case "read": resp = model.id != undefined ? store.find(model) : store.findAll(); break;
case "create": resp = store.create(model); break;
case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break;
}
if (resp) {
options.success(resp);
} else {
options.error("Record not found");
}
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) {
if(model.localStorage || (model.collection && model.collection.localStorage))
{
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options, error) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options, error]);
};
})();
/**
* Backbone localStorage Adapter
* Version 1.1.0
*
* https://github.com/jeromegn/Backbone.localStorage
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["underscore", "backbone"], function (_, Backbone) {
// Use global variables if the locals are undefined.
return factory(_ || root._, Backbone || root.Backbone);
});
} else {
// RequireJS isn't being used. Assume underscore and backbone are loaded in <script> tags
factory(_, Backbone);
}
}(this, function (_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
// Generate four random hex digits.
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function (name) {
this.name = name;
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function () {
this.localStorage().setItem(this.name, this.records.join(","));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function (model) {
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return this.find(model);
},
// Update a model by replacing its copy in `this.data`.
update: function (model) {
this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString()))
this.records.push(model.id.toString()); this.save();
return this.find(model);
},
// Retrieve a model from `this.data` by id.
find: function (model) {
return this.jsonData(this.localStorage().getItem(this.name + "-" + model.id));
},
// Return the array of all models currently in storage.
findAll: function () {
return _(this.records).chain()
.map(function (id) {
return this.jsonData(this.localStorage().getItem(this.name + "-" + id));
}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function (model) {
if (model.isNew())
return false
this.localStorage().removeItem(this.name + "-" + model.id);
this.records = _.reject(this.records, function (id) {
return id === model.id.toString();
});
this.save();
return model;
},
localStorage: function () {
return localStorage;
},
// fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) {
return data && JSON.parse(data);
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function (method, model, options) {
var store = model.localStorage || model.collection.localStorage;
var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
try {
switch (method) {
case "read":
resp = model.id != undefined ? store.find(model) : store.findAll();
break;
case "create":
resp = store.create(model);
break;
case "update":
resp = store.update(model);
break;
case "delete":
resp = store.destroy(model);
break;
}
} catch (error) {
if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
errorMessage = "Private browsing is unsupported";
else
errorMessage = error.message;
}
if (resp) {
model.trigger("sync", model, resp, options);
if (options && options.success)
if (Backbone.VERSION === "0.9.10") {
options.success(model, resp, options);
} else {
options.success(resp);
}
if (syncDfd)
syncDfd.resolve(resp);
} else {
errorMessage = errorMessage ? errorMessage
: "Record Not Found";
model.trigger("error", model, errorMessage, options);
if (options && options.error)
if (Backbone.VERSION === "0.9.10") {
options.error(model, errorMessage, options);
} else {
options.error(errorMessage);
}
if (syncDfd)
syncDfd.reject(errorMessage);
}
// add compatibility with $.ajax
// always execute callback for success and error
if (options && options.complete) options.complete(resp);
return syncDfd && syncDfd.promise();
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function (model) {
if (model.localStorage || (model.collection && model.collection.localStorage)) {
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function (method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
};
return Backbone.LocalStorage;
}));
\ No newline at end of file
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