Structuring backbone.js app with require.js - backbone.js

I started following Organizing your application using Modules (require.js tutorial, but after adding first event handler to my view - I encountered problem:
// Filename: views/project/list
define([
'jquery',
'underscore',
'backbone',
'handlebars',
'collections/projects',
'text!templates/projects/list.js'
], function ($, _, Backbone, Handlebars, ProjectsCollection, projectListTemplate) {
var ProjectListView = Backbone.View.extend({
el: $('#container'),
events: {
"click .open-proj": "openProject",
},
initialize: function () {
...
},
render: function () {
...
},
openProject: function(e) {
// HERE I WANT TO TRIGGER ROUTING VIA router.navigate
alert("opened");
}
});
// Our module now returns our view
return ProjectListView;
});
In the openProject callback I want to trigger routing, but I cannot introduce dependency to app.js as it would cause circular dependency(router depends on view). How should I handle this?

You can pass your router to the ProjectListView when you create it:
var projectListView = new ProjectListView({
router: app_router
});
Since ProjectListView is a Backbone.View, it can then access the router using this.options.router, no circular dependency problem here.

Related

Simplest static rendering using backbone.js only for routing

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.

JST is undefined - Yeoman , Backbone - Template cannot be loaded

Am new to Yeoman and I generated a Backbone application using generator-backbone
This is my main.js ( require js config)
/*global require*/
'use strict';
require.config({
shim: {,
handlebars: {
exports: 'Handlebars'
}
},
paths: {
jquery: '../bower_components/jquery/dist/jquery',
backbone: '../bower_components/backbone/backbone',
underscore: '../bower_components/lodash/dist/lodash',
handlebars: '../bower_components/handlebars/handlebars'
}
});
require([
'backbone'
], function (Backbone) {
Backbone.history.start();
});
Now I created a view using yo backbone:view Login
This is the generated view
define([
'jquery',
'underscore',
'backbone',
'templates'
], function ($, _, Backbone, JST) {
'use strict';
var LoginViewView = Backbone.View.extend({
template: JST['app/scripts/templates/LoginView.hbs'],
tagName: 'div',
id: '',
className: '',
events: {},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
return LoginView;
});
When I run the app with LoginView, i get an error
Error: Script error for: templates http://requirejs.org/docs/errors.html#scripterror
Apparently I see templates is no where defined in main.js. What am i missing while running the yeoman generator
If you did the same blunder mistake like m, just verify this
After generating the code, and if its shows template is undefined or u find the template.js missing, check the following steps
run grunt handlebars (include --force if you want to ignore any warning/errors)
This will pre compile the .hbs file make a single template.js file.
But the catch here is, it wont work out of the box, the template.js is written in the location app/.tmp/scripts/template.js location.
You can modify main require configure file accordingly, or open GruntFile.js and modify this function
grunt.registerTask('createDefaultTemplate', function () {
grunt.file.write('/your/custom/path/to/templates.js', 'this.JST = this.JST || {};');
});
Also verify if u have this line in GruntFile.js
grunt.loadNpmTasks('grunt-contrib-handlebars');
if not, add it at the very last (ofcourse not after the function block)

Requirejs2: How to handle own files?

I have configured requirejs to load the core libs (jquery, underscore, backbone).
Now I would like to add my backbone models, controllers, views, etc to be loaded asyncronly
I found a lots of tutorials to this topic and lots of "ready" boilerplates unfortunatly I mentioned that most approaches are depreceated or rather complicated (even there are better approaches).
One example is how I configured requirejs for the main libs:
https://stackoverflow.com/a/10914666/1309847
So how do I load Backbone Views, Models, Collections, Routers, Controllers and Templates with a simple and valid Requirejs configuration?
I followed youre advice but get some strange error
main.js
require.config({
paths: {
jquery: 'vendors/jquery/jquery',
underscore: 'vendors/underscore/underscore',
backbone: 'vendors/backbone/backbone'
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require(['app'], function(app){
});
app.js
define(['jquery', 'underscore', 'backbone'], function($, _, Backbone){
var Message = new Backbone.Model.extend({
//idAttribute: '_id',
//defaults: { body: '' }
//url: function(){ return this.id ? '/messages/' + this.id : '/messages'; }
});
var newMessage = new Message({ body: 'hi' });
newMessage.save();
});
The error occours in app.js:
Uncaught TypeError: Object [object Object] has no method 'apply'
When I comment the new Backbone.Model.extend part I don't get any error anymore.
in my experience, the best way to bootstrap your application is by creating a Backbone.Router. So you can associate urls with your application functionality.
If you are using RequireJS+Backbone, you probably have a main.js where RequireJS is configured (paths, shims, etc). The first call to "require" is used to load a initial script in order to bootstrap the whole app.
For example:
/**
* main.js - RequireJS bootstrap
*/
require.config({
paths: {
//your paths
},
shim: {
//your shims
}
});
require(
[
'app' //app.js is at the same directory as main.js
],
function(app) {
app.init();
}
);
then in app.js you can create a new Router instance, or you can just start creating Views and Models.
For further reference: http://addyosmani.github.com/backbone-fundamentals/
So as I have now understood right: You have to wrap a requirejs function around youre own custom js file.
The function is called define. The first parameter is an array of the dependencies which you have defined in the main.js file or a relative path to another custom js from you.
The second parameter is the callback which holds the original file. Important is that you return the object, function, array or variable which you want to share.
The whole thing looks like this:
define(
['underscore', 'backbone'], // the dependencies (either relative paths or shortcuts defined in main.js
function(_, Backbone){ // the return statement of the deps mapped to a var
var MessageModel = Backbone.Model.extend({ // the original code, file
defaults: { body: '' },
initialize: function(){}
});
return MessageModel; // the return statement, sharing the "final result", sometimes you return the initialize parameter
});
The same for a collection wrapping the models:
define(
['jquery', 'underscore', 'backbone', 'models/message_model'], // deps and the last one is the relative path
function($, _, Backbone,MessageModel){ // same as above explained
var MessageCollection = Backbone.Collection.extend({
model: MessageModel,
initialize: function(){}
});
return MessageCollection;
});
I now only have to figure out how I can bootstrap to whole application. But I think I need more knowledge of backbone to do this :)

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.

BackboneJS + Requirejs: Backbone is null issue

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._;
});

Resources