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 :)
Related
The "change" event is not firing in the following code.
var PageView = Backbone.View.extend({
el: $("body"),
initialize: function(){
this.model.on("change:loading", this.loader, this);
},
loader: function(){
if(this.model.get("loading")){
this.$el.find('.loader').fadeIn(700);
}
else
this.$el.find('.loader').fadeOut(700);
},
});
var PageModel = Backbone.Model.extend({
defaults: {
loading: null,
},
initialize: function(){
this.set({loading:false});
},
});
$(function(){
var pageModel = new PageModel({});
var pageView = new PageView({model: pageModel});
})
It works if I'm adding this in the model's initialize function:
setTimeout(function() {
this.set({'loading': 'false'});
}, 0);
I can leave it this way, but this is a bug.
The situation explained
Here's the order the code runs:
the model is created,
model's initialize function is called, setting the loading attribute to false,
then the model is passed to the view,
then a listener is registered for the "change:loading"
The event handler is never called because the event never occurs after it was registered.
Quick fix
First remove the set from the model.
var PageModel = Backbone.Model.extend({
defaults: {
loading: null
}
});
Then, after creating the view, set the loading attribute.
var pageModel = new PageModel();
var pageView = new PageView({ model: pageModel });
pageModel.set('loading', false); // now the event should trigger
Since the listener is now registered before the model's loading attribute is changed, the event handler will be called.
Optimized solution
Use Backbone's best practices:
Favor .listenTo over .on to avoid memory leaks
Cache jQuery objects
Try to avoid setting the el property on the view
A view is an atomic component that should only care about itself and its sub-views.
While in your case, it wouldn't matter much that you use the el property on the view, it still goes beyond the responsibilities of the view. Let the calling code deal with passing the element to use for this view.
var PageView = Backbone.View.extend({
initialize: function() {
this.model = new PageModel();
this.$loader = this.$('.loader');
this.listenTo(this.model, "change:loading", this.loader);
},
loader: function() {
this.$loader[this.model.get("loading")? 'fadeIn': 'fadeOut'](700);
},
render: function() {
this.loader();
return this;
}
});
Put the defaults where they belong.
var PageModel = Backbone.Model.extend({
defaults: {
loading: false
}
});
Here we choose the body as the element to use for the view, using the el option, and then call render when ready.
$(function() {
var pageView = new PageView({ el: 'body' }).render();
});
The event won't be triggered by the listener right away, instead, we use the render function to put the view in its default state. Then, any subsequent changes of the loading attribute will trigger the callback.
I have listed the most useful answers I've written about Backbone on my profile page. You should take a look, it goes from the beginning to advanced and even provides some clever Backbone components that solves common problems (like detecting a click outside a view).
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);
I got following example Backbone.View:
var View = Backbone.View.extend({
tagName: "div",
className: "advertisement",
initialize: function () {
this.on('switch', this.
switch, this);
this.views = {};
this.views.laptop = new LaptopAdView();
this.views.piano = new PianoAdView();
this.views.food = new FoodAdView();
},
render: function () {
this.$el.empty();
this.
switch ('laptops');
return this;
},
switch: function (ad) {
var el;
if (ad === 'laptop') {
el = this.views.laptop.render().el;
} else if (ad === 'piano') {
el = this.views.piano.render().el;
} else {
el = this.views.food.render().el;
}
// reinsert the chosen view
this.$el.empty().append(el);
// bind all events new
this.delegateEvents();
}
});
As you see I use this.delegatEvents() to recreate all event bindings. This is actually not a perfect solution... It would be far better when I use this.$el.detach(); or an other method so I cache the whole object with its event instead of re-rendering and recreating all events.
Now with working fiddle:
http://jsfiddle.net/RRXnK/66/
You are trying to bind to a Backbone view instead of a DOM element:
this.on('switch', this.switch, this);
It's all good, but who triggers 'switch' event? You do not have bindings that trigger DOM events, and this.delegateEvents(); only works on DOM events.
Once you set up DOM events, e.g.
this.$el.on('click', 'a', this.switch) -- 'switch' event will be triggered, and you will NOT have to re-delegate events as long as $el is in not removed from DOM.
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.