src/marionette.enhancedController.js
(function(factory) { if (typeof define === 'function' && define.amd) {

AMD. Register as an anonymous module.

define(['marionette', 'backbone', 'jquery', 'underscore'], factory); } else {

Use browser globals. Will fail if they are not yet loaded.

/*globals Marionette, Backbone, $, _ */ factory(Marionette, Backbone, $, _); } }(function(Marionette, Backbone, $, _) { /*jshint camelcase: false */ var LoadingController; var LoadingView; var Spinner; var controllerRegistry = {}; var originalFunctions; var showLoading; var whenFetched; Spinner = // @include ../tmp/spin.js whenFetched = function(entities, callback) { var xhrs; xhrs = _.chain([entities]).flatten().pluck('_fetch').value(); $.when.apply(this, xhrs).done(callback); };

Save a copy of the original functions.

originalFunctions = { applicationConstructor: Marionette.Application.prototype.constructor, controllerConstructor: Marionette.Controller.prototype.constructor, destroy: Marionette.Controller.prototype.destroy }; (function overrideSync() { var _sync = Backbone.sync; Backbone.sync = function(method, entity, options) { var sync; if (_.isUndefined(options) || options === null) { options = {}; } sync = _sync(method, entity, options); if (!entity._fetch && method === 'read') { entity._fetch = sync; } return sync; }; })(); Marionette.Controller = Marionette.Controller.extend({

Constructor

Override Marionette.Controller.constructor. Here, we'll save the region passed in via options and save a reference to this controller.

constructor: function(options) { options = options || {}; if (options.region) {

The app controller has already been loaded.

Define this.region so this controller knows where to show itself.

this.region = options.region;

Save the instance id. This will help with memory leak tracking.

this._instanceId = _.uniqueId('controller');

Save instance in the registry.

controllerRegistry[this._instanceId] = this; }

Call the original constructor.

originalFunctions.controllerConstructor.apply(this, arguments); }, show: function(view, options) { if (this.isClosed) { return; } if (!this.region) { throw new Error('A controller must be given a region in its options'); }

Default options to {}

if (_.isUndefined(options) || options === null) { options = {}; }

Set default options

_.defaults(options, { loading: false, region: this.region }); this._setMainView(view); this._manageView(view, options);

When this controller is closed or destroyed, clean up the registry.

this.on('destroy close', function() { delete this.region; delete this.options;

Remove instance from the registry.

delete controllerRegistry[this._instanceId]; this.isClosed = true; }); }, _setMainView: function(view) {

Taken verbatim from backbone rails loading views:

the first view we show is always going to become the mainView of our controller (whether its a layout or another view type). So if this is a layout, when we show other regions inside of that layout, we check for the existance of a mainView first, so our controller is only closed down when the original mainView is closed.

if (this._mainView) { return; } this._mainView = view;

Listen to close and destroy events on the view. Depending on which version of backbone we're using, a different functions are called.

this.listenTo(view, 'close', this.close); this.listenTo(view, 'destroy', this.destroy); }, _manageView: function(view, options) { if (options.loading) {

Show loading view

showLoading(view, options); } else { options.region.show(view); } } }); Marionette.Controller._registry = controllerRegistry; Marionette.Controller._resetRegistry = function() {

Allow develoompent debuggging since this is a development function.

//jshint devel: true var controller; var index; var message; var newCount; var oldCount = _.size(controllerRegistry); for (index in controllerRegistry) { if (controllerRegistry.hasOwnProperty(index)) { controller = controllerRegistry[index]; if (controller.region) { if (controller.region.empty) { controller.region.empty(); } else if (controller.region.close) { controller.region.close(); } } } } newCount = _.size(controllerRegistry); message = 'There were ' + oldCount + ' controllers in the registry, there are now ' + newCount; if (newCount > 0) { console.warn(message, controllerRegistry); } else { console.info(message); } };

Show loading function

showLoading = function(view, options) { new LoadingController({ view: view, region: options.region, config: options }); };

Loading View

LoadingView = Marionette.ItemView.extend({ constructor: function EnhancedController_LoadingView() {

Name constructor.

return Object.getPrototypeOf(Object.getPrototypeOf(this)).constructor.apply(this, arguments); }, template: false, className: 'loading-container', onShow: function() { var opts; opts = this._getOptions(); this.spinner = new Spinner(opts); this.spinner.spin(this.$el.get(0)); }, onClose: function() { this.spinner.stop(); }, _getOptions: function() { return { lines: 10, length: 6, width: 2.5, radius: 7, corners: 1, rotate: 9, direction: 1, color: '#000', speed: 1, trail: 60, shadow: false, hwaccel: true, className: 'spinner', zIndex: 2e9, top: 'auto', left: 'auto' }; } });

Loading Controller

LoadingController = Marionette.Controller.extend({ initialize: function(options) { var config; var loadingView; var view; view = options.view; config = options.config; config = _.isBoolean(config) ? {} : config; _.defaults(config, { debug: false, entities: this.getEntities(view), loadingType: 'spinner', loadingMessage: null }); switch (config.loadingType) { case 'opacity': this.region.show(view); this.region.currentView.$el.css('opacity', 0.5); break; case 'spinner': loadingView = this.getLoadingView(); this.show(loadingView); break; default: throw new Error('Invalid loadingType'); } this.showRealView(view, loadingView, config); }, showRealView: function(realView, loadingView, config) { whenFetched(config.entities, _.bind(function() {

...after the entities are fetched, execute this callback ================================================================ // If the region we are trying to insert is not the loadingView then we know the user has navigated to a different page while the loading view was still open. In that case, we know to manually close the original view so its controller is also closed. We also prevent showing the real view (which would snap the user back to the old view unexpectedly) ================================================================ //

switch (config.loadingType) { case 'opacity': this.region.currentView.$el.css('opacity', ''); break; case 'spinner': if (this.region && this.region.currentView !== loadingView) { return realView.close(); } } if (!config.debug) { this.show(realView); } }, this)); }, getEntities: function(view) { return _.chain(view).pick('model', 'collection').toArray().compact().value(); }, getLoadingView: function() { return new LoadingView(); } }); }));