UNPKG

k.backbone.marionette

Version:
1,604 lines (1,277 loc) 100 kB
// 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