backbone-relational: usage with standalone model without relations - backbone.js

I am using backbone-relational. The usage for a onetomany model works fine.
But I am having troubles using backbone-relational for a single/standalone model:
window.BeerOnlineShopPosition = Backbone.RelationalModel.extend({
urlRoot:"../api/beeronlineshoppositions",
idAttribute: 'id',
relations: [
],
defaults:{
"id":null,
"position_amount":""
}
});
window.BeerOnlineShopPositionCollection = Backbone.Collection.extend({
model:BeerOnlineShopPosition,
url:"../api/beeronlineshoppositions"
});
in main i have:
beeronlineshopproductDetails:function (id) {
var beeronlineshopproduct = BeerOnlineShopProduct.findOrCreate(id);
beeronlineshopproduct.fetch();
var beeronlineshopproduct_view = new BeerOnlineShopProductView({el: $('#content'), model: beeronlineshopproduct});
},
So, when jump to an existing records (...beeronlineshop/beeronlineshopproducts/#4), nothing happens. But debugging shows, that the fetch is executed and the view gets loaded. but the view is not rendered somehow.
When I refresh (f5), the view gets rendered correctly.
As mentioned the whole thing works for one-to-many model, so my question is:
did i make some trivial syntax-error on the model part?... or is there any other obvious reason for the troubles i have?

It may be because of the asynchronous request created by findOrCreate. beeronlineshopproduct.fetch() is making a request to the server but the view is being rendered before the server returns with a response.
You have two options. You can pass in the rendering of the view as a callback upon fetch's success like so:
beeronlineshop.fetch({
success: function() {
var beeronlineshopproduct_view = new BeerOnlineShopProductView({el: $('#content'), model: beeronlineshopproduct})
});
Or you can pass an initializer in your BeerOnlineShopProductView that listens to the its model syncing with the server, and calls for the view to re-render itself, like so:
window.BeerOnlineShopProductView = Backbone.View.extend({
initialize: function() {
this.listenTo (this.model, "sync", this.render)
},
})

Related

Backbone Marionette: Rendering Collection in ItemView

I was unable to find any posts relevant to this error. I am attempting to render a Backbone Collection in a Marionette ItemView. The template is rendered, however, the data related to the collection is not rendered in the template. I am getting no errors or other indicators. For reasons I do not understand, using setTimeout() on App.mainRegion.show(overView). However, I know that that is not an acceptable solution. Could someone give me some insight on how to make an ItemView for a Collection properly render in this case? Here is my simplified code:
My Collection to be rendered:
About.Collection = Backbone.Collection.extend({
url: '/api/about',
idAttribute: '_id',
});
Involved View definitions:
About.ListView = Marionette.CollectionView.extend({
tagName: 'ul',
itemView: App.About.ListItemView,
});
About.OverView = Marionette.ItemView.extend({
tagName: 'div',
className: 'inner',
template: _.template('<h2>About Overview</h2><p><%= items %></p>'),
});
My relevant execution code:
var API = {
getAbouts: function() {
var abouts = new App.About.Collection();
abouts.fetch();
return abouts;
},
...
}
var abouts = API.getAbouts();
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Correctly renders collection data
App.listRegion.show(aboutsListView);
// Does not render collection data
App.mainRegion.show(aboutsOverView);
// unless
setTimeout(function() {App.mainRegion.show(aboutsOverView)}, 50);
For those who are interested, I am using an ItemView with the eventual intent to display aggregate data of About.Collection. I will be happy to provide additional information, if needed.
It's an issue with the asynchronous nature of the fetch call on your collection. The data for the collection has not returned when you show the two views. If you update the execution part of your code something like the following (untested), you should be on the right tracks:
var API = {
getAbouts: function() {
// Just return the new collection here
return new App.About.Collection();
},
...
}
// Fetch the collection here and show views on success
var abouts = API.getAbouts().fetch({
success: function() {
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Should render collection data now
App.listRegion.show(aboutsListView);
// Should render collection data now
App.mainRegion.show(aboutsOverView);
}
});
The abouts.fetch call is asynchronous, and a significant amount of time elapses before the collection receives data from the server. This is the order in which things are happening:
You call getAbouts, which itself calls abouts.fetch to make GET call to server for collection.
The listRegion.show and mainRegion.show calls are made, rendering the 2 views with the empty collection (the collection hasn't received a response from the server yet).
The GET call eventually returns, and the collection is populated with data.
Only the aboutsListView re-renders to show the data (see below for the reason).
The reason that only the aboutsListView re-renders is that the Marionette CollectionView automatically listens for the collection's reset event, which is fired when the collection's contents are replaced.
You can fix this by simply adding an initialize function to your OverView, so that view also re-renders in response to the same event:
// add to About.OverView:
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
}
That will take care of it.

can't get application to work after namespacing

I have a Backbone app that is working properly, however, when I tried to reorganize the code under a namespace I can't get it to do anything. I can't even trigger events (by clicking on ids) for views that I know are getting initialized (through console log messages), so I'm wondering if I've introduced some fundamental flaw somehow. I'm following a pattern set out by this blog (in french) http://www.atinux.fr/2011/12/10/organiser-son-code-backbone-js-en-modules/
In the main application.js (see below), I instantiate all of the views and models after initiating the app on document ready. One change introduced as a result of creating this namespace was setting the models for the views with this.models.game
this.views.clock_view = new this.Views.clockView({ model: this.models.game});
Inside the modules folder, I had a views.js and a models.js. I created each view and object like this, prefaced with app.Views or app.Models accordingly
app.Views.announceView = Backbone.View.extend({
....
app.Views.optionsView = Backbone.View.extend({
...
This app.Views.optionsView is getting initialized (according to a console.log statement in the initializer) but when I click on #new_game, the console.log in the startNewGame is not getting triggered
'click #new_game': 'startNewGame'
// 'click .action_button': 'startNewGame'
},
startNewGame: function() {
console.log("startNewGame");
this.model.new();
},
As a result of the namespacing, one other key change I made was when I created new views inside one of the other views. Under the previous (non-namespaced app), I created individual question items from a QuestionListView
var view = new QuestionListItemView({ model: game });
but now I'm doing
var view = new app.Views.questionListItemView({ model: app.models.game })
because the instance of the model was saved to this.models.game in application.js, however, I also tried using 'this.models.game'
var view = new app.Views.questionListItemView({ model: this.models.game })
Either way, before the games model gets involved, I can't trigger the startNewGame function outlined above, so it's not solely an issue of how to identify the model.
I also wondered whether i should be using this.Views or app.Views after the 'new' when creating new views from within
var view = new app.Views.questionListItemView({ model: this.models.game })
I'd be grateful if you could help me identify any flaws I've introduced.
application.js
var app = {
// Classes
Collections: {},
Models: {},
Views: {},
// Instances
collections: {},
models: {},
views: {},
init: function () {
this.models.game = new this.Models.game();
this.views.story_view = new this.Views.storyView(); #doesn't have a model
this.views.clock_view = new this.Views.clockView({ model: this.models.game});
this.views.field_view = new this.Views.fieldView({ model: this.models.game});
this.views.options_view = new this.Views.optionsView({ model : this.models.game});
this.views.announcement_view = new this.Views.announceView({ model: this.models.game});
this.views.question_list_view = new this.Views.questionListView({ model : this.models.game});
this.views.question_list_item_view = new this.Views.questionListItemView({ model : this.models.game});
}
};
$(document).ready(function () {
app.init();
}) ;
The options view is getting initialized but I can't trigger the startNewGame function when I click that #id
app.Views.optionsView = Backbone.View.extend({
// el: $("#options"),
el: $("#options"),
initialize: function() {
console.log("app views OptionsView initialized");
// this.model.bind("gameStartedEvent", this.removeGetAnswerButton, this);
this.model.bind("gameStartedEvent", this.disableNewGame, this);
},
events: {
'click #demo': 'startDemo',
'click #new_game': 'startNewGame'
// 'click .action_button': 'startNewGame'
},
startDemo: function(){
console.log("start demo");
this.model.demo();
},
startNewGame: function() {
console.log("startNewGame");
this.model.new();
},
disableNewGame: function(){
$('#new_game').attr("disabled", true);
}
});
Update
My file structure looks like this
<%= javascript_include_tag 'application.js'%>
<%= javascript_include_tag 'modules/models'%>
<%= javascript_include_tag 'modules/views'%>
At the top of the views and models file, I just do something like this
app.Views.optionsView = Backbone.View.extend({
ie.. there is no further document ready. In fact, including another document ready in these files breaks the application.js init
Prior to using the namespace, I defined the element this way in the view
el: $("#options")
which, as was pointed out in the comments to this question, is not the ideal way to do it(see #muistooshort comment below), (even though it worked).
Defining the el this way instead
el: '#options'
got it working, and let Backbone "convert it to a node object" on its own.

Backbone.js: just one line to set local storage?

demo fiddle (with problem) http://jsfiddle.net/mjmitche/UJ4HN/19/
I have a collection defined like this
var Friends = Backbone.Collection.extend({
model: Friend,
localStorage: new Backbone.LocalStorage("friends-list")
});
As far as I'm aware, that's all I need to do to get local storage to work (in addition to including it below backbone.js)
One thing I wasn't sure about, does the name "friends-list" have to correspond to a DOM element? I'm trying to save the "friends-list" so I called it that in local storage, however, localstorage doesn't seem to require passing a class or an id.
Here's a fiddle where you can see what I'm trying to do http://jsfiddle.net/mjmitche/UJ4HN/19/
On my local site, I'm adding a couple friends, refreshing the page, but the friends are not re-appearing.
Update
I've also done the following in my code on my local site
console.log(Backbone.LocalStorage);
and it's not throwing an error.
My attempt to debug
I tried this code (taken from another SO answer) in the window.AppView but nothing came up in the console.
this.collection.fetch({}, {
success: function (model, response) {
console.log("success");
},
error: function (model, response) {
console.log("error");
}
})
From the fine manual:
Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database.
This LocalStorage plugin is just a replacement for Backbone.Sync so you still have to save your models and fetch your collections.
Since you're not saving anything, you never put anything into your LocalStorage database. You need to save your models:
showPrompt: function() {
var friend_name = prompt("Who is your friend?");
var friend_model = new Friend({
name: friend_name
});
//Add a new friend model to our friend collection
this.collection.add(friend_model);
friend_model.save(); // <------------- This is sort of important.
},
You might want to use the success and error callbacks on that friend_model.save() too.
Since you're not fetching anything, you don't initialize your collection with whatever is in your LocalStorage database. You need to call fetch on your collection and you probably want to bind render to its "reset" event:
initialize: function() {
_.bindAll(this, 'render', 'showPrompt');
this.collection = new Friends();
this.collection.bind('add', this.render);
this.collection.bind('reset', this.render);
this.collection.fetch();
},
You'll also need to update your render to be able to render the whole collection:
render: function() {
var $list = this.$('#friends-list');
$list.empty();
this.collection.each(function(m) {
var newFriend = new FriendView({ model: m });
$list.append(newFriend.render().el);
});
$list.sortable();
return this;
}
You could make this better by moving the "add one model's view" logic to a separate method and bind that method to the collection's "add" event.
And a stripped down and fixed up version of your fiddle: http://jsfiddle.net/ambiguous/haE9K/

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

One View connected to multiple models

I have the following problem…
MyView which is connected to two views: TaskModel and UserModel
TaskModel = {id: 1, taskName: "myTask", creatorName: "myName", creator_id: 2 },
UserModel = {id: 2, avatar: "someAvatar"}
The view should display
{{taskName}}, {{creatorName}}, {{someAvatar}}
As you can see the fetch of TaskModel and UserModel should be synchronized, because the userModel.fetch needs of taskModel.get("creator_id")
Which approach do you recommend me to display/handle the view and the two models?
You could make the view smart enough to not render until it has everything it needs.
Suppose you have a user and a task and you pass them both to the view's constructor:
initialize: function(user, task) {
_.bindAll(this, 'render');
this.user = user;
this.task = task;
this.user.on('change', this.render);
this.task.on('change', this.render);
}
Now you have a view that has references to both the user and the task and is listening for "change" events on both. Then, the render method can ask the models if they have everything they're supposed to have, for example:
render: function() {
if(this.user.has('name')
&& this.task.has('name')) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;​​​​
}
So render will wait until both the this.user and this.task are fully loaded before it fills in the proper HTML; if it is called before its models have been loaded, then it renders nothing and returns an empty placeholder. This approach keeps all of the view's logic nicely hidden away inside the view where it belongs and it easily generalizes.
Demo: http://jsfiddle.net/ambiguous/rreu5jd8/
You could also use Underscore's isEmpty (which is mixed into Backbone models) instead of checking a specific property:
render: function() {
if(!this.user.isEmpty()
&& !this.task.isEmpty()) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;​​​​
}
That assumes that you don't have any defaults of course.
Demo: http://jsfiddle.net/ambiguous/4q07budc/
jQuery's Deferreds work well here. As a crude example:
var succesFunction = function () {
console.log('success');
};
var errorFunction = function () {
console.log('error');
};
$.when(taskModel.fetch(), userModel.fetch()).then(successFunction, errorFunction);
You could also pipe the request through using the crude data (remember that fetch, save, create are really just wrappers around jQuery's $.ajax object.
var taskModelDeferred = taskModel.fetch();
var userModelDeferred = taskModelDeferred.pipe(function( data ) {
return userModel.fetch({ data: { user: data.userId }});
});
note: Backbone returns the collection and model in the success / error functions by default on collections and models so if you need this be sure have a reference handy.
I've run into this very same issue with a complex layout that used two models and multiple views. For that, instead of trying to synchronize the fetches, I simply used the "success" function of one model to invoke the fetch of the other. My views would listen only to the change of the second model. For instance:
var model1 = Backbone.Model.extend({
...
});
var model2 = Backbone.Model.extend({
...
});
var view1 = Backbone.View.extend({
...
});
var view2 = Backbone.View.extend({
...
});
model2.on("change",view1.render, view1);
model2.on("change",view2.render, view2);
Then...
model1.fetch({
success : function() {
model2.fetch();
}
});
The point to this is you don't have to do any sophisticated synchronization. You simply cascade the fetches and respond to the last model's fetch.

Resources