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
*/
}
Related
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
I use backbone.boilerplate for creating a simple application.
I want to create module that can show collections of sites. Each sites has id and title attributes (example [{ id: 1, title: "github.com" }, { id: 2, title: "facebook.com" }].
My router:
routes: {
"": "index",
"sites": "sites"
},
sites: function () {
require(['modules/sites'], function (Sites) {
var layout = new Sites.Views.Layout();
app.layout.setView('#content', layout);
});
}
So, my sites module has layout, which do this:
Sites.Views.Layout = Backbone.Layout.extend({
template: "sites/layout",
className: 'container-fluid',
initialize: function () {
_.bindAll(this);
this.collection = new Sites.Collections.Sites();
this.collection.fetch({
success: this.render
});
},
beforeRender: function () {
var siteList = new Sites.Views.SiteList({
collection: this.collection
});
this.setView('.content', siteList);
},
});
Sites.Views.SiteList = Backbone.View.extend({
template: 'sites/list',
beforeRender: function () {
this.collection.each(function (model) {
var view = new Sites.Views.SiteItem({
model: model
});
this.insertView('tbody', view);
}, this);
}
});
Sites.Views.SiteItem = Backbone.View.extend({
template: 'sites/item',
tagName: 'tr',
serialize: function () {
return {
title: this.model.get('title')
};
}
});
ok. and now my question: help me please to choose best way to render one site view when user click on element of collection. I want that it is works like gmail: one screen for all letters and all screen for one letter when it choosed. Maybe you have link with example of similar application. I am waiting for your advices.
Looking at your pastebin code it seems like you have a basic understanding of Backbone, which is certainly all you need to get started.
That being said, you might find this article/tutorial helpful. It walks through the process of building inter-connected views (in the tutorial they are related <select> elements) which use AJAX to update their values:
http://blog.shinetech.com/2011/07/25/cascading-select-boxes-with-backbone-js/
I was trying to dynamically change the url inside the router but couldn't manage to do it, it keeps returning to the base Collection URL. Here i posted the code with the 3 different collections which apart from pointing to three different urls they do exactly the same.
I have only one model and three collections that depend on that model and they even render the same view. How can i dynamically change the url so i can create only one Collection and one Model? Is it best pracitce for a case like this?
// MODELS & COLLECTIONS
window.Post = Backbone.Model.extend({
urlRoot: function() {
return 'http://localhost:5000/json/guides/:id'
}
})
App.Collections.RecentPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/recent',
})
App.Collections.PopularPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/popular',
})
App.Collections.FeaturedPosts = Backbone.Collection.extend({
model: Post,
url:'http://localhost:5000/json/posts/featured',
})
// CONTROLLER
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
recent: function(){
//.... same as featured ?
},
popular: function(){
//.... same as featured ?
},
featured: function(){
$("#browser").empty();
var collection = new App.Collections.Posts();
collection.fetch({
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
})
}
});
There are numerous different ways of doing this. Here's what's probably going to be 'best practice'.
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: function(col,posts){
new App.Views.GuideView({collection: posts});
},
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
Since I really don't know how complicated your page is going to get, this is probably the best I can do without more information. But, the idea is that "this.collection" is set on the routers initialization.. so you can keep reusing it. The _showPage method does whatever basic tasks you need done to show the page, and the methods called by the routes use it to do whatever basic stuff needs done before going into detail. The url passed into the config would simply tell the collection where to get its information from - I'm assuming that all of your data has the same format and 'is the same thing'.. just different filters.
You can probably do a similar thing with App.Views.GuideView:
App.Controllers.Documents = Backbone.Router.extend({
routes:{
"recent" : "recent",
"popular" : "popular",
"featured" : "featured",
},
initialize: function () {
this.collection = new App.Collections.Posts();
this.view = new App.Views.GuideView({collection: this.collection});
},
_showPage: function (config) {
$("#browser").empty();
this.collection.fetch({
url: config.url,
success: _.bind(function(col,posts){
this.view.render();
}, this),
error: function(error){
console.log(error)
}
});
},
recent: function(){
this._showPage({url:'http://localhost:5000/json/posts/recent'});
},
popular: function(){
this._showPage({url:'http://localhost:5000/json/posts/popular'});
},
featured: function(){
this._showPage({url:'http://localhost:5000/json/posts/featured'});
}
});
The 'render' would just rebuild the view, and since you've already got the collection referenced in the view as "this.options.collection" (or you could add an 'initialize' to the view and set this.collection to be this.options.collection). When the collection gets updated, all of that information is by reference in the view.. so no need to reset it.
I think the best pratice would be to have 3 collections, each with it's on URL and properties.
This makes the code easier to maintain as you can assign different events and listeners to them in a separate file instead of having a "God Collection" that have all the logic inside it.
Of course you can still be DRY and keep a helper object or a parent collection with code that is commmon to all those collections.
I have a parent view ProductListView containing multiple child views ProductView in a multi-step wizard. When a user click on a ProductView, its model's id should be stored somewhere (possibly in an array) so that it can be sent back to the server side for processing.
Problem: Where should I store the id of the ProductView that has been clicked by the user? I tried storing it in its parent view ProductListView but cannot seem to access the array selectedProducts in the parent view from the child view ProductView.
Is this the correct approach? How should this be done?
Model
ProductCollection = Backbone.Collection.extend({
model: Product,
url: '/wizard'
});
Parent View
ProductListView = Backbone.View.extend({
el: '#photo_list',
selectedProducts: {}, // STORING SELECTED PRODUCTS IN THIS ARRAY
initialize: function() {
this.collection.bind('reset', this.render, this);
},
render: function() {
this.collection.each(function(product, index){
$(this.el).append(new ProductView({ model: product }).render().el);
}, this);
return this;
}
});
Child View
ProductView = Backbone.View.extend({
tagname: 'div',
className: 'photo_box',
events: {
'click': 'toggleSelection'
},
template: _.template($('#tpl-PhotoListItemView').html()),
render: function() {
this.$el.html(this.template( this.model.toJSON() ));
return this;
},
// ADDS ITS MODEL'S ID TO ARRAY
toggleSelection: function() {
this.parent.selectedProducts.push(this.model.id);
console.log(this.parent.selectedProducts);
}
});
I don't think parent is a property of a backbone View type, and you haven't defined it, so there's no way this line is going to work:
this.parent.selectedProducts.push(this.model.id);
It seems like the correct approach would be to add a selected property to the Product model; toggle that property in the click handler. Then, when it's time to submit to the server, collect the IDs by filtering the Products collection for selected items (underscore.js included with Backbone makes this easy).
Why not try to keep selected information, directly in model. So, you will be easily tracking change state of selected using events, and use that information on further wizard steps?
toggleSelection: function () {
this.model.set({ selected: true });
}
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))