In my view class initialize function, _.bind(this.appendSection, this) does not work, but _.bindAll(this, 'appendSection') works. I am very confused...
Here is the code:
TemplateBuilder.Views.TemplateView = Backbone.View.extend({
el: $('div#evalTemplate'),
initialize: function(){
this.collection.on('reset', this.render, this);
//_.bind(this.appendSection, this);
_.bindAll(this, 'appendSection');
},
events: {
'click button#addSection': 'addSection'
},
render: function(){
this.collection.each(this.appendSection);
return this;
},
appendSection: function(section){
var view = new TemplateBuilder.Views.InstructionView({model: section});
this.$el.append(view.render().el);
},
addSection: function(){
var newSection = new TemplateBuilder.Models.Section();
this.collection.add(newSection);
this.appendSection(newSection);
},
});
From the fine manual:
bind _.bind(function, object, [*arguments])
Bind a function to an object, meaning that whenever the function is called, the value of this will be the object. [...]
var func = function(greeting){ return greeting + ': ' + this.name };
func = _.bind(func, {name : 'moe'}, 'hi');
func();
=> 'hi: moe'
Unfortunately the manual isn't quite so fine and you have to see what's implied by the example code:
func = _.bind(func, ...);
_.bind returns the bound function, it doesn't modify it in-place. You'd have to say this:
this.appendSection = _.bind(this.appendSection, this);
if you wanted to use _.bind. _.bindAll on the other hand, binds methods in place. There is more discussion on these methods over here.
Related
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
I am in the process of learning backbone / underscore, and I am finding that the more I break away from the really basic stuff in the tutorials, the more come to realize that the tutorials aren't teaching me much of anything.
My current problem is with passing a variable to a view. I have three different templates available, but they all render the same, so I was hoping to just pass which template to use into the view when it was being rendered from the collection. What I thought would work would be just adding a property to the call to the view, and then accessing it with this.options.Property, but this is throwing an error that the property is undefined.
I have tried a number of variant options, but nothing seems to work. What am I doing wrong?
Thanks is advance.
var ProjectListView = Backbone.View.extend({
el: '#projectList',
initialize: function() {
this.collection = masterProjectList;
this.render();
},
render: function() {
this.$el.html("");
this.collection.each(function(project) {
this.renderItem(project);
}, this);
},
renderItem: function(project) {
var projectView = new ProjectView({model: project, projectType: '#theatricalProjectTemplate' });
// Passing in the project type, which determines which template gets used
this.$el.append(projectView.render().el);
}
});
var ProjectView = Backbone.View.extend({
tagName: "div",
className: "project-wrap",
template: _.template($(this.options.projectType).html()),
// use this.options to access the value
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
When you're defining your ProjectView:
var ProjectView = Backbone.View.extend({
//...
template: _.template($(this.options.projectType).html()),
//...
});
you're executing some code (i.e. calling extend) and in this context, this will be the global object (AKA window in a browser) and that probably won't have an options property. If you want to use the projectType option that you pass to the ProjectView constructor, move the template assignment into initialize:
var ProjectView = Backbone.View.extend({
tagName: "div",
className: "project-wrap",
initialize: function() {
this.template = _.template($(this.options.projectType).html());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
This assumes that projectType will be a valid jQuery selector, you might want to use '#' + this.options.projectType instead but I'm not sure what exactly will be in projectType.
mu is too short is correct and if you define template method as follows, you can share the template method with all instances of ProjectView:
var ProjectView = Backbone.View.extend({
tagName: "div",
className: "project-wrap",
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
template: function() {
return _.template($(this.options.projectType).html());
}
});
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);
I have a Backbone collection and when I add a new model to it the "add" event doesn't seem to work as I'd expect. I've bound 2 views to listen for add events on the collection, but only one seems to get notified of the event, and when this happens, no PUT request is sent to my server. When I remove the second bind, the other one works and the PUT request is sent. Here's the code snippets:
var FlagList = Backbone.Collection.extend({
model: Flag // model not shown here... let me know if it would help to see
});
var FlagCollectionView = Backbone.View.extend({
el: $('ul.#flags'),
initialize: function() {
flags.bind('add', this.addFlag, this); // this one doesn't fire!!
},
addFlag: function(flag) {
alert("got it 1"); // I never see this popup
}
});
var AddFlagView = Backbone.View.extend({
el: $("#addFlagPopup"),
events: {
"click #addFlag": "addFlag"
},
initialize: function() {
flags.bind('add', this.closePopup, this); // this one fires!!
}
addFlag: function() {
flags.create(new Flag);
},
closePopup: function() {
alert("got it 2"); // I see this popup
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
A few suggestions:
ID's vs Classes
you've over qualified your selector by combining a class and an id. jQuery allows this, but the ID selector should be unique on the page anyway so change el: $('ul.#flags') to el: $('ul#flags').
Leveraging Backbone
I like to explicitly pass my collections and/or models to my views and use the magic collection and model attributes on views.
var flags = new FlagList;
var addFlagView = new AddFlagView({collection: flags});
var flagCollectionView = new FlagCollectionView({collection: flags});
which now means that in your view, you will automagically have access to this.collection
unbinding events to avoid ghost views
var FlagCollectionView = Backbone.View.extend(
{
initialize: function (options)
{
this.collection.bind('add', this.addFlag, this);
},
addFlag: function (flag)
{
alert("got it 1");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.addFlag);
}
});
var AddFlagView = Backbone.View.extend(
{
initialize: function ()
{
this.collection.bind('add', this.closePopup, this);
},
closePopup: function ()
{
alert("got it 2");
},
destroyMethod: function()
{
// you need some logic to call this function, this is not a default Backbone implementation
this.collection.unbind('add', this.closePopup);
}
});
It looks like I have to agree with #fguillen, that your problem must be somewhere in how you initialize the view, as in my comment I mention that it's most likely related to timing, ie: binding your event to the collection after the 'add' event has already fired.
This code works for me:
var FlagList = Backbone.Collection.extend({});
var FlagCollectionView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.addFlag, this);
},
addFlag: function(flag) {
alert("got it 1");
}
});
var AddFlagView = Backbone.View.extend({
initialize: function() {
flags.bind('add', this.closePopup, this);
},
closePopup: function() {
alert("got it 2");
}
});
var flags = new FlagList;
var addFlagView = new AddFlagView;
var flagCollectionView = new FlagCollectionView;
flags.add({key:"value"});
check the jsFiddle
Your problem is somewhere else.
If you ended up here after making the same stupid mistake I did, make sure you've got:
this.collection.bind( 'add', this.render )
and NOT:
this.collection.bind( 'add', this.render() )
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() {
}
});