k.backbone.marionette
Version:
Make your Backbone.js apps dance!
1,564 lines (1,338 loc) • 118 kB
JavaScript
// MarionetteJS (Backbone.Marionette)
// ----------------------------------
// v2.2.2
//
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://marionettejs.com
/*!
* Includes BabySitter
* https://github.com/marionettejs/backbone.babysitter/
*
* Includes Wreqr
* https://github.com/marionettejs/backbone.wreqr/
*/
(function(root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
define(['backbone', 'underscore'], function(Backbone, _) {
return (root.Marionette = factory(root, Backbone, _));
});
} else if (typeof exports !== 'undefined') {
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = factory(root, Backbone, _);
} else {
root.Marionette = factory(root, root.Backbone, root._);
}
}(this, function(root, Backbone, _) {
'use strict';
/* istanbul ignore next */
// Backbone.BabySitter
// -------------------
// v0.1.4
//
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://github.com/marionettejs/backbone.babysitter
(function(Backbone, _) {
"use strict";
var previousChildViewContainer = Backbone.ChildViewContainer;
// BabySitter.ChildViewContainer
// -----------------------------
//
// Provide a container to store, retrieve and
// shut down child views.
Backbone.ChildViewContainer = function(Backbone, _) {
// Container Constructor
// ---------------------
var Container = function(views) {
this._views = {};
this._indexByModel = {};
this._indexByCustom = {};
this._updateLength();
_.each(views, this.add, this);
};
// Container Methods
// -----------------
_.extend(Container.prototype, {
// Add a view to this container. Stores the view
// by `cid` and makes it searchable by the model
// cid (and model itself). Optionally specify
// a custom key to store an retrieve the view.
add: function(view, customIndex) {
var viewCid = view.cid;
// store the view
this._views[viewCid] = view;
// index it by model
if (view.model) {
this._indexByModel[view.model.cid] = viewCid;
}
// index by custom
if (customIndex) {
this._indexByCustom[customIndex] = viewCid;
}
this._updateLength();
return this;
},
// Find a view by the model that was attached to
// it. Uses the model's `cid` to find it.
findByModel: function(model) {
return this.findByModelCid(model.cid);
},
// Find a view by the `cid` of the model that was attached to
// it. Uses the model's `cid` to find the view `cid` and
// retrieve the view using it.
findByModelCid: function(modelCid) {
var viewCid = this._indexByModel[modelCid];
return this.findByCid(viewCid);
},
// Find a view by a custom indexer.
findByCustom: function(index) {
var viewCid = this._indexByCustom[index];
return this.findByCid(viewCid);
},
// Find by index. This is not guaranteed to be a
// stable index.
findByIndex: function(index) {
return _.values(this._views)[index];
},
// retrieve a view by its `cid` directly
findByCid: function(cid) {
return this._views[cid];
},
// Remove a view
remove: function(view) {
var viewCid = view.cid;
// delete model index
if (view.model) {
delete this._indexByModel[view.model.cid];
}
// delete custom index
_.any(this._indexByCustom, function(cid, key) {
if (cid === viewCid) {
delete this._indexByCustom[key];
return true;
}
}, this);
// remove the view from the container
delete this._views[viewCid];
// update the length
this._updateLength();
return this;
},
// Call a method on every view in the container,
// passing parameters to the call method one at a
// time, like `function.call`.
call: function(method) {
this.apply(method, _.tail(arguments));
},
// Apply a method on every view in the container,
// passing parameters to the call method one at a
// time, like `function.apply`.
apply: function(method, args) {
_.each(this._views, function(view) {
if (_.isFunction(view[method])) {
view[method].apply(view, args || []);
}
});
},
// Update the `.length` attribute on this container
_updateLength: function() {
this.length = _.size(this._views);
}
});
// Borrowing this code from Backbone.Collection:
// http://backbonejs.org/docs/backbone.html#section-106
//
// Mix in methods from Underscore, for iteration, and other
// collection related features.
var methods = [ "forEach", "each", "map", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "toArray", "first", "initial", "rest", "last", "without", "isEmpty", "pluck" ];
_.each(methods, function(method) {
Container.prototype[method] = function() {
var views = _.values(this._views);
var args = [ views ].concat(_.toArray(arguments));
return _[method].apply(_, args);
};
});
// return the public API
return Container;
}(Backbone, _);
Backbone.ChildViewContainer.VERSION = "0.1.4";
Backbone.ChildViewContainer.noConflict = function() {
Backbone.ChildViewContainer = previousChildViewContainer;
return this;
};
return Backbone.ChildViewContainer;
})(Backbone, _);
/* istanbul ignore next */
// Backbone.Wreqr (Backbone.Marionette)
// ----------------------------------
// v1.3.1
//
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://github.com/marionettejs/backbone.wreqr
(function(Backbone, _) {
"use strict";
var previousWreqr = Backbone.Wreqr;
var Wreqr = Backbone.Wreqr = {};
Backbone.Wreqr.VERSION = "1.3.1";
Backbone.Wreqr.noConflict = function() {
Backbone.Wreqr = previousWreqr;
return this;
};
// Handlers
// --------
// A registry of functions to call, given a name
Wreqr.Handlers = function(Backbone, _) {
"use strict";
// Constructor
// -----------
var Handlers = function(options) {
this.options = options;
this._wreqrHandlers = {};
if (_.isFunction(this.initialize)) {
this.initialize(options);
}
};
Handlers.extend = Backbone.Model.extend;
// Instance Members
// ----------------
_.extend(Handlers.prototype, Backbone.Events, {
// Add multiple handlers using an object literal configuration
setHandlers: function(handlers) {
_.each(handlers, function(handler, name) {
var context = null;
if (_.isObject(handler) && !_.isFunction(handler)) {
context = handler.context;
handler = handler.callback;
}
this.setHandler(name, handler, context);
}, this);
},
// Add a handler for the given name, with an
// optional context to run the handler within
setHandler: function(name, handler, context) {
var config = {
callback: handler,
context: context
};
this._wreqrHandlers[name] = config;
this.trigger("handler:add", name, handler, context);
},
// Determine whether or not a handler is registered
hasHandler: function(name) {
return !!this._wreqrHandlers[name];
},
// Get the currently registered handler for
// the specified name. Throws an exception if
// no handler is found.
getHandler: function(name) {
var config = this._wreqrHandlers[name];
if (!config) {
return;
}
return function() {
var args = Array.prototype.slice.apply(arguments);
return config.callback.apply(config.context, args);
};
},
// Remove a handler for the specified name
removeHandler: function(name) {
delete this._wreqrHandlers[name];
},
// Remove all handlers from this registry
removeAllHandlers: function() {
this._wreqrHandlers = {};
}
});
return Handlers;
}(Backbone, _);
// Wreqr.CommandStorage
// --------------------
//
// Store and retrieve commands for execution.
Wreqr.CommandStorage = function() {
"use strict";
// Constructor function
var CommandStorage = function(options) {
this.options = options;
this._commands = {};
if (_.isFunction(this.initialize)) {
this.initialize(options);
}
};
// Instance methods
_.extend(CommandStorage.prototype, Backbone.Events, {
// Get an object literal by command name, that contains
// the `commandName` and the `instances` of all commands
// represented as an array of arguments to process
getCommands: function(commandName) {
var commands = this._commands[commandName];
// we don't have it, so add it
if (!commands) {
// build the configuration
commands = {
command: commandName,
instances: []
};
// store it
this._commands[commandName] = commands;
}
return commands;
},
// Add a command by name, to the storage and store the
// args for the command
addCommand: function(commandName, args) {
var command = this.getCommands(commandName);
command.instances.push(args);
},
// Clear all commands for the given `commandName`
clearCommands: function(commandName) {
var command = this.getCommands(commandName);
command.instances = [];
}
});
return CommandStorage;
}();
// Wreqr.Commands
// --------------
//
// A simple command pattern implementation. Register a command
// handler and execute it.
Wreqr.Commands = function(Wreqr) {
"use strict";
return Wreqr.Handlers.extend({
// default storage type
storageType: Wreqr.CommandStorage,
constructor: function(options) {
this.options = options || {};
this._initializeStorage(this.options);
this.on("handler:add", this._executeCommands, this);
var args = Array.prototype.slice.call(arguments);
Wreqr.Handlers.prototype.constructor.apply(this, args);
},
// Execute a named command with the supplied args
execute: function(name, args) {
name = arguments[0];
args = Array.prototype.slice.call(arguments, 1);
if (this.hasHandler(name)) {
this.getHandler(name).apply(this, args);
} else {
this.storage.addCommand(name, args);
}
},
// Internal method to handle bulk execution of stored commands
_executeCommands: function(name, handler, context) {
var command = this.storage.getCommands(name);
// loop through and execute all the stored command instances
_.each(command.instances, function(args) {
handler.apply(context, args);
});
this.storage.clearCommands(name);
},
// Internal method to initialize storage either from the type's
// `storageType` or the instance `options.storageType`.
_initializeStorage: function(options) {
var storage;
var StorageType = options.storageType || this.storageType;
if (_.isFunction(StorageType)) {
storage = new StorageType();
} else {
storage = StorageType;
}
this.storage = storage;
}
});
}(Wreqr);
// Wreqr.RequestResponse
// ---------------------
//
// A simple request/response implementation. Register a
// request handler, and return a response from it
Wreqr.RequestResponse = function(Wreqr) {
"use strict";
return Wreqr.Handlers.extend({
request: function() {
var name = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
if (this.hasHandler(name)) {
return this.getHandler(name).apply(this, args);
}
}
});
}(Wreqr);
// Event Aggregator
// ----------------
// A pub-sub object that can be used to decouple various parts
// of an application through event-driven architecture.
Wreqr.EventAggregator = function(Backbone, _) {
"use strict";
var EA = function() {};
// Copy the `extend` function used by Backbone's classes
EA.extend = Backbone.Model.extend;
// Copy the basic Backbone.Events on to the event aggregator
_.extend(EA.prototype, Backbone.Events);
return EA;
}(Backbone, _);
// Wreqr.Channel
// --------------
//
// An object that wraps the three messaging systems:
// EventAggregator, RequestResponse, Commands
Wreqr.Channel = function(Wreqr) {
"use strict";
var Channel = function(channelName) {
this.vent = new Backbone.Wreqr.EventAggregator();
this.reqres = new Backbone.Wreqr.RequestResponse();
this.commands = new Backbone.Wreqr.Commands();
this.channelName = channelName;
};
_.extend(Channel.prototype, {
// Remove all handlers from the messaging systems of this channel
reset: function() {
this.vent.off();
this.vent.stopListening();
this.reqres.removeAllHandlers();
this.commands.removeAllHandlers();
return this;
},
// Connect a hash of events; one for each messaging system
connectEvents: function(hash, context) {
this._connect("vent", hash, context);
return this;
},
connectCommands: function(hash, context) {
this._connect("commands", hash, context);
return this;
},
connectRequests: function(hash, context) {
this._connect("reqres", hash, context);
return this;
},
// Attach the handlers to a given message system `type`
_connect: function(type, hash, context) {
if (!hash) {
return;
}
context = context || this;
var method = type === "vent" ? "on" : "setHandler";
_.each(hash, function(fn, eventName) {
this[type][method](eventName, _.bind(fn, context));
}, this);
}
});
return Channel;
}(Wreqr);
// Wreqr.Radio
// --------------
//
// An object that lets you communicate with many channels.
Wreqr.radio = function(Wreqr) {
"use strict";
var Radio = function() {
this._channels = {};
this.vent = {};
this.commands = {};
this.reqres = {};
this._proxyMethods();
};
_.extend(Radio.prototype, {
channel: function(channelName) {
if (!channelName) {
throw new Error("Channel must receive a name");
}
return this._getChannel(channelName);
},
_getChannel: function(channelName) {
var channel = this._channels[channelName];
if (!channel) {
channel = new Wreqr.Channel(channelName);
this._channels[channelName] = channel;
}
return channel;
},
_proxyMethods: function() {
_.each([ "vent", "commands", "reqres" ], function(system) {
_.each(messageSystems[system], function(method) {
this[system][method] = proxyMethod(this, system, method);
}, this);
}, this);
}
});
var messageSystems = {
vent: [ "on", "off", "trigger", "once", "stopListening", "listenTo", "listenToOnce" ],
commands: [ "execute", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ],
reqres: [ "request", "setHandler", "setHandlers", "removeHandler", "removeAllHandlers" ]
};
var proxyMethod = function(radio, system, method) {
return function(channelName) {
var messageSystem = radio._getChannel(channelName)[system];
var args = Array.prototype.slice.call(arguments, 1);
return messageSystem[method].apply(messageSystem, args);
};
};
return new Radio();
}(Wreqr);
return Backbone.Wreqr;
})(Backbone, _);
var previousMarionette = root.Marionette;
var Marionette = Backbone.Marionette = {};
Marionette.VERSION = '2.2.2';
Marionette.noConflict = function() {
root.Marionette = previousMarionette;
return this;
};
Backbone.Marionette = Marionette;
// Get the Deferred creator for later use
Marionette.Deferred = Backbone.$.Deferred;
/* jshint unused: false */
// Helpers
// -------
// For slicing `arguments` in functions
var slice = Array.prototype.slice;
// Marionette.extend
// -----------------
// Borrow the Backbone `extend` method so we can use it as needed
Marionette.extend = Backbone.Model.extend;
// Marionette.getOption
// --------------------
// Retrieve an object, function or other value from a target
// object or its `options`, with `options` taking precedence.
Marionette.getOption = function(target, optionName) {
if (!target || !optionName) { return; }
var value;
if (target.options && (target.options[optionName] !== undefined)) {
value = target.options[optionName];
} else {
value = target[optionName];
}
return value;
};
// Proxy `Marionette.getOption`
Marionette.proxyGetOption = function(optionName) {
return Marionette.getOption(this, optionName);
};
// Marionette.normalizeMethods
// ----------------------
// Pass in a mapping of events => functions or function names
// and return a mapping of events => functions
Marionette.normalizeMethods = function(hash) {
var normalizedHash = {};
_.each(hash, function(method, name) {
if (!_.isFunction(method)) {
method = this[method];
}
if (!method) {
return;
}
normalizedHash[name] = method;
}, this);
return normalizedHash;
};
// utility method for parsing @ui. syntax strings
// into associated selector
Marionette.normalizeUIString = function(uiString, ui) {
return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) {
return ui[r.slice(4)];
});
};
// allows for the use of the @ui. syntax within
// a given key for triggers and events
// swaps the @ui with the associated selector.
// Returns a new, non-mutated, parsed events hash.
Marionette.normalizeUIKeys = function(hash, ui) {
if (typeof(hash) === 'undefined') {
return;
}
hash = _.clone(hash);
_.each(_.keys(hash), function(key) {
var normalizedKey = Marionette.normalizeUIString(key, ui);
if (normalizedKey !== key) {
hash[normalizedKey] = hash[key];
delete hash[key];
}
});
return hash;
};
// allows for the use of the @ui. syntax within
// a given value for regions
// swaps the @ui with the associated selector
Marionette.normalizeUIValues = function(hash, ui) {
if (typeof(hash) === 'undefined') {
return;
}
_.each(hash, function(val, key) {
if (_.isString(val)) {
hash[key] = Marionette.normalizeUIString(val, ui);
}
});
return hash;
};
// Mix in methods from Underscore, for iteration, and other
// collection related features.
// Borrowing this code from Backbone.Collection:
// http://backbonejs.org/docs/backbone.html#section-121
Marionette.actAsCollection = function(object, listProperty) {
var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
'select', 'reject', 'every', 'all', 'some', 'any', 'include',
'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
'last', 'without', 'isEmpty', 'pluck'];
_.each(methods, function(method) {
object[method] = function() {
var list = _.values(_.result(this, listProperty));
var args = [list].concat(_.toArray(arguments));
return _[method].apply(_, args);
};
});
};
// Trigger an event and/or a corresponding method name. Examples:
//
// `this.triggerMethod("foo")` will trigger the "foo" event and
// call the "onFoo" method.
//
// `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and
// call the "onFooBar" method.
Marionette.triggerMethod = function(event) {
// split the event name on the ":"
var splitter = /(^|:)(\w)/gi;
// take the event section ("section1:section2:section3")
// and turn it in to uppercase name
function getEventName(match, prefix, eventName) {
return eventName.toUpperCase();
}
// get the method name from the event name
var methodName = 'on' + event.replace(splitter, getEventName);
var method = this[methodName];
var result;
// call the onMethodName if it exists
if (_.isFunction(method)) {
// pass all arguments, except the event name
result = method.apply(this, _.tail(arguments));
}
// trigger the event, if a trigger method exists
if (_.isFunction(this.trigger)) {
this.trigger.apply(this, arguments);
}
return result;
};
// triggerMethodOn invokes triggerMethod on a specific context
//
// e.g. `Marionette.triggerMethodOn(view, 'show')`
// will trigger a "show" event or invoke onShow the view.
Marionette.triggerMethodOn = function(context, event) {
var args = _.tail(arguments, 2);
var fnc;
if (_.isFunction(context.triggerMethod)) {
fnc = context.triggerMethod;
} else {
fnc = Marionette.triggerMethod;
}
return fnc.apply(context, [event].concat(args));
};
// DOMRefresh
// ----------
//
// Monitor a view's state, and after it has been rendered and shown
// in the DOM, trigger a "dom:refresh" event every time it is
// re-rendered.
Marionette.MonitorDOMRefresh = (function(documentElement) {
// track when the view has been shown in the DOM,
// using a Marionette.Region (or by other means of triggering "show")
function handleShow(view) {
view._isShown = true;
triggerDOMRefresh(view);
}
// track when the view has been rendered
function handleRender(view) {
view._isRendered = true;
triggerDOMRefresh(view);
}
// Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
function triggerDOMRefresh(view) {
if (view._isShown && view._isRendered && isInDOM(view)) {
if (_.isFunction(view.triggerMethod)) {
view.triggerMethod('dom:refresh');
}
}
}
function isInDOM(view) {
return Backbone.$.contains(documentElement, view.el);
}
// Export public API
return function(view) {
view.listenTo(view, 'show', function() {
handleShow(view);
});
view.listenTo(view, 'render', function() {
handleRender(view);
});
};
})(document.documentElement);
/* jshint maxparams: 5 */
// Marionette.bindEntityEvents & unbindEntityEvents
// ---------------------------
//
// These methods are used to bind/unbind a backbone "entity" (collection/model)
// to methods on a target object.
//
// The first parameter, `target`, must have a `listenTo` method from the
// EventBinder object.
//
// The second parameter is the entity (Backbone.Model or Backbone.Collection)
// to bind the events from.
//
// The third parameter is a hash of { "event:name": "eventHandler" }
// configuration. Multiple handlers can be separated by a space. A
// function can be supplied instead of a string handler name.
(function(Marionette) {
'use strict';
// Bind the event to handlers specified as a string of
// handler names on the target object
function bindFromStrings(target, entity, evt, methods) {
var methodNames = methods.split(/\s+/);
_.each(methodNames, function(methodName) {
var method = target[methodName];
if (!method) {
throw new Marionette.Error('Method "' + methodName +
'" was configured as an event handler, but does not exist.');
}
target.listenTo(entity, evt, method);
});
}
// Bind the event to a supplied callback function
function bindToFunction(target, entity, evt, method) {
target.listenTo(entity, evt, method);
}
// Bind the event to handlers specified as a string of
// handler names on the target object
function unbindFromStrings(target, entity, evt, methods) {
var methodNames = methods.split(/\s+/);
_.each(methodNames, function(methodName) {
var method = target[methodName];
target.stopListening(entity, evt, method);
});
}
// Bind the event to a supplied callback function
function unbindToFunction(target, entity, evt, method) {
target.stopListening(entity, evt, method);
}
// generic looping function
function iterateEvents(target, entity, bindings, functionCallback, stringCallback) {
if (!entity || !bindings) { return; }
// type-check bindings
if (!_.isFunction(bindings) && !_.isObject(bindings)) {
throw new Marionette.Error({
message: 'Bindings must be an object or function.',
url: 'marionette.functions.html#marionettebindentityevents'
});
}
// allow the bindings to be a function
if (_.isFunction(bindings)) {
bindings = bindings.call(target);
}
// iterate the bindings and bind them
_.each(bindings, function(methods, evt) {
// allow for a function as the handler,
// or a list of event names as a string
if (_.isFunction(methods)) {
functionCallback(target, entity, evt, methods);
} else {
stringCallback(target, entity, evt, methods);
}
});
}
// Export Public API
Marionette.bindEntityEvents = function(target, entity, bindings) {
iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings);
};
Marionette.unbindEntityEvents = function(target, entity, bindings) {
iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings);
};
// Proxy `bindEntityEvents`
Marionette.proxyBindEntityEvents = function(entity, bindings) {
return Marionette.bindEntityEvents(this, entity, bindings);
};
// Proxy `unbindEntityEvents`
Marionette.proxyUnbindEntityEvents = function(entity, bindings) {
return Marionette.unbindEntityEvents(this, entity, bindings);
};
})(Marionette);
var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number'];
Marionette.Error = Marionette.extend.call(Error, {
urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/',
constructor: function(message, options) {
if (_.isObject(message)) {
options = message;
message = options.message;
} else if (!options) {
options = {};
}
var error = Error.call(this, message);
_.extend(this, _.pick(error, errorProps), _.pick(options, errorProps));
this.captureStackTrace();
if (options.url) {
this.url = this.urlRoot + options.url;
}
},
captureStackTrace: function() {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Marionette.Error);
}
},
toString: function() {
return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : '');
}
});
Marionette.Error.extend = Marionette.extend;
// Callbacks
// ---------
// A simple way of managing a collection of callbacks
// and executing them at a later point in time, using jQuery's
// `Deferred` object.
Marionette.Callbacks = function() {
this._deferred = Marionette.Deferred();
this._callbacks = [];
};
_.extend(Marionette.Callbacks.prototype, {
// Add a callback to be executed. Callbacks added here are
// guaranteed to execute, even if they are added after the
// `run` method is called.
add: function(callback, contextOverride) {
var promise = _.result(this._deferred, 'promise');
this._callbacks.push({cb: callback, ctx: contextOverride});
promise.then(function(args) {
if (contextOverride){ args.context = contextOverride; }
callback.call(args.context, args.options);
});
},
// Run all registered callbacks with the context specified.
// Additional callbacks can be added after this has been run
// and they will still be executed.
run: function(options, context) {
this._deferred.resolve({
options: options,
context: context
});
},
// Resets the list of callbacks to be run, allowing the same list
// to be run multiple times - whenever the `run` method is called.
reset: function() {
var callbacks = this._callbacks;
this._deferred = Marionette.Deferred();
this._callbacks = [];
_.each(callbacks, function(cb) {
this.add(cb.cb, cb.ctx);
}, this);
}
});
// Marionette Controller
// ---------------------
//
// A multi-purpose object to use as a controller for
// modules and routers, and as a mediator for workflow
// and coordination of other objects, views, and more.
Marionette.Controller = function(options) {
this.options = options || {};
if (_.isFunction(this.initialize)) {
this.initialize(this.options);
}
};
Marionette.Controller.extend = Marionette.extend;
// Controller Methods
// --------------
// Ensure it can trigger events with Backbone.Events
_.extend(Marionette.Controller.prototype, Backbone.Events, {
destroy: function() {
var args = slice.call(arguments);
this.triggerMethod.apply(this, ['before:destroy'].concat(args));
this.triggerMethod.apply(this, ['destroy'].concat(args));
this.stopListening();
this.off();
return this;
},
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: Marionette.triggerMethod,
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption
});
// Marionette Object
// ---------------------
//
// A Base Class that other Classes should descend from.
// Object borrows many conventions and utilities from Backbone.
Marionette.Object = function(options) {
this.options = _.extend({}, _.result(this, 'options'), options);
this.initialize.apply(this, arguments);
};
Marionette.Object.extend = Marionette.extend;
// Object Methods
// --------------
_.extend(Marionette.Object.prototype, {
//this is a noop method intended to be overridden by classes that extend from this base
initialize: function() {},
destroy: function() {
this.triggerMethod('before:destroy');
this.triggerMethod('destroy');
this.stopListening();
},
// Import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: Marionette.triggerMethod,
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption,
// Proxy `unbindEntityEvents` to enable binding view's events from another entity.
bindEntityEvents: Marionette.proxyBindEntityEvents,
// Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
unbindEntityEvents: Marionette.proxyUnbindEntityEvents
});
// Ensure it can trigger events with Backbone.Events
_.extend(Marionette.Object.prototype, Backbone.Events);
/* jshint maxcomplexity: 10, maxstatements: 29 */
// Region
// ------
//
// Manage the visual regions of your composite application. See
// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
Marionette.Region = function(options) {
this.options = options || {};
this.el = this.getOption('el');
// Handle when this.el is passed in as a $ wrapped element.
this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el;
if (!this.el) {
throw new Marionette.Error({
name: 'NoElError',
message: 'An "el" must be specified for a region.'
});
}
this.$el = this.getEl(this.el);
if (this.initialize) {
var args = slice.apply(arguments);
this.initialize.apply(this, args);
}
};
// Region Class methods
// -------------------
_.extend(Marionette.Region, {
// Build an instance of a region by passing in a configuration object
// and a default region class to use if none is specified in the config.
//
// The config object should either be a string as a jQuery DOM selector,
// a Region class directly, or an object literal that specifies both
// a selector and regionClass:
//
// ```js
// {
// selector: "#foo",
// regionClass: MyCustomRegion
// }
// ```
//
buildRegion: function(regionConfig, DefaultRegionClass) {
if (_.isString(regionConfig)) {
return this._buildRegionFromSelector(regionConfig, DefaultRegionClass);
}
if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) {
return this._buildRegionFromObject(regionConfig, DefaultRegionClass);
}
if (_.isFunction(regionConfig)) {
return this._buildRegionFromRegionClass(regionConfig);
}
throw new Marionette.Error({
message: 'Improper region configuration type.',
url: 'marionette.region.html#region-configuration-types'
});
},
// Build the region from a string selector like '#foo-region'
_buildRegionFromSelector: function(selector, DefaultRegionClass) {
return new DefaultRegionClass({ el: selector });
},
// Build the region from a configuration object
// ```js
// { selector: '#foo', regionClass: FooRegion }
// ```
_buildRegionFromObject: function(regionConfig, DefaultRegionClass) {
var RegionClass = regionConfig.regionClass || DefaultRegionClass;
var options = _.omit(regionConfig, 'selector', 'regionClass');
if (regionConfig.selector && !options.el) {
options.el = regionConfig.selector;
}
var region = new RegionClass(options);
// override the `getEl` function if we have a parentEl
// this must be overridden to ensure the selector is found
// on the first use of the region. if we try to assign the
// region's `el` to `parentEl.find(selector)` in the object
// literal to build the region, the element will not be
// guaranteed to be in the DOM already, and will cause problems
if (regionConfig.parentEl) {
region.getEl = function(el) {
if (_.isObject(el)) {
return Backbone.$(el);
}
var parentEl = regionConfig.parentEl;
if (_.isFunction(parentEl)) {
parentEl = parentEl();
}
return parentEl.find(el);
};
}
return region;
},
// Build the region directly from a given `RegionClass`
_buildRegionFromRegionClass: function(RegionClass) {
return new RegionClass();
}
});
// Region Instance Methods
// -----------------------
_.extend(Marionette.Region.prototype, Backbone.Events, {
// Displays a backbone view instance inside of the region.
// Handles calling the `render` method for you. Reads content
// directly from the `el` attribute. Also calls an optional
// `onShow` and `onDestroy` method on your view, just after showing
// or just before destroying the view, respectively.
// The `preventDestroy` option can be used to prevent a view from
// the old view being destroyed on show.
// The `forceShow` option can be used to force a view to be
// re-rendered if it's already shown in the region.
show: function(view, options){
this._ensureElement();
var showOptions = options || {};
var isDifferentView = view !== this.currentView;
var preventDestroy = !!showOptions.preventDestroy;
var forceShow = !!showOptions.forceShow;
// We are only changing the view if there is a current view to change to begin with
var isChangingView = !!this.currentView;
// Only destroy the current view if we don't want to `preventDestroy` and if
// the view given in the first argument is different than `currentView`
var _shouldDestroyView = isDifferentView && !preventDestroy;
// Only show the view given in the first argument if it is different than
// the current view or if we want to re-show the view. Note that if
// `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true.
var _shouldShowView = isDifferentView || forceShow;
if (isChangingView) {
this.triggerMethod('before:swapOut', this.currentView);
}
if (_shouldDestroyView) {
this.empty();
}
if (_shouldShowView) {
// We need to listen for if a view is destroyed
// in a way other than through the region.
// If this happens we need to remove the reference
// to the currentView since once a view has been destroyed
// we can not reuse it.
view.once('destroy', this.empty, this);
view.render();
if (isChangingView) {
this.triggerMethod('before:swap', view);
}
this.triggerMethod('before:show', view);
Marionette.triggerMethodOn(view, 'before:show');
this.attachHtml(view);
if (isChangingView) {
this.triggerMethod('swapOut', this.currentView);
}
this.currentView = view;
if (isChangingView) {
this.triggerMethod('swap', view);
}
this.triggerMethod('show', view);
Marionette.triggerMethodOn(view, 'show');
return this;
}
return this;
},
_ensureElement: function(){
if (!_.isObject(this.el)) {
this.$el = this.getEl(this.el);
this.el = this.$el[0];
}
if (!this.$el || this.$el.length === 0) {
throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM');
}
},
// Override this method to change how the region finds the
// DOM element that it manages. Return a jQuery selector object.
getEl: function(el) {
return Backbone.$(el);
},
// Override this method to change how the new view is
// appended to the `$el` that the region is managing
attachHtml: function(view) {
// empty the node and append new view
this.el.innerHTML='';
this.el.appendChild(view.el);
},
// Destroy the current view, if there is one. If there is no
// current view, it does nothing and returns immediately.
empty: function() {
var view = this.currentView;
// If there is no view in the region
// we should not remove anything
if (!view) { return; }
view.off('destroy', this.empty, this);
this.triggerMethod('before:empty', view);
this._destroyView();
this.triggerMethod('empty', view);
// Remove region pointer to the currentView
delete this.currentView;
return this;
},
// call 'destroy' or 'remove', depending on which is found
// on the view (if showing a raw Backbone view or a Marionette View)
_destroyView: function() {
var view = this.currentView;
if (view.destroy && !view.isDestroyed) {
view.destroy();
} else if (view.remove) {
view.remove();
}
},
// Attach an existing view to the region. This
// will not call `render` or `onShow` for the new view,
// and will not replace the current HTML for the `el`
// of the region.
attachView: function(view) {
this.currentView = view;
return this;
},
// Checks whether a view is currently present within
// the region. Returns `true` if there is and `false` if
// no view is present.
hasView: function() {
return !!this.currentView;
},
// Reset the region by destroying any existing view and
// clearing out the cached `$el`. The next time a view
// is shown via this region, the region will re-query the
// DOM for the region's `el`.
reset: function() {
this.empty();
if (this.$el) {
this.el = this.$el.selector;
}
delete this.$el;
return this;
},
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption,
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: Marionette.triggerMethod
});
// Copy the `extend` function used by Backbone's classes
Marionette.Region.extend = Marionette.extend;
// Marionette.RegionManager
// ------------------------
//
// Manage one or more related `Marionette.Region` objects.
Marionette.RegionManager = (function(Marionette) {
var RegionManager = Marionette.Controller.extend({
constructor: function(options) {
this._regions = {};
Marionette.Controller.call(this, options);
},
// Add multiple regions using an object literal or a
// function that returns an object literal, where
// each key becomes the region name, and each value is
// the region definition.
addRegions: function(regionDefinitions, defaults) {
if (_.isFunction(regionDefinitions)) {
regionDefinitions = regionDefinitions.apply(this, arguments);
}
var regions = {};
_.each(regionDefinitions, function(definition, name) {
if (_.isString(definition)) {
definition = {selector: definition};
}
if (definition.selector) {
definition = _.defaults({}, definition, defaults);
}
var region = this.addRegion(name, definition);
regions[name] = region;
}, this);
return regions;
},
// Add an individual region to the region manager,
// and return the region instance
addRegion: function(name, definition) {
var region;
if (definition instanceof Marionette.Region) {
region = definition;
} else {
region = Marionette.Region.buildRegion(definition, Marionette.Region);
}
this.triggerMethod('before:add:region', name, region);
this._store(name, region);
this.triggerMethod('add:region', name, region);
return region;
},
// Get a region by name
get: function(name) {
return this._regions[name];
},
// Gets all the regions contained within
// the `regionManager` instance.
getRegions: function(){
return _.clone(this._regions);
},
// Remove a region by name
removeRegion: function(name) {
var region = this._regions[name];
this._remove(name, region);
return region;
},
// Empty all regions in the region manager, and
// remove them
removeRegions: function() {
var regions = this.getRegions();
_.each(this._regions, function(region, name) {
this._remove(name, region);
}, this);
return regions;
},
// Empty all regions in the region manager, but
// leave them attached
emptyRegions: function() {
var regions = this.getRegions();
_.each(regions, function(region) {
region.empty();
}, this);
return regions;
},
// Destroy all regions and shut down the region
// manager entirely
destroy: function() {
this.removeRegions();
return Marionette.Controller.prototype.destroy.apply(this, arguments);
},
// internal method to store regions
_store: function(name, region) {
this._regions[name] = region;
this._setLength();
},
// internal method to remove a region
_remove: function(name, region) {
this.triggerMethod('before:remove:region', name, region);
region.empty();
region.stopListening();
delete this._regions[name];
this._setLength();
this.triggerMethod('remove:region', name, region);
},
// set the number of regions current held
_setLength: function() {
this.length = _.size(this._regions);
}
});
Marionette.actAsCollection(RegionManager.prototype, '_regions');
return RegionManager;
})(Marionette);
// Template Cache
// --------------
// Manage templates stored in `<script>` blocks,
// caching them for faster access.
Marionette.TemplateCache = function(templateId) {
this.templateId = templateId;
};
// TemplateCache object-level methods. Manage the template
// caches from these method calls instead of creating
// your own TemplateCache instances
_.extend(Marionette.TemplateCache, {
templateCaches: {},
// Get the specified template by id. Either
// retrieves the cached version, or loads it
// from the DOM.
get: function(templateId) {
var cachedTemplate = this.templateCaches[templateId];
if (!cachedTemplate) {
cachedTemplate = new Marionette.TemplateCache(templateId);
this.templateCaches[templateId] = cachedTemplate;
}
return cachedTemplate.load();
},
// Clear templates from the cache. If no arguments
// are specified, clears all templates:
// `clear()`
//
// If arguments are specified, clears each of the
// specified templates from the cache:
// `clear("#t1", "#t2", "...")`
clear: function() {
var i;
var args = slice.call(arguments);
var length = args.length;
if (length > 0) {
for (i = 0; i < length; i++) {
delete this.templateCaches[args[i]];
}
} else {
this.templateCaches = {};
}
}
});
// TemplateCache instance methods, allowing each
// template cache object to manage its own state
// and know whether or not it has been loaded
_.extend(Marionette.TemplateCache.prototype, {
// Internal method to load the template
load: function() {
// Guard clause to prevent loading this template more than once
if (this.compiledTemplate) {
return this.compiledTemplate;
}
// Load the template and compile it
var template = this.loadTemplate(this.templateId);
this.compiledTemplate = this.compileTemplate(template);
return this.compiledTemplate;
},
// Load a template from the DOM, by default. Override
// this method to provide your own template retrieval
// For asynchronous loading with AMD/RequireJS, consider
// using a template-loader plugin as described here:
// https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
loadTemplate: function(templateId) {
var template = Backbone.$(templateId).html();
if (!template || template.length === 0) {
throw new Marionette.Error({
name: 'NoTemplateError',
message: 'Could not find template: "' + templateId + '"'
});
}
return template;
},
// Pre-compile the template before caching it. Override
// this method if you do not need to pre-compile a template
// (JST / RequireJS for example) or if you want to change
// the template engine used (Handebars, etc).
compileTemplate: function(rawTemplate) {
return _.template(rawTemplate);
}
});
// Renderer
// --------
// Render a template with data by passing in the template
// selector and the data to render.
Marionette.Renderer = {
// Render a template with data. The `template` parameter is
// passed to the `TemplateCache` object to retrieve the
// template function. Override this method to provide your own
// custom rendering and template handling for all of Marionette.
render: function(template, data) {
if (!template) {
throw new Marionette.Error({
name: 'TemplateNotFoundError',
message: 'Cannot render the template since its false, null or undefined.'
});
}
var templateFunc;
if (typeof template === 'function') {
templateFunc = template;
} else {
templateFunc