Accessing Properties of Parent Backbone View - backbone.js

I have a backbone view that calls to a sub-view:
lr.MapView = Backbone.View.extend({
el: $('#map'),
foo: "bar",
initialize: function() {
var that = this;
_.bindAll(this, "render", "addAllEvents", "addOneEvent");
this.collection = new lr.Events();
this.collection.fetch({
success: function(resp) {
that.render();
that.addAllEvents();
}
});
},
addAllEvents: function() {
this.collection.each(this.addOneEvent);
},
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e
});
},
render: function() {
}
});
Here is the sub-view:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.foo); // will console.log 'undefined'
},
render: function() {
}
});
I'd like to be able to access properties the parent view within the sub-view, but it isn't working with the above code. For example, how can I access the 'foo' variable within the sub-view?

lr.MapView is a "class", everything that Backbone.View.extend builds will be in lr.MapView.prototype, not in lr.MapView. Run this with the console open and you'll see whats going on:
var MapView = Backbone.View.extend({ foo: 'bar' });
console.log(MapView);
console.log(MapView.prototype);
console.log(MapView.prototype.foo);
Demo: http://jsfiddle.net/ambiguous/DnvR5/
If you're only going to have a single MapView then you can refer to lr.MapView.prototype.foo everywhere:
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.prototype.foo);
}
Note that everywhere includes within lr.MapView instances so your foo will act like a "class variable" from non-prototype based OO languages.
The right way to do this is to use an instance variable for foo and pass the parent view instance to the sub-view instances when they're created:
// In MapView
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e,
parent: this
});
}
// In EventView
initialize: function(options) {
_.bindAll(this, "render");
this.parent = options.parent; // Or use this.options.parent everywhere.
console.log(this.parent.foo);
}
Or better, add an accessor method to MapView:
_foo: 'bar',
foo: function() { return this._foo }
and use that method in EventView:
initialize: function(options) {
// ...
console.log(this.parent.foo());
}
Proper encapsulation and interfaces are a good idea even in JavaScript.

Just a guess, but could you try something like this in MapView:
addOneEvent: function(e) {
var that = this,
ev = new lr.EventView({
model: e,
parentView = that
});
}
And then access it like this:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(this.parentView.foo);
},
render: function() {
}
});

Related

backbonejs model.toJSON & render

I have some model and I want to bind render method to it on change. I'm trying to pass the model.toJSON to the render, but it doesn't work. However it works if I pass model and apply toJSON inside render.
(the whole code is here: http://plnkr.co/edit/xoeY4hexnqgHnkxap5uj?p=preview)
window.onload=function(){
var defaultModel = Backbone.Model.extend({
defaults: {
greeting: 'Hello, Dude',
content: 'Coming soon...'
}
}),
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.template = $('#tmpl-default').html();
this.render();
var _this = this;
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
$('[name="default-input"]').on('blur', function() {
console.log('got blurred....');
_this.model.set('content', this.value);
});
},
render: function(content) {
if (!content) {
console.log('%cno content', 'color: green');
content = this.model.toJSON();
}
this.$el.html(_.template(this.template)(content));
$('#content').html(this.$el);
return this;
}
}),
viewDefault = new defaultView({
model: new defaultModel()
});
};
the code above doesn't work. If I change
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
to
this.model.bind('change', _.bind(this.render, this, this.model));
and
if (!content) {
content = this.model.toJSON();
}
to
if (!content) {
content = this.model.toJSON();
}else{
content = content.toJSON();
}
But why?!
A more appropriate way is to use the listenTo function on the view, such as:
this.listenTo(this.model, "change", this.render);
I think the reason it doesn't work as you expect is because when you do
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
The argument this.model.toJSON() passed to render method will always be the initial state of the model at the point when _.bind was called.
When you do content = this.model.toJSON(); inside render method, you get the current state, including the expected changes that triggered render.
You can better structure your view like this:
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.render();
this.model.on('change', _.bind(this.render, this));
},
template: _.template($('#tmpl-default').html()),
events: {
'blur [name="default-input"]': 'eventHandler'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
$('#content').html(this.$el);
return this;
},
eventHandler: function(event) {
var val = $(event.currentTarget).val();
this.model.set('content', val);
}
});
Also, look into listenTo than on like #Jayem suggested to aviod unexpected memory leak issues

Calling destroy on collection

I am doing a sample application similar to the Backbone-Todo. But when I am invoking destroy on collection it's giving error:
Uncaught TypeError: Cannot read property 'destroy' of undefined
How can I solve this problem. Please suggest.
Following is my method code:
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {
title: "empty todo...",
order: Todos.nextOrder(),
done: false
};
}
});
var TodoList = Backbone.Collection.extend({
model : Todo,
localStorage: new Backbone.LocalStorage("todos-backbone"),
done: function() {
return this.where({done: true});
},
remaining: function() {
return this.without.apply(this, this.done());
},
nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: 'order'
});
var TodoView = Backbone.View.extend({
tagName: "li",
template: _.template($('#item-template').html()),
events: {
"click a.destroy" : "clear"
},
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function(){
this.model.destroy();
}
});
var AppView = Backbone.View.extend({
el: $("#todoapp"),
statsTemplate: _.template($('#stats-template').html()),
events: {
"keypress #new-todo": "createOnEnter",
"click #remove-all": "clearCompleted"
},
initialize: function() {
this.input = this.$("#new-todo");
this.main = $('#main');
this.footer = this.$('footer');
this.listenTo(Todos, 'add', this.addOne);
this.listenTo(Todos, 'all', this.render);
Todos.fetch();
},
render: function() {
var done = Todos.done().length;
var remaining = Todos.remaining().length;
if (Todos.length) {
this.main.show();
this.footer.show();
this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
} else {
this.main.hide();
this.footer.hide();
}
},
createOnEnter: function(e){
if(e.keyCode != 13) return;
if (!this.input.val()) return;
Todos.create({
title: this.input.val()
})
this.input.val('');
},
addOne: function(todo){
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
});
for this answer I assume Todos is an instance of TodoList. I also assume that your error is fired by this function in your AppView
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
In there you're trying to treat your Backbone.js Collection instance like what it is, a collection eg a list. But Backbone collections are not simply lists, they are objects that have the property models which is a list that contains all your models. So trying to use underscore's invoke (which works on lists) on an object is bound to cause errors.
But don't worry, Backbone neatly implements many Underscore methods for its Model and Collection, including invoke. This means you can invoke destroy for each model in a collection like this
SomeCollection.invoke('destroy');
Hope this helps!

View doesn't shows backbone models in template

i'm starting with Backbone and Laravel, and i have severals questions because i don`t find anything in Spanish (maybe i don't know how to search, therefore it's easier to ask).
Here are my Models:
window.mMateria = Backbone.Model.extend({
defaults: {
nombremateria: ""
},
});
window.cMaterias = Backbone.Collection.extend({
url: "materias",
model: mMateria,
initialize: function() {
this.fetch();
}
});
Here are my Views:
window.vMaterias = Backbone.View.extend({
tagName: 'ul',
model: cMaterias,
className:'list-materias',
initialize: function () {
_.bindAll(this, "render");
},
render: function(){
$(this.el).append("Renderizando!"); //It appears
_.each(this.model.models, function (aMater) {
console.log(aMater); //HERE IT DOESN'T ENTER, doesn't show anything
$(this.el).append(new vMateria({model:aMater}).render().el);
}, this);
return this;
},
el: $(".container-fluid")
});
window.vMateria = Backbone.View.extend({
initialize:function () {
_.bindAll(this, "render");
this.model.bind("change", this.render(), this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
className: "item-materia",
el: $(".container-fluid"),
template: _.template($('#pl_materia').val()),
});
Then initialize:
cmaterias = new cMaterias();
console.log(cmaterias); //it returns 41 signatures
vmaterias = new vMaterias({model: cmaterias});
console.log(vmaterias); //Shows child {cid: "view1", model: child, ...
vmaterias.render().el;
Please help me and excuse my English, I dont know if Laravel with the
return Response::eloquent(Materia::all()); is the problem. Be as especific as possible. Dios los bendiga.
Try these changes.
window.vMaterias = Backbone.View.extend({
tagName: 'ul',
className:'list-materias',
render: function(){
this.$el.empty();
this.$el.append("Renderizando!"); //It appears
this.collection.each(function (aMater) {
console.log(aMater); //HERE IT DOESN'T ENTER, doesn't show anything
this.$el.append(new vMateria({model:aMater}).render().el);
}, this);
return this;
},
el: $(".container-fluid")
});
cmaterias = new cMaterias();
console.log(cmaterias); //it returns 41 signatures
vmaterias = new vMaterias({collection: cmaterias});

Render not being called in Backbone view

Here is my Backbone:
App.Models.Count = Backbone.Model.extend({
url: this.url,
initialize: function() {
this.fetch({
success: function(data, response) {
this.count = data.get('count');
console.log(this.count); // 9, correct answer
}
});
}
});
App.Views.Count = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.count = this.options.count;
console.log(options); // returns correctly
this.model.on('reset', this.render, this);
},
render: function() {
console.log('test'); // not called
this.$el.html(this.model.toJSON());
return this;
}
});
And in my route:
var mc = new (App.Models.Count.extend({'url' : 'main-contact-count'}))();
var mcv = new (App.Views.Count.extend({ model: mc }))();
console.log(mcv); // 9, correct answer
$('#contactCount').html(mcv);
As you can see, my render method is never called. Also, it seems that my view is being called before my model, based on what I see console.log'd in Firebug. Is that because of the async? Why isn't render being called?
You're using Backbone in a funky way. Here's the more standard way to do this:
App.Models.Count = Backbone.Model.extend({
urlRoot: "main-contact-count"
});
App.Views.Count = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.model.on('change', this.render, this);
},
render: function() {
console.log('test');
this.$el.html(this.model.toJSON());
return this;
}
});
And in the router:
var mc = new App.Models.Count();
var mcv = new App.Views.Count({model: mc});
mc.fetch();
$('#contactCount').html(mcv.el);
EDIT
It turns out you're listening to "reset" on a Backbone model. This will never happen. Try listening on "change" instead of reset:
this.model.on('change', this.render, this);

How to pass a template to a view in Backbone

I'm trying to pass a template to my view. I have several different templates I want to use and want to be able to switch them up in my router. I get no errors, but I get no results. It looks like the initialize method isn't being called in my second view. Here is my code:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
var vent = _.extend({}, Backbone.Events);
_.templateSettings.interpolate = /\[\[(.+?)\]\]/g;
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'send-message' : 'sendMessage',
'*other' : 'other'
},
index: function() {
t = new (App.Collections.Tables.extend({ url: 'main-contact'}))();
tables = new (App.Views.Tables.extend({
collection: t, template: template('mainContactTemplate')}))();
$('#web-leads').html(tables.el);
},
sendMessage: function() {
t = new (App.Collections.Tables.extend({ url: 'send-message'}))();
tables = new App.Views.Tables.extend({
collection: t, template: template('sendMessageTemplate')});
$('#web-leads').html(tables.el);
},
other: function() {
}
});
// Main Contact
App.Models.Table = Backbone.Model.extend({});
App.Collections.Tables = Backbone.Collection.extend({
model: App.Models.Table,
initialize: function(models, options) {
this.fetch({
success: function(data) {
//console.log(data.models);
}
});
if (options) {
this.url = this.url || options.url;
}
}
});
App.Views.Tables = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('reset', this.render, this);
},
render: function() {
return this.collection.each(this.addOne, this);
},
addOne: function(model) {
var t = new App.Views.Table({ model: model, template: template});
this.$el.append(t.render().el);
return this;
}
});
App.Views.Table = Backbone.View.extend({
tagName: 'li',
template: this.template,
initialize: function (attrs) {
this.options = attrs;
console.log(this.options);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
new App.Router();
Backbone.history.start();
})();
EDIT: I was missing some parenthesis. But now I get an error of an unrecognized expression. Initialize is now being called.
The way you are doing it in App.Views.Table is (as far as I can tell) the "standard" way of using templates with Backbone. There are of course several alternatives though, and none of them are "wrong" per say.
That being said, you do have a couple problems in your code. Let's start with:
template: this.template,
At the time that code runs you're not in an instance of App.Views.Tables, you're in the global space declaring a class that (later) will be used to make instances. At that moment though, this just refers to window. What you really want to do is set the template in your initialize, which leads me to:
initialize: function(options) {
this.template = options.template;
},
But then there's one last problem:
var t = new App.Views.Table({ model: model, template: template});
there is no template variable in that function, so you're really doing template: undefined. That should use a real template.
All that being said, you might want to just consider putting the template on the view directly, the way you sort of tried to:
template: Handlebars.compile('<span>{{test}}</span>'),
After all, any given view should always use the same template, right? Also, you might want to consider moving the:
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
in to a parent class, so that you can share it between all of your templated views, instead of having to repeat it.

Resources