Adding items from View to an array (backbone.js) - javascript

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 });
}

Related

Using tagName and className properties on Backbone View Vs using template

I have a Backbone View that is using a Backbone Collection to pull data from an api:
var HousesView = Backbone.View.extend({
initialize: function() {
this.collection = new Houses();
var that = this;
this.collection.fetch({
data: {
pageSize: 50
},
success: function () {
that.render();
},
error: function () {
console.error('There was an error in fetch');
}
});
},
tagName: 'section',
template: Handlebars.getTemplate('houses'),
render: function() {
this.$el.html(this.template({ houses : this.collection.toJSON() }));
return this;
}
});
It then creates models from the json pulled from the api and passes that as a collection to my template which takes each model and for e.g.just outputs the name attribute of that model as a list. That works fine.
My question is: because I am using a template to parse the data from this.collection.toJSON() the tagName I set on HouseView doesn't seem to be output in the html. If I am using a template for a View does this mean properties like tagName, className etc wont be output?
Also, ideally I would like to create a HouseView for each model from the collection and then display all of those in a wrapper HousesView.
In your example you are using custom rendered. In that case the auto generation of view from tagName, className and id will be spiked (overwrites by method this.$el.html in render function.).
The best approach is as you mentioned to create separate view and append it in render of main view to html.

Multiple times rendered collection in Backbone app

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

Backbone change event not firing when certain attributes are changed

In my backbone application, I have a model that looks a little like this,
{
"id" : 145,
"name" : "Group Number 1",
"information" : "Some kind of blurb about group number 1",
"members" : {[
"id" : 1,
"first_name" : "John",
"last_name" : "Doe",
"email" : "johndoe#goog.ecom"
]}
}
Now if I run this.model.get('members').add(newUser) a new user gets added to the members collection within my model - however it does not fire a change event, why is this? Buy yet if I change the name of the model, then a change event is fired?
All this is done with a view that looks like this,
Individual model view
Views.OrganisationView = Backbone.View.extend({
tagName: 'div',
className:'group group--panel col-sm-3',
template : _.template( $('#tpl-single-group').html() ),
events: {
"click a[data-type=organisation], button[data-type=organisation]" : "edit",
"click .js-delete-group" : "removeOrganisation",
},
initialize: function() {
this.model.on("error", function(model, xhr, options){
console.log(model, xhr, options);
console.log(this);
});
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.removeView);
},
render: function() {
this.$el.html( this.template({
group: this.model.toJSON()
}));
return this;
},
removeView: function() {
this.remove();
},
removeOrganisation: function(e) {
this.model.destory();
this.remove();
},
edit: function(e) {
e.preventDefault();
Routes.Application.navigate('/organisation/edit/' + this.model.get('id'), { trigger: false } );
var editClient = new Views.OrganisastionEditView({
model: this.model
});
}
});
The second confusing thing that a request event gets thrown, (makes sense seen as though I am saving the model, but an error event gets thrown as well, but there are no errors the xhr and I am not currently validating the model?
Here is how I am saving the user to members collection in my model,
var member = new Pops.Models.User({ id: element.data('id') });
member.fetch({
success:function() {
self.model.get('members').add(member);
var model = self.model;
self.$('.search').hide();
self.$('button').show();
var projectMember = new Pops.Views.UserInitialsWithAdmin({
model: member
});
self.model.save({validate:false});
self.$('.search').parent().append( projectMember.render().el );
self.$('.search').remove();
}
});
(I'm assuming the first bit of code you've given is just a guideline of what a plain JSON representation of your model would look like, and that members is a real Collection with an add method available.)
In answer to the first question: change events are only fired when changing a model attribute using set. In your case, you're adding to the collection stored in the members attribute, but the members attribute still contains a reference to the same collection it did before, which means from Backbone's perspective this attribute has not changed. I would suggest attaching listeners directly to the members collection. Also see How can I "bubble up" events on nested Backbone collections?.
In general nesting models in Backbone is not straightforward, as Jeremy Ashkenas has pointed out. It's often better to keep models flat and store references to related models as arrays of ids, which can then be fetched as necessary.

Individual view from an item in an ArrayController

I have an ember.js model and controller setup like so:
//model
App.Order = Ember.Object.extend({
content: null,
create: function(data) {
this.set('content', data);
return this._super();
}
});
//orders controller
App.ordersController = Ember.ArrayController.create({
content: [],
init: function() {
var self = this;
var orders = [];
$.getJSON('js/data.json', function(data) {
data.forEach(function(item) {
var order = App.Order.create(item);
orders.push(order);
});
self.set('content', orders);
});
},
selectItem: function(data) {
alert(data);
}
});
With the following view:
{{#each App.ordersController}}
<div {{action selectItem target="App.ordersController"}}>{{order_number}}</div>
{{/each}}
Which prints out a list of orders with a click action that alerts the corresponding item. This works fine.
What I want to do is show a clicked item in a separate view, eventually with the goal of creating a floating dialogue with orders details shown. I'm new to ember and not sure how this should be implemented. I have a function selectItem which alert's a clicked order but I need to link this to a separate view and print the order details.
Should I store the selected item in a separate controller with a corresponding view and update this when selectItem is clicked? Or could I update a sperate view from the ordersController? Any advice is much appreciated.
When you use the router ember does the instantiation of your class for you. By specifing the "orders" route is looks for a template called orders and a controller called OrdersController if it can't find one it'll generate one for you. (I've omitted the controller for clearity). To load your model from a json source you could have a look at ember-data.
here is a jsfiddle so you can fiddle with it a bit.
You should definitely have a look here these are guides for ember that really help you on the way. The documentation is getting better and better. :)
JS:
window.App = App = Em.Application.create();
//model
App.Order = Ember.Object.extend({
order_number: null,
});
//we create 2 routes one for all the order and one per specific order
App.Router.map(function(){
this.resource('orders', { path: "/" });
this.resource("order", { path: "/:order_id" });
});
//the route supplies the model and handles the event to transition to a new route.
App.OrdersRoute = Em.Route.extend({
events: {
selectItem: function (orderId) {
//select the order by the "orderId" you want as a model for your new view.
this.transitionTo("order", order);
}
},
model: function(){
return content; //an array containing the orders;
}
});
//supplies the model for the "order" route by selecting one acording to the params;
App.OrderRoute = Em.Route.extend({
model: function(params){
return order; //select an object from the array according to the params
},
});
HBS:
<script type="text/x-handlebars" data-template-name="orders">
{{#each controller}}
<!-- this calls the event handler "selectItem" on the ordersroute -->
<div {{action "selectItem" order_number}}>{{order_number}}</div>
{{/each}}
<!-- this is handled by "App.OrderRoute" -->
<a href="#/3"/>with a direct link</a>
</script>
<script type="text/x-handlebars" data-template-name="order">
{{content.order_number}}
{{#linkTo "orders"}}Back to orders{{/linkTo}}
</script>

Backbone Collection refresh all items

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
*/
}

Categories

Resources