I'm implementing an application which has two separate submodules within top level application module.
I have an admin module with a convention for routes to start with /admin and user module having routes that start with /user. Top level application defines a rootRoute so that when you navigate to http://url/ you are redirected to either admin or user page based on permissions. What i'm trying to understand is whether it is possible to start and stop specific modules based on the route. Here is an example of what i mean:
Let's assume i have a top level application (in coffeescript)
#ClientApp = do (Backbone, Marionette) ->
App = new Marionette.Application
navigate: (route, options = {}) ->
Backbone.history.navigate route, options
App.on "start", (options) ->
if Backbone.history
Backbone.history.start()
App.on "stop", ->
App.module("AdminApp").stop()
App.module("UserApp").stop()
class App.Router extends Marionette.AppRouter
initialize: (options) ->
#route /^admin(.*)/, 'startAdminApp', options.controller.startAdminApp
#route /^user(.*)/, 'startUserApp', options.controller.startUserApp
appRoutes:
"": "redirectToRoot"
App.addInitializer ->
new App.Router
controller: API
API =
redirectToRoot: ->
# some redirect logic that will lead you to either /admin or /user
startAdminApp: ->
App.mainRegion.show new App.Layouts.Admin
App.module("AdminApp").start()
startUserApp: ->
App.mainRegion.show new App.Layouts.User
App.module("UserApp").start()
App
Inside admin and user submodules i also have defined routes
#ClientApp.module "AdminApp.DashboardApp", (DashboardApp, App, Backbone, Marionette, $, _) ->
_.extend DashboardApp, Backbone.Wreqr.radio.channel("dashboard")
class DashboardApp.Router extends Marionette.AppRouter
appRoutes:
"admin/dashboard": "statistics"
API =
getLayout: ->
new DashboardApp.Layout.View
statistics: ->
DashboardApp.StatisticsAp.start()
DashboardApp.on "start", ->
#layout = API.getLayout().render()
API.statistics()
App.addInitializer ->
new DashboardApp.Router
controller: API
If i navigate to / the application works as expected, i'm redirected to necessary namespace and a specific sub-module is started. However if i define some other routes within a submodule, they seem to override the existing regexp matchers. So if i open the browser and navigate to /admin/statistics it will not start the admin application and the callback for /admin/statistics will fail with error. That is because the admin application won't start and the mainRegion is not filled with a corresponding layout. Note that the file containing top level application definition is required before any of the submodules (i guess that is why routes are overridden). I also understand that backbone router will invoke route callback when the first match is met.
So the question is whether it's possible to implement a kind of route manager that will check current route with a regular expression and start or stop the corresponding application (either admin or user) with all defined sub-routes being persistent and bookmarkable?
Had close task to resolve, haven't found any existing solution, so here is a small stub - project i've created
To resolve such task there are few problems to resolve :
1) Async routing. something like rootRouter should load app module and moduleRouter should call controller methods
2) Clear up backbone history handlers on module stop. The problem is even after module stop, route and handler still exist in BB history
So my hack, i mean solution :)
We need some router that will watch URL change and load module, Let it be ModuleManager
define(
[
'application'
],
function(App) {
App.module('ModuleManager', function(ModuleManager, Application, Backbone, Marionette) {
var currentPageModule = false,
stopModule = function(name) {
name && Application.module(name).stop();
},
startModule = function(name) {
Application.module(name).start();
};
ModuleManager.getModuleNameByUrl = function() {
var name = Backbone.history.getHash().split('/')[0];
return name ? (name.charAt(0).toUpperCase() + name.slice(1)) : 'Home'
};
ModuleManager.switchModule = function(name) {
if (!name) return;
stopModule(currentPageModule);
startModule(name);
currentPageModule = name;
};
ModuleManager.requireModule = function(name, callback) {
require(['apps/pages/' + name + '/index'],
callback.bind(this),
function() {
require(['apps/pages/404/index'], function() {
ModuleManager.switchModule('404');
})
}
);
};
/*
* this is key feature - we should catch all routes
* and load module by url path
*/
ModuleManager.FrontRouter = Marionette.AppRouter.extend({
routes: {
'*any': 'loadModule'
},
loadModule: function() {
var name = ModuleManager.getModuleNameByUrl();
ModuleManager.requireModule(name, function() {
ModuleManager.switchModule(name);
})
}
});
ModuleManager.addInitializer(function() {
new ModuleManager.FrontRouter;
});
ModuleManager.addFinalizer(function() {
delete ModuleManager.FrontRouter;
});
});
}
);
Great, that will load module with routes inside. But we'll get another problem - on sub module start we init its router, but we already routed to the page on sub-router init and URL still same. So sub-router will not be invoked till next navigation. So we need special router, that will handle such situation. Here is 'ModuleRouter':
App.ModuleRouter = Marionette.AppRouter.extend({
forceInvokeRouteHandler: function(routeRegexp, routeStr, callback) {
if(routeRegexp.test(Backbone.history.getHash()) ) {
this.execute(callback, this._extractParameters(routeRegexp, routeStr));
}
},
route: function(route, name, callback) {
var routeString = route,
router = this;
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
// проблема - RouterManager уже стригерил событие route, загрузил саб-роутер.
// при создании саб роутера его колбэк уже вызван не будет, поскольку адрес страницы не изменился
// при добавлении роутов используется нативный ВВ route - который вещает колбэк на указанный фрагмент
// расширяем - если мы уже находимся на фрагменте на который устанавливается колбэк - принудительно вызвать
// выполнение обработчика совпадения фрагмента
/*
* PROBLEM : AppsManager already triggered 'route' and page fragments still same,
* so module router will not be checked on URL matching.
*
* SOLUTION : updated route method, add route to Backbone.history as usual, but also check if current page
* fragment match any appRoute and call controller callback
* */
this.forceInvokeRouteHandler(route, routeString, callback);
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
router.execute(callback, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
return this;
},
// implementation destroy method removing own handlers anr routes from backbone history
destroy: function() {
var args = Array.prototype.slice.call(arguments),
routKeys = _.keys(this.appRoutes).map(function(route) {
return this._routeToRegExp(route).toString();
}.bind(this));
Backbone.history.handlers = Backbone.history.handlers.reduce(function(memo, handler) {
_.indexOf(routKeys, handler.route.toString()) < 0 && memo.push(handler)
return memo;
}, []);
Marionette.triggerMethod.apply(this, ['before:destroy'].concat(args));
Marionette.triggerMethod.apply(this, ['destroy'].concat(args));
this.stopListening();
this.off();
return this;
}
})
Please fill free to ask question or chat, i guess there are some point might need to be clarified.
Related
I'm creating a simple application using Backbone and Marionette. It's just to fetch a list of Wordpress posts (using an API) and display it.
It's a very simple app so it's not modularized.
I have the following (it's all placed in the same file):
if ( Backbone.history )
Backbone.history.start({ pushState: false });
if ( Backbone.history.fragment === '' )
API.listAllPosts();
else
API.listSinglePost( Backbone.history.fragment );
// Is not firing anything from here...
MyBlog.Router = Marionette.AppRouter.extend({
appRoutes: {
'': 'listPosts',
':post_name': 'listSingle'
},
listPosts: function() {
console.log('router');
API.listAllPosts();
},
listSingle: function(model) {
console.log('router, single');
API.listSinglePost(model);
}
});
// ...to here
var API = {
listAllPosts: function() {
// Fetch all posts and display it. It's working
},
listSinglePost: function(model) {
// Fetch a single post and display it. It's working
}
}
MyBlog.addInitializer(function() {
console.log('initializer'); // It's firing
new MyBlog.Router({
controller: API
});
});
As Derick Bailey, Marionette's creator, said about using triggers on naviagate:
it encourages bad app design and it is strongly recommended you don’t
pass trigger:true to Backbone.history.navigate.
What I'm missing here?
You start the Backbone history before creating the router instance.
Just move that to after the router is created.
MyBlog.addInitializer(function() {
new MyBlog.Router({ controller: API });
// should be started after a router has been created
Backbone.history.start({ pushState: false });
});
The other thing is that the callbacks should be defined inside of a controller or you should change appRoutes to routes.
The major difference between appRoutes and routes is that we provide
callbacks on a controller instead of directly on the router itself.
[...]
As the AppRouter extends Backbone.Router, you can also define a routes
attribute whose callbacks must be present on the AppRouter.
Move this
if ( Backbone.history )
Backbone.history.start({ pushState: false });
if ( Backbone.history.fragment === '' )
API.listAllPosts();
else
API.listSinglePost( Backbone.history.fragment );
after the point where your App gets started or inside an initialize:after event handler.
Check this previous question: Marionette.js appRouter not firing on app start
We have a large Marionette app, with sub apps/modules.
Each of these registers its own router within the App.addInitializer.
What is the best way to flag certain routes as public and others as requiring authentication?
I have a way in the app to check if the user is authenticated or not, but I'm trying to avoid having to implement that check in every route handler.
PrivateModuleRouter.Router = Marionette.AppRouter.extend({
appRoutes: {
"privateRoute(/)" : "handlePrivateRoute",
}
});
var API = {
handlePrivateRoute: function() {
//I don't want to repeat this everywhere..
if(!Auth.isAuthenticated()) {
App.navigate('/login', {trigger:true});
} else {
PrivateRouteController.showForm();
}
};
App.addInitializer(function(){
new PrivateModuleRouter.Router({
controller: API
});
});
Is there way in the route definition to flag it as private, and then a top level route handler performs this check?
If it's on a Router event though, this may not trigger if the route handler was triggered directly (not passing trigger:true, and calling API.handlePrivateRoute() directly.
Disclaimer: as I don't personally use Marionette, this answer is based on Backbone only.
The execute function
Backbone provides the execute function in the router as a way to handle that kind of logic. Even the example has authentication logic in it:
var Router = Backbone.Router.extend({
execute: function(callback, args, name) {
if (!loggedIn) {
goToLogin();
return false;
}
args.push(parseQueryString(args.pop()));
if (callback) callback.apply(this, args);
}
});
The authentication router
One way to avoid repeating the execute in each router would be to make a base router for your app.
var BaseRouter = Backbone.Router.extend({
constructor: function(prefix, opt) {
// get the hash
this.auth = _.result(this, "auth", {});
BaseRouter.__super__.constructor.apply(this, arguments);
},
// requires auth by default?
authDefault: false,
/**
* Check the `auth` hash for a callback. Returns `authDefault` if
* the callback is not specified.
* #param {String} callbackName name of the function.
* #return {Boolean} true if the callback is private.
*/
hasAuth: function(callbackName) {
return _.result(this.auth, callbackName, this.authDefault);
},
// To easily override the auth logic in a specific router
checkAuth: function(){
return Auth.isAuthenticated();
},
execute: function(callback, args, name) {
if (this.hasAuth(name) && !this.checkAuth()) {
this.navigate('/login', { trigger: true });
return false;
}
}
});
Defining the specific routers
Then for each of your router, extend BaseRouter.
var SpecificRouter = BaseRouter.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'public': 'publicRoute',
'private': 'privateRoute',
'unspecified': 'defaultAccessRoute'
},
/**
* The auth hash works like this:
* "functionName": [boolean, true if needs auth]
*
* home and publicRoute could be left out as it's the default here.
*/
auth: {
home: false, // public
publicRoute: false, // public
privateRoute: true, // needs authentication
// defaultAccessRoute will be public because BaseRouter
// defines `authDefault: false`.
},
home: function() {},
publicRoute: function() {},
privateRoute: function() {},
defaultAccessRoute: function() {},
});
And for a router which all routes are private by default:
var PrivateRouter = BaseRouter.extend({
authDefault: true,
routes: {
'*otherwise': 'home', // private
'only-private': 'onlyPrivate', // private
},
// ...snip...
/**
* Optional example on how to override the default auth behavior.
*/
checkAuth: function() {
var defaultAuthResult = PrivateRouter.__super__.checkAuth.call(this);
return this.specificProperty && defaultAuthResult;
}
});
In github you can find many solution for calling some methods before router's execution. For marionette you can use ideas from marionette-lite extension based in filters system.
You should define filter, for example RequresAuthFilter as:
import { Filter } from 'marionette-lite';
const RequresAuthFilter = Filter.extend({
name: 'requresAuth', // name is used in controller for detect filter
async: true, // async mode
execution: Filter.Before,
handler(fragment, args, next) {
// Requesting server to check if user is authorised
$.ajax({
url: '/auth',
success: () => {
this.isSignedIn = true;
next();
},
error: () => {
Backbone.navigate('login', true);
}
});
},
});
or short sync way:
import { Filter } from 'marionette-lite';
const RequresAuthFilter = Filter.extend({
name: 'requresAuth',
handler(fragment, args) {
if (!window.isSignedIn) {
Backbone.navigate('login', true);
}
},
});
And add this filter to Controller as:
const AppController = Marionette.Object.extend({
// Add available filters map
filtersMap: [
new RequresAuthFilter()
],
filters: {
// e.g. Action that need authentication and if user isn't
// authenticated gets redirect to login page
requresAuth: ['logout', 'private'],
},
logout() { /* ... */ },
private() { /* ... */ }
});
I am trying to add some middleware to Marionette's extended version of Backbone's router. Here's my code.
AppName.Router = Backbone.Marionette.AppRouter.extend({
appRoutes:{
// routes
},
route: function(route, name, callback) {
var router = this;
if (!callback) {
callback = this[name];
}
var middleware = function() {
console.log('in middleware');
callback.apply(router, arguments);
};
return Backbone.Router.prototype.route.call(this, route, name, middleware);
}
});
What I think should be happening is whenever I load a route, the console prints 'in middleware'.
What is happening is whenever I load the first route and only the first route, the console prints 'in middleware'.
I researched by using the top solution on this question and these are the results that I get.
Edit: I have also tried 'execute' as specified in the documentation and have had the same results.
I have built a small prototype project using CloudKit JS and am now starting to build the next version of it and am wanting to use Ember as I have some basic experience with it. However, I am not too sure where to place the CloudKit JS code. For example where should I add the configure part and the auth function? I think that once I find the spot for the auth code, I could then add some of my query functions into the individual views and components, right?
Here is my configure code (with the container and id removed):
CloudKit.configure({
containers: [{
containerIdentifier: '###',
// #todo Must generate a production token for app store version
apiToken: '###',
auth: {
persist: true
},
// #todo Must switch to production for app store version
environment: 'development'
}]
});
Here is the auth function:
function setupAuth() {
// Get the container.
var container = CloudKit.getDefaultContainer();
//Function to call when user logs in
function gotoAuthenticatedState( userInfo ) {
// Checks if user allows us to look up name
var userName = '';
if ( userInfo.isDiscoverable ) {
userName = userInfo.firstName + ' ' + userInfo.lastName;
} else {
userName = 'User record name: ' + userInfo.userRecordName;
}
//Calls out initialization function
init();
//Sets up UI for logged in users
setAuthenticatedUI( userName );
//Register logged out function
container
.whenUserSignsOut()
.then( gotoUnauthenticatedState );
}
//Function to call when user logs out
function gotoUnauthenticatedState( error ) {
//Checks if error occurred
if ( error && error.ckErrorCode === 'AUTH_PERSIST_ERROR' ) {
displayError( logOutError, 'Error code: AUTH_PERSIST_ERROR' );
}
// Sets up the UI for logged out users
setUnauthenticatedUI();
//Register logged in function
container
.whenUserSignsIn()
.then( gotoAuthenticatedState )
.catch( gotoUnauthenticatedState );
}
// Check a user is signed in and render the appropriate button.
return container.setUpAuth()
.then( function( userInfo ) {
// userInfo is the signed-in user or null.
if ( userInfo ) {
gotoAuthenticatedState( userInfo );
} else {
gotoUnauthenticatedState();
}
});
}
The init() then calls functions to setup the queries to adds a chart to the page using records. The setAuthenticatedUI() and setUnauthenticatedUI() functions simply apply and remove classes once the user has been authenticated.
The answer pretty much depends on the version of Ember you're using and if how you are planning on using it. With routes? Simple routes? RouteHandlers?
For example, if you are at Ember v2.3.0, you could consider using dependency injection (https://guides.emberjs.com/v2.3.0/applications/dependency-injection/) to provide a configured container instance to the rest of your app, e.g.:
export function initialize(application) {
var container = CloudKit.configure(config).getDefaultContainer();
application.register('ckcontainer:main', container);
application.inject('route', 'ckcontainer', 'ckcontainer:main');
}
export default {
name: 'ckcontainer',
initialize: initialize
};
Then in a route, you can obtain a reference like so:
export default Ember.Route.extend({
activate() {
// The ckcontainer property is injected into all routes
var db = this.get('ckcontainer').privateCloudDatabase;
}
});
-HTH
I have a single Ember app that needs to use different data depending on the domain that it's running on. For example, on domain1.com the site title might be "Domain 1 Website", whereas for domain2.org the site title could be "Cool Site!"
I need to be able to use the data in routes, controllers and templates.
My initializer so far:
import { request } from 'ic-ajax';
export function initialize(container, application) {
var domain = document.location.host;
return request('http://api/sites?domain=' + domain, {} ).then(function(response) {
// make the response available to routes, controllers and templates
});
}
export default {
name: 'site-data',
initialize: initialize
};
If you take a look at the docs:
http://guides.emberjs.com/v1.10.0/understanding-ember/dependency-injection-and-service-lookup/#toc_dependency-injection-with-code-register-inject-code
The second example
Ember.Application.initializer({
name: 'logger',
initialize: function(container, application) {
var logger = {
log: function(m) {
console.log(m);
}
};
application.register('logger:main', logger, { instantiate: false });
application.inject('route', 'logger', 'logger:main');
}
});
and the following explanation shows you how you can use .inject to make one of your variables, services or functions available to whatever objects you need within your application.