Handlebars with Backbone template not rendering - backbone.js

I have an app which uses both Backbone and Handlebars on a server and a client.
Server
Backbone and Express-Handlebars installed
app.js
app.set('views', path.join(__dirname, '/views'));
app.engine('.hbs', expHbs({
defaultLayout: 'index',
extname: '.hbs'
}));
app.set('view engine', '.hbs');
index.js
exports.init = function(req, res){
res.render('contact/index');
};
index.hbs
<div class="row">
<div class="col-sm-6">
<div class="page-header">
<h1>Send A Message</h1>
</div>
<div id="contact"></div>
</div>
....some code
<script id="tmpl-contact" type="text/x-handlebars-template">
<form>
bootstrap with handlebars temlates {{....}} in here
</form>
</script>
Client
On the client I have Backbone and Handlebars installed via Bower
In index.js Backbone.view
var Handlebars = require('handlebars');
app.ContactView = Backbone.View.extend({
el: '#contact',
template: Handlebars.compile( $('#tmpl-contact').html() ),
events: {
'submit form': 'preventSubmit',
'click .btn-contact': 'contact'
},
initialize: function() {
this.model = new app.Contact();
this.listenTo(this.model, 'sync', this.render);
this.render();
},
render: function() {
this.$el.html(this.template( this.model.attributes ));
this.$el.find('[name="name"]').focus();
},
preventSubmit: function(event) {
event.preventDefault();
},
contact: function() {
this.$el.find('.btn-contact').attr('disabled', true);
this.model.save({
name: this.$el.find('[name="name"]').val(),
email: this.$el.find('[name="email"]').val(),
message: this.$el.find('[name="message"]').val()
});
}
});
What happens is that index.hbs renders on the server-side normally, but it is not rendering a form inside script; it shows empty <div id="contact"></div> and doesn't shows any errors in console.
As shown here Using Handlebars with Backbone, a way to replace underscore templating with handlebars is simply to replace _.template with Handlebars.compile, but neither of these options works for me. I also tried different type attributes for <script> and it's still not working.
How can I fix this? Any help is appreciated. Thanks.
Added full index.js on client
/* global app:true */
var Handlebars = require('Нandlebars');
(function() {
'use strict';
app = app || {};
app.Contact = Backbone.Model.extend({
url: '/contact/',
defaults: {
success: false,
errors: [],
errfor: {},
name: '',
email: '',
message: ''
}
});
app.ContactView = Backbone.View.extend({
el: '#contact',
template: Handlebars.compile( $('#tmpl-contact').html() ),
events: {
'submit form': 'preventSubmit',
'click .btn-contact': 'contact'
},
initialize: function() {
this.model = new app.Contact();
this.listenTo(this.model, 'sync', this.render);
this.render();
},
render: function() {
this.$el.html(this.template( this.model.attributes ));
this.$el.find('[name="name"]').focus();
},
preventSubmit: function(event) {
event.preventDefault();
},
contact: function() {
this.$el.find('.btn-contact').attr('disabled', true);
this.model.save({
name: this.$el.find('[name="name"]').val(),
email: this.$el.find('[name="email"]').val(),
message: this.$el.find('[name="message"]').val()
});
}
});
$(document).ready(function() {
app.contactView = new app.ContactView();
});
}());

So as mentioned here Handlebars.compile must load after handlebars template in <script></script> is loaded into the DOM. In this case when using express-hanldebars, a link to js file must be placed after {{{ body }}} element in the main layout. That is the right answer.
There is one more problem here that app can't be defined globally as it appears in index.js
In order to define app globally you have to move app = app || {}; outside the IIFE scope and put it at the beginning of a file and declare it as a global variable: var app = app || {};. If there are multiple backbone files they should all implement a similar structure in order to app be global.

Related

reuse Bootsrap Modal view in BackboneJS

I just started to learn BackboneJS and getting deeper inside I face a problem. I have a bootstrap modal where I would like populate the modal-content in function of a called event fired in my main view and try to figure out how to inject a subview in my Modal view which is dynamically generated. So far my code looks like but not working
Main view
//here events are mapped
Fefe.Views = Fefe.Views || {};
(function () {
'use strict';
Fefe.Views.Editor = Backbone.View.extend({
template: JST['app/scripts/templates/editor.ejs'],
tagName: 'div',
el: '.container',
id: '',
className: '',
events: {
"click button.expand" : "controlToggle",
"click .grid" : "grid"
},
controlToggle: function(e){
var controlls = $(e.currentTarget).closest('.editor-controls')
$(controlls).find('.active').removeClass('active')
$(e.currentTarget).parent().addClass('active')
},
grid: function() {
this.model = new Fefe.Models.Grids({
'title': 'Edit Grids'
})
var gridView = new Fefe.Views.Grids({
model: this.model
})
var grids = new Fefe.Views.Modal({
model : this.model,
subview: gridView
}).render()
},
initialize: function () {
var body = $('body')
var rows = body.find('.row')
$.each(rows, function(e , v){
$(this).addClass('editor-row empty-row')
})
$('.sortable-rows').sortable({ handle: 'button.row-handle.btn.btn-default' })
this.listenTo(this.model, 'change', this.render);
},
render: function () {
return this;
}
});
})();
Modal view
//this one holds the modal markup
Fefe.Views = Fefe.Views || {};
(function () {
'use strict';
Fefe.Views.Modal = Backbone.Marionette.View.extend({
template: JST['app/scripts/templates/modal.ejs'],
subview: '',
className: "modal",
attributes: {
tabindex: "-1",
role: "dialog",
},
initialize: function() {
this.template = this.template;
console.log(this)
},
events: {
"click .save": "save",
"click .close": "close",
"change input": "modify",
},
render: function(e) {
this.$el.html(this.template(this.model.toJSON())).modal()
$(".modal-dialog").draggable({
handle: ".modal-header"
})
return this
},
show: function() {
$(document.body).append(this.render().el);
},
close: function() {
this.remove();
},
save: function() {
if(this.model.id == null) {
tasks.create(this.model);
}
else {
this.model.save();
}
this.remove();
},
edit: function(e) {
var attribute = {};
attribute[e.currentTarget.name] = e.currentTarget.value;
this.model.set(attribute);
},
});
})();
Maybe the approach is wrong and I'm on the wrong track
You should checkout the way with custom regions, described by Brian Mann at backbonerails.com
So the idea is following:
1) Define a region in your app with special class, lets call it DialogRegion
regions: {
dialogs: {
selector: '#dialogs',
regionClass: DialogRegion
}
}
2) Extend DialogRegion like following. I used Bootstrap modal API, please expect
var DialogRegion = Marionette.Region.extend({
onShow: function(view) {
view.$el.addClass('modal');
view.$el.modal();
// add handler to close popup via event
view.on('before:destroy', function() {
view.$el.modal('hide');
});
//destroy view on popup close
view.$el.on('hidden.bs.modal', function (e) {
view.destroy();
});
})
})
3) Later from any place of your app you can render Modal via rendering any view in dialogs App region:
App.dialogs.show( new SomeSuperView({
model: model
}))
I recommend you to checkout tutorial at Backbonerails to clarify this way. Hope you will find it usefull

Understanding the el in backbone

I don't quite understand how the el works in backbone.
I was under the assumption that el defaulted to body when it wasn't specified. I created a fiddle to illustrate my misunderstanding.
When I specify the el everything works fine. Unspecified returns nothing though.
http://jsfiddle.net/9R9zU/70/
HTML:
<div class="foo">
<p>Foo</p>
</div>
<div class="bar">
</div>
<script id="indexTemplate" type="text/template">
Bar?
</script>
JS:
app = {};
app.Router = Backbone.Router.extend({
routes: {
"" : "index"
},
index: function() {
if (!this.indexView) {
this.indexView = new app.IndexView();
this.indexView.render();
} else {
this.indexView.refresh();
}
}
});
app.IndexView = Backbone.View.extend({
// el: $('.bar'),
template : _.template( $('#indexTemplate').html() ),
render: function() {
this.$el.html(this.template());
return this;
},
refresh: function() {
console.log('we\'ve already been here hombre.')
}
});
var router = new app.Router();
Backbone.history.start();
If you do not specify element in the Backbone view, it will create an html node in memory, render the view into it and bind all event handlers based on that node. Then you will need to manually append it to the dom like this:
$('body').append(this.indexView.render().el);

Proper way to create a collection list view in Backbone

I'm currently learning Backbone.js and I'm having a hard time learning how to properly use Views (since I have experienced when it comes to MVC), so here is what I'm trying to do:
templates:
<script type="text/template" id="todolist-template">
<ul></ul>
</script>
<script type="text/template" id="todo-template">
<li>
<%= item.name %>
<%= item.description %>
<%= item.priority %>
</li>
</script>
html:
<div id="container"></div>
Views:
var TodoView = Backbone.View.extend({
tagName: 'li',
className: 'todo',
initialize: function() {
this.template = _.template($('#todo-template').html());
this.render();
},
render: function() {
this.$el.html(this.template({item: this.model}));
return this;
}
});
var TodoListView = Backbone.View.extend({
el: '#container',
tagName: 'ul',
className: 'todolist',
initialize: function() {
this.template = _.template($('#todolist-template').html());
this.render();
},
render: function() {
that = this;
this.$el.empty();
this.$el.append(this.template());
this.collection.each(function(model) {
that.$el.append(new TodoView({model: model.toJSON()}));
});
return this;
}
});
Models and Collections:
var Todo = Backbone.Model.extend({
defaults : {
name : '',
priority: '',
description: ''
}
});
var TodoList = Backbone.Collection.extend({
model: Todo
});
var todoList = new app.TodoList([
new Todo({
name: 'unclog the sink',
priority: '10',
description: 'FIX THE SINK!!!'
}),
new Todo({
name: 'get bread',
priority: '0',
description: 'We are out of bread, go get some'
}),
new Todo({
name: 'get milk',
priority: '2',
description: 'We are out of milk, go get some'
})
]);
"misc":
$(function() {
new HeaderView();
new TodoListView({collection: todoList});
router = new AppRouter();
Backbone.history.start();
});
What I'm trying to do is to create a ul which will then get populated with lis that contain the collection's data. I've been trying to fix/debug this code for a while now (at least 3 hours) but I'm constantly hitting errors or wrong results, so please someone explain to me the proper way of implementing this.
edit (resulting HTML):
<div id="container">
<ul></ul>
</div>
At least one problem lies here:
that.$el.append(new TodoView({model: model.toJSON()}));
Should be
that.$el.append(new TodoView({model: model.toJSON()}).render().el);
Since you can't append a view to $el, but rather you should be appending the rendered html
You don't need <li> in your template as your view already wraps the template in those tags. If it still doesn't work, check the DOM and post it here. Same goes for <ul>...
Also, I don't see where you add your ListView to the DOM. render only operates on a local element which isn't part of the DOM yet. Once rendered, you have to add it to the DOM.

How to render Backbone el correctly into the view page

I'm trying to working correctly with my first Backbone app and trying to render it into my page.
I've wrote this app but I didn't got how I shoud put the app html rendered in the html view:
<script type="text/javascript">
$(function(){
var SearchApp = new Search.Views.App({
id:"product-name-results"
});
SearchApp.render();
});
<script>
This is my app
var Search = {
Models: {},
Collections: {},
Views: {},
Templates:{}
}
Search.Views.App = Backbone.View.extend({
initialize:function () {
console.log('Search.Views.App initialize')
},
render:function (options) {
this.$el.html('hello world');
}
});
Obviously this render method not appending in the html view, but how to append it into the view?
See this Fiddle
Basically, I changed id: '' to el: '', and added HTML container

backbone.js history with only one route?

I'm developing my first backbone project and I have requirement that I'm not sure how to meet. I'm sure the solution has something to do with properly routing my app, but I'm not sure...
App.Router = Backbone.Router.extend({
initialize: function(options) {
this.el = options.el;
},
routes: {
'': 'search',
'search': 'search'
},
search: function() {
var search = new App.SearchView();
search.render();
}
}
});
I have three views:
// Defines the View for the Search Form
App.SearchView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
this.render();
},
template: _.template($('#search-form').html()),
el: $('#search-app'),
events: {
'click .n-button' : 'showResults'
},
showResults: function() {
this.input = $('#search');
var search = new App.ResultsSearchView();
var grid = new App.GridView({ query: this.input.val() });
search.render();
grid.render();
},
render: function() {
$(this.el).html(this.template());
return this;
},
name: function() { return this.model.name(); }
}); // App.SearchView
//Defines the View for the Search Form when showing results
App.ResultsSearchView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
this.render();
},
template: _.template($('#results-search-form').html()),
el: $('#search-input'),
render: function() {
$(this.el).html(this.template());
return this;
},
events: {
'click .n-button' : 'showResults'
},
showResults: function() {
this.input = $('#search');
var grid = new App.GridView({ query: this.input.val() });
grid.render();
},
name: function() { return this.model.name(); }
}); // App.ResultsSearchView
// Defines the View for the Query Results
App.GridView = Backbone.View.extend({
initialize: function(options) {
var resultsData = new App.Results();
resultsData.on("reset", function(collection) {
});
resultsData.fetch({
data: JSON.stringify({"query":this.options.query, "scope": null}),
type: 'POST',
contentType: 'application/json',
success: function(collection, response) {
$('#grid').kendoGrid({
dataSource: {
data: response.results,
pageSize: 5
},
columns: response.columns,
pageable: true,
resizable: true,
sortable: {
mode: "single",
allowUnsort: false
},
dataBinding: function(e) {
},
dataBound: function(){
}
});
},
error: function(collection, response) {
console.log("Error: " + response.responseText);
}
});
_.bindAll(this, 'render');
this.render();
},
el: $('#search-app'),
template: _.template($('#results-grid').html()),
render: function() {
$(this.el).html(this.template());
return this;
}
}); // App.GridView
The issue I am having is that we want our users to be able to use the back button to navigate back to the initial search and also from there, be able to move forward again to their search results. I just have no idea how to do this. Any assistance would be a huge help.
Thanks!
Backbone handles the browser history -- all you have to do is call Backbone.history.start() on startup. Well, that and make sure to call Router.navigate whenever you want to save the current navigation state.
In your example, the appropriate time would be when the user clicks "search". In the searchView.showResults method, instead of creating and rendering the results view, call:
myRouter.navigate("results/" + this.input.val(), { trigger: true });
This causes the router to go to the results/query route, which you have to add:
'results/:query': 'results'
Finally, create the results method within your router, and put the view-creating logic there:
results: function(query) {
var search = new App.ResultsSearchView();
var grid = new App.GridView({ query: query });
search.render();
grid.render();
}
Here's a working demo -- it's a bit hard to see on JSFiddle because the page is within an iFrame, but you can confirm it's working by hitting Alt+Left, Alt+Right to call the browser's back and forward respectively.
And for contrast, here's a similar demo, except it uses a single route. It calls router.navigate without trigger: true. You can see that, using this single-route method, you're able to navigate back; however, you can't go forward again to the results view, because Backbone has no way to re-trace the steps to get there.
App
var HomeView = Backbone.View.extend({
initialize: function() {
this.render();
},
el: "#container",
events: {
"submit #search": "search"
},
template: _.template($("#search-template").html()),
render: function() {
var html = this.template();
this.$el.html(html);
},
search: function(e) {
e.preventDefault();
router.navigate("results/" + $(e.target).find("[type=text]").val(), { trigger: true });
}
});
var ResultsView = Backbone.View.extend({
initialize: function() {
this.render();
},
el: "#container",
render: function() {
var html = "Results test: " + this.model.get("query");
this.$el.html(html);
}
});
var Router = Backbone.Router.extend({
routes: {
"" : "search",
"results/:query": "results"
},
search: function() {
console.log("search");
var v = new HomeView();
},
results: function(query) {
console.log("results");
var v = new ResultsView({ model: new Backbone.Model({ query: query }) });
}
});
var router = new Router();
Backbone.history.start();
HTML
<script type='text/template' id='search-template'>
<form id="search">
<input type='text' placeholder='Enter search term' />
<input type='submit' value='Search' />
</form>
</script>
<div id="container"></div>​

Resources