Backbone > Multiple Routers and History.start - backbone.js

I'd like to have multiple routers living on a single page for modularity. I initialize the routers on $(document).ready() in different js files. When I had just one router that worked fine because I could call History.start() right after initializing the router, but now that I have multiple routers that could be initialized from different files, I'm not sure when to call History.start().
For example:
<script src="router1.js" type="text/javascript"></script>
<script src="router2.js" type="text/javascript"></script>
In router1.js:
$(document).ready(function(){
new Core.Routers.Router1()
});
and likewise for router2.
Is the best solution just to add a new $(document).ready() that calls History.start() at the end of the page? I don't think the doc ready calls are blocking, so doesn't that introduce a race condition where all the Routers may not have been initialized by the time History.start() has been called.

You only need to call Backbone.history.start() once in your app and the only criteria for when you call it is that at least one router has to be instantiated already.
So, you could easily do this:
$(function(){
new MyRouter();
Backbone.history.start();
});
$(function(){
new AnotherRouter();
});
$(function(){
new AndMoreRouters();
});
I do a similar thing with routers on a regular basis, and I often start up new routers long after the page has been loaded and the user is interacting with the page.
FWIW though, you might be interested in the idea of initializers that I have in my Backbone.Marionette plugin and documented as part of this blog post: http://lostechies.com/derickbailey/2011/12/16/composite-javascript-applications-with-backbone-and-backbone-marionette/

You also may be able to check Backbone.History.started...
var Router = Backbone.Router.extend({
routes: {
'': 'load'
},
initialize: function () {
},
load: function () {
}
});
$(function () {
new Router();
if (!Backbone.History.started) {
Backbone.history.start();
}
});
It was added recently in a pull request.
Be sure and check out Derick's Marionette plugin as well, it's pretty awesome.

Related

Routing Cakephp with BACKBONE JS

<script>
var HomeView = Backbone.View.extend({
template: '<h1>Home</h1>',
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template);
}
});
var AboutView = Backbone.View.extend({
template: '<h1>About</h1>',
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template);
}
});
var AppRouter = Backbone.Router.extend({
routes: {
'': 'homeRoute',
'home': 'homeRoute',
'about': 'aboutRoute',
},
homeRoute: function () {
var homeView = new HomeView();
$(".content").html(homeView.el);
},
aboutRoute: function () {
var aboutView = new AboutView();
$(".content").html(aboutView.el);
}
});
var appRouter = new AppRouter();
Backbone.history.start();
</script>
<ul>
<li><?php echo $this->Html->link('Home',array('controller' =>'pages','action' => 'home')); ?></li>
<li><?php echo $this->Html->link('About',array('controller' =>'pages','action' => 'about')); ?></li>
</ul>
How to convert the code above to make backbone.js like this in manual coding I seen in the NET.
<div id="navigation">
Home
About
</div>
<div class="content">
</div>
Im just new to this one guys please help me. Im reading Backbone js now, can anybody help me with this problem. If you have experience with cakephp backbone js.. I also wanted using it on CRUD cakephp.
Just a friendly reminder: CakePHP is a server-side library whose responsibility is to send HTML or some serialized data (JSON, XML, binary, etc.) to your browser.
backbone.js is a client-side library that provides building blocks for client-side content and event-handling. The only relationship between CakePHP and backbone.js is that CakePHP is perhaps responsible for delivering whatever scripts and assets your client needs. There is probably nothing that you can learn about backbone.js that is specific to CakePHP. So try to think in terms of "client" and "server" rather than backbone and Cake.
Now, almost every web site will have some links. When a user clicks a link the browser treats it as an event and responds accordingly.
JavaScript clients can extend the browser functionality by getting in between the browser and the event - that is, by telling the browser, "I will handle this event, not you."
In backbone.js, the Backbone.history.start method is what tells the browser to back off and let backbone take care of certain events:
window.onhashchange: This is supported by all browsers and is called whenever the "hash" part of the URL changes; for instance, if you click an anchor link Go somewhere the event will fire and the listener will be called. If the listener returns something other than true then the browser will not handle the event as usual, but otherwise, your browser URL will change to /path/to/page#somewhere
history.pushState and window.onpopstate: This is a new feature in HTML5 so it is not supported by older browsers, but at this point, it is fairly widely-adopted and available. Basically, this is a powerful API that gives client developers a way to manipulate and rewrite the history of a browser's navigation - not just the hash part of the URL, the whole thing - using syntax (see link) like history.pushState(undefined, undefined, 'not/a/real/path');. Correspondingly, there is an event handler window.onpopstate that will be called whenever the browser moves forward or backward in the history chain, but !NOT! when we call history.pushState or history.replaceState. This is a good behavior and by design.
You need to decide what you want to use - hash-style URLs and/or "real" URLs - and then configure Backbone.history.start arguments accordingly. You can use both but I don't recommend it.
Finally, please make sure **you are setting everything up in your JavaScript wrapped in a $(document).ready(function() { ... });. Right now you are not.

Backbone.history is not updating in IE 9. Back button broken

In our app, we actually have two Backbone SPA applications. The first one is for login, registration and other features for unauthenticated users. The URL for this would be something like http://www.example.com/registration#signin. Once you login, you are redirected to our main Backbone app at http://www.example.com/ui#home.
In my main UI app, I am using Backbone.history without pushState. The App file looks something like:
define(function (require) {
var App = new Marionette.Application();
App.addInitializer(function (options) {
...
});
...
App.on('initialize:after', function () {
$(function(){
if (Backbone.history) {
Backbone.history.start({ root: '/ui' });
}
});
$.log("**WebApp**: Marionette app started.");
});
return App;
});
Of course, everything works flawlessly in any browser except IE 9 (and maybe 10, I need to check). In IE 9, all the routing works fine. Clicking links such as http://www.example.com/ui#anotherpage works. However, when the user clicks the Back button in their browser, they are not sent back to the last route fired. Instead, they are sent to http://www.example.com/registration#signin, which is the last page served by Node, our web server. As I click through links, I can see that history.length and Backbone.history.history.length are not updating.
All routes are fired from links/URL's. I'm not using router.navigate() within the code. Here are examples of our Router:
define(function (require) {
var Backbone = require('backbone'),
Marionette = require('marionette');
return Backbone.Marionette.AppRouter.extend({
appRoutes: {
"": "showHome",
"home": "showHome",
"foo": "showFoo"
}
});
});
And Controller:
define(function (require) {
var Backbone = require('backbone'),
Marionette = require('marionette');
return Backbone.Marionette.Controller.extend({
showHome: function () {
require(['webapp','modules/home'], function (WebApp) {
WebApp.module("Home").start();
WebApp.module("Home").controller.showModule();
});
},
showFoo: function () {
require(['webapp', 'modules/foo'], function (WebApp) {
WebApp.module("Foo").start();
WebApp.module("Foo").controller.showModule();
});
}
});
});
UPDATE:
On further research, it turns out the problem is that older versions of IE don't record hash changes in their history. See - Change location.hash and then press Back button - IE behaves differently from other browsers. But I'm still not sure what the workaround for this would be. I'm guessing it would somehow involve manually handling hash change events with a plugin such as jQuery Hashchange and doing... something? Manually setting IE's history? Or crafting a custom history object and using it when we detect a Back button in IE?
I was having the same problem in one of our apps for IE.
Starting backbone history like below works.
Backbone.history.start({
pushState: true,
hashChange: false
});
Update: As mentioned By T Nguyen,
When you set pushState to true, hash URL's no longer trigger routes. Unless you add server-side support for all your Backbone routes, you need to add an event handler on the client side which captures appropriate links and calls .navigate() on the route

Where to put onload code in RequireJs and Backbone App

Doing my first backbone app and I'm using a structure somewhat like this tutorial
I'm wondering where the correct place for me to put my onload code, such as setting up onclick listeners etc would be?
I have:
A simple Bootstrap
require.config({
paths: {
jquery: 'libs/jquery/jquery',
underscore: 'libs/underscore/underscore',
backbone: 'libs/backbone/backbone'
}
});
require([
// Load our app module and pass it to our definition function
'app',
], function(App){
// The "app" dependency is passed in as "App"
App.initialize();
});
The App.js
define(['routers/search'], function(router){
var initialize = function(){
this.router = new router();
}
return { initialize: initialize};
});
And then a simple router that calls the relevenent function in the router also defined as a module that calls the relevent function on the router depending on the page.
My feeling is that this function in the router is where I should be putting my onload code. Is that correct?
One possibility is to use the RequireJS domReady plugin (it's available for download from their short plugins list): http://requirejs.org/docs/api.html#pageload
Here's the example they give:
require(['domReady'], function (domReady) {
domReady(function () {
//This function is called once the DOM is ready.
//It will be safe to query the DOM and manipulate
//DOM nodes in this function.
});
});
So then you can just incorporate it into your normal RequireJS structure, knowing that both the DOM is loaded plus any additional dependencies you might have listed alongside it.

Backbone.js Pushstate: true not returning callback function

I am pretty new to backbone js and I am having some problem getting the pushstate functionality of my app to work. Here is an eg of my route:
var TodoRouter = new (Backbone.Router.extend({
routes: {
"": "index",
"item/add": "AddTodoItem",
"list/add": "AddTodoList"
},
AddTodoItem: function() {
//e.preventDefault();
alert("add new item");
},
AddTodoList: function(e) {
//e.preventDefault();
alert("add new list");
},
Start: function(){
//note: my directory structure is localhost/playground/todo/
Backbone.history.start({pushState: true, root: "/playground/todo/"});
},
initalize: function(){
},
index: function(){
var todoListView = new TodoListView({ collection: TodoItemCollection });
}
}));
Here is how I call my route:
$(function() {
TodoRouter.Start();
});
And lastly here is how I call a link:
New List
The problem that I am running into is that when I call the link, the page stays the same, no alert and the browser displays:
http://localhost/playground/todo/#list/add
Now here is the funny part, if I refresh the page, the url become:
http://localhost/playground/todo/list/add
and I get the alert. So I have a feeling I am missing a key point somewhere. Any help would be greatly appreciated!
You have pushState: true and that's why it preferred the slash / instead of hash #
Either change that or remove the hash
You're trying to use Backbone Routes and html5 pushState.
As the backbone documentation said:
"if you have a route of /documents/100, your web server must be able
to serve that page, if the browser visits that URL directly."
So if you want to trigger some functions through uri (localhost/webapp/#about) you just need to use Backbone Routes.
If you want to use Backbone Routes and pushState, you'll need a back-end to answer requests made to your readable url (localhost/webapp/about) and need to use backbone.navigate method to avoid the browser understand <a href="#someRoute"> as a html anchor.
here you can see a complete example
You can't trigger your route, because when pushState:true the href="#route" has the same behavior as a html anchor.

How should I bootstrap my web app?

I am looking for the best way to bootstrap my web app using Backbone.Marionette, Backbone.Router and Requirejs.
The following implementation works but I would like to know if this is the right way to make things.
Here's some of my code (*).
My questions are:
1) Is right the following data flow (index.html -> conf.js -> router.js -> app.js) ?
2) The Backbone.View for each region (header, sidebar .....) should be instantiate in router.js or app.js or booths according the context?
// index.html
<!doctype html>
<html lang="en">
<head>
<!-- Load the script "/js/conf.js" as our entry point -->
<script data-main="js/conf" src="js/vendor/require.js"></script>
</head>
<body>
</body>
// js/config.js
require.config({
// some code
});
require(['app']); // instead of require(['conf']);
// router.js
define([
'app',
// others modules
],
function(App, $, _, Backbone, Marionette){
"use strict";
var AppRouter = Backbone.Marionette.AppRouter.extend({
routes: {
test: test,
"*defaults": "home"
}
var initialize = function ()
{
var app_router = new AppRouter;
};
return {
initialize: initialize
};
});
// js/app.js
define(
[
// some modules
],
function ($, _, Backbone, Router, Mustache, Layout, SidebarView) {
var MyApp = new Backbone.Marionette.Application();
MyApp.addInitializer(function () {
$('body').html(Layout);
MyApp.addRegions({
header: '#header',
sidebar: '#sidebar',
mainColumn: '#main-column',
rightColumn: '#right-column'
});
});
MyApp.addInitializer(function () {
var sidebarView = new SidebarView();
MyApp.sidebar.show(sidebarView);
});
MyApp.on("initialize:after", function () {
// Router.initialize();
});
MyApp.start();
return MyApp;
});
This looks pretty good overall. There are a few things I might change, but these are mostly personal preferences:
1) Invert the relationship between the router and app files, and use an initializer to start the router.
Right now you have a circular dependency between the router and the app files, and that's never a good thing. Even though RequireJS can handle that fine, it's a bad idea in many other ways as it can lead to code that doesn't quite work the way you expect.
2) In your router file, set up an initializer that instantiates the router.
3) Don't start backbone.history from the router file.
It's common, and suggested, to have multiple routers in a project. But you can only call Backbone.History.start() once. Start that in the app.js file, using the "after:initialize" event of the router
MyApp.on("after:initialize", function(){ Backbone.History.start(); }
4) Extract your initializer callbacks in to functions that are called from a single initializer
While there's nothing technically wrong with using multiple initializers - and you will need multiple initializers across multiple modules - I suggest using a single initializer within a single module, and have that one initializer call other functions defined in your module.
5) Call addRegions outside of initializers
There's no guarantee that your initializers will run in the order you add them. It depends on how the individual browser handles things.
For example, your app.js file could look like this:
// js/app.js
define(
[
// some modules
],
function ($, _, Backbone, Router, Mustache, Layout, SidebarView) {
var MyApp = new Backbone.Marionette.Application();
MyApp.addRegions({
header: '#header',
sidebar: '#sidebar',
mainColumn: '#main-column',
rightColumn: '#right-column'
});
MyApp.addInitializer(function(){
showLayout();
initSidebar();
});
MyApp.on("initialize:after", function(){
Backbone.History.start();
});
function initSidebar() {
var sidebarView = new SidebarView();
MyApp.sidebar.show(sidebarView);
}
function showLayout() {
$('body').html(Layout);
}
MyApp.start();
return MyApp;
});
...
alright, that looks like a lot more changes than I originally thought. :) But like I said, your set up looks fine over all. These are things that I would do, but are not necessarily requirements to make your app work.

Resources