I'm going through examples with routing from David Sulc's book Backbone.Marionette.js: A Gentle Introduction
https://leanpub.com/marionette-gentle-introduction
ContactManager.navigate = function (route, options) {
options || (options = {});
Backbone.history.navigate(route, options);
};
ContactManager.getCurrentRoute = function () {
return Backbone.history.fragment;
};
ContactManager.on("initialize:after", function () {
if (Backbone.history) {
Backbone.history.start();
if (this.getCurrentRoute() === "") {
ContactManager.trigger("contacts:list");
}
}
As you can see if the history fragment is empty, it will trigger the contacts:list event which will render the list of contacts. However, it doesn't redirect at all, and I've found out that fragment is preset to "contacts" somehow, so the event doesn't get fired at all. It also happened to me once that initially the fragment was empty and got everything rendered, and url changed properly, but upon refresh fragment was still "contacts" and again nothing was rendered.
ContactsApp.Router = Marionette.AppRouter.extend({
AppRoutes: {
"contacts": "listContacts"
}
});
ContactManager.on("contacts:list", function () {
ContactManager.navigate("contacts");
API.listContacts();
});
This is the code that handles the event. What seems to be the problem? Thanks.
I think there is some missing code. I would expect to find something like this in the router:
var myController = {
listContacts: function () {
ContactManager.trigger("contacts:list");
}
};
ContactsApp.Router = Marionette.AppRouter.extend({
controller: myController,
appRoutes: {
"contacts": "listContacts"
}
});
Note that appRoutes starts with a lowercase a.
Now the route contacts will call the controller's listContacts method and trigger the ContactManager.on("contacts:list"... callback, running the appropriate API method.
Related
I've inherited a Cordova/PhoneGap app running Cordova 3.4. My first task was to implement a Client-Side Routing framework to make it easier to navigate between pages. I chose Flatiron Director as my client-side router, but when I went to implement it I started to get weird functionality out of the app.
My first router setup:
var routing = {
testHandler: function(){
console.log('Route ran');
},
routes: function(){
return {
"/testhandler": testHandler
}
}
};
console.log('Routes added');
The routes are added (at least based on the console output). When I attempt to hit the /testhandler hash, I receive a "Failed to load resource: file:///testhandler" error when I set window.location.hash to "/testhandler". I noticed the "Route ran" statement was never printed.
My next attempt was just using the hashchange event with jQuery.
$(window).on('hashchange', function(){ console.log('Ran'); });
On this attempt, regardless of what I change the hash to, I see the 'Ran' output, but I still receive the "Failed to load resource: " error.
Is this a problem with PhoneGap/Cordova? Or our implementation? Is it just not possible to use client-side routing with Cordova? What am I doing wrong?
I know that this doesn't answer your question directly but you may consider making your own provisional router. This may help you to debug your app and to figure out what's the problem.
Something like this for example:
var router = (function (routes) {
var onRouteChange = function () {
// removes hash from the route
var route = location.hash.slice(1);
if (route in routes) {
routes[route]();
} else {
console.log('Route not defined');
}
};
window.addEventListener('hashchange', onRouteChange, false);
return {
addRoute: function (hashRoute, callback) {
routes[hashRoute] = callback;
},
removeRoute: function (hashRoute) {
delete routes[hashRoute];
}
};
})({
route1: function () {
console.log('Route 1');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
},
route2: function () {
console.log('Route 2');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
}
});
I want to set up routing with CanJS so that depending on what url I hit, some corresponding control is set up. My problem is trying to find a way of expressing a default route to listen for in the control: "If none match, then do this". Any tips on doing this?
This is my code so far. Seems to work for urls like /#!/plant/1/day/3 and /#!/plant/1, but not for /#! or /
can.route('plant/:plant_id/day/:day', {});
can.route('plant/:plant_id', {});
can.route('', {});
var Router = can.Control({}, {
init : function () {},
"{can.route}" : function (route, event, newVal, oldVal) {
console.log('Init default control');
},
"{can.route} plant_id" : function (route, event, newVal, oldVal) {
console.log('Init plant control');
},
"{can.route} day" : function (route, event, newVal, oldVal) {
console.log('Init day control');
}
});
P.S. I actually managed to do it using the Can.Control.route plugin by doing this:
"route" : function (route, event, newVal, oldVal) {
console.log('route Default route', arguments);
}
But the route plugin seems to both set up routes and react to them, and I wanted to know how to do this without the specific plugin.
Your solution works but you could also use the can.Control.route plugins which lets you define and listen to routes directly in a controller action:
var Router = can.Control({
init : function(el, options) {
},
"/:plantId route" : function(data) {
// the route says /id
// data.id is the id or default value
},
route : function(data){
// the route is empty
}
});
new Router(window);
Ended up doing something like the follow, but it feels less than ideal ...
"{can.route} change" : function (ev, attr, how, newVal, oldVal) {
if(newVal=== 'add' && (!ev.attr('plant_id') && !ev.attr('day')))
console.log('{can.route} Default route', arguments);
},
I noticed the same problem with the default '/' route. I dove into pushstate.js and found that it wasn't working properly with the default route. You can see more details here: https://forum.javascriptmvc.com/#Topic/32525000001460049
I haven't tested anything without pushstate.js (using #!), so this only applies to using pushstate.
What I Have
Trying to understand what's going on and how to control it. I have a "public" view for users that have not yet been authenticated, and a "home" view for users that are authenticated. Here's my route config:
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
//configure routing
router.useConvention();
router.mapRoute('home', 'viewmodels/home', 'Test App', true);
router.mapRoute('public', 'viewmodels/public', 'Test App', true);
router.mapRoute('set/:id', 'viewmodels/set', 'Set');
router.mapRoute('folder/:id', 'viewmodels/folder', 'Folder');
router.mapRoute('api', 'viewmodels/api', 'API Reference');
router.mapRoute('error', 'viewmodels/error', 'Error', false);
app.adaptToDevice();
//Show the app by setting the root view model for our application with a transition.
if (dataservice.isAuthenticated() === true) {
app.setRoot('viewmodels/shell', 'entrance');
router.navigateTo('home');
} else {
app.setRoot('viewmodels/public');
router.navigateTo('#/public');
}
router.handleInvalidRoute = function (route, params) {
logger.logError('No route found', route, 'main', true);
router.navigateTo('#/error');
};
});
The Problems
When I run the app for the first time, I'm not authenticated, and I get an error:
Uncaught TypeError: Cannot call method 'lookupRoute' of undefined
Originating from the 'router.navigateTo('#/public');' line.
Then when I try to click the login button, I get the same error from this:
define(['durandal/app', 'durandal/plugins/router', 'services/dataservice'], function (app, router, dataservice) {
var publicViewModel = function () {
self.logIn = function () {
app.setRoot('viewmodels/shell');
router.navigateTo('#/home');
};
But the content loads correctly. When I navigate to a particular page by clicking, say to /folder/2, and then change the url to /folders/2 (invalid), I get "route not found" in my log, as expected, but I run into a few other issues:
I don't get the error page, or any errors (as I think I should, per my handleInvalidRoute)
If I click on something else, the url doesn't change, and new content isn't loaded, again with no errors.
I think I'm breaking routing somehow, but I'm not sure how. How can I correct the above issues?
Screen:
I suspect calling navigateTo where you are might be too soon for some reason. To test this theory try move this code.
if (dataservice.isAuthenticated() === true) {
app.setRoot('viewmodels/shell', 'entrance');
router.navigateTo('home');
} else {
app.setRoot('viewmodels/public');
router.navigateTo('#/public');
}
into an "activate" method on your publicviewmodel, and in the main.js just leave this:
app.setRoot('viewmodels/public');
EDIT: Old suggestion
I believe on your viewmodel for the root you need a router property. So modify your public viewmodel to add one:
define(['durandal/app', 'durandal/plugins/router', 'services/dataservice'], function (app, router, dataservice) {
var publicViewModel = function () {
self.router = router;
self.logIn = function () {
app.setRoot('viewmodels/shell');
router.navigateTo('#/home');
};
(where do you define self though?)
I have a messageBox in my Durandal app and whether you click no or yes you are sent throw to an other page. I want to do this with the router, but the pages aren't switched.
I can see the code is executing the line but nothing happens!
define(function(require) {
var app = require('durandal/app'),
system = require('durandal/system'),
router = require('durandal/plugins/router');
return {
router: router,
displayName: 'SometingApp Startpage',
activate: function() {
system.log("Application started!");
},
createEstimate: function() {
app.showMessage('Do you want to create a new something?', 'New something', ['Yes', 'No']).then(function(result) {
if (result == "Yes") {
return router.activate('otherpage');
}
});
}
};
});
THe user click a button that is bind to createEstimate!
Hope someone can help!
I think that what you need to do is call router.navigateTo('#/yourUrl').
If i understand right the documentation, router.activate must be call only one time, usually at the shell activation.
The route functions available for your viewModel navigation is listed in the documentation
http://durandaljs.com/documentation/Router/ under the section "Other APIs"
I have the following situation:
app.js: Singleton Marionette.Application() where I define a nav, a footer, and a main region. In the initializer I construct Marionette.Contoller's and attach them to the app's this.controller object for later control. I might not construct all the Controller's here, just the ones I want to Eagerly Load. Some are Lazy Loaded later. I also instantiate a Backbone.Router here, and pass in a reference to my app object:
var theApp = new TP.Application();
theApp.addRegions(
{
navRegion: "#navigation",
mainRegion: "#main",
footerRegoin: "#footer"
});
theApp.addInitializer(function()
{
// Set up controllers container and eagerly load all the required Controllers.
this.controllers = {};
this.controllers.navigationController = new NavigationController({ app: this });
this.controllers.loginController = new LoginController({ app: this });
this.controllers.calendarController = new CalendarController({ app: this });
this.router = new Router({ app: this });
});
**Controller.js: this is a general use controller that handles view & model intsantiation and eventing. Each Controller owns its own Marionette.Layout, to be filled into the App.mainRegion. Each Controller binds to the layout's "show" event to fill in the layout's regions with custom views. Each Controller offers a getLayout() interface that returns the controller's associated layout.
Marionette.Controller.extend(
{
getLayout: function() { return this.layout; },
initialize: function()
{
this.views.myView = new MyView();
...
this.layout.on("show", this.show, this);
...
},
show: function()
{
this.layout.myViewRegion.show(myView);
}
});
router.js: the router uses the app singleton to load a Controller's layout into the App's main region:
...
routes:
{
"home": "home",
"login": "login",
"calendar": "calendar",
"": "calendar"
},
home: function ()
{
var lazyloadedController = new LazyLoadController();
this.theApp.mainRegion.show(lazyLoadController.getLayout());
},
login: function (origin)
{
this.theApp.mainRegion.show(this.theApp.controllers.loginController.layout);
}
As it is, everything works fine except for reloading the same layout / controller twice. What happens is that the DOM events defined in the LoginView do not re-bind on second show. Which is easily solved by moving the LoginView initialization code into the "show" event handler for that Controller:
LoginController = Marionette.Controller.extend(
{
...
show: function()
{
if (this.views.loginView)
delete this.views.loginView.close();
this.views.loginView = new LoginView({ model: this.theApp.session });
this.views.loginView.on("login:success", function()
{
});
this.layout.mainRegion.show(this.views.loginView);
}
Now everything works fine, but it undoes part of the reason I created Controller's to begin with: I want them to own a View and its Models, create them once, and not have to destroy & recreate them every time I switch layouts.
Am I missing something? Is this not how I should be using Layouts? Isn't the whole point of Layouts and Regions that I can switch in & out Views at will?
Obviously I wouldn't jump back to LoginController/Layout often, but what about between a HomeController/Layout, CalendarController/Layout, SummaryController/Layout, etc... in a single page application I might switch between those 'top-level' layouts rather often and I would want the view to stay cached in the background.
I think your problem is that you don't maintain a single instance of the controller. The recommended way to handle routing/controllers (based on Brian Mann's videos) is like this
App.module('Routes', function (Routes, App, Backbone, Marionette, $, _) {
// straight out of the book...
var Router = Marionette.AppRouter.extend({
appRoutes: {
"home": "home",
"login": "login",
"calendar": "calendar"
}
});
var API = {
home: function () {
App.Controller.go_home();
},
login: function () {
App.Controller.go_login();
},
calendar: function () {
App.Controller.go_calendar();
}
};
App.addInitializer(function (options) {
var router = new Router({controller: API});
});
});
... and the controller:
App.module("Controller", function (Controller, App, Backbone, Marionette, $, _) {
App.Controller = {
go_home: function () {
var layout = new App.Views.Main();
layout.on('show', function () {
// attach views to subregions here...
var news = new App.Views.News();
layout.newsRegion.show(news);
});
App.mainRegion.show(layout);
},
go_login: function () {
....
},
go_calendar: function () {
....
}
};
});
I suspect your problem is the lazy-loaded controller...