// Backbone.Marionette, v1.0.0-rc5 // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // http://github.com/marionettejs/backbone.marionette /*! * Includes BabySitter * https://github.com/marionettejs/backbone.babysitter/ * * Includes Wreqr * https://github.com/marionettejs/backbone.wreqr/ */ // Backbone.BabySitter, v0.0.4 // Copyright (c)2012 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // http://github.com/marionettejs/backbone.babysitter // Backbone.ChildViewContainer // --------------------------- // // Provide a container to store, retrieve and // shut down child views. Backbone.ChildViewContainer = (function (Backbone, _) { // Container Constructor // --------------------- var Container = function (initialViews) { this._views = {}; this._indexByModel = {}; this._indexByCollection = {}; this._indexByCustom = {}; this._updateLength(); this._addInitialViews(initialViews); }; // Container Methods // ----------------- _.extend(Container.prototype, { // Add a view to this container. Stores the view // by `cid` and makes it searchable by the model // and/or collection of the view. 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 it by collection if (view.collection) { this._indexByCollection[view.collection.cid] = viewCid; } // index by custom if (customIndex) { this._indexByCustom[customIndex] = viewCid; } this._updateLength(); }, // Find a view by the model that was attached to // it. Uses the model's `cid` to find it, and // retrieves the view by it's `cid` from the result findByModel: function (model) { var viewCid = this._indexByModel[model.cid]; return this.findByCid(viewCid); }, // Find a view by the collection that was attached to // it. Uses the collection's `cid` to find it, and // retrieves the view by it's `cid` from the result findByCollection: function (col) { var viewCid = this._indexByCollection[col.cid]; 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 it's `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 collection index if (view.collection) { delete this._indexByCollection[view.collection.cid]; } // delete custom index var cust; for (var key in this._indexByCustom) { if (this._indexByCustom.hasOwnProperty(key)) { if (this._indexByCustom[key] === viewCid) { cust = key; break; } } } if (cust) { delete this._indexByCustom[cust]; } // remove the view from the container delete this._views[viewCid]; // update the length this._updateLength(); }, // 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, args) { args = Array.prototype.slice.call(arguments, 1); this.apply(method, args); }, // 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) { var view; // fix for IE < 9 args = args || []; _.each(this._views, function (view, key) { if (_.isFunction(view[method])) { view[method].apply(view, args); } }); }, // Update the `.length` attribute on this container _updateLength: function () { this.length = _.size(this._views); }, // set up an initial list of views _addInitialViews: function (views) { if (!views) { return; } var view, i, length = views.length; for (i = 0; i < length; i++) { view = views[i]; this.add(view); } } }); // 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.Wreqr, v0.1.1 // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // http://github.com/marionettejs/backbone.wreqr Backbone.Wreqr = (function (Backbone, Marionette, _) { "use strict"; var Wreqr = {}; // Handlers // -------- // A registry of functions to call, given a name Wreqr.Handlers = (function (Backbone, _) { "use strict"; // Constructor // ----------- var Handlers = function () { this._handlers = {}; }; Handlers.extend = Backbone.Model.extend; // Instance Members // ---------------- _.extend(Handlers.prototype, { // Add a handler for the given name, with an // optional context to run the handler within addHandler: function (name, handler, context) { var config = { callback: handler, context: context }; this._handlers[name] = config; }, // Get the currently registered handler for // the specified name. Throws an exception if // no handler is found. getHandler: function (name) { var config = this._handlers[name]; if (!config) { throw new Error("Handler not found for '" + name + "'"); } 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._handlers[name]; }, // Remove all handlers from this registry removeAllHandlers: function () { this._handlers = {}; } }); return Handlers; })(Backbone, _); // Wreqr.Commands // -------------- // // A simple command pattern implementation. Register a command // handler and execute it. Wreqr.Commands = (function (Wreqr) { "use strict"; return Wreqr.Handlers.extend({ execute: function () { var name = arguments[0]; var args = Array.prototype.slice.call(arguments, 1); this.getHandler(name).apply(this, args); } }); })(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); 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, _); return Wreqr; })(Backbone, Backbone.Marionette, _); var Marionette = (function (Backbone, _, $) { "use strict"; var Marionette = {}; Backbone.Marionette = Marionette; // 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 it's `options`, with `options` taking precedence. Marionette.getOption = function (target, optionName) { if (!target || !optionName) { return; } var value; if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)) { value = target.options[optionName]; } else { value = target[optionName]; } return value; }; // Mairionette.createObject // ------------------------ // A wrapper / shim for `Object.create`. Uses native `Object.create` // if available, otherwise shims it in place for Marionette to use. Marionette.createObject = (function () { var createObject; // Define this once, and just replace the .prototype on it as needed, // to improve performance in older / less optimized JS engines function F() { } // Check for existing native / shimmed Object.create if (typeof Object.create === "function") { // found native/shim, so use it createObject = Object.create; } else { // An implementation of the Boodman/Crockford delegation // w/ Cornford optimization, as suggested by @unscriptable // https://gist.github.com/3959151 // native/shim not found, so shim it ourself createObject = function (o) { // set the prototype of the function // so we will get `o` as the prototype // of the new object instance F.prototype = o; // create a new object that inherits from // the `o` parameter var child = new F(); // clean up just in case o is really large F.prototype = null; // send it back return child; }; } return createObject; })(); // Trigger an event and 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 () { var args = Array.prototype.slice.apply(arguments); var eventName = args[0]; var segments = eventName.split(":"); var segment, capLetter, methodName = "on"; for (var i = 0; i < segments.length; i++) { segment = segments[i]; capLetter = segment.charAt(0).toUpperCase(); methodName += capLetter + segment.slice(1); } this.trigger.apply(this, args); if (_.isFunction(this[methodName])) { args.shift(); return this[methodName].apply(this, 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 () { // track when the view has been rendered function handleShow(view) { view._isShown = true; triggerDOMRefresh(view); } // track when the view has been shown in the DOM, // using a Marionette.Region (or by other means of triggering "show") 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) { if (_.isFunction(view.triggerMethod)) { view.triggerMethod("dom:refresh"); } } } // Export public API return function (view) { view.listenTo(view, "show", function () { handleShow(view); }); view.listenTo(view, "render", function () { handleRender(view); }); }; })(); // Marionette.bindEntityEvents & unbindEntityEvents // --------------------------- // // These methods are used to bind/unbind a backbone "entity" (collection/model) // to methods on a target object. // // The first paremter, `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 Error("Method '" + methodName + "' was configured as an event handler, but does not exist."); } target.listenTo(entity, evt, method, target); }); } // Bind the event to a supplied callback function function bindToFunction(target, entity, evt, method) { target.listenTo(entity, evt, method, target); } // 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[method]; target.stopListening(entity, evt, method, target); }); } // Bind the event to a supplied callback function function unbindToFunction(target, entity, evt, method) { target.stopListening(entity, evt, method, target); } // generic looping function function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { if (!entity || !bindings) { return; } // 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); }; })(Marionette); // 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 = $.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) { this._callbacks.push({ cb: callback, ctx: contextOverride }); this._deferred.done(function (context, options) { if (contextOverride) { context = contextOverride; } callback.call(context, 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(context, options); }, // 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 that = this; var callbacks = this._callbacks; this._deferred = $.Deferred(); this._callbacks = []; _.each(callbacks, function (cb) { that.add(cb.cb, cb.ctx); }); } }); // 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.triggerMethod = Marionette.triggerMethod; 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, { close: function () { this.stopListening(); this.triggerMethod("close"); this.unbind(); } }); // 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 = Marionette.getOption(this, "el"); if (!this.el) { var err = new Error("An 'el' must be specified for a region."); err.name = "NoElError"; throw err; } if (this.initialize) { var args = Array.prototype.slice.apply(arguments); this.initialize.apply(this, args); } }; // Region Type methods // ------------------- _.extend(Marionette.Region, { // Build an instance of a region by passing in a configuration object // and a default region type to use if none is specified in the config. // // The config object should either be a string as a jQuery DOM selector, // a Region type directly, or an object literal that specifies both // a selector and regionType: // // ```js // { // selector: "#foo", // regionType: MyCustomRegion // } // ``` // buildRegion: function (regionConfig, defaultRegionType) { var regionIsString = (typeof regionConfig === "string"); var regionSelectorIsString = (typeof regionConfig.selector === "string"); var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); var regionIsType = (typeof regionConfig === "function"); if (!regionIsType && !regionIsString && !regionSelectorIsString) { throw new Error("Region must be specified as a Region type, a selector string or an object with selector property"); } var selector, RegionType; // get the selector for the region if (regionIsString) { selector = regionConfig; } if (regionConfig.selector) { selector = regionConfig.selector; } // get the type for the region if (regionIsType) { RegionType = regionConfig; } if (!regionIsType && regionTypeIsUndefined) { RegionType = defaultRegionType; } if (regionConfig.regionType) { RegionType = regionConfig.regionType; } // build the region instance var regionManager = new RegionType({ el: selector }); return regionManager; } }); // 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 `close` method on your view, just after showing // or just before closing the view, respectively. show: function (view) { this.ensureEl(); this.close(); view.render(); this.open(view); Marionette.triggerMethod.call(view, "show"); Marionette.triggerMethod.call(this, "show", view); this.currentView = view; }, ensureEl: function () { if (!this.$el || this.$el.length === 0) { this.$el = this.getEl(this.el); } }, // Override this method to change how the region finds the // DOM element that it manages. Return a jQuery selector object. getEl: function (selector) { return $(selector); }, // Override this method to change how the new view is // appended to the `$el` that the region is managing open: function (view) { this.$el.empty().append(view.el); }, // Close the current view, if there is one. If there is no // current view, it does nothing and returns immediately. close: function () { var view = this.currentView; if (!view || view.isClosed) { return; } if (view.close) { view.close(); } Marionette.triggerMethod.call(this, "close"); delete this.currentView; }, // 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; }, // Reset the region by closing 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.close(); delete this.$el; } }); // Copy the `extend` function used by Backbone's classes Marionette.Region.extend = Marionette.extend; // 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 that = this; 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 = Array.prototype.slice.apply(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 it's own state // and know whether or not it has been loaded _.extend(Marionette.TemplateCache.prototype, { // Internal method to load the template load: function () { var that = this; // 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 = $(templateId).html(); if (!template || template.length === 0) { var msg = "Could not find template: '" + templateId + "'"; var err = new Error(msg); err.name = "NoTemplateError"; throw err; } 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) { var templateFunc = typeof template === 'function' ? template : Marionette.TemplateCache.get(template); var html = templateFunc(data); return html; } }; // Marionette.View // --------------- // The core view type that other Marionette views extend from. Marionette.View = Backbone.View.extend({ constructor: function () { _.bindAll(this, "render"); var args = Array.prototype.slice.apply(arguments); Backbone.View.prototype.constructor.apply(this, args); Marionette.MonitorDOMRefresh(this); this.listenTo(this, "show", this.onShowCalled, this); }, // import the "triggerMethod" to trigger events with corresponding // methods if the method exists triggerMethod: Marionette.triggerMethod, // 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 Marionette.getOption(this, "template"); }, // 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.templateHelpers; if (_.isFunction(templateHelpers)) { templateHelpers = templateHelpers.call(this); } return _.extend(target, templateHelpers); }, // Configure `triggers` to forward DOM events to view // events. `triggers: {"click .foo": "do:foo"}` configureTriggers: function () { if (!this.triggers) { return; } var that = this; var triggerEvents = {}; // Allow `triggers` to be configured as a function var triggers = _.result(this, "triggers"); // Configure the triggers, prevent default // action and stop propagation of DOM events _.each(triggers, function (value, key) { // build the event handler function for the DOM event triggerEvents[key] = function (e) { // stop the event in it's tracks if (e && e.preventDefault) { e.preventDefault(); } if (e && e.stopPropagation) { e.stopPropagation(); } // buil the args for the event var args = { view: this, model: this.model, collection: this.collection }; // trigger the event that.triggerMethod(value, args); }; }); return triggerEvents; }, // Overriding Backbone.View's delegateEvents to handle // the `triggers`, `modelEvents`, and `collectionEvents` configuration delegateEvents: function (events) { this._delegateDOMEvents(events); Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); }, // internal method to delegate DOM events and triggers _delegateDOMEvents: function (events) { events = events || this.events; if (_.isFunction(events)) { events = events.call(this); } var combinedEvents = {}; var triggers = this.configureTriggers(); _.extend(combinedEvents, events, triggers); 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 = Array.prototype.slice.call(arguments); Backbone.View.prototype.undelegateEvents.apply(this, args); Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); }, // Internal method, handles the `show` event. onShowCalled: function () { }, // Default `close` implementation, for removing a view from the // DOM and unbinding it. Regions will call this method // for you. You can specify an `onClose` method in your view to // add custom code that is called after the view is closed. close: function () { if (this.isClosed) { return; } // allow the close to be stopped by returning `false` // from the `onBeforeClose` method var shouldClose = this.triggerMethod("before:close"); if (shouldClose === false) { return; } // mark as closed before doing the actual close, to // prevent infinite loops within "close" event handlers // that are trying to close other views this.isClosed = true; this.triggerMethod("close"); this.remove(); }, // 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; } var that = this; if (!this.uiBindings) { // We want to store the ui hash in uiBindings, since afterwards the values in the ui hash // will be overridden with jQuery selectors. this.uiBindings = _.result(this, "ui"); } // refreshing the associated selectors since they should point to the newly rendered elements. this.ui = {}; _.each(_.keys(this.uiBindings), function (key) { var selector = that.uiBindings[key]; that.ui[key] = that.$(selector); }); } }); // 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({ constructor: function () { var args = Array.prototype.slice.apply(arguments); Marionette.View.prototype.constructor.apply(this, args); }, // Serialize the model or collection for the view. If a model is // found, `.toJSON()` is called. If a collection is found, `.toJSON()` // is also called, but is used to populate 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 = this.model.toJSON(); } else if (this.collection) { data = { items: this.collection.toJSON() }; } return data; }, // 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.isClosed = false; this.triggerMethod("before:render", this); this.triggerMethod("item:before:render", this); var data = this.serializeData(); data = this.mixinTemplateHelpers(data); var template = this.getTemplate(); var html = Marionette.Renderer.render(template, data); this.$el.html(html); this.bindUIElements(); this.triggerMethod("render", this); this.triggerMethod("item:rendered", this); return this; }, // Override the default close event to add a few // more events that are triggered. close: function () { if (this.isClosed) { return; } this.triggerMethod('item:before:close'); var args = Array.prototype.slice.apply(arguments); Marionette.View.prototype.close.apply(this, args); this.triggerMethod('item:closed'); } }); // Collection View // --------------- // A view that iterates over a Backbone.Collection // and renders an individual ItemView for each model. Marionette.CollectionView = Marionette.View.extend({ // used as the prefix for item view events // that are forwarded through the collectionview itemViewEventPrefix: "itemview", // constructor constructor: function (options) { this._initChildViewStorage(); var args = Array.prototype.slice.apply(arguments); Marionette.View.prototype.constructor.apply(this, args); this._initialEvents(); }, // Configured the initial events that the collection view // binds to. Override this method to prevent the initial // events, or to add your own initial events. _initialEvents: function () { if (this.collection) { this.listenTo(this.collection, "add", this.addChildView, this); this.listenTo(this.collection, "remove", this.removeItemView, this); this.listenTo(this.collection, "reset", this.render, this); } }, // Handle a child item added to the collection addChildView: function (item, collection, options) { this.closeEmptyView(); var ItemView = this.getItemView(item); var index = this.collection.indexOf(item); this.addItemView(item, ItemView, index); }, // Override from `Marionette.View` to guarantee the `onShow` method // of child views is called. onShowCalled: function () { this.children.each(function (child) { Marionette.triggerMethod.call(child, "show"); }); }, // Internal method to trigger the before render callbacks // and events triggerBeforeRender: function () { this.triggerMethod("before:render", this); this.triggerMethod("collection:before:render", this); }, // Internal method to trigger the rendered callbacks and // events triggerRendered: function () { this.triggerMethod("render", this); this.triggerMethod("collection:rendered", this); }, // Render the collection of items. Override this method to // provide your own implementation of a render function for // the collection view. render: function () { this.isClosed = false; this.triggerBeforeRender(); this.closeEmptyView(); this.closeChildren(); if (this.collection && this.collection.length > 0) { this.showCollection(); } else { this.showEmptyView(); } this.triggerRendered(); return this; }, // Internal method to loop through each item in the // collection view and show it showCollection: function () { var that = this; var ItemView; this.collection.each(function (item, index) { ItemView = that.getItemView(item); that.addItemView(item, ItemView, index); }); }, // Internal method to show an empty view in place of // a collection of item views, when the collection is // empty showEmptyView: function () { var EmptyView = Marionette.getOption(this, "emptyView"); if (EmptyView && !this._showingEmptyView) { this._showingEmptyView = true; var model = new Backbone.Model(); this.addItemView(model, EmptyView, 0); } }, // Internal method to close an existing emptyView instance // if one exists. Called when a collection view has been // rendered empty, and then an item is added to the collection. closeEmptyView: function () { if (this._showingEmptyView) { this.closeChildren(); delete this._showingEmptyView; } }, // Retrieve the itemView type, either from `this.options.itemView` // or from the `itemView` in the object definition. The "options" // takes precedence. getItemView: function (item) { var itemView = Marionette.getOption(this, "itemView"); if (!itemView) { var err = new Error("An `itemView` must be specified"); err.name = "NoItemViewError"; throw err; } return itemView; }, // Render the child item's view and add it to the // HTML for the collection view. addItemView: function (item, ItemView, index) { var that = this; // get the itemViewOptions if any were specified var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); if (_.isFunction(itemViewOptions)) { itemViewOptions = itemViewOptions.call(this, item); } // build the view var view = this.buildItemView(item, ItemView, itemViewOptions); // set up the child view event forwarding this.addChildViewEventForwarding(view); // this view is about to be added this.triggerMethod("before:item:added", view); // Store the child view itself so we can properly // remove and/or close it later this.children.add(view); // Render it and show it this.renderItemView(view, index); // call the "show" method if the collection view // has already been shown if (this._isShown) { Marionette.triggerMethod.call(view, "show"); } // this view was added this.triggerMethod("after:item:added", view); }, // Set up the child view event forwarding. Uses an "itemview:" // prefix in front of all forwarded events. addChildViewEventForwarding: function (view) { var prefix = Marionette.getOption(this, "itemViewEventPrefix"); // Forward all child item view events through the parent, // prepending "itemview:" to the event name this.listenTo(view, "all", function () { var args = slice.call(arguments); args[0] = prefix + ":" + args[0]; args.splice(1, 0, view); Marionette.triggerMethod.apply(this, args); }, this); }, // render the item view renderItemView: function (view, index) { view.render(); this.appendHtml(this, view, index); }, // Build an `itemView` for every model in the collection. buildItemView: function (item, ItemViewType, itemViewOptions) { var options = _.extend({ model: item }, itemViewOptions); var view = new ItemViewType(options); return view; }, // get the child view by item it holds, and remove it removeItemView: function (item) { var view = this.children.findByModel(item); this.removeChildView(view); this.checkEmpty(); }, // Remove the child view and close it removeChildView: function (view) { // shut down the child view properly, // including events that the collection has from it if (view) { this.stopListening(view); if (view.close) { view.close(); } this.children.remove(view); } this.triggerMethod("item:removed", view); }, // helper to show the empty view if the collection is empty checkEmpty: function () { // check if we're empty now, and if we are, show the // empty view if (!this.collection || this.collection.length === 0) { this.showEmptyView(); } }, // Append the HTML to the collection's `el`. // Override this method to do something other // then `.append`. appendHtml: function (collectionView, itemView, index) { collectionView.$el.append(itemView.el); }, // Internal method to set up the `children` object for // storing all of the child views _initChildViewStorage: function () { this.children = new Backbone.ChildViewContainer(); }, // Handle cleanup and other closing needs for // the collection of views. close: function () { if (this.isClosed) { return; } this.triggerMethod("collection:before:close"); this.closeChildren(); this.triggerMethod("collection:closed"); var args = Array.prototype.slice.apply(arguments); Marionette.View.prototype.close.apply(this, args); }, // Close the child views that this collection view // is holding on to, if any closeChildren: function () { this.children.each(function (child) { this.removeChildView(child); }, this); this.checkEmpty(); } }); // Composite View // -------------- // Used for rendering a branch-leaf, hierarchical structure. // Extends directly from CollectionView and also renders an // an item view as `modelView`, for the top leaf Marionette.CompositeView = Marionette.CollectionView.extend({ constructor: function (options) { var args = Array.prototype.slice.apply(arguments); Marionette.CollectionView.apply(this, args); this.itemView = this.getItemView(); }, // Configured the initial events that the composite view // binds to. Override this method to prevent the initial // events, or to add your own initial events. _initialEvents: function () { if (this.collection) { this.listenTo(this.collection, "add", this.addChildView, this); this.listenTo(this.collection, "remove", this.removeItemView, this); this.listenTo(this.collection, "reset", this.renderCollection, this); } }, // Retrieve the `itemView` to be used when rendering each of // the items in the collection. The default is to return // `this.itemView` or Marionette.CompositeView if no `itemView` // has been defined getItemView: function (item) { var itemView = Marionette.getOption(this, "itemView") || this.constructor; if (!itemView) { var err = new Error("An `itemView` must be specified"); err.name = "NoItemViewError"; throw err; } return itemView; }, // Serialize the collection for the view. // 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 = this.model.toJSON(); } return data; }, // Renders the model once, and the collection once. Calling // this again will tell the model's view to re-render itself // but the collection will not re-render. render: function () { this.isClosed = false; this.resetItemViewContainer(); var html = this.renderModel(); this.$el.html(html); // the ui bindings is done here and not at the end of render since they // will not be available until after the model is rendered, but should be // available before the collection is rendered. this.bindUIElements(); this.triggerMethod("composite:model:rendered"); this.renderCollection(); this.triggerMethod("composite:rendered"); return this; }, // Render the collection for the composite view renderCollection: function () { var args = Array.prototype.slice.apply(arguments); Marionette.CollectionView.prototype.render.apply(this, args); this.triggerMethod("composite:collection:rendered"); }, // Render an individual model, if we have one, as // part of a composite view (branch / leaf). For example: // a treeview. renderModel: function () { var data = {}; data = this.serializeData(); data = this.mixinTemplateHelpers(data); var template = this.getTemplate(); return Marionette.Renderer.render(template, data); }, // Appends the `el` of itemView instances to the specified // `itemViewContainer` (a jQuery selector). Override this method to // provide custom logic of how the child item view instances have their // HTML appended to the composite view instance. appendHtml: function (cv, iv) { var $container = this.getItemViewContainer(cv); $container.append(iv.el); }, // Internal method to ensure an `$itemViewContainer` exists, for the // `appendHtml` method to use. getItemViewContainer: function (containerView) { if ("$itemViewContainer" in containerView) { return containerView.$itemViewContainer; } var container; if (containerView.itemViewContainer) { var selector = _.result(containerView, "itemViewContainer"); container = containerView.$(selector); if (container.length <= 0) { var err = new Error("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer); err.name = "ItemViewContainerMissingError"; throw err; } } else { container = containerView.$el; } containerView.$itemViewContainer = container; return container; }, // Internal method to reset the `$itemViewContainer` on render resetItemViewContainer: function () { if (this.$itemViewContainer) { delete this.$itemViewContainer; } } }); // Layout // ------ // Used for managing application layouts, nested layouts and // multiple regions within an application or sub-application. // // A specialized view type that renders an area of HTML and then // attaches `Region` instances to the specified `regions`. // Used for composite view management and sub-application areas. Marionette.Layout = Marionette.ItemView.extend({ regionType: Marionette.Region, // Ensure the regions are avialable when the `initialize` method // is called. constructor: function () { this._firstRender = true; this.initializeRegions(); var args = Array.prototype.slice.apply(arguments); Marionette.ItemView.apply(this, args); }, // Layout's render will use the existing region objects the // first time it is called. Subsequent calls will close the // views that the regions are showing and then reset the `el` // for the regions to the newly rendered DOM elements. render: function () { if (this._firstRender) { // if this is the first render, don't do anything to // reset the regions this._firstRender = false; } else { // If this is not the first render call, then we need to // re-initializing the `el` for each region this.closeRegions(); this.reInitializeRegions(); } var args = Array.prototype.slice.apply(arguments); var result = Marionette.ItemView.prototype.render.apply(this, args); return result; }, // Handle closing regions, and then close the view itself. close: function () { if (this.isClosed) { return; } this.closeRegions(); this.destroyRegions(); var args = Array.prototype.slice.apply(arguments); Marionette.ItemView.prototype.close.apply(this, args); }, // Initialize the regions that have been defined in a // `regions` attribute on this layout. The key of the // hash becomes an attribute on the layout object directly. // For example: `regions: { menu: ".menu-container" }` // will product a `layout.menu` object which is a region // that controls the `.menu-container` DOM element. initializeRegions: function () { if (!this.regionManagers) { this.regionManagers = {}; } var that = this; var regions = this.regions || {}; _.each(regions, function (region, name) { var regionManager = Marionette.Region.buildRegion(region, that.regionType); regionManager.getEl = function (selector) { return that.$(selector); }; that.regionManagers[name] = regionManager; that[name] = regionManager; }); }, // Re-initialize all of the regions by updating the `el` that // they point to reInitializeRegions: function () { if (this.regionManagers && _.size(this.regionManagers) === 0) { this.initializeRegions(); } else { _.each(this.regionManagers, function (region) { region.reset(); }); } }, // Close all of the regions that have been opened by // this layout. This method is called when the layout // itself is closed. closeRegions: function () { var that = this; _.each(this.regionManagers, function (manager, name) { manager.close(); }); }, // Destroys all of the regions by removing references // from the Layout destroyRegions: function () { var that = this; _.each(this.regionManagers, function (manager, name) { delete that[name]; }); this.regionManagers = {}; } }); // AppRouter // --------- // Reduce the boilerplate code of handling route events // and then calling a single method on another object. // Have your routers configured to call the method on // your object, directly. // // Configure an AppRouter with `appRoutes`. // // App routers can only take one `controller` object. // It is recommended that you divide your controller // objects in to smaller peices of related functionality // and have multiple routers / controllers, instead of // just one giant router and controller. // // You can also add standard routes to an AppRouter. Marionette.AppRouter = Backbone.Router.extend({ constructor: function (options) { var args = Array.prototype.slice.apply(arguments); Backbone.Router.prototype.constructor.apply(this, args); this.options = options; if (this.appRoutes) { var controller = Marionette.getOption(this, "controller"); this.processAppRoutes(controller, this.appRoutes); } }, // Internal method to process the `appRoutes` for the // router, and turn them in to routes that trigger the // specified method on the specified `controller`. processAppRoutes: function (controller, appRoutes) { var method, methodName; var route, routesLength, i; var routes = []; var router = this; for (route in appRoutes) { if (appRoutes.hasOwnProperty(route)) { routes.unshift([route, appRoutes[route]]); } } routesLength = routes.length; for (i = 0; i < routesLength; i++) { route = routes[i][0]; methodName = routes[i][1]; method = controller[methodName]; if (!method) { var msg = "Method '" + methodName + "' was not found on the controller"; var err = new Error(msg); err.name = "NoMethodError"; throw err; } method = _.bind(method, controller); router.route(route, methodName, method); } } }); // Application // ----------- // Contain and manage the composite application as a whole. // Stores and starts up `Region` objects, includes an // event aggregator as `app.vent` Marionette.Application = function (options) { this.initCallbacks = new Marionette.Callbacks(); this.vent = new Backbone.Wreqr.EventAggregator(); this.commands = new Backbone.Wreqr.Commands(); this.reqres = new Backbone.Wreqr.RequestResponse(); this.submodules = {}; _.extend(this, options); this.triggerMethod = Marionette.triggerMethod; }; _.extend(Marionette.Application.prototype, Backbone.Events, { // Command execution, facilitated by Backbone.Wreqr.Commands execute: function () { var args = Array.prototype.slice.apply(arguments); this.commands.execute.apply(this.commands, args); }, // Request/response, facilitated by Backbone.Wreqr.RequestResponse request: function () { var args = Array.prototype.slice.apply(arguments); return this.reqres.request.apply(this.reqres, args); }, // Add an initializer that is either run at when the `start` // method is called, or run immediately if added after `start` // has already been called. addInitializer: function (initializer) { this.initCallbacks.add(initializer); }, // kick off all of the application's processes. // initializes all of the regions that have been added // to the app, and runs all of the initializer functions start: function (options) { this.triggerMethod("initialize:before", options); this.initCallbacks.run(options, this); this.triggerMethod("initialize:after", options); this.triggerMethod("start", options); }, // Add regions to your app. // Accepts a hash of named strings or Region objects // addRegions({something: "#someRegion"}) // addRegions{{something: Region.extend({el: "#someRegion"}) }); addRegions: function (regions) { var that = this; _.each(regions, function (region, name) { var regionManager = Marionette.Region.buildRegion(region, Marionette.Region); that[name] = regionManager; }); }, // Removes a region from your app. // Accepts the regions name // removeRegion('myRegion') removeRegion: function (region) { this[region].close(); delete this[region]; }, // Create a module, attached to the application module: function (moduleNames, moduleDefinition) { // slice the args, and add this application object as the // first argument of the array var args = slice.call(arguments); args.unshift(this); // see the Marionette.Module object for more information return Marionette.Module.create.apply(Marionette.Module, args); } }); // Copy the `extend` function used by Backbone's classes Marionette.Application.extend = Marionette.extend; // Module // ------ // A simple module system, used to create privacy and encapsulation in // Marionette applications Marionette.Module = function (moduleName, app) { this.moduleName = moduleName; // store sub-modules this.submodules = {}; this._setupInitializersAndFinalizers(); // store the configuration for this module this.app = app; this.startWithParent = true; this.triggerMethod = Marionette.triggerMethod; }; // Extend the Module prototype with events / listenTo, so that the module // can be used as an event aggregator or pub/sub. _.extend(Marionette.Module.prototype, Backbone.Events, { // Initializer for a specific module. Initializers are run when the // module's `start` method is called. addInitializer: function (callback) { this._initializerCallbacks.add(callback); }, // Finalizers are run when a module is stopped. They are used to teardown // and finalize any variables, references, events and other code that the // module had set up. addFinalizer: function (callback) { this._finalizerCallbacks.add(callback); }, // Start the module, and run all of it's initializers start: function (options) { // Prevent re-starting a module that is already started if (this._isInitialized) { return; } // start the sub-modules (depth-first hierarchy) _.each(this.submodules, function (mod) { // check to see if we should start the sub-module with this parent var startWithParent = true; startWithParent = mod.startWithParent; // start the sub-module if (startWithParent) { mod.start(options); } }); // run the callbacks to "start" the current module this.triggerMethod("before:start", options); this._initializerCallbacks.run(options, this); this._isInitialized = true; this.triggerMethod("start", options); }, // Stop this module by running its finalizers and then stop all of // the sub-modules for this module stop: function () { // if we are not initialized, don't bother finalizing if (!this._isInitialized) { return; } this._isInitialized = false; Marionette.triggerMethod.call(this, "before:stop"); // stop the sub-modules; depth-first, to make sure the // sub-modules are stopped / finalized before parents _.each(this.submodules, function (mod) { mod.stop(); }); // run the finalizers this._finalizerCallbacks.run(undefined, this); // reset the initializers and finalizers this._initializerCallbacks.reset(); this._finalizerCallbacks.reset(); Marionette.triggerMethod.call(this, "stop"); }, // Configure the module with a definition function and any custom args // that are to be passed in to the definition function addDefinition: function (moduleDefinition, customArgs) { this._runModuleDefinition(moduleDefinition, customArgs); }, // Internal method: run the module definition function with the correct // arguments _runModuleDefinition: function (definition, customArgs) { if (!definition) { return; } // build the correct list of arguments for the module definition var args = _.flatten([ this, this.app, Backbone, Marionette, $, _, customArgs ]); definition.apply(this, args); }, // Internal method: set up new copies of initializers and finalizers. // Calling this method will wipe out all existing initializers and // finalizers. _setupInitializersAndFinalizers: function () { this._initializerCallbacks = new Marionette.Callbacks(); this._finalizerCallbacks = new Marionette.Callbacks(); } }); // Type methods to create modules _.extend(Marionette.Module, { // Create a module, hanging off the app parameter as the parent object. create: function (app, moduleNames, moduleDefinition) { var that = this; var module = app; // get the custom args passed in after the module definition and // get rid of the module name and definition function var customArgs = slice.apply(arguments); customArgs.splice(0, 3); // split the module names and get the length moduleNames = moduleNames.split("."); var length = moduleNames.length; // store the module definition for the last module in the chain var moduleDefinitions = []; moduleDefinitions[length - 1] = moduleDefinition; // Loop through all the parts of the module definition _.each(moduleNames, function (moduleName, i) { var parentModule = module; module = that._getModule(parentModule, moduleName, app); that._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); }); // Return the last module in the definition chain return module; }, _getModule: function (parentModule, moduleName, app, def, args) { // Get an existing module of this name if we have one var module = parentModule[moduleName]; if (!module) { // Create a new module if we don't have one module = new Marionette.Module(moduleName, app); parentModule[moduleName] = module; // store the module on the parent parentModule.submodules[moduleName] = module; } return module; }, _addModuleDefinition: function (parentModule, module, def, args) { var fn; var startWithParent; if (_.isFunction(def)) { // if a function is supplied for the module definition fn = def; startWithParent = true; } else if (_.isObject(def)) { // if an object is supplied fn = def.define; startWithParent = def.startWithParent; } else { // if nothing is supplied startWithParent = true; } // add module definition if needed if (fn) { module.addDefinition(fn, args); } // `and` the two together, ensuring a single `false` will prevent it // from starting with the parent var tmp = module.startWithParent; module.startWithParent = module.startWithParent && startWithParent; // setup auto-start if needed if (module.startWithParent && !module.startWithParentIsConfigured) { // only configure this once module.startWithParentIsConfigured = true; // add the module initializer config parentModule.addInitializer(function (options) { if (module.startWithParent) { module.start(options); } }); } } }); return Marionette; })(Backbone, _, $ || window.jQuery || window.Zepto || window.ender);