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.
Related
Helllo, probably this is a common question, but I didn't find the appropriate answer.
I have an html file: header.html which contains a header which I want to display
Here is my Backbone.View
el: $(".header"),
initialize: function () {
this.render();
},
render: function () {
var template = _.template( $("#header_template").html(), {} );
this.$el.html( template );
},
When I put the code inside a java script template, it works:
<script type="text/template" id="header_template">
code of header.html goes here
</script>
But when I use it this way:
<script type="text/template" id="about_template" src="header.html"></script>
it stops working even though, the firebug sees the code inside a template.
Can someone tell me what my mistake is and how to solve it
A clean way to organize your templates is to create a subfolder tpl. Here you add all your .html files
Coenraets has a nice tutorial where he uses this approach together with a templateloader function.
You then load the templates in memory before bootstrapping your backbone application.
tpl.loadTemplates(['header', 'wine-details', 'wine-list-item'], function() {
app = new AppRouter();
Backbone.history.start();
});
I want to do some basic i18n in my application
I loaded the i18n plugin (from require.js) to my Application and created a directory nls in which i have several files e.g. project.js
I set up the default location in main.js file like
config : {
i18n : {
locale : 'de-de'
}
}
I now want to use the files in my View / Template. Can somebody explain to me how this is done? The templates are setup in one template.js file
My View:
define(['marionette', 'templates', 'i18n!nls/project'], function (marionette, templates, msg) {
return marionette.CompositeView.extend({
template : templates.project
});
});
My template:
<div class="container">
<div class="well">
<h2>Projects</h2>
</div>
</div>
Can someone explain to me how to use the file in my View / Template? Thanks in advance!
EDIT:
I figured out the solution by some try&error. As i am loading the templates via the require.js tpl! plugin i dont need to compile them if i call them by require('tpl!templates/dashboard.tmpl'). I can simply pass the i18n file i loaded by 'i18n!nls/dashboard'. In Marionette the view are rendered by default, so i did this:
define(['marionette', 'templates', 'i18n!nls/dashboard'], function (Marionette, templates, msg) {
return Marionette.CompositeView.extend({
template : function () {
return templates.dashboard(msg);
},
initialize : function() {
}
});
});
The files for the i18n plugin are well explained here:
http://requirejs.org/docs/api.html#i18n
I had to do this step by step, first i missed the root, then i wondered why my german locale did not load, but i simply forgot to set de-de : true in the root file. Now everything is working like a charm
first you load the i18 file via require.js to your view.
I use the handlebars templates in this example.
define([
'marionette',
'handlebars',
'text!modules/tableModule/templates/myTmpl.html',
'i18n!nls/dashboard',
],
function(Marionette, Handlebars, tmpl, locals) { ...
then you compile and load your template with the i18 object.
var template = Handlebars.compile(tmpl);
this.template = template(locals.myVar);
you can go and do complex combinations as well
var template = Handlebars.compile(tmpl);
var data =_.extend(options, {lang:locals});
this.template = template(data);
your nls file will look like this
define({
"root": {
"myVar" : "some text in",
"canBeAnObjectTo": {
"title" : "my title ",
"contact" : "Contact",
}
and your view will be something like this:
<div class="cssClass">
<div class="table-caption pull-left">{{this.myVar}}</div>
</div>
hope that helps
I am using an .ejs template in my view. But for some reason the view does not load the given template. It returns undefined. Here's the code:
sandplate2.applicationView = Backbone.View.extend({
el: 'div',
template: _.template($("appl.ejs").html()),
initialize: function(){
console.log("Application view initialize");
_.bindAll(this, "render");
this.render();
},
render: function(){
console.log("Application view rendering");
this.$el.html(this.template());
return this;
}
});
Do I have to configure something else in order to load a template?
I structured my app using Yeoman. I used the init and backbone generators.
FYI - The template I am trying to load is loaded in the index.html using a script element.
If you built it using Yeoman, take a look at app.js to see if you are using Backbone.LayoutManager. You might have to change the configuration there for EJS to work. By default, I think it should be expecting Underscore templates.
I'm using Handlebars and I updated my app.js to look like this:
Backbone.LayoutManager.configure({
manage: true,
paths: {
layout: "templates/layouts/",
template: "templates/"
},
fetch: function(path) {
path = path + ".html";
if (!JST[path]) {
$.ajax({ url: app.root + path, async: false }).then(function(contents) {
JST[path] = Handlebars.compile(contents);
});
}
return JST[path];
}
});
I also added Handlebars to the module's define() call, passing in 'Handlebars' as a reference. You might need to do something similar for EJS.
Please try with latest backbone genearator with yeoman 1.0beta.
We have made lot of improvements on it including Precompiling ejs templates. You don't want to worry about templates, yeoman will precompile and load it for you.
A sample generated code for input-view is provided below.
Todo.Views.InputView = Backbone.View.extend({
template: JST['app/scripts/templates/input.ejs'],
render: function(){
$(this.el).html(this.template());
}
});
Conventions aside, it looks like the problem is simply your jQuery lookup.
_.template($("appl.ejs")...
$('appl.ejs') isn't a valid DOM selector unless you have an element like this in your index.html
<appl class="ejs"></appl>
If you're trying to target your template with jQuery, give it an ID or something that jQuery can find like so:
<script type="text/template" id="appl">
<div></div><!-- template html here -->
</script>
// later...
$('#appl').html()
// will get your template html
However, as others have mentioned, in a yeoman and require.js workflow, you can ask require.js to fetch the template for you as text and throw it around as a variable before creating an underscore template.
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 :)
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.