I have several views.
In some of them I have the similar events like
events: {
'click #save': 'save'
}
When I create and render new view old event listening remains so old algorythm still works when I already change the view.
As I know there is a stopListening() function but how can I activate for all previous views.
So when I change the view/page I want disable all previous events.
How I can do that?
ID's are global, you shouldn't have more than one per page. Append your events to a class instead.
events: {
'click .save-btn': 'save'
}
Also, make sure you're disposing your views once you finished using them:
var MyView = Backbone.View.extend({
events: {
'click .save-btn': 'save'
},
...
dispose: function() {
this.unbind();
this.remove();
}
};
var view = MyView();
...
view.dispose();
Cheers.
try to use el.
var MyView = Backbone.View.extend({
el: '#wrapper_of_save_element',
events: {
'click #save': 'save'
},
save: function() {
...
}
});
so your event is only inside your #wrapper_of_save_element (eg. a wrapper div)
http://backbonejs.org/#View-el
Related
I'm having an issue when I am trying to switch the view after a model save on a click event.
The flow I am trying to create is a reorder process, the user will have a confirmation page to reorder. On clicking submit an api call will execute and the invoice page will load on success.
Currently when I click the submit button the first time nothing happens and when i click again I can get an invoice page. no such issue for the cancel button.
var confirmView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#confirmReorder_template").html());
this.$el.html(template);
},
events: {
"click #submitButton": "submitReorder",
"click #cancelButton": "cancelReorder"
},
submitReorder: function(event){
var URI='<config property="api.url.itemReorder"/>';
var ItemReorderModel = new itemReorderModel({url:URI});
$("#submitButton").click(function(event) {
event.preventDefault();
ItemReorderModel.set('id','1');
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
});
},
cancelReorder: function(event){
document.location.href = "items_list.ctl";
}
});
second view
var invoiceView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#reorderInvoice_template").html());
this.$el.html(template);
},
events: {
"click #returnButton": "itemlist",
"click #printButton": "print"
},
itemlist: function(event){
document.location.href = "items_list.ctl";
},
print: function(event){
}
});
loading of first view
$(document).ready(function() {
var ConfirmView = new confirmView({el:$('#itemData')});
});
I'm new to backbone so not sure if I should be using a route, I also have read something about binding, but still trying to get my head around how it all works.
any advice is much appreciated.
You are binding a new event handler in submitReorder method, and your actual functionality is inside that event handler.
So the fist time you click the button, the event handler delegated toview via backbone event hash will trigger submitReorder, which binds a new event handler with actual functionality directly to the button element.
Next time when you click it, this new direct handler will also trigger and fire the functionality you expect.
Each time you click the button you're adding a new event handler.
Your code should be simply:
submitReorder: function(event){
event.preventDefault();
var URI='<config property="api.url.itemReorder"/>';
//-----^------ if this is hardcoded, why not specify this in the model itself..?
var ItemReorderModel = new itemReorderModel({url:URI});
//-------------^----------- why not do this just once while initializing view..?
ItemReorderModel.set('id','1');
//-------------^----------- if this is hardcoded, why not set specify it in model..?
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
},
I also suggest initializing the model in the view's initialize method and caching it as it's property rather than initializing a new model on every click.
I have a simple backbone view as follows:
/**
* Renders a form view for an event object.
*/
APP.EventFormView = Backbone.View.extend({
tagName: 'form',
events: {
'keydown': 'keyPressed',
'focus input': 'inputChanged',
'change select': 'selectChanged',
'change textarea': 'textareaChanged'
},
initialize: function() {
this.template = _.template($('#newevent-form').html());
this.listenTo(this.model, 'change', this.render);
this.listenTo(APP.eventTypes, 'update', this.render);
this.listenTo(APP.selectedEvent, 'update', this.render);
},
render: function() {
var modelJSON = this.model.toJSON();
if ('id' in modelJSON && modelJSON.id !== "") {
this.loadForm();
} else if (!('id' in modelJSON) || modelJSON.id === "") {
this.loadForm();
} else {
this.$el.html('');
}
return this;
},
loadForm: function() {
var templateData = $.extend(this.model.toJSON(),
{"event_types":APP.eventTypes.toJSON()});
this.$el.html('');
this.$el.html(this.template($.extend(this.model.toJSON(),
{event_types: APP.eventTypes.toJSON()})));
$('.ev-main-container').html('').html(this.el);
},
inputChanged: function(e) {
console.log('inputChanged');
},
selectChanged: function(e) {
console.log('selectChanged');
},
textareaChanged: function(e) {
console.log('textareaChanged');
},
keyPressed: function(e) {
console.log('key pressed');
}
});
I initialize this view as follows under document.ready:
// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();
But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?
Update:
Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:
APP.eventFormView = new APP.EventFormView({
model: APP.selectedEvent,
el: $('.ev-main-container'),
});
I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.
jQuery's html function has a side effect that many people seem to forget about, from the fine manual:
jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Consider what that means when you do something like this:
container.html(view.el);
container.html(view.el);
Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?
You have lots of things that will call render on your view and render will eventually do this:
$('.ev-main-container').html('').html(this.el);
Your events will silently disappear the second time that gets called but the HTML will look just fine.
Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):
var V = Backbone.View.extend({
tagName: 'form',
events: {
'click button': 'clicked'
},
initialize: function() {
this.template = _.template($('#t').html());
},
render: function() {
this.$el.html('');
this.$el.html(this.template());
$('.ev-main-container').html('').html(this.el);
return this;
},
clicked: function() {
console.log('clicked');
}
});
var v = new V;
v.render();
$('#re-render').click(function() {
v.render();
console.log('Re-rendered');
});
and you'll see exactly your problem.
If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.
My rules of thumb for preventing event problems with Backbone:
Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
Call remove on views to dispose of them when they're no longer needed.
Don't try to re-use views, create them when you need them and remove them when you don't need them.
No view references anything outside its el.
There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.
I'm writing Todo app with Backbone.js
You can see part of my code below.
Model:
var Todo = Backbone.Model.extend({
defaults : {
title: 'Task Title',
complete: false
},
initialize: function(){
this.on("change:complete", function () {
alert("foo");
});
}
});
View:
var AppView = Backbone.View.extend({
collection: todoCollection,
el: 'body',
events: {
'click #tasks li .complete-task' : 'toggleComplete'
}
toggleComplete: function (e) {
var modelCid = $(e.target).parent('li').attr('id');
if ( this.collection.get(modelCid)['complete'] ){
this.collection.get(modelCid)['complete'] = false;
} else {
this.collection.get(modelCid)['complete'] = true;
};
}
});
But something working wrong and change event in the model doesn't working. I can't understand where I have mistakes.
Help me, please.
10q.
As per the Backbone Documentation:
Set model.set(attributes, [options])
Set a hash of attributes (one or
many) on the model. If any of the attributes change the model's state,
a "change" event will be triggered on the model. Change events for
specific attributes are also triggered, and you can bind to those as
well, for example: change:title, and change:content. You may also pass
individual keys and values.
So you need to be using the set method on the model for these events to be fired. So you would need to use something like this:
this.collection.get(modelCid).set('complete',false);
The 2nd answer to this question nicely explains how event declarations in Backbone.js views are scoped to the view's el element.
It seems like a reasonable use case to want to bind an event to an element outside the scope of el, e.g. a button on a different part of the page.
What is the best way of achieving this?
there is not really a reason you would want to bind to an element outside the view,
there are other methods for that.
that element is most likely in it's own view, (if not, think about giving it a view!)
since it is in it's own view, why don't you just do the binding there, and in the callback Function,
use .trigger(); to trigger an event.
subscribe to that event in your current view, and fire the right code when the event is triggered.
take a look at this example in JSFiddle, http://jsfiddle.net/xsvUJ/2/
this is the code used:
var app = {views: {}};
app.user = Backbone.Model.extend({
defaults: { name: 'Sander' },
promptName: function(){
var newname = prompt("Please may i have your name?:");
this.set({name: newname});
}
});
app.views.user = Backbone.View.extend({
el: '#user',
initialize: function(){
_.bindAll(this, "render", "myEventCatcher", "updateName");
this.model.bind("myEvent", this.myEventCatcher);
this.model.bind("change:name", this.updateName);
this.el = $(this.el);
},
render: function () {
$('h1',this.el).html('Welcome,<span class="name"> </span>');
return this;
},
updateName: function() {
var newname = this.model.get('name');
console.log(this.el, newname);
$('span.name', this.el).text(newname);
},
myEventCatcher: function(e) {
// event is caught, now do something... lets ask the user for it's name and add it in the view...
var color = this.el.hasClass('eventHappened') ? 'black' : 'red';
alert('directly subscribed to a custom event ... changing background color to ' + color);
this.el.toggleClass('eventHappened');
}
});
app.views.sidebar = Backbone.View.extend({
el: '#sidebar',
events: {
"click #fireEvent" : "myClickHandler"
},
initialize: function(){
_.bindAll(this, "myClickHandler");
},
myClickHandler: function(e) {
window.user.trigger("myEvent");
window.user.promptName();
}
});
$(function(){
window.user = new app.user({name: "sander houttekier"});
var userView = new app.views.user({model: window.user}).render();
var sidebarView = new app.views.sidebar({});
});
Update: This answer is no longer valid/right. Please see other answers below!
Why do you want to do this?
Apart from that, you could always just bind it using regular jQuery handlers. E.g.
$("#outside-element").click(this.myViewFunction);
IIRC, Backbone.js just uses the regular jQuery handlers, so you're essentially doing the same thing, but breaking the scope :)
My events aren't working as I'd hoped, and I think I know why. When the perpage span is clicked, everything renders correctly. But I realized - maybe the events aren't reattached to the new markup? Could that be why it only works once? (If I click the span with the number 10 in it, 10 items appear like it should be. But afterwards, anything I click doesn't change anything)
What's a better way to organize this? Should the template not include the pagination portion? How do I attach backbone events to markup after it has rendered again?
var ListView = Backbone.View.extend({
initialize: function() {
var self = this;
this.collection.bind("refresh", function(){self.render();});
this.render();
},
events: {
'click ul#perpage span': 'setperpage'
},
setperpage: function(event) {
this.collection.perpageurl = '/perpage/' + $(event.target).text();
this.collection.fetch();
this.collection.refresh();
},
render: function() {
template = _.template('\
<table>\
<% _(collection).each(function(model){%>\
<tr><td><%=model.id%></td><td><%=model.name%></td><td><%=model.email%></td></tr>\
<%}); %>\
</table>\
<ul id="perpage">\
<li><span>5</span></li>\
<li><span>10</span></li>\
</ul>\
');
var context = {collection: this.collection.toJSON()};
$(this.el).html(template(context));
$('#app').html(this.el);
return this;
}
});
try:
render: function()
{
// …
this.delegateEvents();
return this;
}
For debugging events in JavaScript use Visual Event. It will tell you which elements have events attached to them.