k.backbone.marionette
Version:
Make your Backbone.js apps dance!
1,604 lines (1,277 loc) • 100 kB
JavaScript
// MarionetteJS (Backbone.Marionette)
// ----------------------------------
// v2.2.2
//
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://marionettejs.com
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['backbone', 'underscore', 'jquery', 'backbone.wreqr', 'backbone.babysitter'], function(Backbone, _, $) {
return (root.Marionette = factory(root, Backbone, _, $));
});
} else if (typeof exports !== 'undefined') {
var $ = require('jquery');
var Backbone = require('backbone');
var _ = require('underscore');
var Wreqr = require('backbone.wreqr');
var BabySitter = require('backbone.babysitter');
module.exports = factory(root, Backbone, _, $);
} else {
root.Marionette = factory(root, root.Backbone, root._, root.$);
}
}(this, function(root, Backbone, _, $) {
'use strict';
var previousMarionette = root.Marionette;
var Marionette = Backbone.Marionette = {};
Marionette.VERSION = '2.2.2';
Marionette.noConflict = function() {
root.Marionette = previousMarionette;
return this;
};
Backbone.$ = $;
// 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 = Marionette.TemplateCache.get(template);
}
return templateFunc(data);
}
};
/* jshint maxlen: 114, nonew: false */
// Marionette.View
// ---------------
// The core view class that other Marionette views extend from.
Marionette.View = Backbone.View.extend({
constructor: function(options) {
_.bindAll(this, 'render');
// this exposes view options to the view initializer
// this is a backfill since backbone removed the assignment
// of this.options
// at some point however this may be removed
this.options = _.extend({}, _.result(this, 'options'), _.isFunction(options) ? options.call(this) : options);
this._behaviors = Marionette.Behaviors(this);
Backbone.View.apply(this, arguments);
Marionette.MonitorDOMRefresh(this);
this.listenTo(this, 'show', this.onShowCalled);
},
// Get the template for this view
// instance. You can set a `template` attribute in the view
// definition or pass a `template: "whatever"` parameter in
// to the constructor options.
getTemplate: function() {
return this.getOption('template');
},
// Serialize a model by returning its attributes. Clones
// the attributes to allow modification.
serializeModel: function(model){
return model.toJSON.apply(model, slice.call(arguments, 1));
},
// Mix in template helper methods. Looks for a
// `templateHelpers` attribute, which can either be an
// object literal, or a function that returns an object
// literal. All methods and attributes from this object
// are copies to the object passed in.
mixinTemplateHelpers: function(target) {
target = target || {};
var templateHelpers = this.getOption('templateHelpers');
if (_.isFunction(templateHelpers)) {
templateHelpers = templateHelpers.call(this);
}
return _.extend(target, templateHelpers);
},
// normalize the keys of passed hash with the views `ui` selectors.
// `{"@ui.foo": "bar"}`
normalizeUIKeys: function(hash) {
var ui = _.result(this, 'ui');
var uiBindings = _.result(this, '_uiBindings');
return Marionette.normalizeUIKeys(hash, uiBindings || ui);
},
// normalize the values of passed hash with the views `ui` selectors.
// `{foo: "@ui.bar"}`
normalizeUIValues: function(hash) {
var ui = _.result(this, 'ui');
var uiBindings = _.result(this, '_uiBindings');
return Marionette.normalizeUIValues(hash, uiBindings || ui);
},
// Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}`
configureTriggers: function() {
if (!this.triggers) { return; }
var triggerEvents = {};
// Allow `triggers` to be configured as a function
var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
// Configure the triggers, prevent default
// action and stop propagation of DOM events
_.each(triggers, function(value, key) {
triggerEvents[key] = this._buildViewTrigger(value);
}, this);
return triggerEvents;
},
// Overriding Backbone.View's delegateEvents to handle
// the `triggers`, `modelEvents`, and `collectionEvents` configuration
delegateEvents: function(events) {
this._delegateDOMEvents(events);
this.bindEntityEvents(this.model, this.getOption('modelEvents'));
this.bindEntityEvents(this.collection, this.getOption('collectionEvents'));
_.each(this._behaviors, function(behavior) {
behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents'));
behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
}, this);
return this;
},
// internal method to delegate DOM events and triggers
_delegateDOMEvents: function(eventsArg) {
var events = eventsArg || this.events;
if (_.isFunction(events)) { events = events.call(this); }
// normalize ui keys
events = this.normalizeUIKeys(events);
if(_.isUndefined(eventsArg)) {this.events = events;}
var combinedEvents = {};
// look up if this view has behavior events
var behaviorEvents = _.result(this, 'behaviorEvents') || {};
var triggers = this.configureTriggers();
var behaviorTriggers = _.result(this, 'behaviorTriggers') || {};
// behavior events will be overriden by view events and or triggers
_.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
// Overriding Backbone.View's undelegateEvents to handle unbinding
// the `triggers`, `modelEvents`, and `collectionEvents` config
undelegateEvents: function() {
var args = slice.call(arguments);
Backbone.View.prototype.undelegateEvents.apply(this, args);
this.unbindEntityEvents(this.model, this.getOption('modelEvents'));
this.unbindEntityEvents(this.collection, this.getOption('collectionEvents'));
_.each(this._behaviors, function(behavior) {
behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents'));
behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
}, this);
return this;
},
// Internal method, handles the `show` event.
onShowCalled: function() {},
// Internal helper method to verify whether the view hasn't been destroyed
_ensureViewIsIntact: function() {
if (this.isDestroyed) {
throw new Marionette.Error({
name: 'ViewDestroyedError',
message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.'
});
}
},
// Default `destroy` implementation, for removing a view from the
// DOM and unbinding it. Regions will call this method
// for you. You can specify an `onDestroy` method in your view to
// add custom code that is called after the view is destroyed.
destroy: function() {
if (this.isDestroyed) { return; }
var args = slice.call(arguments);
this.triggerMethod.apply(this, ['before:destroy'].concat(args));
// mark as destroyed before doing the actual destroy, to
// prevent infinite loops within "destroy" event handlers
// that are trying to destroy other views
this.isDestroyed = true;
this.triggerMethod.apply(this, ['destroy'].concat(args));
// unbind UI elements
this.unbindUIElements();
// remove the view from the DOM
this.remove();
// Call destroy on each behavior after
// destroying the view.
// This unbinds event listeners
// that behaviors have registered for.
_.invoke(this._behaviors, 'destroy', args);
return this;
},
bindUIElements: function() {
this._bindUIElements();
_.invoke(this._behaviors, this._bindUIElements);
},
// This method binds the elements specified in the "ui" hash inside the view's code with
// the associated jQuery selectors.
_bindUIElements: function() {
if (!this.ui) { return; }
// store the ui hash in _uiBindings so they can be reset later
// and so re-rendering the view will be able to find the bindings
if (!this._uiBindings) {
this._uiBindings = this.ui;
}
// get the bindings result, as a function or otherwise
var bindings = _.result(this, '_uiBindings');
// empty the ui so we don't have anything to start with
this.ui = {};
// bind each of the selectors
_.each(_.keys(bindings), function(key) {
var selector = bindings[key];
this.ui[key] = this.$(selector);
}, this);
},
// This method unbinds the elements specified in the "ui" hash
unbindUIElements: function() {
this._unbindUIElements();
_.invoke(this._behaviors, this._unbindUIElements);
},
_unbindUIElements: function() {
if (!this.ui || !this._uiBindings) { return; }
// delete all of the existing ui bindings
_.each(this.ui, function($el, name) {
delete this.ui[name];
}, this);
// reset the ui element to the original bindings configuration
this.ui = this._uiBindings;
delete this._uiBindings;
},
// Internal method to create an event handler for a given `triggerDef` like
// 'click:foo'
_buildViewTrigger: function(triggerDef) {
var hasOptions = _.isObject(triggerDef);
var options = _.defaults({}, (hasOptions ? triggerDef : {}), {
preventDefault: true,
stopPropagation: true
});
var eventName = hasOptions ? options.event : triggerDef;
return function(e) {
if (e) {
if (e.preventDefault && options.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation && options.stopPropagation) {
e.stopPropagation();
}
}
var args = {
view: this,
model: this.model,
collection: this.collection
};
this.triggerMethod(eventName, args);
};
},
setElement: function() {
var ret = Backbone.View.prototype.setElement.apply(this, arguments);
// proxy behavior $el to the view's $el.
// This is needed because a view's $el proxy
// is not set until after setElement is called.
_.invoke(this._behaviors, 'proxyViewProperties', this);
return ret;
},
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: function() {
var args = arguments;
var triggerMethod = Marionette.triggerMethod;
var ret = triggerMethod.apply(this, args);
_.each(this._behaviors, function(b) {
triggerMethod.apply(b, args);
});
return ret;
},
// Imports the "normalizeMethods" to transform hashes of
// events=>function references/names to a hash of events=>function references
normalizeMethods: Marionette.normalizeMethods,
// 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
});
// Item View
// ---------
// A single item view implementation that contains code for rendering
// with underscore.js templates, serializing the view's model or collection,
// and calling several methods on extended views, such as `onRender`.
Marionette.ItemView = Marionette.View.extend({
// Setting up the inheritance chain which allows changes to
// Marionette.View.prototype.constructor which allows overriding
constructor: function() {
Marionette.View.apply(this, arguments);
},
// Serialize the model or collection for the view. If a model is
// found, the view's `serializeModel` is called. If a collection is found,
// each model in the collection is serialized by calling
// the view's `serializeCollection` and put into an `items` array in
// the resulting data. If both are found, defaults to the model.
// You can override the `serializeData` method in your own view definition,
// to provide custom serialization for your view's data.
serializeData: function(){
var data = {};
if (this.model) {
data = _.partial(this.serializeModel, this.model).apply(this, arguments);
}
else if (this.collection) {
data = { items: _.partial(this.serializeCollection, this.collection).apply(this, arguments) };
}
return data;
},
// Serialize a collection by serializing each of its models.
serializeCollection: function(collection){
return collection.toJSON.apply(collection, slice.call(arguments, 1));
},
// Render the view, defaulting to underscore.js templates.
// You can override this in your view definition to provide
// a very specific rendering for your view. In general, though,
// you should override the `Marionette.Renderer` object to
// change how Marionette renders views.
render: function() {
this._ensureViewIsIntact();
this.triggerMethod('before:render', this);
this._renderTemplate();
this.bindUIElements();
this.triggerMethod('render', this);
return this;
},
// Internal method to render the template with the serialized data
// and template helpers via the `Marionette.Renderer` object.
// Throws an `UndefinedTemplateError` error if the template is
// any falsely value but literal `false`.
_renderTemplate: function() {
var template = this.getTemplate();
// Allow template-less item views
if (template === false) {
return;
}
if (!template) {
throw new Marionette.Error({
name: 'UndefinedTemplateError',
message: 'Cannot render the template since it is null or undefined.'
});
}
// Add in entity data and template helpers
var data = this.serializeData();
data = this.mixinTemplateHelpers(data);
// Render and add to el
var html = Marionette.Renderer.render(template, data, this);
this.attachElContent(html);
return this;
},
// Attaches the content of a given view.
// This method can be overridden to optimize rendering,
// or to render in a non standard way.
//
// For example, using `innerHTML` instead of `$el.html`
//
// ```js
// attachElContent: function(html) {
// this.el.innerHTML = html;
// return this;
// }
// ```
attachElContent: function(html) {
this.$el.html(html);
return this;
},
// Override the default destroy event to add a few
// more events that are triggered.
destroy: function() {
if (this.isDestroyed) { return; }
return Marionette.View.prototype.destroy.apply(this, arguments);
}
});
/* jshint maxstatements: 14 */
// Collection View
// ---------------
// A view that iterates over a Backbone.Collection
// and renders an individual child view for each model.
Marionette.CollectionView = Marionette.View.extend({
// used as the prefix for child view events
// that are forwarded through the collectionview
childViewEventPrefix: 'childview',
// constructor
// option to pass `{sort: false}` to prevent the `CollectionView` from
// maintaining the sorted order of the collection.
// This will fallback onto appending childView's to the end.
constructor: function(options){
var initOptions = options || {};
this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort;
this.once('render', this._initialEvents);
this._initChildViewStorage();
Marionette.View.apply(this, arguments);
this.initRenderBuffer();
},
// Instead of inserting elements one by one into the page,
// it's much more performant to insert elements into a document
// fragment and then insert that document fragment into the page
initRenderBuffer: function() {
this.elBuffer = document.createDocumentFragment();
this._bufferedChildren = [];
},
startBuffering: function() {
this.initRenderBuffer();
this.isBuffering = true;
},
endBuffering: function() {
this.isBuffering = false;
this._triggerBeforeShowBufferedChildren();
this.attachBuffer(this, this.elBuffer);
this._triggerShowBufferedChildren();
this.initRenderBuffer();
},
_triggerBeforeShowBufferedChildren: function() {
if (this._isShown) {
_.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show'));
}
},
_triggerShowBufferedChildren: function() {
if (this._isShown) {
_.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show'));
this._bufferedChildren = [];
}
},
// Internal method for _.each loops to call `Marionette.triggerMethodOn` on
// a child view
_triggerMethodOnChild: function(event, childView) {
Marionette.triggerMethodOn(childView, event);
},
// Configured the initial events that the collection view
// binds to.
_initialEvents: function() {
if (this.collection) {
this.listenTo(this.collection, 'add', this._onCollectionAdd);
this.listenTo(this.c