Scope error using Backbone.js? - javascript

I believe my problem relates to scope somehow, as I'm a js newbie. I have a tiny backbone.js example where all I am trying to do is print out a list of items fetched from the server.
$(function(){
// = Models =
// Video
window.Video = Backbone.Model.extend({
defaults: function() {
return {
title: 'No title',
description: 'No description'
};
},
urlRoot: 'api/v1/video/'
});
// VideoList Collection
// To be extended for Asset Manager and Search later...
window.VideoList = Backbone.Collection.extend({
model: Video,
url: 'api/v1/video/'
});
// = Views =
window.VideoListView = Backbone.View.extend({
tagName: 'ul',
render: function(eventName) {
$(this.el).html("");
_.each(this.model.models, function(video) {
$(this.el).append(new VideoListRowView({model:video}).render().el);
}, this);
return this;
}
});
// VideoRow
window.VideoListRowView = Backbone.View.extend({
tagName: "li",
template: _.template("id: <%= id %>; title: <%= title %>"),
className: "asset-video-row",
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"assetManager"
},
assetManager:function() {
this.assetList = new VideoList();
this.assetListView = new VideoListView({model:this.assetList});
this.assetList.fetch();
$('#content').html(this.assetListView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
// The following works fine:
window.mylist = new VideoList();
window.mylistview = new VideoListView({model:window.mylist});
});
If I access mylist.fetch(); mylist.toJSON() from the console, mylist populates fine. I can tell that this.assetList.fetch() is accurately fetching the data from the backend, but it doesn't appear to be adding the objects to this.assetList.

The fetch method on Backbone collections is asynchronous:
Fetch the default set of models for this collection from the server, resetting the collection when they arrive. [...] Delegates to Backbone.sync under the covers, for custom persistence strategies.
And Backbone.sync says:
Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server. By default, it uses (jQuery/Zepto).ajax to make a RESTful JSON request.
So fetch involves an (asynchronous) AJAX call and that means that you're trying to use the collection before fetch has retrieved the data from the server. Note that fetch supports success and error callbacks so you can do this instead:
var self = this;
this.assetList.fetch({
success: function(collection, response) {
$('#content').html(self.assetListView.render().el);
}
});
Or you could bind a callback to the collection's reset event as fetch will reset the collection. Then render your assetListView when the collection's reset event is triggered.
Also, your assetList is a collection so you should be doing:
this.assetListView = new VideoListView({collection: this.assetList});
and:
window.VideoListView = Backbone.View.extend({
tagName: 'ul',
render: function(eventName) {
$(this.el).html("");
_.each(this.collection.models, function(video) {
// ...

Related

each() function is not defined on this.collection

I am trying to make ajax request to server. I am using fetch() method on collection. It successfully retrieves data from server after monitoring google chrome network tab.
Here is scenario.
I have Model below
var Model = Backbone.Model.extend({
urlRoot: '/contacts',
});
collection below
var Collection = Backbone.Collection.extend({
model: Model,
url: '/contacts'
});
ViewOne below
var ViewOne = Backbone.View.extend({
initialize: function () {
var collection = new Collection().fetch(); // also tried with these options { wait:true, reset: true, async: false }
new ViewTwo({ collection: collection });
}
});
Below is ViewTwo
var ViewTwo = Backbone.View.extend({
initialize: function () {
this.collection.each(this.render, this); // Uncaught TypeError: undefined is not a function
},
render: function () {
console.log(this.model);
}
});
As you can see I am getting Uncaught TypeError: undefined is not a function error.
this.render exist so that is not a problem. The problem is each function on this.collection. How can I solve this proble?
simply create new view in success callback of fetch
var ViewOne = Backbone.View.extend({
initialize: function () {
var collection = new Collection();
collection.fetch({
success:function(){
new ViewTwo({ collection: collection });
}
});
}
});
Backbone API is not fluent, which means that Collection.fetch does not return itself, it returns the Ajax object used in fetch.
Try
var ViewOne = Backbone.View.extend({
initialize: function () {
var collection = new Collection();
collection.fetch();
new ViewTwo({ collection: collection });
}
});
And note that the collection.fetch is asynchronous, this.collection will be probably empty in ViewTwo.initialize. See Backbone.js - data not being populated in collection even though fetch is successful for example.

Backbone view doesn't update after event fires for the first time

I have a backbone view that has a single click event to update a collection. In my console, I can see the object being updated and the number of models being returned is changing, however the view stays static after the event is fired for the first time and any second attempt to fire it gives me the error TypeError: text is undefined. For reference, I have my script loading at the bottom of the page and the template (using underscore) is above it.
Here's my view
app.MyView = Backbone.View.extend({
el: 'body',
events: {
'click #submit': 'fetchData'
},
initialize: function() {
this.collection = new app.MyCollection();
// On my first attempt, I tried having a the render tied to the sync event when the view initiated, although the sync event doesn't actually occur until something is clicked
// this.collection.on('sync', this.render, this);
},
render: function() {
console.log('rendering');
var schedule = $('#schedule');
var template = _.template($('#times-template').html());
schedule.html(template({ collection: this.collection.toJSON() }));
},
fetchData: function() {
that = this;
stationQuery.station_start = $('#start').val();
stationQuery.station_end = $('#end').val();
var query = stationQuery;
this.collection.fetch({
data: query,
success: function(collection, response) {
console.log('fetching data');
console.log(collection);
// attempt #2 - moving the sync event to the success callback of fetch doesnt allow the view to update a second time either
// collection.on('sync', that.render, that);
},
error: function(collection, response) {
console.log('error on fetch', response);
}
});
},
});
app.myView = new app.MyView;
// Third attempt, this time checking if listenTo will allow the view to update every time the event is fired. It still only works on the first time and fails to render for consecutive clicks, even though the console is showing updated models
app.myView.listenTo(app.myView.collection, 'sync', app.myView.render);
Below is a working code, I just added initial call to fetch data at the end
app.myView.fetchData();
And uncommented your .on('sync', ... inside initialize
this.collection.on('sync', this.render, this);
I tested it with jsbin so you can see what's was wrong for you. As a result I see initial rendering of fetched data and clicking the #submit will re-fetch and re-render view.
app.MyView = Backbone.View.extend({
el: 'body',
events: {
'click #submit': 'fetchData'
},
initialize: function() {
this.collection = new app.MyCollection();
// Here is a binding to each fetch data and on success render view
this.collection.on('sync', this.render, this);
},
render: function() {
console.log('rendering');
var schedule = $('#schedule');
var template = _.template($('#times-template').html());
schedule.html(template({ collection: this.collection.toJSON() }));
},
fetchData: function() {
that = this;
var stationQuery = {};
stationQuery.station_start = $('#start').val();
stationQuery.station_end = $('#end').val();
var query = stationQuery;
this.collection.fetch({
data: query,
success: function(collection, response) {
console.log('fetching data');
},
error: function(collection, response) {
console.log('error on fetch', response);
}
});
},
});
app.myView = new app.MyView;
// Here is first call to fetch data and on success render view
app.myView.fetchData();

BackBoneJs donot Delete/Update Model to the server

I have an application that reads/create/update model and save it to the server. But presently I am able to read and save my models from and to the database. But I am unable to delete / update the model from and to the server. currently the views gets deleted of the model but not the model it self
here is the JSFiddle path http://jsfiddle.net/u17xwzLh/1/
$(function() {
Model
var modelContact = Backbone.Model.extend({
defaults: function() {
return {
Id: 0,
Name: "",
Address: ""
};
},
//if i add this idAttribute = "Id" it deletes the value from the server
//but i am unable to create a new model/new entry to the database
clear: function () {
// Deletes the model but the changes are not posted back to the server
this.destroy();
}
});
Collection
// runs fine
var contactCollection = Backbone.Collection.extend({
model: modelContact,
url: 'api/Contact'
});
var contacts = new contactCollection;
ModelView
var contactView = Backbone.View.extend({
tagName: "tr",
events: { // runs fine
"click a.destroy": "clear"
},
template: _.template($("#newContacttemplate").html()), // runs fine
initialize: function() {
this.model.on("change", this.render, this);
this.model.on('destroy', this.remove, this);
},
render: function() { // runs fine
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function () {
this.model.clear();
}
});
MainView
var main = Backbone.View.extend({
el: $("#contactApp"),
events: { // runs fine
"click #btnsave": "CreateNewContact"
},
initialize: function() { // runs fine
this.Nameinput = this.$("#contactname");
this.Addressinput = this.$("#contactaddress");
contacts.on("add", this.AddContact, this);
contacts.on("reset", this.AddContacts, this);
contacts.fetch(); // Note : populates all the database values
},
AddContact: function(contact) { // runs fine
var view = new contactView({ model: contact });
this.$("#tblcontact tbody").append(view.render().el);
},
AddContacts: function() { // runs fine
contacts.each(this.AddContact);
},
CreateNewContact: function(e) { // runs fine
contacts.create({ Name: this.Nameinput.val(), Address: this.Addressinput.val() });
}
});
var m = new main;
});
Right now you have a URL defined on your Backbone.Collection but not on your Backbone.Model, which means you have to do all AJAX work through the Collection. It doesn't have to be that way though: you can add a second URL on yours server-side for Model AJAX operations, or the two could even share a URL (if you set it up appropriately).
The important part, if you want to be able to call this.destroy(); and have it reflected on your server, is that you need:
a URL on your server that can handle requests with the DELETE method (vs. the usual GET or POST methods)
a url property on your Backbone.Model that is set to that server-side URL
Once you have that your call to this.destroy(); will create a DELETE AJAX request, your server will receive that request and know that it should delete the appropriate database record, and then that model will be deleted on both the client- and server-side.

Backbone.js render called before content is fetched

I found an example of some Backbone.js code that I then adopted to my needs.
The render function of CommentListView is called before any content is fetched. It seems that it not called again when there are content to render.
The backend returns two results, so that is not the problem.
// Models
window.Comment = Backbone.Model.extend();
window.CommentCollection = Backbone.Collection.extend({
model:Comment,
url:"/api/comments/cosmopolitan"
});
// Views
window.CommentListView = Backbone.View.extend({
tagName:'ul',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
console.log(this.model.models);
_.each(this.model.models, function (comment) {
console.log(comment);
$(this.el).append(new CommentListItemView({model:comment}).render().el);
}, this);
return this;
}
});
window.CommentListItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#tpl-comment-list-item').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.commentList.fetch();
$('#sidebar').html(this.commentListView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
The behavior of fetch has changed a bit in Backbone 1.0.0. From the ChangeLog:
Renamed Collection's "update" to set, for parallelism with the similar model.set(), and contrast with reset. It's now the default updating mechanism after a fetch. If you'd like to continue using "reset", pass {reset: true}.
And Collection#fetch says:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, setting them on the collection when they arrive. [...] When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true},
Your initialize just binds to "reset":
this.model.bind("reset", this.render, this);
You can either bind to the "add", "remove", and "change" events that Collection#set will generate or you can explicitly ask for a "reset" event when you fetch:
this.commentList.fetch({ reset: true });
A couple other things while I'm here:
Since your CommentListView view is using a collection rather than a model, you might want to call it collection:
this.commentListView = new CommentListView({collection: this.commentList});
and then refer to this.collection inside the view. See View#initialize for details on how view constructors handle their arguments.
Collections have various Underscore methods mixed in so you can say this.collection.each(function(model) { ... }) instead of _.each(this.model.models, ...).
Views maintain a cached version of the jQuery-wrapped el in $el so you can say this.$el instead of $(this.el).
Be careful with things like console.log(this.model.models). The console usually grabs a live reference so what shows up in the console will be the state of this.model.models when you look rather than when console.log is called. Using console.log(this.model.toJSON()) is more reliable when faced with timing and AJAX issues.
You might want to switch to listenTo instead of bind (AKA on) as that is less susceptible to memory leaks.
Usually is use to create a listener for the fetch, when fetch is complete and change the model or collection there is a callback. try this:
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.listenTo(this.commentList,'change', this.makeRender);
this.commentList.fetch();
},
makeRender: function(){
$('#sidebar').html(this.commentListView.render().el);
}
});

How do I bind an event to a model that isn't loaded yet?

So I've got a pretty simple backbone app with a model, a collection, and a couple of views. I'm fetching the actual data from the server by doing a collection.fetch() at page load.
My problem is that one of my views is a "detail" view, and I want to bind it to a particular model - but I don't have the model yet when the page loads. My code looks a lot like this:
window.App = {
Models: {},
Collections: {},
Views: {},
Routers: {}
}
App.Models.Person = Backbone.Model.extend({
urlRoot: '/api/people'
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: '/api/people'
});
people = new App.Collections.People()
App.Views.List = Backbone.View.extend({
initialize: function() {
this.collection.bind('reset', this.render());
},
render: function() {
$(this.el).html("We've got " + this.collection.length + " models." )
}
});
listView = new App.Views.List({collection: people})
App.Views.Detail = Backbone.View.extend({
initialize: function() {
this.model.bind('change', this.render());
},
render: function() {
$(this.el).html("Model goes here!")
}
});
App.Routers.Main = Backbone.Router.extend({
routes: {
'/people': 'list',
'/people/:id': 'detail'
},
list: function() {
listView.render();
},
detail: function(id) {
detailView = new App.Views.Detail({model: people.get(id)})
detailView.render()
}
})
main = new App.Routers.Main();
Backbone.history.start();
people.fetch();
But if I start with the detail route active, the people collection is empty, so people.get(id) doesn't return anything, so my new view has this.model undefined, and won't let me bind any events relating to it. The error is:
Uncaught TypeError: Cannot call method 'bind' of undefined
If I start with the list route active, then by the time I click on an item to bring up the detail view people is populated, so everything works.
What's the right way to bind model-related events for a "detail" view when you're fetching the data after page load?
You have a part of the answer here: Backbone.js Collections not applying Models (using Code Igniter)
Indeed, you need to wait that people.fetch finishes its ajax request before to call Backbone.history.start(); and trigger the actual route.
Your code should look like:
// [...]
main = new App.Routers.Main();
peoples.fetch({
success: function (collection, response) {
// The collection is filled, trigger the route
Backbone.history.start();
}
});
You can add a loader on the page and hide it when the collection is loaded.

Categories

Resources