Skip to content

Instantly share code, notes, and snippets.

@isochronous
Last active November 17, 2018 10:13
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save isochronous/4185418 to your computer and use it in GitHub Desktop.
Save isochronous/4185418 to your computer and use it in GitHub Desktop.
Root app for drop-in multi-app Marionette framework using requireJS and the subapprouter it works with
define([
"underscore",
"backbone",
"marionette",
"vent"
],
/**
* Creates the primary `Marionette.Application` object that runs the admin framework. Provides a central point
* from which all other sub-applications are started, shown, hidden, and stopped.
* @requires module:underscore
* @requires module:backbone
* @requires module:marionette
* @requires module:supervent // this is a custom eventAggregator with Wreqr.commands() and Wreqr.reqres() properties
*/
function (_, Backbone, Marionette, vent) {
var rootApp = new Marionette.Application(),
subApps = {};
rootApp.subApps = subApps;
rootApp.addRegions({
body: "body"
});
rootApp.addInitializer(function (options) {
var i, il, anApp;
var that = this;
rootApp.router = new options.Router({
controller: options.controller
});
// All routers in the `applications` array should be Marionette.SubAppRouters. When the router is instantiated,
// the first parameter passed to the constructor (`anApp.name`) is interpreted as the prefix for all `routes`
// and `appRoutes` in the router. All routes will have the prefix prepended to them, then a forward slash, then
// the "regular" route. For example, if you have a route that's `search/:id` and the prefix is "library", then
// the actual route added to Backbone will be `library/search/:id`.
for(i= 0, il=options.applications.length; i<il; i++) {
anApp = options.applications[i];
// TODO: should we hook this up here, or send out an event that tells apps where to display themselves?
anApp.parentRegion = that.body;
anApp.application.router =
new anApp.Router(anApp.name, {
controller: anApp.controller
});
subApps[anApp.name] = anApp.application;
}
});
rootApp.on("initialize:after", function () {
var that = this;
// Start all of our sub-applications
_.each(subApps, function(app, key, list){
// notice we pass this app's region to the sub-app, so it can either grab the $el or use the region directly
app.start({parentRegion: that.body});
// Trigger a notification and pass the app along in case anyone wants a reference
vent.trigger("starting:app:" + key, app);
});
// And hook up history tracking
if (Backbone.history) {
Backbone.history.start();
}
});
// Now actually display the application
rootApp.on("start", function () {
vent.trigger("started:app:rootapp", rootApp);
vent.commands.execute("initialize:ui");
});
return rootApp;
});
//main.js
require([
'app',
'router',
'controller',
'subapp/app',
'subapp/router',
'subapp/controller'
],
function(rootApp, AppRouter, AppController, subApp, SubAppRouter, SubAppController){
"use strict";
var options = {
controller: AppController,
Router: AppRouter,
applications: [
{
name: "contestcreator",
application: subApp,
controller: SubAppController,
Router: SubAppRouter
}
]
};
rootApp.start(options);
});
// This is heavily based on Backbone.SubRoute (https://github.com/ModelN/backbone.subroute) by Dave Cadwallader, who
// helped me out in this discussion thread:
// https://groups.google.com/forum/?fromgroups=#!topic/backbone-marionette/KTw7USoA6Gs
define([
'underscore',
'marionette'
],
/**
* A module that defines and adds Marionette.SubAppRouter to the Marionette object
* @module marionette.subapprouter
* @requires module:marionette
* @param {Function} Marionette - Marionette.js
* @exports marionette
*/
function(_, Marionette) {
/**
* Creates an AppRouter with a particular prefix at the beginning of each route.
* @class Marionette.SubAppRouter
* @alias module:marionette.subapprouter
* @classdesc A router that prepends a specified prefix (passed on instantiation) onto each specified `route` or
* `appRoute`. Directly extends from Marionette.AppRouter to enable controller objects linked to the `appRoutes`
* hash. Useful for sub applications that live in sub-directories under root and want to handle all of their own
* routing.
*/
Marionette.SubAppRouter = Marionette.AppRouter.extend({
/**
* @constructs Marionette.SubAppRouter
* @param {string} [prefix] - The prefix string to prepend to all routes, making them act as if relative. If
* blank, then it just acts like a regular Backbone.Router.
* @param {Object} [options] - The options object expected by Marionette.AppRouter.
* @param {Object} [options.controller] - An object with function properties corresponding to the hash values
* from `routes` and `appRoutes`.
*/
constructor: function(prefix, options) {
var controller,
appRoutes,
routes = {};
// Prefix is optional, set to empty string if not passed
this.prefix = prefix = prefix || "";
// SubRoute instances may be instantiated using a prefix with or without a trailing slash.
// If the prefix does *not* have a trailing slash, we need to insert a slash as a separator
// between the prefix and the sub-route path for each route that we register with Backbone.
this.separator =
( prefix.slice( -1 ) === "/" )
? ""
: "/";
// if you want to match "books" and "books/" without creating separate routes, set this
// option to "true" and the sub-router will automatically create those routes for you.
var createTrailingSlashRoutes = options && options.createTrailingSlashRoutes;
if(this.appRoutes) {
appRoutes = this.appRoutes;
controller = this.controller;
if(options && options.controller) {
controller = options.controller;
}
_.each(appRoutes, function(callback, path) {
if (path) {
// strip off any leading slashes in the sub-route path,
// since we already handle inserting them when needed.
if (path.substr(0) === "/") {
path = path.substr(1, path.length);
}
routes[prefix + this.separator + path] = callback;
if (createTrailingSlashRoutes) {
routes[prefix + this.separator + path + "/"] = callback;
}
} else {
// default routes (those with a path equal to the empty string)
// are simply registered using the prefix as the route path.
routes[prefix] = callback;
if (createTrailingSlashRoutes) {
routes[prefix + "/"] = callback;
}
}
}, this);
// Override the local sub-routes with the fully-qualified routes that we just set up.
this.appRoutes = routes;
}
Marionette.AppRouter.prototype.constructor.call(this, options);
}
});
return Marionette.SubAppRouter;
});
// This is heavily over-engineered, honestly, as you should really only need a single vent across which
// to echo (which should be the "root" vent in your project). But since I bothered to make it more flexible,
// I figured I'd put up the more flexible version rather than the single-echo version I'm currently using.
// underscore.deepclone is just a module that adds the "cloneDeep" method from lo-dash to underscore, which
// is documented here: http://lodash.com/docs#cloneDeep
define([
'underscore',
'marionette',
'vent',
'base/Vent.super',
'underscore.deepclone'
],
/**
* @module components/Vent.echo-supervent
* @requires module:underscore
* @requires module:Marionette
* @requires module:vent
* @requires module:base/Vent.super
*/
function(_, Marionette, rootVent, SuperVent){
var console = window.console;
var getOpt = Marionette.getOption;
var nullOrEmpty = function(obj) {
return (!obj || (obj && _.isEmpty(obj)));
};
var isVent = function(obj) {
return (obj instanceof Marionette.EventAggregator);
};
/**
* @alias module:components/Vent.echo-supervent
* @constructor
* @param {String} namespace - The namespace to prepend onto events when echoing
* @param {Object} options - A hash of
*/
var EchoVent = SuperVent.extend({
/**
* The namespace to prepend onto events when echoing back across the root vent
* @property namespace
* @type String
* @default ""
*/
namespace: "",
echoOn: {},
debug: false,
constructor: function(namespace, vents, options) {
var args = _.toArray(arguments);
var that = this,
config = args[0];
// Handle the one-big-config-param case
if (args.length === 1 && _.isObject(config) && !isVent(config)) {
namespace = config.namespace || this.namespace;
vents = config.echoOn || this.echoOn;
options = _.omit(config, ["namespace", "vents"]);
}
this.options = options || {};
this.debug = getOpt(this, "debug");
// Call the superclass constructor to get event binder mixed in early
SuperVent.prototype.constructor.apply(this, arguments);
this.namespace = namespace;
if (!nullOrEmpty(vents)) {
this._addInitialVents(vents);
}
// We want any event send through this aggregator to echo in the root vent as well - just prefixed by
// a namespace (the name of the app/module, typically)
this.bindTo(this, "all", function(){
var args = _.toArray(arguments);
var originalName = args[0];
var vents = that.echoOn;
// prefix namespace to event name
args[0] = [that.namespace, originalName].join(":");
// re-trigger that event on every registered vent
_.each(vents, function (vent) {
vent.trigger.apply(vent, args);
if (that.debug) {
console.log("event \"%s\" triggered on %o with args %o", args[0], vent, _.rest(args, 1));
}
});
});
},
_addInitialVents: function(vents) {
var echoOn = nullOrEmpty(vents) ? false : vents,
that = this;
// Return if there's nothing to do
if (!echoOn) { return; }
// Clear current list of vents (we still have existing ones in the temp clone)
this.echoOn = {};
var addEach = function(vent, key) {
var args = _.toArray(arguments);
// We don't want pure numeric indexes as custom IDs, so if we've gotten this via an array, set
// it to null so addEcho will generate a valid unique ID.
if (_.isNumber(key)) { args[1] = false; }
that.addEcho.apply(that, args);
};
// If it's an array, or it's an object with id/aggregator pairs, go ahead and run it through addEach
if (isVent(echoOn)) {
this.addEcho(echoOn);
} else if (_.isArray(echoOn) || _.isObject(echoOn)) {
_.each(echoOn, addEach);
} else {
var msg = "Cannot add object to EchoVent with namespace " + this.namespace + ", unknown object";
var err = new Error(msg);
err.catalyst = echoOn;
throw err;
}
},
setNamespace: function(newNs) {
this.trigger("echovent:change:namespace:from:" + this.namespace + ":to:" + newNs, this);
this.namespace = newNs;
},
addEcho: function(newVent, customIndex) {
customIndex = customIndex || _.unique("ev_");
this.echoOn[customIndex] = newVent;
this.trigger("echovent:added:vent", newVent);
},
removeEcho: function(vent) {
var vents = this.echoOn;
var ventName = _.isString(vent) ? vent : this.getIdOf(vent);
vent = vents[ventName];
this.trigger("echovent:removed:vent:" + ventName, vents[vent]);
delete vents[ventName];
},
getIdOf: function(vent) {
var vents = this.echoOn;
if (nullOrEmpty(vents)) { return false; }
var vals = _.values(vents);
var index = vals.indexOf(vent);
return _.keys(vents)[index];
}
});
return EchoVent;
});
define([
'backbone',
'marionette'
],
/**
* A module that exports `SuperVent` - simply an event aggregator that has all the different `Wreqr` functionality
* built in. Should only be used by the root application, all other applications should use `EchoVent`.
* @module supervent
* @requires module:backbone
* @requires module:marionette
* @exports SuperVent
*/
function(Backbone, Marionette) {
var SuperVent = {};
Marionette.EventAggregator.extend = Backbone.Model.extend;
/**
* An event aggregator with Wreqr.Commands and Wreqr.RequestResponse objects added.
* @class SuperVent
* @alias module:supervent
* @classdesc A `Marionette.EventAggregator` object with `Wreqr.Commands` and `Wreqr.RequestResponse` instances
* built in.
* @property {Backbone.Wreqr.Commands} commands - A Wreqr.Commands instance
* @property {Backbone.Wreqr.RequestResponse} reqres - A Wreqr.RequestResponse instance
*/
SuperVent = Marionette.EventAggregator.extend({
/**
* @constructs SuperVent
*/
constructor: function(debug){
this.debug = debug || false;
// And here's why it's a Super Vent
this.commands = new Backbone.Wreqr.Commands();
this.reqres = new Backbone.Wreqr.RequestResponse();
// Call the superclass constructor to get event binder mixed in
Marionette.EventAggregator.prototype.constructor.apply(this, arguments);
if (this.debug) {
this.bindTo(this, "all", this.logToConsole, this);
}
},
logToConsole: function() {
var args = Array.prototype.slice.call(arguments);
var eventName = args[0];
console.log("vent triggered :%s with args %o", eventName, args.slice(1));
}
});
return SuperVent;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment