Backbone render after cancelling - backbone.js

I have the following backbone view. I had a doubt. In case if a model is deleted, i call the render after cancel(First Approach), the other way of doing it would be to have an initialize function, which renders the model listening to the event changes, inside the views.(Second Approach)
Could someone please let me know, the difference between one and two. As to which of the two is better.
First Approach
var AppointmentView = Backbone.View.extend({
template: _.template('">' +
'<%= title %>' +
'x'),
events: { "click a": "cancel" },
cancel: function(){
this.model.cancel();
this.render(); // rendering after cancel
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
}
});
Second Approach
var AppointmentView = Backbone.View.extend({
template: _.template('<span class="<% if(cancelled) print("cancelled") %>">' +
'<%= title %></span>' +
'x'),
initialize: function(){
this.model.on("change", this.render, this);
},
events: { "click a": "cancel" },
cancel: function(){
this.model.cancel();
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
}
});

I would define a custom cancelled event, trigger that from your cancel method, and bind to that event in the view.
var Appointment = Backbone.Model.extend({
cancel: function() {
//cancellation code...
this.trigger('cancelled', this);
}
});
var AppointmentView = Backbone.Model.extend({
initialize: function() {
this.listenTo(this.model, 'cancelled', this.render);
}
});
This way your view will re-render, even if the model is cancelled from elsewhere than the view itself, but you still get the specific behavior or only re-rendering upon cancel, and not on every change.

Related

backbonejs model.toJSON & render

I have some model and I want to bind render method to it on change. I'm trying to pass the model.toJSON to the render, but it doesn't work. However it works if I pass model and apply toJSON inside render.
(the whole code is here: http://plnkr.co/edit/xoeY4hexnqgHnkxap5uj?p=preview)
window.onload=function(){
var defaultModel = Backbone.Model.extend({
defaults: {
greeting: 'Hello, Dude',
content: 'Coming soon...'
}
}),
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.template = $('#tmpl-default').html();
this.render();
var _this = this;
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
$('[name="default-input"]').on('blur', function() {
console.log('got blurred....');
_this.model.set('content', this.value);
});
},
render: function(content) {
if (!content) {
console.log('%cno content', 'color: green');
content = this.model.toJSON();
}
this.$el.html(_.template(this.template)(content));
$('#content').html(this.$el);
return this;
}
}),
viewDefault = new defaultView({
model: new defaultModel()
});
};
the code above doesn't work. If I change
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
to
this.model.bind('change', _.bind(this.render, this, this.model));
and
if (!content) {
content = this.model.toJSON();
}
to
if (!content) {
content = this.model.toJSON();
}else{
content = content.toJSON();
}
But why?!
A more appropriate way is to use the listenTo function on the view, such as:
this.listenTo(this.model, "change", this.render);
I think the reason it doesn't work as you expect is because when you do
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
The argument this.model.toJSON() passed to render method will always be the initial state of the model at the point when _.bind was called.
When you do content = this.model.toJSON(); inside render method, you get the current state, including the expected changes that triggered render.
You can better structure your view like this:
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.render();
this.model.on('change', _.bind(this.render, this));
},
template: _.template($('#tmpl-default').html()),
events: {
'blur [name="default-input"]': 'eventHandler'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
$('#content').html(this.$el);
return this;
},
eventHandler: function(event) {
var val = $(event.currentTarget).val();
this.model.set('content', val);
}
});
Also, look into listenTo than on like #Jayem suggested to aviod unexpected memory leak issues

Multiple Views and Sub Views with 1 collection in Backbone

I have issue in rendering Shopping Bag Views using Backbone for my website.
I am using 1 collection for all Bag Views (“Quick_View” of items list & “Normal_View” of Items list). Also I created “Item_View”, which is being used to render each item in both the Views.
It is a SPA (Single Page Application) and “Quick_View” is initiated and rendered for all Backbone routes and hidden by default. Whenever user clicks on “Quick_View” link from any page it is showing. There is no route defined for it.
The “Normal_View”, can be accessed using Checkout button given in “Quick_View”. It is bind with “domain/checkout” route.
When I access “Normal_View” from “Quick_View” check button; it works fine and both (Quick and Normal) views are in Sync. Means, when we add, delete, update any item in any of the View, both Views are getting updated accordingly.
But when I access “domain/checkout” route directly in a new browser, both views are getting rendered fine, but they are not in sync. Means, change in 1 view does not update another view.
The reason, I tracked is, when I access “Normal_View” through “Quick_View”, model for each item in both the Views having same CID, so the both Views are in sync, if there is any change in a model from any of the View.
And, when I access “Normal_View” directly, model for each item in both the views are not having same CID, so they do not work as expected.
There are few more points to consider:
Collection is firing reset event twice for “Quick_View” and each item
in “Quick_View” is rendering twice.
When, I access “Normal_View” (in either way), “Quick_View” is again getting rendered but once “Normal_View” rendering is over.
// Main View
var mainView = Backbone.View.extend({
el: 'body',
template: {
header: Handlebars.compile(headerTemplate),
mainNav: Handlebars.compile(mainNavtemplate),
footer: Handlebars.compile(footerTemplate)
},
initialize: function() {
_.bindAll();
AW.collection.bag = new bagCollection();
//AW.collection.bag.fetch({reset:true});
},
render: function() {
this.$el.html(this.template());
this.loadSubView('bagQV');
},
loadSubView: function(subView) {
switch(subView) {
case 'home' :
if(!AW.view.home) AW.view.home = new homepageView();
AW.view.home.render();
break;
case 'bagQV' :
if(!AW.view.bagQV) AW.view.bagQV = new bagQuickView({collection: AW.collection.bag});
//AW.view.bagQV.render();
break;
case 'checkout' :
if(!AW.view.checkout) AW.view.checkout = new checkoutView({collection: AW.collection.bag});
AW.view.checkout.render();
break;
}
}
});
// Single Item View
var bagItemView = Backbone.View.extend({
tagName: 'tr',
template: Handlebars.compile(bagItemTemplate),
initialize: function() {
_.bindAll(this);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'remove', this.removeItem);
$(document).on('keyup', this.listenKeyboard);
},
events: {
'click .qtyInput .button' : 'updateItem',
'click .controls a.remove' : 'removeModel'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.attr('data-id',this.model.cid);
return this;
},
updateItem: function(e) {
e.preventDefault();
e.stopPropagation();
var newQty = this.$el.find('.qtyInput input').val();
var newAmt = newQty * parseFloat(this.model.get('prodRate').replace('$',''));
this.model.set({prodQty: newQty, amount: '$' + newAmt});
this.cancelEdit(e);
},
removeModel: function(e) {
e.preventDefault();
e.stopPropagation();
if(AW.collection.bag) AW.collection.bag.remove(this.model);
},
removeItem: function() {
this.$el.remove();
}
});
// Bag Quick View
var bagQuickView = Backbone.View.extend({
tagName: 'div',
id: 'myBagQV',
template: Handlebars.compile(bagQuickViewTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
if($('#myBagQV').length == 0) {
this.$el.html(this.template());
$('body').append(this.el);
}
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
$('#itemsInBag table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
$('#itemsInBag table tbody').html('');
this.collection.each(this.addItem, this);
}
},
});
// Normal Bag View
var bagView = Backbone.View.extend({
tagName: 'div',
id: 'myBag',
template: Handlebars.compile(checkoutBagTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.$el.html(this.template());
$('#checkoutContainer #details').append(this.el);
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
this.$el.find('table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
this.$el.find('table tbody').html('');
this.collection.each(this.addItem, this);
}
}
});
Looking for you help.
Thank you in advance
Cheers,
Vikram

detect select status of options in a multiselect with backbone.picky

I have a multiselect dropdown that I'm rendering with Backbone. As user selects or deselects options, I'd like those (de)selections to be saved asynchronously via Backbone.
I found Backbone.Picky, and thought it might be helpful in my endeavor, but I can't seem to get it to detect selects.
In my FieldView's clicked function below, console.log(this.model.selected); always writes undefined to the log. Why?
var Field = Backbone.Model.extend({
initialize: function(){
var selectable = new Backbone.Picky.Selectable(this);
_.extend(this, selectable);
}
});
var FieldView = Backbone.View.extend({
tagName: "option",
initialize: function(){
_.bindAll(this, 'render');
},
events: {
"click":"clicked"
},
clicked: function(e) {
var data_type = this.model.get("DATA_TYPE");
console.log(this.model.selected); // why is this undefined?
console.log("it's a " + data_type);
},
render: function(){
this.$el.attr('value', this.model.get('COLUMN_NAME')).html(this.model.get('display_name'));
return this;
}
});
Here's a jsfiddle http://jsfiddle.net/EAZCt/2/ for more context.
Using Backbone, how can I asynchronously save the select-status of options in a multiselect list?
Your model object doesn't ever have "selected" property because you never select the model. I have never used Backbone.Picky but it seems that you could try:
var FieldView = Backbone.View.extend({
tagName: "option",
initialize: function(){
this.model.on('selected', this.selected, this);
},
events: {
"click":"clicked"
},
clicked: function() {
this.model.select();
},
selected: function() {
var data_type = this.model.get("DATA_TYPE");
console.log(this.model.selected);
console.log("it's a " + data_type);
},
render: function(){
this.$el.attr('value', this.model.get('COLUMN_NAME')).html(this.model.get('display_name'));
return this;
}
});
http://jsfiddle.net/hGEYL/

Backbone.js fire event on .remove()

I need to fire a function when a Backbone.js view is removed. I guess something like a de-constructor. The code below is a snippet I wrote; I know it won't work. However, I do remember seeing in the past a video tutorial on how to write a function that does this. The reason I need to run a de-constructing function is to clear the interval set inside a view when the view is removed.
ViewWeather = Backbone.View.extend({
interval: setInterval(function() {console.log('interval fire');}, 1000),
// made up function
deconstructor: function () {
// if the view is removed
clearInterval(this.interval);
}
});
var viewweather = new ViewWeather();
This blog post should give you some better info
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
most notable parts
Backbone.View.prototype.close = function(){
this.remove();
this.unbind();
if (this.onClose){
this.onClose();
}
}
and then
MyView = Backbone.View.extend({
initialize: function(){
this.model.bind("change", this.render, this);
},
render: function(){ ... },
onClose: function(){
this.model.unbind("change", this.render);
}
});
i am not sure if I understand you correctly, but the approach you are showing seems correct:
dispose: function(id) {
clearInterval(this.interval);
this.remove();
}
you are going to have to call dispose yourself, by e.g. an event:
initialize: function(opts) {
router.bind('route:leave', this.dispose, this);
},
edit after comments: this should work to overload remove
remove: function() {
clearInterval(this.interval);
Backbone.View.prototype.remove.call(this);
}

Backbone.js: how to perform garbage collection on parent views as well as child views

I have implemented a simple close() method for all the Backbone views which disposes of a view when its not needed/needs to be reset.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove();
this.unbind();
};
NewView = Backbone.View.extend({
el: '#List ul',
initialize: function() {},
render: function() {
_(this.collection.models).each(function(item) {
this.renderChildren(item);
}, this);
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
},
onClose: function() {
this.collection.reset();
// I want to remove the child views as well
}
});
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
}
});
Now, when I remove the parent view, I also want to remove all the child views here. Any ideas how can I can do this without looping through the models like this....
_(this.collection.models).each(function(item) {
item.close();
}, this);
I think in most of the cases you should keep the view removal in the view layer, without affecting your models.
For example, if you remove a view with comments, maybe another view in your app shows a selection of comments, or some statistics, and resetting the collection would affect those views too.
So I think you should keep it all in the view (only relevant methods included):
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
}
});

Resources