Backbone View Events Not Firing, EL exists - backbone.js

I've been through similar discussions and still had no clue why my events are not firing. It seems I lack some fundamental understanding about how backbone events work.
Here is my code:
(function() {
MP.IndicatorView = Backbone.View.extend({
template: JST['backbone/templates/indicator'],
className: 'row indicator',
initialize: function() {
console.log(this);
console.log(this.el);
_.bindAll(this, 'collapse');
},
events: {
"click .control" : "collapse"
},
collapse: function (e) {
console.log('shit');
var $el = $( e.target );
$el.toggleClass( 'expand' );
var $panelToPoke = $el.
parents( '.top-container' ).
siblings( '.bottom-container' )
, $parentPanel = $panelToPoke.parents('.indicator')
, isVisible = $panelToPoke.is( ':visible' );
$parentPanel.toggleClass( 'expanded' );
isVisible ? $panelToPoke.slideUp() : $panelToPoke.slideDown();
},
render: function () {
// Don't show offers that have no transactions
if (this.model.get('transactionCount') > 0) {
this.$el.html( this.template(this.model.for_template()) );
}
return this.$el;
}
});
MP.IndicatorsView = Backbone.View.extend({
el: '#indicators',
render: function () {
var view;
var subViews = this.collection.reduce(function ( memo, indicator ) {
view = new MP.IndicatorView({ model: indicator });
memo += view.render().html();
return memo
}, '');
this.$el.html( subViews );
return this;
}
});
})();
How views are instantiated:
var indicators = new MP.Indicators( coordinator.dump() );
var indicatorsView = new MP.IndicatorsView({ collection: indicators });
indicatorsView.render();
Template:
Views:
<div class="row indicator">
<div class='top-container'>
<ul class="inline-list mainpanel">
<li>
<div class='control expand'></div></li>
<li class="state <%= is_active ? 'active' : 'expired' %>"></li>

Here is a working solution http://jsfiddle.net/hcKv2/
The main problem was that you are used .html() instead of backbone view element .$el inside of your indicatorsView.render() method.
You need to return this inside of render methods to achieve chain pattern, but it is not necessary it is ok if you are returning $el in child view.
render: function () {
var view;
var subViews = this.collection.reduce(function ( memo, indicator ) {
view = new MP.IndicatorView({ model: indicator });
//memo += view.render().html();
this.$el.append( view.render() );
// or this.$el.append( view.render().$el );
// if you return this inside render method
return memo
}, '');
//this.$el.html( subViews );
return this;
}
Have a good coding.

some reference to furthur explain why:
http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/

Related

Backbone Views listening to collection events is firing multiple times

I'm facing a very weird problem using ListenTo on subviews.
Basically I've a main view that contains multiple subviews.
Those subviews are initialized when their parent view is also initialized.
Some of those subviews are listening to the same global collection.
One of those views is a form that allow me to insert new entries into the collection
The second one is a list of all those entries contained into the collection.
So, basically it looks like this
$('button').on('click'....
app.views.MyMainView = new MyMainView()
//launch new modal window
//with html body = app.views.MyMainView.el
var MyMainView = Backbone.view.extend({
initialize: function(){
// new Form_View()
// new List_View()
}
});
var Form_View = Backbone.view.extend({
//get input value
// create new entrie into the collection
});
var List_View = Backbone.view.extend({
initialize: function(){
// listenTo(this.collection, 'add', this.addOne)
// new List_View()
this.addAll();
},
addAll: function(){...},
addOne: function(model){ .... }
});
The problem is the following:
When the user launch the modal for the first time and we add a new entry to the collection
the listenTo add on the List_View fires as expected.
If I close the modal and the user clicks on 'button' to launch the modal window once again if I add a new entrie the view will fire 2 times the function that is listening to the collection add event.
If i close the modal again and re-open it the function will fire 3 times and so on.
This is weird because I'm creating a new instance of the view and their subviews everytime the user clicks on the 'button'. That's why it doesn't make sense to me.
Any help?
EDIT
I also checked my collection by listening to the 'add' event inside of it.
var MyCollection = Backbone.View.extend({
initialize: function(){
this.listenTo( this, 'add', doSomething );
},
doSomething: function( model ){
//do something fires as it should be firing the event: 1 time per each item inserted
}
})
EDIT 2
var MyMainView = Backbone.View.extend({
.....
close_modal: function(e){
if(e){
e.preventDefault();
}
var viewsLen = this.views.length,
_that = this;
_.each(this.views, function(view, key){
view.remove();
if(key + 1 == viewsLen )
_that.dialog.close();
})
}
...
})
EDIT 3:
ALL CODE
//initialize modal
$('button').on('click', function(){
app.views.storePayment_View = new StorePayment_View();
})
var TMPL_StorePayment = '<div class="store-payment">'
+ '<div class="store-payment-header">'
+ '<div class="client"></div>'
+ '<div class="status"></div>'
+ '</div>'
+ '<div class="payment-form"></div>'
+ '<div class="payment-list"></div>'
+ '<div class="x-form-actions">'
+ '</div>'
+ '</div>';
var StorePayment_View = Backbone.View.extend({
views: {},
wrappers: {},
collections: {},
events: {
"click .back": "close_modal",
"click .finish-payment": "finish_payment"
},
initialize: function(){
var _that = this;
this.dialog = new BootstrapDialog({
title: appLang["h67"],
message: this.$el,
closable: true,
onhide: function(dialogRef){
_that.remove();
}
});
this.dialog.realize();
this.dialog.getModalFooter().hide();
this.dialog.open();
this.$el.html('').append( TMPL_StorePayment );
this.wrappers.$client = this.$el.find('.client');
this.wrappers.$status = this.$el.find('.status');
this.wrappers.$payment_form = this.$el.find('.payment-form');
this.wrappers.$payment_list = this.$el.find('.payment-list');
this.wrappers.$form_actions = this.$el.find('.x-form-actions');
this.render()
},
render: function(){
this.views.StorePaymentForm_View = new StorePaymentForm_View();
this.wrappers.$payment_form.html('').append( this.views.StorePaymentForm_View.el );
this.views.StorePaymentList_View = new StorePaymentList_View();
this.wrappers.$payment_list.html('').append( this.views.StorePaymentList_View.el );
},
close_modal: function(e){
if(e){
e.preventDefault();
}
var viewsLen = this.views.length,
_that = this;
_.each(this.views, function(view, key){
view.remove();
if(key + 1 == viewsLen ){
_that.dialog.close();
}
})
}
})
var StorePaymentForm_View = Backbone.View.extend({
error_tmpl: _.template('<div class="alert alert-warning"><%= message %></div>') ,
template: _.template('<div> <div class="input-field"> <input type="text" class="montant form-control" value="<%= restant %>"> </div> <div class="input-select"> <select name="payment-type"><% _.each(paymentTypeList, function(paymentType){ %> <option value="<%= paymentType.typeMode %>"><%= paymentType.libelle %></option> <% }) %></select> </div> <div class="actions">Add newRemove All </div></div><div class="error_placeholder"></div>'),
events:{
"click .add-new": "add_new",
"click .remove-all": "remove_all"
},
initialize: function(){
this.collection = app.collections.StorePaymentList;
this.listenTo( this.collection, 'add', this.render )
this.listenTo( this.collection, 'destroy', this.render )
this.listenTo( this.collection, 'change', this.render )
this.listenTo( this.collection, 'reset', this.render )
this.render()
},
render: function(){
console.log("RENDER FIRED ON STOREPAYMENTFORM")
var restant = this.collection.getRestant();
if ( restant <= 0){
restant = 0;
}
this.$el.html('').append( this.template( { "restant" : restant, "paymentTypeList": app.collections.PaymentTypeList.toJSON() } ) )
var _that = this;
setTimeout(function(){
_that.$el.find('select').selectBoxIt({
native: true,
autoWidth: false
})
_that.$el.find('input').focus();
}, 50 )
},
add_new: function(e){
console.log("add_new");
if(e){
e.preventDefault();
}
var _that = this,
input_val = this.$el.find('input').val(),
select_val = this.$el.find('select :selected').val(),
libelle = this.$el.find('select :selected').text(),
wasNaN = false;
input_val = parseInt(input_val);
if (isNaN(input_val)){
wasNaN = true;
input_val = 0;
}
if (wasNaN){
_that.$el.find('.error_placeholder').html( _that.error_tmpl( { "message": appLang["h69"] } ) );
} else {
if ( input_val <= 0 ){
_that.$el.find('.error_placeholder').html( _that.error_tmpl( { "message": appLang["h70"] } ) );
} else {
this.collection.add( new StorePaymentModel( { "libelle": libelle , "paymentId": select_val, "montant": input_val } ) )
}
}
},
remove_all: function(e){
if(e){
e.preventDefault();
}
var _that = this;
//dialog are you sure?
var dialog = new BootstrapDialog({
title: "Do you want to continue",
message: "Do you really want to empty your current list of payments?",
buttons: [{
label: appLang["a187"], //cancel
action: function(dialog) {
dialog.close();
}
}, {
label: appLang["a1621"], //ok
cssClass: 'btn-primary',
action: function(dialog) {
_that.collection.reset([]);
dialog.close();
}
}]
})
dialog.realize();
dialog.open();
}
})
var StorePaymentListItem_View = Backbone.View.extend({
events:{
"click .remove": "remove_item",
"click .save": "save"
},
template: _.template( '<%= libelle %> <%= montant %> <i class="fa fa-trash-o"></i>' ),
tagName: 'li',
className: 'list-group-item',
initialize: function(){
this.render()
//console.log("StorePaymentListItem_View initialized")
this.listenTo( this.model, 'hide', this.remove )
},
render: function(){
this.$el.html('').append( this.template( this.model.toJSON() ) )
},
edit: function(){
this.render_edit();
},
save: function(e){
if (e){
e.preventDefault(); e.stopPropagation();
}
this.render();
},
remove_item: function(e){
if (e){
e.preventDefault(); e.stopPropagation();
}
this.model.destroy();
}
})
var StorePaymentList_View = Backbone.View.extend({
$wrapper: $('<ul />', {'class': 'list-group' }),
initialize: function(){
this.$el.html('');
this.collection = app.collections.StorePaymentList;
this.listenTo( this.collection , 'add', this.addOne );
this.listenTo( this.collection , 'change', this.render );
this.listenTo( this.collection , 'reset', this.render );
this.render()
},
render: function(){
var totalItems = this.collection.length;
this.$wrapper.html('')
if (totalItems == 0){
this.appendToRoot();
} else {
this.addAll()
}
},
addAll:function(){
var _that = this,
totalItems = this.collection.length;
this.collection.forEach(function(model, key){
_that.addOne(model)
if (totalItems == key + 1)
_that.appendToRoot();
})
},
addOne:function( model ){
var storePaymentListItem_View = new StorePaymentListItem_View({ model: model });
this.$wrapper.append( storePaymentListItem_View.el );
},
appendToRoot:function(){
this.$el.html('').append( this.$wrapper );
}
})
My guess is closing the modal doesn't call view.remove, and so we end up with a zombie view that is removed from the DOM but still lives in memory listening to events.
Another guess is you never remove the List_View instance, so they are the zombies.
In other words, this is likely to be related to insufficient garbage collection.
This is a guess - it's impossible to tell without seeing the relevant parts of the code.

simple Backbone event not firing

I have the following view and I'm trying to bind a click event to a button with the id of #add. However, it won't even register the preventDefault function.
var app = app || {};
app.LibraryView = Backbone.View.extend({
el: '#books',
events: {
'click #add' : 'addBook'
},
initialize: function() {
this.collection = new app.Library();
this.render();
this.listenTo( this.collection, 'add', this.renderBook);
},
render: function() {
this.collection.each(function( item ) {
this.renderBook( item );
}, this );
},
renderBook: function( item ) {
var bookView = new app.BookView({
model: item
});
this.$el.append( bookView.render().el );
},
addBook: function(e) {
e.preventDefault();
var formData = {};
$('#addBook div').children('input').each(function(index, el) {
if($(el).val() != '') {
formData[el.id] = $(el).val();
}
});
this.collection.add(new app.Book(formData));
console.log("book added");
}
});
Also, I can call renderBook in the console and it will add a new bookView, however, the render function doesn't seem to be working when the page loads initially.
Here is my html in jade.
block content
#books
form#addBook(action'#')
div
label(for='coverImage').span5 Image URL:
input#coverImage(type='text')
label(for='title').span5 Title:
input#title(type='text').span5
label(for='author') Author:
input#author(type='text').span5
label(for='date') Date:
input#date(type='text').span5
label(for='tags') Tags:
input#tags(type='text').span5
button#add Add
.bookContainer
ul
li Title
li Author
li Date
button Delete
You have to instantiate your view new app.LibraryView() in order for your view's events to be binds.

Backbone.js model.save() fire a "too much recursion" error in underscore

I've got a problem trying to use backbone on saving my Model from a form. Here I want my my view to actually be an editing form:
(function() {
'use strict';
var YachtEditor = {};
window.YachtEditor = YachtEditor;
var template = function(name) {
return Mustache.compile($('#' + name + 'Template').html());
};
YachtEditor.Tank = Backbone.Model.extend({
defaults : {
dCapacity : "",
sType : ""
}
});
YachtEditor.Tanks = Backbone.Collection.extend({
// url: "/rest/tanks",
localStorage: new Store("tanks"),
model : YachtEditor.Tank
});
YachtEditor.TankView = Backbone.View.extend({
template: template("tank"),
events: {
'click .save' : 'save',
'click .remove' : 'remove'
},
initialize: function() {
console.log("initialize tank View :");
console.log(this.model.get("id"));
},
render: function() {
this.$el.html(this.template(this));
return this;
},
save: function() {
console.log('change');
var self = this;
var values = {
sType: self.$("#sType").val(),
dCapacity: self.$("#dCapacity").val()
};
console.log("dCapacity : " + values.dCapacity);
console.log("sType : " + values.sType);
this.model.save(values);
},
remove: function() {
this.model.destroy();
},
dCapacity : function() {
return this.model.get("dCapacity");
},
sType : function() {
return this.model.get("sType");
}
});
YachtEditor.TanksView = Backbone.View.extend({
el: $("div.tankZone"),
template: template("tanks"),
events: {
"click .add" : "addTank",
"click .clear" : "clear"
},
initialize: function() {
this.tanks = new YachtEditor.Tanks();
// this.tanks.on('all', this.render, this);
this.tanks.fetch();
this.render();
},
render: function() {
this.$el.html(this.template(this));
this.tanks.each(this.renderTank, this);
return this;
},
renderTank: function(tank) {
var view = new YachtEditor.TankView({model: tank});
$(".tanks").append(view.render().el);
return this;
},
addTank: function() {
this.tanks.create({});
this.render();
},
clear: function() {
this.tanks.each(function(tank) {
tank.destroy();
});
this.render();
}
});
...
})();
Here is the mustache template i use for each tank
<script id="tankTemplate" type="text/x-mustache-template">
<div class="tankView">
<h1>Tank</h1>
<select id="sType" value="{{ sType }}">
#for(option <- Tank.Type.values().toList) {
<option>#option.toString</option>
}
</select>
<input id="dCapacity" type="text" value="{{ dCapacity }}">
<button class="destroy">x</button>
</div>
</script>
My problem here is that this.model.save() triggers a 'too much recursion' in underscore. js. (chrome is displaying an error also.
Here is the call stack on error:
_.extend
_.clone
_.extend.toJSON
_.extend.save
_.extend.update
Backbone.sync
_.extend.sync
_.extend.save
YachtEditor.TankView.Backbone.View.extend.save
st.event.dispatch
y.handle
I suspect the save to recall the blur event but i cannot find a way to explicit it... Maybe I'm not using backbone as i should?
My problem, aside of some pointed out by Yurui Ray Zhang (thank you), was that I was using a backbone-localstorage.js from an example I found here : git://github.com/ngauthier/intro-to-backbone-js.git
The "too much recursion error" stopped to appear as soon a I replaced it with a storage I found here : https://github.com/jeromegn/Backbone.localStorage
a few things. you defined your tank model as
app.Tank = ...
but in your collection you are referencing it as:
model : YachtEditor.Tank
and in your view, you are trying to assign elements before they are rendered on the page:
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
I'm not sure how your view is rendered to the page, some people, like me, like to use render() to render the template directly to the page:
render: function() {
this.$el.html(this.template(this));
//done, you should be able to see the form on the page now.
},
some others, will use something else to insert the el, eg:
//in another view
tankView.render().$el.appendTo('body');
but either way, if you want to cache your elements, you need to do it after they are rendered to the page, not in initialize.
//this method is only called after render() is called!
cacheElements: function() {
this.input = {}
this.input.sType = this.$("#sType");
this.input.dCapacity = this.$("#dCapacity");
}
I'd suggest, first, try to fix this things, and then, try to add some console log or debuggers in your readForm method to see if the values are grabbed correctly:
readForm: function() {
var input = this.input;
console.log(input.sType.val());
console.log(input.dCapacity.val());
this.model.save({
sType: input.sType.val(),
dCapacity: input.dCapacity.val()
});
},

Backbone appends to list view instead of replacing it

I am producing a single page website with Wordpress and Backbone.js, i have come into a problem with when i fetch new data. It simply adds DOM elements onto the container el rather than replacing them. The collection updates correctly as i can see it has the right amount of elements in the console.
var PostItem = Backbone.Model.extend();
var PostItems = Backbone.Collection.extend({
model: PostItem,
url: '/wp-admin/admin-ajax.php'
});
var postItems = new PostItems();
var PostView = Backbone.View.extend({ /* Model View */
tagName : 'article',
className : 'widget',
template : _.template( $('#widgetPost').html() ),
render: function(){
var attributes = this.model.toJSON();
this.$el.html( this.template( attributes ) );
return this;
}
});
var PostListView = Backbone.View.extend({ /* Collection View */
el : '#content',
initialize: function(){
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.addAll, this);
},
addOne: function(postItem){
var postView = new PostView({ model : postItem });
this.$el.append( postView.render().el );
},
addAll: function(){
this.collection.forEach(this.addOne, this);
},
render: function(){
this.addAll();
},
});
var postListView = new PostListView({
collection : postItems
});
$(function(){
$('a#posts').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_posts'
}
});
return false;
});
$('a#pages').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_pages'
}
});
return false;
});
});
You need to clear out your collectionView's $el! :)
addAll: function(){
this.$el.empty();
this.collection.forEach(this.addOne, this);
}
This should do the trick.

_.delay call from inside a Backbone.View event handler

I have a relatively simple Backbone handled message container. I can add messages, and they display fine, but adding a delay to close them automatically does not work as undefined is passed on to the callback.
Could someone tell me how to properly specify the argument to be called? The main line is _.delay(function(){ messages.remove(this.model) }, 3000);
var Message = Backbone.Model.extend({
defaults: {
message: 'No message',
type: 'error'
},
/*validate: function( attrs ) {
if ( !attrs.type in ['error', 'warning', 'success', 'info'] ) {
return 'Wrong message type given';
}
}*/
});
var Messages = Backbone.Collection.extend({
model: Message
})
var messages = new Messages;
var MessageView = Backbone.View.extend({
template: Handlebars.compile( $('#t-message').html() ),
initialize: function() {
messages.bind('delete', this.remove, this);
},
render: function( event ) {
$(this.el).html( this.template( this.model.toJSON() ) );
// TODO: this never fires properly
_.delay(function(){ messages.remove(this.model) }, 3000);
return this;
}
});
var MessageContainerView = Backbone.View.extend({
id: 'messages',
initialize: function() {
messages.bind('add', this.addMessage, this);
},
render: function( event ) {
return this;
},
addMessage: function( message ) {
var view = new MessageView({model: message});
$('#' + this.id).append(view.render().el);
}
});
var messagecontainerview = new MessageContainerView;
messages.add(new Message({message: 'Close this in 3 secs.', type: 'success'}))
Of course this.model is defined inside the render method, but it's undefined in the remove call.
It's because this is bound to whatever is calling the delay callback function, not the view. Before you call _.delay assign var self=this; Then use self.model in the callback function.

Resources