// MarionetteJS (Backbone.Marionette) // ---------------------------------- // v1.0.3 // // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://marionettejs.com /*! * Includes BabySitter * https://github.com/marionettejs/backbone.babysitter/ * * Includes Wreqr * https://github.com/marionettejs/backbone.wreqr/ */ // Backbone.BabySitter // ------------------- // v0.0.5 // // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. // Distributed under MIT license // // http://github.com/babysitterjs/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` 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(arguments); var length = args.length; if (length > 0){ for(i=0; i 0) { this.showCollection(); } else { this.showEmptyView(); } }, // Internal method to loop through each item in the // collection view and show it showCollection: function(){ var ItemView; this.collection.each(function(item, index){ ItemView = this.getItemView(item); this.addItemView(item, ItemView, index); }, this); }, // 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){ throwError("An `itemView` must be specified", "NoItemViewError"); } return itemView; }, // Render the child item's view and add it to the // HTML for the collection view. addItemView: function(item, ItemView, index){ // get the itemViewOptions if any were specified var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); if (_.isFunction(itemViewOptions)){ itemViewOptions = itemViewOptions.call(this, item, index); } // 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(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); return new ItemViewType(options); }, // 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); // call 'close' or 'remove', depending on which is found if (view.close) { view.close(); } else if (view.remove) { view.remove(); } 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"); Marionette.View.prototype.close.apply(this, slice(arguments)); }, // 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({ // 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._renderChildren, 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){ throwError("An `itemView` must be specified", "NoItemViewError"); } 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.isRendered = true; this.isClosed = false; this.resetItemViewContainer(); this.triggerBeforeRender(); 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._renderChildren(); this.triggerMethod("composite:rendered"); this.triggerRendered(); return this; }, _renderChildren: function(){ if (this.isRendered){ Marionette.CollectionView.prototype._renderChildren.call(this); 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, index){ 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) { throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); } } 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 available when the `initialize` method // is called. constructor: function (options) { options = options || {}; this._firstRender = true; this._initializeRegions(options); Marionette.ItemView.call(this, options); }, // 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.isClosed){ // a previously closed layout means we need to // completely re-initialize the regions this._initializeRegions(); } else { // If this is not the first render call, then we need to // re-initializing the `el` for each region 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.regionManager.close(); var args = Array.prototype.slice.apply(arguments); Marionette.ItemView.prototype.close.apply(this, args); }, // Add a single region, by name, to the layout addRegion: function(name, definition){ var regions = {}; regions[name] = definition; return this.addRegions(regions)[name]; }, // Add multiple regions as a {name: definition, name2: def2} object literal addRegions: function(regions){ this.regions = _.extend(this.regions || {}, regions); return this._buildRegions(regions); }, // Remove a single region from the Layout, by name removeRegion: function(name){ return this.regionManager.removeRegion(name); }, // internal method to build regions _buildRegions: function(regions){ var that = this; var defaults = { parentEl: function(){ return that.$el; } }; return this.regionManager.addRegions(regions, defaults); }, // Internal method to initialize the regions that have been defined in a // `regions` attribute on this layout. _initializeRegions: function (options) { var regions; this._initRegionManager(); if (_.isFunction(this.regions)) { regions = this.regions(options); } else { regions = this.regions || {}; } this.addRegions(regions); }, // Internal method to re-initialize all of the regions by updating the `el` that // they point to _reInitializeRegions: function(){ this.regionManager.closeRegions(); this.regionManager.each(function(region){ region.reset(); }); }, // Internal method to initialize the region manager // and all regions in it _initRegionManager: function(){ this.regionManager = new Marionette.RegionManager(); this.listenTo(this.regionManager, "region:add", function(name, region){ this[name] = region; this.trigger("region:add", name, region); }); this.listenTo(this.regionManager, "region:remove", function(name, region){ delete this[name]; this.trigger("region:remove", name, region); }); } }); // 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 pieces 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){ Backbone.Router.prototype.constructor.apply(this, slice(arguments)); 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 routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes _.each(routeNames, function(route) { var methodName = appRoutes[route]; var method = controller[methodName]; if (!method) { throw new Error("Method '" + methodName + "' was not found on the controller"); } this.route(route, methodName, _.bind(method, controller)); }, this); } }); // 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._initRegionManager(); 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){ return this._regionManager.addRegions(regions); }, // Removes a region from your app. // Accepts the regions name // removeRegion('myRegion') removeRegion: function(region) { this._regionManager.removeRegion(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(arguments); args.unshift(this); // see the Marionette.Module object for more information return Marionette.Module.create.apply(Marionette.Module, args); }, // Internal method to set up the region manager _initRegionManager: function(){ this._regionManager = new Marionette.RegionManager(); this.listenTo(this._regionManager, "region:add", function(name, region){ this[name] = region; }); this.listenTo(this._regionManager, "region:remove", function(name, region){ delete this[name]; }); } }); // 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 its 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 if (mod.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, 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 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(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 = this._getModule(parentModule, moduleName, app); this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); }, this); // 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 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; })(this, Backbone, _);