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.
Related
I'm trying really hard to wrap my head around Backbone.js with Require.js and Handlebars.js. I'm still not 100% sure what the best combination is but this is what i'm currently using to redo the marketing site at work.
We've added some more pages and as it grows I thought it would be good to put the static site into an MV like Backbone.js. It seems that this would only be a good option if you have dynamic data as templates only seem suited for this typical scenario of looping through data and rendering the DOM elements.
But what if that's even too advanced for your needs and you just want to use the SOC and DRY practices of keeping your code in modules for easier maintenance and not having to put huge blocks of markup in your .html files.
But it seems like every tut just goes over the same telling of the backbone/require.js story. I'm assuming it's because no one uses backbone/require for static sites? I hope I'm wrong, don't people still have a need for something like backbone/require.js even for larger static sites just to make them easier to maintain? It seems like a logical solution.
I'm having the hardest time understanding how to link from one static page to another just using the Router file in Backbone.
Ideally I would like to have a header and footer template that are universal throughout the site and then just have large blocks of code for the content areas of each page, why is this so hard to accomplish with backbone/require and handlebars?
Can anyone give me a simple solution to what doesn't seem like a complicated problem so I don't have to create 17 static pages all repeating the same header and footer.
I think starting with a simpler project like this will help me understand more complicated examples later.
I have included a sample index.html, a sample view, a sample router, config file and app.js file so you can see how i'm trying to pull this together but no matter how I look at this it seems that the only feasible way is to create a bunch of static pages and link them through the Router. If at the end of the day that's all I could accomplish then I'm ok with that.
Thanks.
index.html:
<body>
<div id="container">
<!-- BODY WRAPPER -->
<section class="body-wrapper">
{{Header Template Here}}
{{Body Content Here}}
{{Footer Template Here}}
</section>
<!-- /.body-wrapper -->
</div>
<!-- /#container -->
<script data-main="js/config" src="js/libs/require.js"></script>
</body>
config.js:
// Set the require.js configuration for you application.
requirejs.config({
// Initialize the application with the main application file
baseUrl: 'js',
paths:
{
jquery : [
'//ajax.goolgleapis.com/ajax/libs/jquery/1.9.1/jquery.min',
'libs/jquery.min'
],
modernizr : [
'//cdjns.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min',
'libs/modernizr'
],
hbs : '../bower_components/require-handlebars-plugin/hbs',
underscore : '../node_modules/underscore/underscore-min',
backbone : '../node_modules/backbone/backbone-min',
handlebars : '../node_modules/handlebars/handlebars',
text : '../node_modules/text/text'
},
hbs: {
helpers: true,
i18n: false,
templateExtensions: 'hbs',
partialsUrl: ''
},
shim: {
'jquery' : {
exports: '$'
},
'underscore': {
exports: '_'
},
'handlebars': {
exports: 'Handlebars'
}
}
});
// Launch the App
require(['app'],
function(App){
App.initialize();
});
app.js
define(
['jquery','underscore','backbone','router'],
function($, _, Backbone, Router){
var initialize = function() {
Router.initialize();
}
return {
initialize: initialize
};
});
router.js
define(
['jquery',
'underscore',
'backbone',
'views/HomeView',
'views/HeaderView',
'views/FooterView',
'models/FeatureModel',
'collections/FeatureCollection'],
function($, _, Backbone, HomeView, HeaderView, FooterView, FeatureModel, FeatureCollection){
var AppRouter = Backbone.Router.extend({
routes: {
'' : 'home', //#index
'/feature/:page' : 'featurePage',
'*actions' : 'defaultAction',
'about' : 'about', //#about
'/support' : 'support', //#support
}
});
var initialize = function(options) {
var appView = options.appView;
var router = new AppRouter(options);
router.on('home', function(){
var homeView = new HomeView();
homeView.render();
});
router.on('route:defaultAction', function(actions){
var homeView = new HomeView();
homeView.render();
});
router.on('support', function(){
var supportView = new SupportView();
supportView.render();
});
var headerView = new HeaderView();
var footerView = new FooterView();
Backbone.history.start();
};
return {
initialize: initialize
};
});
views/homeView.js
define(
['jquery','underscore','backbone' , 'text!/templates/home.html'],
function($, _, Backbone, homeTemplate){
var HomeView = Backbone.View.extend({
el : $('#content'),
render : function() {
this.$el.html(homeTemplate);
}
});
return HomeView;
});
templates/home.html
Big block of HTML content for the body of the index.html page
Few things:
Per http://backbonejs.org/#Router-routes the route callback should be in the form of route:(callback) so your home should be:
router.on('route:home', function(){});
You could also use
router.on('route', function(route, params){})
The router fires both events and you can handle as you wish. You can see the events documentation here: http://backbonejs.org/#Events-catalog
Also, not sure why you need handlebars or any templating language at all if they are all static html? You are already appending the html with your this.$el.html call.
If you just had simple html with:
<body>
<div id="header">
<div id="content">
<div id="footer">
</body>
Then you can stick your view el attribute like you have $('#header') etc and render accordingly.
Also not sure if you want to just have a single content view and swap out the html content in there instead on your render
routes: {
'feature/:page' : 'featurePage'
}
//route callback ex '/feature/feature1'
featurePage : function(page){
console.log(page) //'feature1'
//here you can create/render/set models/views accordingly
})}
Hopefully some of this helps.
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.
While I developing with requirejs and backbonejs, I got confused with how the import mechanism works, as I thought the imported class only applies to that functional scope. However, when I try to debug, I found out for some requirejs classes that I didn't import jquery or backbone or underscore, it still able to work properly, but this does not applies to other classes I created.
The below example illustrate what I meant:
1) index.html -> initial load file
2) init.js -> importing all required classes and output whether the class is available
3) base.js -> base class, importing all required libraries
4) shop.js -> extend from base class, didn't not import jquery,backbone files, but it is working
index.html
<html>
<head>
<title>Testing</title>
<script data-main='init' src='http://requirejs.org/docs/release/2.0.4/minified/require.js'></script>
</head>
<body>
halo world
</body>
</html>
init.js
require.config({
paths: {
jquery: 'http://code.jquery.com/jquery-1.7.2.min',
underscore: 'http://underscorejs.org/underscore-min',
backbone: 'http://backbonejs.org/backbone-min'
},
shim: {
backbone: {
deps: ["underscore", "jquery"],
exports: "Backbone"
},
underscore: {
exports: "_"
}
}
});
require([
'views/shop',
],function(ShopView){
var shopView = new ShopView();
shopView.render();
console.log('Backbone - ');
console.log(Backbone);
console.log('Underscore - ');
console.log(_);
console.log('jQuery - ');
console.log($);
console.log('BaseView - ');
console.log(BaseView);
});
shop.js
define([
'views/base'
], function(BaseView) {
var ShopView = BaseView.extend({
initialize:function(){
console.log('ShopView');
}
});
return ShopView;
});
base.js
define([
'jquery',
'underscore',
'backbone'
], function($,_,Backbone) {
var BaseView = Backbone.View.extend({
initialize:function(){
console.log('BaseView');
}
});
return BaseView;
});
The result output from init.js:
console.log(Backbone); -> returns me backbone classes even I didn't import it
console.log($); -> returns me jquery classes even I didn't import it
console.log(_); -> returns me underscore classes even I didn't import it
console.log(BaseView); -> returns undefined for my custom classes
If I remove views/shop from init.js.
require([
],function(){
console.log('Backbone - ');
console.log(Backbone);
console.log('Underscore - ');
console.log(_);
console.log('jQuery - ');
console.log($);
console.log('BaseView - ');
console.log(BaseView);
});
console.log(Backbone); -> Undefined
console.log($); -> break
console.log(_); -> break
console.log(BaseView); -> break
I couldn't explain how previous scenario works, I thought backbone, jquery, underscore is global variables, but it seems I remove the view class, then the variables goes undefined, while if it is imported class, I could bring forward my previous imported class from base.js to future imported class. Sorry if it confused.
Can someone shed some lights on how the import in requirejs works and the scope of import. Do I have to re-require classes I previously required.
require.config it's not importing anything by its own.
It is used for declaring alias (paths) and dependencies of non-AMD files.
Take in consideration that when you use 'define' , you are actually requiring its dependencies.
define ([ array of dependencies ], function)
.
init: require shop (ok, let's bring shop here:
shop: require base (ok, let's bring base here:
base: require (so you need backbone, underscore and jquery.. ok,
I'll bring them for you)
Then, when every dependency is required and loaded, the init execution starts.
When you remove shop from init.js, you are not requiring anything. Again require.config doesn't import anything.
PS. This is personal taste, but
I like more to have a file app.js where jquery,underscore and backbone are required. And then every module requires 'app'. It's also useful to share that namespace in case you want to add functions to it. (app.myFunction)
See this backbone poilerplate: https://github.com/tbranyen/backbone-boilerplate
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.
In the router.js. The firebug console alerts Backbone is null there. Why???
app.js
define([
'order!jQuery',
'order!Underscore',
'order!Backbone',
'order!router' // Request router.js
],
function($, _, Backbone, Router){
App = {
initialize: function() {
console.log("app.js initalize");
Router.initialize();
}
};
return App;
});
router.js
define([
'order!Underscore',
'order!Backbone'
],
function(_, Backbone){
var AppRouter = Backbone.Router.extend({
// Console shows Backbone is null here, why?
// I'm sure the config is correct.
routes: {
'*actions': "defaultAction"
},
defaultAction: function(actions){
// We have no matching route, lets just log what the URL was
console.log('No route:', actions);
}
});
var initialize = function(){
console.log("Router initialize");
var app_router = new AppRouter;
Backbone.history.start();
};
return {
initialize: initialize
};
});
Backbone does not support the AMD and it doesn't register as module. When required it registers normally as a global Backbone object, also since 1.3 Underscore doesn't support AMD neither and if you will require Backbone and Underscore under Backbone and _ namespaces they will overwrite its values in this modules scope to undefined cause of that.
jQuery supports AMD but it also registers itself as a global instance. Basically it means that you don't need to require jquery, underscore and backbone multiple times - it's enough if you do it once in your requirejs main script
The alternative is to hack the backbone.js library.
Note: This will allow you to reference Backbone.js and underscore.js library within your require.js defines, but it will not stop them from being added to the global namespace/window object. This requires a little more hacking.
Find:
(function(){var l=this,y=
Replace it with:
define('backbone',['underscore','jquery'],function(_,$){
var l = this;
(function(){var y=
Add this to the bottom of the page:
return l.Backbone;
});
Then do the same for underscore.js
Prefix the beginning with:
define('underscore',function(){
Add to the bottom of the page:
return this._;
});