Passing context with bind in backbone.js - javascript

I want my panels to re-render themselves when they are clicked.
However, when I perform a click I get the following:
Uncaught TypeError: Cannot call method 'get' of undefined
It appears that the "this" that I'm logging is in fact the model itself:
_callbacks: Object
_changed: true
_escapedAttributes: Object
_previousAttributes: Object
attributes: Object
cid: "c0"
collection: r.d
id: "f5589ba4-a0aa-dd86-9697-30e532e0f975"
__proto__: n
I'm having trouble figuring out why the appropriate "this" isn't preserved by passing my context into model.bind.
Here's my code:
// Models
window.Panel = Backbone.Model.extend({
defaults: function(){
return {
flipped: false,
};
},
toggle: function(){
this.save({flipped: !this.get("flipped")});
},
});
// Collections
window.PanelList = Backbone.Collection.extend({
model:Panel,
localStorage: new Store("panels"),
flipped: function(){
return this.filter(function(panel){ return panel.get("flipped"); })
}
});
// Global collection of Panels
window.Panels = new PanelList;
// Panel View
window.PanelView = Backbone.View.extend({
tagName: 'div',
template: _.template($("#panel-template").html()),
events: {
"click" : "toggle"
},
initialize: function(){
this.model.bind("change", this.render, this)
$(this.el).html(this.template(this.model.toJSON()));
},
render: function(){
console.log(this);
var flipped = this.model.get("flipped")
if (flipped){
$(this.el).addClass("flip");
} else{
$(this.el).removeClass("flip");
}
return this
},
toggle: function(){
this.model.toggle();
}
});

The backbone-y way of doing this is to use the _.bindAll(...) function provided by underscore:
initialize: function(){
_.bindAll(this, "render");
this.model.bind("change", this.render)
$(this.el).html(this.template(this.model.toJSON()));
}
What _.bindAll does is documented here, but it is essentially built exactly for this purpose. If you want to have this properly set in all functions of the object, you can call _.bindAll(this) with no list of functions. I tend to have this global bind function in most of my views.

I ran into the same issue and ended up using underscore.js's _.bind() method instead. I queried SO for a response, and it was the reply I got.
Try changing: this.model.bind("change", this.render, this)
To: this.model.bind("change", _.bind(this.render, this))

Related

Multiple times rendered collection in Backbone app

In my ongoing self thought process by building my simple blog app I am finding solutions to problems and encountering new ones.
Now successfully routing to a second view from a first one, and page is populated by the new views html.
Successfully save to the db new posts from second view, which is a form to add new posts.
First problem is:
In the first view I have the posts rendered five times, in order. There is not any js console messages. I have saved those posts each only one time from the second view, which is my postformview for saving posts.
Second problem is: From second view to the first view when navigated with the browser back button no posts rendered into page only the headers etc in one of the templates of this page is rendered.
What can be the issue here which I miss?
first view:
var postsListView = Backbone.View.extend({
collection: new postsCollection(),//! The Collection may be created to use in view. with new Coolectionname(). SOLVED it must be created, this attr is not suffcent and is not crating it.
template1: _.template( $('#postsListTemplate').html() ),//!!!Once forgot .html().
template2: _.template( $('#postsListItemTemplate').html() ),
initialize: function(){
this.collection.fetch();
this.collection.on('add', this.renderPostsListItem, this);
},
render: function(){
this.$el.html( this.template1() );//!this.el or this.$el. (each) or (each.toJSON()). SOLVED: use this.$el alongside el: a string, without $().
return this;
//* return this in every views render: if you want to chain el to render() of the view, for example in router while pcaing the rendered views el into DOM.
},
renderPostsListItem: function(){
console.log("view method renderPostsListItem have been reached.");
this.ul = 'ul';
this.collection.forEach(function(each){
$(this.ul).append( this.template2( each.attributes ) );
}, this);
return this;
},
events: {
"click a": 'toPostFormRoute'
},
toPostFormRoute: function(e){
console.log("view method toPostFormRoute have been reached.");
e.preventDefault();
Backbone.history.navigate( '/posts/postform' , {trigger: true});
console.log("view method toPostFormRoute have been reached.");
}
});
router:
//Define Client-Side Routes
var appRouter = Backbone.Router.extend({
el: 'body',
routes: {
'posts/postform': 'viewPostForm',
'': 'viewPosts'
},
viewPosts: function(){
console.log("router method viewPosts have been reached.");
this.postslistview = new postsListView();
$(this.el).html( this.postslistview.render().el );
},
viewPostForm: function(){
console.log("router method viewPostForm have been reached.");
this.postformview = new postFormView();
$(this.el).html( this.postformview.render().el );
}
});
UPDATE: Variation. adding each model when an add event fired y passing the model added to the method and rendering template only with it, appending only it. not iterating through collection them all.
This solves first issue but not the second issue. What can be the specific issue for this?
code fragment from the first view:
initialize: function(){
this.collection.fetch();
this.collection.on('add', this.renderPostsListItem, this);
},
renderPostsListItem: function(model){
console.log("view method renderPostsListItem have been reached.");
this.$el.find('ul').append( this.template2(model.toJSON()) );
return this;
},
Issue :
When a new item/model is added to the collection, all the items present in the collection are rendered/appended to the view's EL instead of only the newly added.
Root Cause :
renderPostsListItem#3
Solution
renderPostsListItem: function(model){
console.log("view method renderPostsListItem have been reached.");
this.collection.forEach(function(each){
this.$el.find('ul').append( this.template2(model.toJSON()) );
}, this);
return this;
},
http://backbonejs.org/#Collection-add

Giving a single reference to multiple Backbone.Models

I have a Backbone.Model which looks something like:
var FooModel = Backbone.Model.extend({
defaults: {
details: '',
operatingSystem: ''
};
});
There are many instances of FooModel which are stored in a collection:
var FooCollection = Backbone.Collection.extend({
model: FooModel
});
FooModel's OperatingSystem is a property which only needs to be calculated once and is derived asynchronously. For example:
chrome.runtime.getPlatformInfo(function(platformInfo){
console.log("Operating System: ", platformInfo.os);
});
If I perform this logic at the FooModel level then I will need to perform the logic every time I instantiate a FooModel. So, I think that this operation should be performed at a higher level. However, it is bad practice to give properties to a Backbone.Collection.
As such, this leaves me thinking that I need a parent model:
var FooParentModel = Backbone.Model.extend({
defaults: {
platformInfo: '',
fooCollection: new FooCollection()
},
initialize: function() {
chrome.runtime.getPlatformInfo(function(platformInfo){
this.set('platformInfo', platformInfo);
}.bind(this));
},
// TODO: This will work incorrectly if ran before getPlatformInfo's callback
createFoo: function(){
this.get('fooCollection').create({
details: 'hello, world',
operatingSystem: this.get('platformDetails').os
});
}
});
This works and is semantically correct, but feels over-engineered. The extra layer of abstraction feels unwarranted.
Is this the appropriate way to go about giving a property to a model?
Although Backbone Collections may not have attributes, they may have properties (as well as any object) which you can use to store shared data.
var FooCollection = Backbone.Collection.extend({
model: FooModel
initialize: function() {
this.platformInfo = null; // shared data
chrome.runtime.getPlatformInfo(function(platformInfo){
this.platformInfo = platformInfo;
}.bind(this));
},
// wrapper to create a new model within the collection
createFoo: function(details) {
this.create({
details: details,
operatingSystem: this.platformInfo? this.platformInfo.os : ''
});
}});
});

Backbone app: Can't get model data in View

I'm building my first, relatively simple Backbone app. Right now, it's fetching JSON of local weather information:
Weather = Backbone.Model.extend({
url: '/api/v1/weather',
initialize: function(){
this.fetch({
success: this.fetchSuccess,
error: this.fetchError
});
},
parse: function(response)
{
return response;
},
fetchSuccess: function (model, response) {
console.log('FETCH SUCCESS:', response);
},
fetchError: function (model, response) {
throw new Error("FETCH ERROR");
}
});
The above seems to work just fine, as the fetchSuccess console log returns the JSON response as expected.
The problem happens when I attempt to access this data from the view. Here's my code for that:
WeatherView = Backbone.View.extend({
el: $('#widget__weather'),
initialize: function()
{
_.bindAll(this, 'render');
this.model = new Weather();
this.model.bind('reset', this.render);
this.render();
},
render: function()
{
console.log(this.model.toJSON());
// var template = _.template(weatherTemplate, { weather : this.model });
// this.$el.html(template);
}
});
The console log for the view is an empty Object { }. My attempts to use this.model.get('timezone') result in undefined.
When I console.log(this.model) I get this:
s {cid: "c3", attributes: Object, _changing: false, _previousAttributes: Object, changed: Object…}
_changing: false
_events: Object
_pending: false
_previousAttributes: Object
attributes: Object
currently: Object
daily: Object
flags: Object
headers: Array[13]
hourly: Object
minutely: Object
offset: -4
timezone: "America/New_York"
__proto__: Object
changed: Object
cid: "c3"
__proto__: n
It seems that my JSON data is in the 'attributes' object of the model, but I don't know why it's there or how to access it.
To be clear, when I do console.log(this.model.toJSON()); I get an empty Object { }.
I feel like I'm missing something obvious here and could use any and all help. What am I doing wrong? Is it possible that the structure of the returned JSON data may be causing this? It's a pretty standard response from Forecast.io API.
Let me know if you need any more code / information.
EDIT:
With some help I fixed the issue. Turns out this was the culprit: this.model.bind('reset', this.render);. Changing that to this.model.bind('change', this.render); fixed the problem.
yes is correct.
If you write this:
console.log(this.model);
Uou get the instance of the model, every property that you have parsed are inside attributes.
Instead if you use
console.log(this.model.toJSON());
you transform the instance of the model to a json and you can get directly your property without passing by attributes
Then if you wanna print your data inside your template you need to do this:
if you use:
var template = _.template(weatherTemplate, { weather : this.model });
inside template you need to print your var in this way:
weather.attributes.timezone;
Instead if you use
var template = _.template(weatherTemplate, { weather : this.model.toJSON() });
inside template you need to print your var in this way:
weather.timezone;

TypeError trying to set model object in Backbone.js

I've been playing around with the backbone.js + cordova + require.js frameworks, based mainly off of Cristophe Coenraets' PhoneGap examples on GitHub. Displaying my model in a view seems to be straightforward, but I'm still unable to update the model via calls to set or save.
My model looks something like this:
SourcePhrase = Backbone.Model.extend({
// default values
defaults: {
id: null,
markers: "",
orig: null,
source: "",
target: ""
},
sync: function (method, model, options) {
if (method === "read") {
findById(this.id).done(function (data) {
options.success(data);
});
}
}
}),
// etc
I can pull objects out of my collection by making a call to get:
// find and update the model object
var strID = $(event.currentTarget.parentElement).attr('id');
var model = this.collection.get(strID);
So far, so good:
model.set('target', trimmedValue);
TypeError: 'undefined' is not a function (evaluating '(i=t[r]).callback.call(i.ctx,n,a)')
Hmm...that's not right. Any idea where I need to start looking to track this down?
EDIT: console output for model just before the call to set:
model: Object
_changing: false
_events: Object
_pending: false
_previousAttributes: Object
attributes: Object
id: "RUT001-10"
markers: "\hdr"
orig: null
source: "Ruth"
target: "Ruth"
__proto__: Object
changed: Object
cid: "c15"
collection: Object
id: "RUT001-10"
__proto__: Object
strID: "RUT001-10"
Yes it's the right method to use, and you can even change your code like this :
this.model.bind('change', this.render, this);
and it will work.
Ok, I think I might have tracked it down, maybe? I had in my View for a single item:
initialize: function () {
this.model.bind('change', this.render());
this.render();
},
The bind() call was causing the TypeError, which means I might have been running into a "this" issue? (backbone.js and binding "this".) At any rate, I've replaced the block with this one:
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
It seems to do the trick. If someone with more backbone.js expertise could comment on this approach, I'd very much appreciate it. Am I doing this correctly?

Backbone Collection refresh all items

I'm having problems refreshing collection or more precisely collection view after updating all models on the server. Here's my scenario:
I have a collection of questions fetched from the server. Each question has a position attribute so I can manipulate the order in the list and save it back to the server with appropriate order.
I have a view for each single list item and a view with a more global scope that generates each list items and updates the collection. Basically I was using an example from O'Reilly book "Javascript Web Applications" which resembles a lot to the famous Todo annotated tutorial found here: http://documentcloud.github.com/backbone/docs/todos.html
So the structure is almost identical apart from a few custom models. Everythings works fine.
However, I'm having problems updating the collection with I reorder items in the
I've a method in my global view which fires evert time I drag list items in the list. Btw it works well and updates the order of the items on the server, but I also want to be able to update the digit in from of each item in the list.
window.QuestionView = Backbone.View.extend({
el: $("#content"),
events : {
'sortupdate ol#questions': 'sortStuff'
},
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'render', 'addNewItem', 'addItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('all', this.render);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
},
render: function() {},
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
}
});
},
addItem: function() {
this.collection.add({title: 'New title goes here'});
return false;
},
addNewItem: function(question) {
var view = new ItemView({model: question, parent: this});
var element = view.render().el;
this.$("#questions").prepend(element);
$that = this;
$(view.render().el).addClass('new');
},
addOne: function(question) {
var view = new ItemView({model: question, parent: this});
this.$("#questions").prepend(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
return false;
}
});
So my question is.. what do I do on success: to be able to refresh each little model separately so it updates the digit to the proper order? Maybe some sort of _.each on Collection? Or maybe some sort of global view refresh on the whole collection?
Also my
success: function(data)
returns the new order as a list (or JSON object) from the server. maybe I can reuse this order to set each model with a new value without making unnecessary fetch() call on the server each time the order is changed?
EDIT:
I finally managed to get it to work with a reset, clearing the view and re-fetching a new collection. Perhaps it isn't the best way to do it since there's additional call to the server with a fetch()..
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
$this.rerender();
}
});
},
rerender: function() {
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
$("#questions").html("");
this.collection.reset();
this.addAll();
},
I think your approach should be in two separate steps:
1) On one hand you update the data on the server
2) On the other hand you update the collection client-side
So, you are Ok on step 1, you said it works.
For step 2, you can take advantage of the event driven programming.
The logic is this one:
YOU JUST ADD ONE ELEMENT TO THE COLLECTION (collection.add(model) fires an 'add' event).
In the collection, you listen for the 'add' event. When you catch it, you sort your collection again (collection.sort fires a 'reset' event)
In your view for the list (questionView in your case) you listen for the collection reset event, and once it is fired you re-render your view
Example code:
1) QuestionView: addItem removed and addNewItem simplified (it must no render)
window.QuestionView = Backbone.View.extend({
el: $("#content"),
events : {
'sortupdate ol#questions': 'sortStuff'
},
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
},
render: function() {},
sortStuff: function() {
$this = this;
$.ajax({
url: "/hq/reorder/",
type: "POST",
data: $("#questions").sortable("serialize")+"&id="+$qid,
dataType: 'json',
success: function(data) {
}
});
},
//METHOD addItem REMOVED!!!
addNewItem: function(question) {
this.collection.add({title: 'New title goes here'}); //***IT FIRES AN ADD EVENT
},
addOne: function(question) {
var view = new ItemView({model: question, parent: this});
this.$("#questions").prepend(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
return false;
}
});
2) the collection catch the add event and sorts (trigger 'reset' event)
you can handle it always in the QuestionView, your initialize function becomes.
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
//ADD THIS*****
this.collection.on('add', function(){
this.collection.sort();
});
},
3) the third step is already done, you just re-render the view
The best would be that you sort elements in your collection defining a new 'comparator' function, which uses the 'position' attribute of your list
something like (in QuestionView)
this.collection.comparator: function(){
return this.collection.get("position");
}
so that items get ordered by position CLIENT SIDE
**EDIT**
Initialize function modified. Fetch is used instead of 'sort', which is unuseful as long as the 'position' attribute of each element in the collection is not updated.
initialize: function(collection) {
this.collection = new QuestionsList;
_.bindAll(this, 'addOne', 'addAll', 'addNewItem');
this.collection.bind('add', this.addNewItem);
this.collection.bind('reset', this.addAll);
this.collection.fetch({
data: { quiz_id: $qid },
processData:true
});
//ADD THIS*****
this.collection.on('add', function(){
this.collection.fetch();
});
You should do Questions.reset(data);, however you need to tell ajax that the response is json:
sortStuff: function() {
$.ajax({
url: '/order',
data: $("ol#questions").sortable('serialize'),
dataType: 'json',
success: function(data) {
// now data is an object
Questions.reset(data);
});
});
}
I hope you have learned that backbone is event driven, and that you have an event for collection reset bound to the render method, so there's no need to explicitly call render here.
EDIT:
Ok, now with the code I see what you're trying to accomplish, and your logic is flawed. You shouldn't wait for the ajax call to return the new order. It's better if you update the order on the client side, and then save the model.
Here's a jsfiddle sample: http://jsfiddle.net/b75px/
Combine that with backbone, and you should have:
Note that I'm just guessing how you have organized the questions. If you have any other problems, update your question with more details.
events: {
'sortstart ol#questions': 'startSortingQuestions',
'sortupdate ol#questions': 'updateQuestions'
},
startSortingQuestions: function(event, ui) {
this.beforeIndex = ui.item.index();
},
updateQuestions: function(event, ui) {
var before = this.beforeIndex,
after = ui.item.index();
// now that you have the index change, all you have to do is set the new index
// and save them to the database
this.collection.at(before).set({ index: after; }).save();
this.collection.at(after).set({ index: before; }).save();
// passing silent: true because sort invokes the reset event,
// and we don't need to redraw anything
this.collection.sort({ silent: true });
/* or maybe an even better approach:
var beforeModel = this.collection.at(before);
var toSwapModel = this.collection.at(after);
this.collection.remove(beforeModel);
this.collection.remove(toSwapModel);
this.collection.add(beforeModel, { at: after, silent: true });
this.collection.add(toSwapModel, { at: before, silent: true });
note that I have no idea on how your database is structured, so my guess is
every question has it's index so you know it's order
therefore, you should still update the model's index field or whatever it is you're doing
*/
}

Categories

Resources