Checkbox still unchecked after triggering the event - backbone.js

I have the following views:
window.DmnView = Backbone.View.extend({
template: _.template($("#tmpl_dmnListItem").html()),
events: {
"click .getWhois": "showWhois",
"click .getDomain": "toBasket"
},
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
this.bind('toBasket', dmnListApp.toBasket, this);
},
render: function() {
return $(this.el)
.attr("class", this.model.get("free") ? "dmnItem green" : this.model.get("checked") ? "dmnItem red" : "dmnItem red loader")
.html(this.template(this.model.toJSON()));
},
remove: function() {
$(this.el).remove();
},
showWhois: function() {
showBoxes(this.model.get("info"));
return false;
},
toBasket: function() {
this.model.toBasket();
this.trigger('toBasket');
}
});
window.DmnListApp = Backbone.View.extend({
el: $("#regWrap"),
events: {
"keypress #dmnName": "checkAll"
},
initialize: function() {
this.input = this.$("#dmnName");
this.list = this.$("#dmnList");
this.basket = this.$("#dmnBasket");
dmnList.bind('add', this.addOne, this);
dmnList.bind('all', this.render, this);
},
render: function() {
},
addOne: function(dmnItem) {
var view = new DmnView({model : dmnItem});
this.list.append(view.render());
},
checkOne: function(name, zone, price, days) {
dmnList.create({name: name, zone: zone, price: price, days: days});
},
checkAll: function(e) {
var name = this.input.val();
if (!name || e.keyCode != 13) return;
if (name == "")
name = "yandex";
dmnList.destroyAll();
var zoneList = dmnList.domainsInfo.Name;
var costList = dmnList.domainsInfo.CostOrder;
var daysList = dmnList.domainsInfo.DaysToProlong;
var parent = this;
$.each(zoneList, function(key, zone) {
parent.checkOne(name, zone, costList[key], daysList[key]);
});
this.input.val("");
},
toBasket: function(){
if (this.model.get("inBasket")){
dmnListApp.basket.append($(this.el));
}else{
dmnListApp.list.append($(this.el));
}
}
});
And I have the following template for DmnItem View:
<script id="tmpl_dmnListItem" type="text/template">
<%= checked&&free ? "<input type='checkbox' class='getDomain' />" : ""%><%= name %>.<%= zone %> <%= (free || !checked ) ? (checked) ? '<p class="fr">'+price+" руб./"+days+'</p>' : "" : "<a href='#' class='getWhois fr'>WhoIs</a>" %>
</script>
DmnView listen for clicking on element with the "getDomain" class. This element is the checkbox. I click on this checkbox. And after calling toBasket() method in both Views I see still unchecked checkbox. Why it happened so?

The bug was in rendering. After setting new value to model's attribute render function of view was called and "redraw" the checkbox (so, it's maybe a bug of backbone - after re-Rendering, state of checkbox don't saved). So I added a short line to template, which add "checked" attribute for checkbox if necessary.

Maybe something is wrong in function toBasket (or other). Script can stop before reach end of your handler.

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.

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

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/

Call method in collection, when View's method called

I have a view and collection like this:
window.DmnView = Backbone.View.extend({
template: _.template($("#tmpl_dmnListItem").html()),
events: {
"click .getWhois": "showWhois",
"click .getDomain": "toBasket"
},
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function() {
return $(this.el)
.attr("class", this.model.get("free") ? "dmnItem green" : this.model.get("checked") ? "dmnItem red" : "dmnItem red loader")
.html(this.template(this.model.toJSON()));
},
remove: function() {
$(this.el).remove();
},
showWhois: function() {
showBoxes(this.model.get("info"));
return false;
},
toBasket: function() {
this.model.toBasket();
console.log("view");
}
});
window.DmnListApp = Backbone.View.extend({
el: $("#regWrap"),
events: {
"keypress #dmnName": "checkAll"
},
initialize: function() {
this.input = this.$("#dmnName");
this.list = this.$("#dmnList");
this.basket = this.$("#dmnBasket");
dmnList.bind('add', this.addOne, this);
dmnList.bind('all', this.render, this);
DmnView.bind('toBasket', this.toBasket, this);
},
render: function() {
},
addOne: function(dmnItem) {
var view = new DmnView({model : dmnItem});
this.list.append(view.render());
},
checkOne: function(name, zone, price, days) {
dmnList.create({name: name, zone: zone, price: price, days: days});
},
checkAll: function(e) {
var name = this.input.val();
if (!name || e.keyCode != 13) return;
if (name == "")
name = "yandex";
dmnList.destroyAll();
var zoneList = dmnList.domainsInfo.Name;
var costList = dmnList.domainsInfo.CostOrder;
var daysList = dmnList.domainsInfo.DaysToProlong;
var parent = this;
$.each(zoneList, function(key, zone) {
parent.checkOne(name, zone, costList[key], daysList[key]);
});
this.input.val("");
},
toBasket: function(){
console.log("collection");
}
});
I want Collection's method toBasket() to be called after View's method toBasket() was called. For this purpose I do the following in Collection:
DmnView.bind('toBasket', this.toBasket, this);
So, if this worked, I should receive two messages in my javascript console:
view
collection
(Maybe in other order)
But I only see "view" message in console. What I do wrong?
TIA!
You're almost there. In your collection view, you're attempting to listen to the DmnView event toBasket, but how you have it setup is a little incorrect. To listen to events, you have to bind to a specific instance you want to listen to, not a class. So you'll want to move the bind from initialize to addOne, like this:
window.DmnListApp = Backbone.View.extend({
// ...
initialize: function() {
this.input = this.$("#dmnName");
this.list = this.$("#dmnList");
this.basket = this.$("#dmnBasket");
dmnList.bind('add', this.addOne, this);
dmnList.bind('all', this.render, this);
// Remove the DmnView bind here
},
addOne: function(dmnItem) {
var view = new DmnView({model : dmnItem});
// Bind to the DmnView instance here
view.bind('toBasket', this.toBasket, this);
this.list.append(view.render());
},
// ...
});
Now that your collection view is listening for the event toBasket, you need to actually fire the event in your DmnView view.
In Backbone views, no events are automatically fired, so you'll need to manually trigger it yourself, like this:
window.DmnView = Backbone.View.extend({
// ...
toBasket: function() {
this.model.toBasket();
console.log("view");
// Trigger the event
this.trigger('toBasket');
}
});
You should now see both messages in your console.

How to bind elements in backbone?

I have a small backbone class:
view = Backbone.View.extend({
events: {
"click textarea" : "doSomething"
},
doSomething : function() {
var textarea = $(this.el).find('textarea')
// I would like to just call, this.textarea, or this.elements.textarea
}
});
Ideally I would like to be able to access my textarea through a variable instead of having to search the element every time. Anyone have any idea on how to accomplish this?
maybe i am under thinking it but how bout giving the textarea a class or id and target specifically when needed,
or create a sub view that generates the textarea
var View = Backbone.View.extend({
el: '#main',
initialize: function(){
this.render();
},
render: function() {
var subview = new SubView();
this.$('form').append(subview.el);
this.$('form').show();
},
});
var SubView = Backbone.View.extend({
tagName: 'textarea',
id: 'whateverId',
events: {
"click" : "doSomething"
},
initialize: function(){
this.render();
},
doSomething : function(event) {
var textarea = $(event.target);
// or var textarea = $(this.el);
},
render: function(){
return $(this.el);
}
});
The other answers get you the reference that you need, but if you really need a handle to the textarea, then you can do something like this:
view = Backbone.View.extend({
initialize: function() {
this.myTextareaElement = $(this.el).find('textarea')
}
events: {
"click textarea" : "doSomething"
},
doSomething : function() {
// use this.myTextareaElement ...
}
});
pass the event as the argument and then use the event target
view = Backbone.View.extend({
events: {
"click textarea" : "doSomething"
},
doSomething : function(event) {
var textarea = $(event.target);
}
});

Resources