I'm trying to use Backbone with REST API:
Here the code
My model:
var PagesModel = Backbone.Model.extend({
idAttribute: 'Guid',
initialize: function () {
this.on('remove', this.destroy);
},
urlRoot: '/api/pages'
});
Collection:
var PagesCollection = Backbone.Collection.extend({
model: PagesModel,
url: '/api/pages'
});
View:
var PagesView = Backbone.View.extend({
el: '#pages',
events: {
'click .removePage': 'remove',
},
initialize: function (collection) {
this.collection = collection;
this.collection.fetch();
this.template = $('#pages-template').html();
this.collection.bind('change reset', this.render, this);
},
render: function () {
var template = _.template(this.template);
$(this.el).html(template({ pages: this.collection.toJSON() }));
return this;
},
remove: function (e) {
e.preventDefault();
var id = $(e.currentTarget).closest('ul').data("id");
var item = this.collection.get(id);
this.collection.remove(item);
$(e.currentTarget).closest('ul').fadeOut(300, function () {
$(this).remove();
});
}
});
And here I'm starting up application:
$(function () {
var pagesCollection = new PagesCollection();
var pagesView = new PagesView(pagesCollection);
});
I'm clicking or Remove and in Network inspector see this link
http://localhost:54286/backbone/function%20()%20%7B%20%20%20%20%20%20var%20base%20=%20getValue(this,%20'urlRoot')%20%7C%7C%20getValue(this.collection,%20'url')%20%7C%7C%20urlError();%20%20%20%20%20%20if%20(this.isNew())%20return%20base;%20%20%20%20%20%20return%20base%20+%20(base.charAt(base.length%20-%201)%20==%20'/'%20?%20''%20:%20'/')%20+%20encodeURIComponent(this.id);%20%20%20%20}
instead of /api/pages/{guid}.
What I'm doing wrong?
I still haven't figured fully why, but you can make it work by destroying your model after the end of its removal (Backbone does one last thing after triggering the remove event: destroy the collection's reference in the model).
But what's even better, is using directly the destroy function on the model, it will remove it from the collection automatically (use {wait: true} if needed).
Edit:
Finally managed to locate the source of the problem. It's rather simple in fact. To override the model's url (calculated with urlRoot but that doesn't matter), you can pass Model#destroy a url option when calling Backbone.sync (or something that'll call it).
Now you're thinking "but I don't!". But you do. The listener (Model#destroy in your case) is given 3 arguments. Model#destroy will take the first one (the model itself) as options.
And here's the fail (I think Backbone needs a patch to this): giving an url option to Backbone.sync is the only time _.result in not used to calculate the url. So you find yourself having as url the url property of your model, which is the function you see in your call.
Now, for a quickfix:
this.on('remove', this.destroy.bind(this, {}));
This will ensure the first argument of your Model#destroy call is {} (as well as binding the context).
Bear with me a little longer.
Now, if you're still willing to call Collection#remove before destroying your model, here's a little hack: because (as I stated above) the remove event is triggered before Backbone makes sure to remove the collection's reference in your model, you don't need the urlRoot property in your model. Indeed, the model won't be in the collection anymore, but Backbone will still take the collection's url into account to get the model's url (as the reference is still there).
Not a definitive answer, but just going by the code in your question and the backbone.js documentation, the problem may be that you named your method remove and this is getting in the way of the remove method in Backbone.View.
http://backbonejs.org/#View-remove
Update:
It also looks like the output you see in the network inspector is that the definition of the Backbone.Model.url function is being appended. Meaning url is not being properly called (Maybe the () is missing by the caller?). Are you overriding Backbone.sync anywhere in your application?
Related
RoomsDGView = Backbone.View.extend({
collection: roomcollection,
initialize: function(){
var template = _.template( $("#search_template").html(), {} );
this.$el.html( template );
this.collection.bind('add', this.modeladded);
this.collection.bind('remove', this.modelremoved);
this.collection.bind('change', this.collectionchanged);
console.log(this);
this.render();
},
render: function(){
// Compile the template using underscore
console.log("running the render function...");
//renderrender();
/*$("#roomsList").jqGrid('clearGridData');
roomcollection.each(function (room,i){
var temp = jQuery.parseJSON(JSON.stringify(room));
$("#roomsList").jqGrid('addRowData', i,{idrooms: temp["idrooms"], roomname: temp["roomname"],
occupants: temp["occupants"]});
});*/
},
events: {
"click input[type=button]": "doSearch"
},
doSearch: function(){
// Button clicked
console.log(this);
},
modeladded: function() {
console.log("room added...");
$("#roomsList").jqGrid('clearGridData');
//My intent is to call the views render function
//her. I tried using this.render() and also
//this.collection.bind('add', this.modeladded(this));
//modeladded: function(view) {
// view.render();
console.log(this);
},
modelremoved: function() {
console.log("room removed...");
$("#roomsList").jqGrid('clearGridData');
},
collectionchanged: function() {
console.log("room changed...");
$("#roomsList").jqGrid('clearGridData');
}
});
I have tried many different ways to call the views render: method from inside the code for modeladded:. Use of this.render inside model added shows that the this object at that point has no render function. I also tried passing the view in something like:
this.collection.bind('add', this.modeladded(this));
modeladded: function(view) {
view.render();
which also leads to a console error that no render() can be found. Does anyone know how to call the views render: from inside modeladded?
For the time being I moved the render function out of the views render: and into a JavaScript function declared renderGlobal() declared in global scope and I know it does work that way but I don't think that is really the backbone.js way.
This is the error that is coming out of the console:
Uncaught TypeError: Object [object Object] has no method 'render'
Thank you for posting....
You're binding your event handler using bind (AKA on):
this.collection.bind('add', this.modeladded);
But, as usual with JavaScript, the value of this inside a function depends on how the function is called, not how it is defined (ignoring bound functions of course). You're not specifying a specific this for your function anywhere so you're not getting any particular this when it is called. If you give bind the third context argument:
this.collection.bind('add', this.modeladded, this);
// ------------------------------------------^^^^
then Backbone will call modeladded with the specific this and you'll find that this inside modeladded will be your view.
You could also use _.bind, Function.prototype.bind, or $.proxy to produce a bound version of your callback:
this.collection.bind('add', _(this.modeladded).bind(this));
this.collection.bind('add', this.modeladded.bind(this));
this.collection.bind('add', $.proxy(this.modeladded, this));
All of those produce new functions so you won't be able to unbind them without stashing the bound functions somewhere. You'll usually avoid using these when you have the option to specify the context (AKA this) explicitly.
There's also listenTo:
listenTo object.listenTo(other, event, callback)
Tell an object to listen to a particular event on an other object. The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.
So you can (and should) say this:
this.listenTo(this.collection, 'add', this.modeladded);
That will take care of giving you the desired this and makes it easier to clean up your event handlers when you're done with them. Similarly for the other event handlers you're using.
I want to pass an extra variable (the userid) with before rendering my backbone view. I am getting the the extra variable with a ajax request but because is asynchronous I think my page is being rendered before I get the variable. For simplicity lets say I have this in my backbone view :
PostsApp.Views.Post = Backbone.View.extend({
template: _.template($('#post-template').html()),
render: function(){
var newObject = this.model.toJSON();
$.ajax({
url:"/user",
success:function(result){
newObject.twittername = result.name; ;
}
});
this.$el.html(this.template(newObject));
}
});
I guess I can put this.$el.html(this.template(newObject)); in the callback but than 'this'refers to something else.. Can anyone think of a work around for this?
Or is it completely very bad to send such a request in the render function..
You are correct in your assumption. It will not render correctly.
The simple fix
Just like the more general case, you can perform the actual rendering inside the success callback.
render: function(){
var newObject = this.model.toJSON();
var that = this; // to fix that `this` refers to in the callback
$.ajax({
url:"/user",
success:function(result){
newObject.twittername = result.name; ;
that.$el.html(that.template(newObject));
}
});
}
The better fix
What I'd do is:
Have the twitter name as a part of the model
fetch it from the model (maybe even with a restful .fetch
listen to change events in the view, and call render on such events.
This is because the view should not be responsible for changing model data in Backbone. It's mixing "business logic" with presentation and it can get ugly pretty fast.
[I think this example 2 from Addy Osmani's "Backbone Fundamentals" should give you a general idea on how this sort of structure is laid out.
There's a addPost function in my router. I don't want to re-create the postAddView every time the function is invoked:
addPost: function () {
var that = this;
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', function () {
that.navigate('#/post/list', { trigger: true });
});
}
this.elms['page-content'].html(this.postAddView.render().el);
}
Here's the PostAddView:
PostAddView = backbone.View.extend({
events: {
'click #post-add-back': 'back'
}
, back: function (e) {
e.preventDefault();
this.trigger('back');
}
});
The first time the postAddView is rendered, the event trigger works well. However, after rendering other views to page-content and render postAddView back, the event trigger won't be trigger anymore. The following version of addPost works well, though.
addPost: function () {
var that = this, view;
view = new PostAddView({
model: new Post()
});
this.elms['page-content'].html(view.render().el);
view.on('back', function () {
delete view;
that.navigate('#/post/list', { trigger: true });
});
}
Somewhere you are calling jQuery's remove and that
In addition to the elements themselves, all bound events and jQuery data associated with the elements are removed.
so the delegate call that Backbone uses to bind events to your postAddView.el will be lost. Then, when you re-add your postAddView.el, there are is no delegate attached anymore and no events are triggered. Note that Backbone.View's standard remove method calls jQuery's remove; a few other things in jQuery, just as empty will do similar things to event handlers. So the actual function call that is killing your delegate could be hidden deep inside something else.
You could try calling delegateEvents manually:
this.elms['page-content'].html(this.postAddView.render().el);
this.postAddView.delegateEvents();
or better, just throw the view away and create a new one every time you need it. Your view objects should be pretty light weight so creating new ones should be cheap and a lot less hassle than trying to keep track of the existing views by hand.
If you really want to reuse the current DOM and View you do not need to set again and again the element as you are doing, everything that you call .html() you are destroying the DOM of the View and generating again and losing events. Also I prefer always to add the "el" in the DOM before render the View. I will have your function in this way:
addPost: function () {
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', this.onBack);
this.elms['page-content'].html(this.postAddView.el);
}
this.postAddView.render();
},
onBack : function () {
this.navigate('#/post/list', { trigger: true });
}
I'm not fan of the use of local variables to refer to "this". If all of your Views uses _.bindAll(this) in the initialize method you could bind your events to your view and could use this(check how I transformed onBack).
With my code there is not a need to manually call this.delegateEvents()
I have a multiple files uploadify setting with:
'onComplete' : function(event, ID, fileObj, response, data) {
myCollection.add({params parsed from response json});
}
which triggers (trough this.collection.bind('add', this.add)) this collection view method:
add: function(obj) {
var view = new MyModelView({model: obj});
this.$('.insert-models-here').append(view.render().el);
return this;
},
The new MyModelView call triggers: MyModelView::initialize() which is here:
initialize: function() {
var t = $('#photo-template').html();
this.template = _.template(t);
this.model.view = this;
},
And every _.template() calls jumps inside __flash__toXML() method from which all thread is stopped.
The result is no model added inside my collection from any uploadify event.
Does anyone knows why and how to avoid this?
Ok, I found solution.
Problem was in using underscore in uploadify events so I replace underscore _.templates with icanhaz and rewrite my add() collection view method this way to workaround any underscore functionality:
add: function(obj) {
var view = new MyModelView({model: obj});
$('.insert-models-here').first().append(view.render().el);
return this;
},
Hope someone will call my name in future..
I'm fiddling with a view and related model that look like that:
App.Views.Addresses = App.Views.Addresses || {};
App.Views.Addresses.Address = Backbone.View.extend({
events: {
"click button#foo" : "clear"
},
initialize: function(model){
this.address = model.model;
this.address.view = this;
_.extend(this, Backbone.Events);
this.render();
},
render: function(){
... rendering stuff
},
clear: function(){
this.address.clear();
}
});
and
var Address = Backbone.Model.extend({
url: function() {
... url stuff
},
clear: function(){
this.destroy();
this.view.remove();
}
});
I'm facing two problems here. The first one:
I have a button with id="foo" in my source and would like the view to catch the 'click' event of this very button and fire the 'clear' event. Problem: This does not work.
Anyway calling 'clear' on my model by hand cleanly removes the data on the server but does not remove the view itself. Thats the second problem. Hopefully someone more experienced can enlighten me.
Thx in advance
Felix
First problem:
Your button must be inside the element rendered by the view.
backbone scope events to inner elements only
You must render your view within this.el element
backbone use that element for delegation
Second problem:
Use events to destroy your view
You should not store the view in the model. This is kind of a "no no" in MVC. Your model already emits a "remove" event when deleted. Your view should listen to it and behave accordingly.
You must remove your view element from the DOM yourself
This is not handled by backbone.
Other general comments:
Views already are extending Backbone.Events
Use this.model instead of this.address