I'm working on a toy backbone.js application, a library application to perform CRUD operations on a library. Here is the book model and the library collection (of books)
var Book = Backbone.Model.extend({
url: function() {
return '/api/books' + this.get('id');
}
});
var Library = Backbone.Collection.extend({
model : Book,
url : '/api/books'
});
This seems pretty straightforward. Next, I obviously want to be able to show the book, so I have a BookView...
var BookView = Backbone.View.extend({
tagName: 'li',
render: function() {
this.$el.html(this.model.get('author'));
$('#list-of-books').append(this.$el);
}
});
all the render method does is append an li to the end of an unordered list with an id of list-of-books that is in my html.
Next, unsurprisingly, if I add the following code, I get a list with one item (the name of the author of the book with id=4)
var a_book = new Book();
a_book.url = '/api/books/4';
a_book.fetch({
success: function() {
var bookView = new BookView({ model: a_book });
bookView.render();
}
});
Here's where I don't understand. I add the following code and nothing happens:
var some_books = new Library();
some_books.fetch();
some_books.forEach(function(book) {
alert('why is this function not being run');
var view = new BookView({ model: book });
view.render();
});
For some reason, I can't even get that loop code to run, I don't even see that alert pop out to the screen. Let me know if you understand what's wrong with this code and how I can make the list render properly. Thanks
You are calling .render inside fetch. Hence the execution stops over there itself. For loop wont run after fetch as it has returned already.
.fetch() is an asynchronous call, so you may not have the collection data populated when you are calling .forEach().
You should either call the rendering code in the success callback or (even better) render the view on events (reset,...) on the collection.
BTW, you have a typo in the url() function:
url: function() {
return '/api/books/' + this.get('id');
}
Why are you defining the url function for the Book?, since you use the model attribute in the Library collection, url for the Book will be set correctly by default.
Related
Now I am using Backbone.Marionette a lot and I have some concerns about what is the best way to deal with DOM external attributes.
Sometimes I need to do something like this:
my-view.js
var MyView = ItemView.extend(
//...
//insert a lot of code here
//...
myFunction: function() {
var someKindOfCalculation = this.$('.my-field').height() + Backbone.$('.my-external-module').height();
//...
}
Where .my-external-moduleis a DOM element which is inside of another MarionetteJS module.
My current way to solve this is something like:
my-view.js
//Some way to obtain the App.events variable(browserify, requireJS, global...)
//...
var MyView = ItemView.extend(
//...
//insert a lot of code here
//...
myFunction: function() {
App.events.on('app:MyOtherModule:height:returned', function(heightForModule) {
var someKindOfCalculation = this.$('.my-field').height() + heightForModule;
//...
});
App.events.trigger('app:MyOtherModule:height');
}
my-other-module.js
//Some way to obtain the App.events variable(browserify, requireJS, global...)
//...
var MyOtherModule = Controller.extend({
//...
//insert a lot of code here
//...
start: function() {
App.Events.on('app:MyOtherModule:height', function() {
App.Events.trigger('app:MyOtherModule:height:returned', this.view.$el.height());
});
}
})
Although it works fine, this way to obtain 'external' DOM attributes is so weird for me, because we are including a new callback every time that you want to get an external attribute.
Are you using another way to get DOM attributes when these DOM elements are outside of our current module/view ? Is this way to get data valid for you?
Currently, Marionette ships with Backone.Wreqr, but the differences between Wreqr and Radio, for the consumer, are only semantic. I'll show you how to set up and consume a Request Response handler with both.
With a Request Response handler you don't have to ensure that my-view.js has been created, because my-view.js will actively request the data, rather than wait for my-other-module.js to publish it.
Backbone.Wreqr
Using the views you shared in your post, you'd first want to set up a Request Response handler in my-other-module.js. Instead triggering an event in a controller, we'll set up a Request Response handler in the my-other-module.js view, which we'll call my-other-view.js.
Setting up the handler
First you have to enable the request messaging system, much like you use App.events. So in some centralized part of your app (like a main controller for example), you'd execute:
App.reqres = new Backbone.Wreqr.RequestResponse();
my-other-view.js
var MyOtherView = ItemView.extend({
initialize: function () {
this.setupHandlers();
},
setupHandlers: function() {
App.reqres.setHandler('app:MyOtherModule:height', function(){
return this.view.$el.height();
});
}
});
Request the data
And on my-view.js you'd simply pass the App reference in an get a hold of App.reqres and invoke the request. Like this:
my-view.js
var MyView = ItemView.extend{(
//...
//insert a lot of code here
//...
myFunction: function() {
var heightForModule= App.reqres.request('app:MyOtherModule:height');
var someKindOfCalculation = this.$('.my-field').height() + heightForModule;
}
});
And that's it! This certainly saves a lot of code.
Backbone.Radio
Radio is an optimized (and smaller) version of Wreqr that preserves its functionality. To use it we simply adopt the language that API, but the usage is essentially identical.
First we set up our request messaging bus, in a central place on our app,
_.extend(App.reqres, Backbone.Radio.Requests);
And then we simply change the method names
my-other-view.js
var MyOtherView = ItemView.extend({
initialize: function () {
this.setupHandlers();
},
setupHandlers: function() {
App.reqres.reply('app:MyOtherModule:height', function(){
return this.view.$el.height();
});
}
});
my-view.js
var MyView = ItemView.extend{(
//...
//insert a lot of code here
//...
myFunction: function() {
var heightForModule= App.reqres.request('app:MyOtherModule:height');
var someKindOfCalculation = this.$('.my-field').height() + heightForModule;
}
});
Final word
Both Wreqr and Radio use channels. With channels you can create dedicated messaging buses that keep your messages separate. Take a look here: Backbone.Radio Channels.
I'm currently testing a backbone view with Jasmine and I am having some trouble. I'm trying to isolate the View from all the other elements (the other view that are instantiated, the collection), but it is nearly impossible.
initialize: function(options) {
if(options.return) {return;}
var view = this;
var name = options.name;
var localizedElements = app.helpers.Locale.l().modules.case[name];
var swap, notification;
this.name = name.capitalizeFirstLetter();
this.collection.on('sort', this.refreshGui, this);
return this.render('/case/' + name + '/' + this.name + 'Box.txt', localizedElements, this.$el).done(function() {
new app.views.Buttons({el: view.$el.find('.Buttons')});
_.each(view.collection.models, function(model) {
new app.views['Folded' + this.name]({model: model, el: this.$('table')});
}, view);
if(!view.collection.findWhere({isPreferred: true}) || !view.collection.findWhere({isPrescribedForms: true})) {
if(!view.collection.findWhere({isPreferred: true})) {
swap = {entity: 'address', preferenceType: 'preferred'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var preferredNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPreferred: preferredNotification});
}
if(!view.collection.findWhere({isPrescribedForms: true})) {
swap = {entity: 'address', preferenceType: 'prescribed forms'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var prescribedFormsNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPrescribedForms: prescribedFormsNotification});
}
}
});
},
For example, where there are the two "ifs", the view is talking to the collection and a helper: "Notification Helper". How am I suppose to test this part of the code if I mocked the collection and the notification helper? I mean I am testing the VIEW, but now it seems like I have to test other elements of my application in my view...
I'm trying to isolate the View from all the other elements
I'm not sure if this will be helpful for you, but I created my own strategy to handle this problem.
The problem with Backbone is often indeed, that the view objects become cluttered with functionality.
Personally, I like my views to handle DOM interactions, listen to DOM events.
But to execute business logic / interactions with the back end, I prefer to delegate this functionality to other 'external' objects.
Models on the other hand 'may' handle basic data validation but do nothing more than making RESTful interactions with the server in my application.
How I solved the problem is by instantiating custom Javascript ('controller') objects that act as intermediaries between the views and the models.
This object is nothing more than an object that looks approximately like this:
var Some_controller = function(options){
this.makeView();
};
Some_controller.prototype.makeView = function(){
var someView = new Some_view({'controller': this});
someView.render(); //Render, but can also take care of proper view cleanup to avoid zombie objects
};
Some_controller.prototype.getModel = function(){
var someModel = new Some_model();
var promise = model.fetch();
return promise; //Promise be called from the view, because the controller is passed via the options, when the view is instantiated.
};
Well, this is how I try to keep my views clean.
In my experience, it is very easy to find everything back immediately; and note that you can also use helper objects to isolate more specific business functionality.
Not sure if anyone else has a better solution for this.
I am following a backbone.js tutorial and a part of the code isn't working, maybe because Backbone has changed in the meantime or because I'm doing something wrong. This is the render function of my view.
// grab and populate our main template
render: function () {
// once again this is using ICanHaz.js, but you can use whatever
this.el = ich.app(this.model.toJSON());
// store a reference to our movie list
this.movieList = this.$('#movieList');
return this;
},
The element gets appended into the document later in the code. Subsequently, when the code tries to adds elements to this.movieList, Javascript says it's undefined.
I have tried changing this.el = ... to
this.setElement(ich.app(this.model.toJSON()));
and that helps because this.$el is now defined, but if i try this.$el.find(...) it never finds anything, even though through inspection in Chrome it does appear to contain the HTML elements.
I never used ICanHaz but it it works like the other template languages probably returns HTML code. In that case i'd do something like:
render: function(){
this.$el.html(ich.app(this.model.toJSON()));
}
addMovie: function (movie) {
var view = new MovieView({model: movie});
this.$el.find("#movieList").append(view.render().el);
},
Hope this helps
PS: This is the first time I see this.$('something') in a backbone code o_O. Is he storing the reference of JQuery on the view?
is it possible to pass the questions variable into the view render?
Ive attempted calling this.render inside the success on the fetch however I got an error, presumably it's because this. is not at the correct scope.
app.AppView = Backbone.View.extend({
initialize: function(){
var inputs = new app.Form();
inputs.fetch({
success: function() {
var questions = inputs.get(0).toJSON().Questions;
app.validate = new Validate(questions);
app.validate.questions();
}, // End Success()
error: function(err){
console.log("Couldn't GET the service " + err);
}
}); // End Input.fetch()
this.render();
}, // End Initialize
render: function(){
el: $('#finder')
var template = _.template( $("#form_template").html(), {} );
this.$el.html(template);
}
The success callback is called with a different this object than your View instance.
The easiest way to fix it is to add something like this before you call inputs.fetch:
var self = this;
And then inside the success callback:
self.render();
I'm not quite sure what you're trying to achieve, but if your problem is calling render from the success callback, you have two options, Function#bind or assigning a self variable.
For more information about "self" variable, see var self = this? . An example:
var self = this;
inputs.fetch({
success: function () {
self.render();
},
...
});
You should probably do some reading on JavaScript scopes, for example "Effective Javascript" or search the topic ( for example this MDN article ) online to get a better idea what happens there.
For Function#bind(), see the MDN article about it. With Backbone I suggest you use Underscore/LoDash's _.bind instead though, to make sure it works even where Function#bind() is not supported.
As for more high-level concepts, the fetching operation looks like it belongs to the model or router level and instead you should assign the questions variable as the model of your view. Ideally views don't do data processing / fetching, they're just given a model that has the methods necessary to perform any data transformations you might need.
The views shouldn't even need to worry about where the model comes from, this is normally handled by a router in case of single page applications or some initialization code on the page.
I have been using Backbone on a new project and so far have loved it, but I have come to a blocker that I can't seem to get around.
Without trying to explain my whole domain model, I have found that when you save a model, the response comes back from the server and gets parsed again, creating new sub objects and therefore breaking and event bindings I had previously put on the object.
For instance, if I save ContentCollection (its a Backbone.Model not a collection) when it comes back from the server, the response gets parsed and creates a new Collection in this.contentItems, which breaks all the binding I had on this.contentItems. Is there any way to get around this? Tell backbone not to parse the response somehow? Grab the bindings off the original list, and then re-attach them to the new list?
App.ContentCollection = Backbone.Model.extend({
urlRoot: '/collection',
initialize: function() {
},
parse: function(resp, xhr) {
this.contentItems = new App.ContentList(resp.items)
this.subscriptions = new App.SubscriptionList(resp.subscriptions)
return resp
},
remove: function(model){
this.contentItems.remove(model)
this.save({'removeContentId':model.attributes.id})
},
setPrimaryContent: function(model){
this.save({'setPrimaryContent':model.attributes.id})
}
})
Has anyone run into this before?
I think the issue here is the way you're using the parse() method. Backbone just expects this method to take a server response and return a hash of attributes - not to change the object in any way. So Backbone calls this.parse() within save(), not expecting there to be any side-effects - but the way you've overridden .parse(), you're changing the model when the function is called.
The way I've dealt with this use case in the past is to initialize the collections when you first call fetch(), something like:
App.ContentCollection = Backbone.Model.extend({
initialize: function() {
this.bind('change', initCollections, this);
},
initCollections: function() {
this.contentItems = new App.ContentList(resp.items);
this.subscriptions = new App.SubscriptionList(resp.subscriptions);
// now you probably want to unbind it,
// so it only gets called once
this.unbind('change', initCollections, this)
},
// etc
});