Multiple Views bound to a single element in backbone.js - backbone.js

I am trying to bind two click events to a single HTML element in two different views. One of the views triggers the event, the other does not.
One of the view has body as its el attribute. If I change this view's el to the same element as the other view's, then both events get triggered.
Is this expected? How can I bind click events for the same element in two different views?

Yes, this is expected. Backbone uses jQuery delegates for the event binding. Which means, the event is actually bound to the view's EL, not directly to the child node.
When you say, "the same element", do you mean literally the exact same node in the DOM? Or, do you mean a node with the same selector? I guess I'm not entirely clear.

can i ask why you want to have 2 views binding to the same element?
from my point of view, you should only have 1 view that represents the element itself
and event's bound to an element should be defined in that view only.
you will run into trouble when you are binding click events to elements that don't belong to the view
if you bind trough the delegateEvents hash, these events are contained within the el of the view.
if you are however defining the click yourself, your code becomes less managable.
so, on to what you can do:
events!
you can define 1 view, holding your button and trigger an event when the button is clicked, while other views that need to handle some code when that button is pressed don't bind directly to the button click itself, they can listen to that raised event.
example on jsfiddle:
http://jsfiddle.net/saelfaer/Qck5w/2/
the gist of it in code here:
// an event aggregator object to trigger and bind to
var events = _.extend({}, Backbone.Events),
// two views that talk to each other trough the event aggregator
var myButtonView = Backbone.View.extend({
// first view binds a click event to the button
events: {
"click a" : "myClickEvent"
},
initialize: function(){
_.bindAll(this, "render");
this.render();
},
render: function(){
return this;
},
// click event executes this function, which triggers a custom event on the events object.
myClickEvent: function(e){
$(e.target).blur();
events.trigger("sidebar:myCustomClickEvent");
return false;
}
});
var myPanelView = Backbone.View.extend({
// second view binds to that event, and executes the custom click handler
initialize: function(){
_.bindAll(this, "render", "myClickEventHandler");
events.bind("sidebar:myCustomClickEvent", this.myClickEventHandler);
this.render();
},
render: function(){
return this;
},
// the click handler does some logic (appends div) when the event is raised.
myClickEventHandler: function(){
var txt = $('<div/>').text("you just clicked the button. (bound in other view)");
$(this.el).append(txt);
}
});

Related

Backbone.js: undelegateEvents not removing events

In the following code:
HTML
<div id="myView">
<button id="test_button">
Test Button
</button>
<ul id="output"></ul>
</div>
JavaScript
var myView = Backbone.View.extend({
initialize: function() {
// why doesn't this remove the previously delegated events?
this.undelegateEvents();
this.delegateEvents({
'click #test_button': 'buttonClicked'
});
},
// this event fires twice for one button click
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
new myView({el: "#myView"});
// instantiate view again
new myView({el: "#myView"});
});
why does
this.undelegateEvents();
in the initialize() method of the Backbone View not remove the previously delegated events from the previous instantiation of the View?
JSFiddle example of above code: https://jsfiddle.net/billb123/o43zruea/28/
I'll try not to shout but please stop trying to bind views to existing elements. Let the view create and own its own el, then call view.remove() to kill it off before replacing it. This simple change solves so many problems with view events that you should always think twice (and twice more) if you don't do it this way.
In your case, you'd have HTML like this:
<script id="t" type="text/x-underscore">
<div id="myView">
<button id="test_button">
Test Button
</button>
</div>
</script>
<div id="container">
</div>
<ul id="output"> <!-- This is outside the container because we're going to empty and refill it -->
</ul>
And your JavaScript would look like this:
var myView = Backbone.View.extend({
events: {
'click #test_button': 'buttonClicked'
},
render: function() {
this.$el.html($('#t').html());
return this;
},
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
var v = new myView();
$('#container').append(v.render().el);
v.remove(); // <----------------- Clean things up before adding a new one
v = new myView();
$('#container').append(v.render().el);
});
Points of interest:
Create the view then render it then put it on the page.
Call remove on the view when you're done with it.
The view goes inside the container. The caller owns the container, the view owns its el.
There are no delegateEvents or undelegateEvents calls anywhere. The presence of those almost always point to structural problems in your application IMO.
Each view is self contained: the outside world doesn't play with anything inside the view and the view keeps its hands to itself.
Updated fiddle: https://jsfiddle.net/bp8fqdgm/
But why didn't your attempted undelegateEvents do anything? undelegateEvents looks like this:
undelegateEvents: function() {
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
The cid is unique per view instance so each view instance uses its own unique namespace for events that delegateEvents binds. That means that this:
this.undelegateEvents();
this.delegateEvents();
is saying:
Remove the events that this instance of the view has bound. These events will be found in the the '.delegateEvents' + this.cid namespace where cid is unique for each view instance.
Bind the events that this instance of the view defines (or the events in the delegateEvents call). These events will be attached using the '.delegateEvents' + this.cid namespace.
So your undelegateEvents call is removing events but not all of them, only the specific event bindings that that view instance adds are removed.
Your this.undelegateEvents() call doesn't actually accomplish anything because it is in the wrong place and called at the wrong time. If the new View caller did the undelegateEvents call:
var v = new myView({el: "#myView"});
v.undelegateEvents();
new myView({el: "#myView"});
then it would happen in the right place and at the right time. Of course this means that your router needs to keep track of the current view so that it can currentView.undelegateEvents() at the right time; but if you're doing that then you'd be better off (IMO) taking the approach I outlined at the top of the answer.

Detect click on itemview container in Backbone/Marionette

I have the following item view:
return Marionette.ItemView.extend({
template:tpl,
tagName: 'div',
className: 'v_itv_record_type_item',
events:{
'click #ui.item':'itemClicked'
},
ui:{
item:'.v_itv_record_type_item'
},
itemClicked:function(e){
console.log('clicked');
}
});
that uses the following handlebars template:
<div class="clicktarget">
Stuff Goes Here
</div>
If you click on one of these item views, it does not register the click event. I understand that Backbone restricts access to just the views slice of the DOM, but apparently this does not extend to the containing div itself, even though that containing div is not part of any template, parent view or otherwise.
If we change the ui hash and point item at .clicktarget the click is registered. But this gives me me a <div><div>stuff goes here</div></div> structure for seemingly no reason. Is this the only way to detect a click on the entirety of an item views DOM element?
You can register a click event on the view element by omitting the selector:
events:{
'click' :'itemClicked'
}
Note that if you have an event handler at view level, all the clicks inside the view will bubble up and trigger it's handler, unless it was stopped (event.stopPropagation()) on the way. This is the expected behavior.

this.$el pointing to container element

I have two views, ListView and ItemView. ItemView is populated like this on render in ListView:
this.items.each(function(model, index) {
new ItemView({
item: model,
});
});
All ItemView share the same container el; being a <tbody>, with its template being the <tr>.
When triggering events, they point to the containing element, causing e.g. save to trigger save on all ItemView rather than just the one that was clicked. Is there a way of circumventing this behavior, or is there a better practice?
you can try this one,
ListView = Backbone.View.extend({
events : {
'click tr' : function(e){
this.items.each(function(model){
model.trigger('save');
});
}
}
})
then bind save events in item view.
this.model.on('save', function(){
//your code here
});

Backbone.js per attribute rendering (multiple small views vs multiple templates per view )

I have a model and a view. The view displays attributes of a model and allows the user to manipulate these attributes. The problem is that when an attribute is modified it re-renders the whole view which causes a lot of problems for me.
Example blur event on a text input saves the new input to an attribute and thus fires render. Which means that if the user clicked from that text input straight to a button on the same view that event will never fire as the first event that fires will be blur causing the whole view to re-render and thus losing the button click event.
I have two ideas:
Have a single view where every attribute is in a separate template. Then I bind to a particular attribute change event and in render I update only the html of the changed attribute. This seems like a hack, as there is a lot of work to force the view to update only the changed attribute. It will add a lot of unnecessary complexity to an already complex view.
Create a master view which consists of views, where each of them represents a model's attribute. This will create a lot of views, with nearly no functionality.
I seem to prefer the 2. option. What do you think? What are the best practices? Is there any better way to handle this?
I think you can do this quite easily.
Take a step back and think about where you are binding your events. It seems that you are binding them directly on top of each individual element instead of using a parent delegate.
Here's an example
Backbone.View.extend({
el: $("div.parent"),
events: function() {
this.$el.on("click", "input[type=button]", function(){});
// jquery cross browser on this
this.$el.on("blur", "input[type=text]", function(){});
},
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function() {
this.$el.html('<input type="text" /><input type="button" />');
}
});
Here's what el and it's structure looks like
<div class="parent">
<input type="text" />
<input type="button" />
</div>
So this.$el points to div.parent. I can constantly rerender the contents of this.$el, and as long as the html structure dosen't change, I don't have to worry about events getting unbound. The other solution is that if I really cannot do delegation, I would just call the events method whenever I render again.
Like you said yourself, both of your options seem very complex. But sometimes additionaly complexity is a necessary evil. However, if the updated fields are something relatively simple (like binding a value to an element or an input field), I would simply update the DOM elements without creating additional View/Template abstractions on top of them.
Say you have a model:
var person = new Person({ firstName: 'John', lastName: 'Lennon', instrument:'Guitar' });
And a view which renders the following template:
<div>First Name: <span class="firstName">{{firstName}}</span></div>
<div>Last Name: <span class="lastName">{{lastName}}</span></div>
<div>Instrument: <input class="instrument" value="{{instrument}}"></input></div>
You could declare in the view which property change should update which element, and bind the model change event to a function which updates them:
var PersonView = Backbone.View.extend({
//convention: propertyName+"Changed"
//specify handler as map of selector->method or a function.
firstNameChanged: { '.firstName': 'text' },
lastNameChanged: { '.lastName': 'text' },
instrumentChanged: { '.instrument': 'val' },
otherFieldChanged: function(val) { //do something else },
initialize: function (opts) {
this.model.on('change', this.update, this);
},
//called when change event is fired
update: function(state) {
_.each(state.changed, function(val, key) {
var handler = this[key + "Changed"];
//handler specified for property?
if(handler) {
//if its a function execute it
if(_.isFunction(handler)) {
handler(val);
//if its an object assume it's a selector->method map
} else if(_.isObject(handler)) {
_.each(handler, function(prop, selector) {
this.$(selector)[prop](val);
}, this);
}
}
}, this);
}
A solution like this doesn't scale to very complex views, because you have to add classed elements to the DOM and maintain them in the View code. But for simpler cases this might work quite well.
In addition it's always good to try to compose views of multiple, smaller views, if they naturally divide into sections. That way you can avoid the need to update single fields separately.

binding backbone events in jqMobi Nav, or when the dom elements are duplicated in the dom

I've been porting my app to use jqMobi and jqUI, but I've run into a problem with backbone delegating events.
The way jqUI creates a side nav bar is umm.... interesting to say the least.
Each panel can have a distinct nav bar, but the nav bar is never actually visible to the user, you populate the nav bar, and then jqUI copies the html into the div#menu element.
My view is fairly straightforward
MyApp.Views.UserMenu = Backbone.View.extend({
el: 'nav#user_menu',
initialize: function(){
//empty out and unbind in-case it is already populated
$(this.el).empty().unbind();
this.render();
},
events: {
"click div#add_friend": "new_friend"
},
render: function(){
$(this.el).append(HandlebarsTemplates['friends/new_friend']());
// here I am trying to change the 'el' to point to where the menu is in the DOM
this.el = 'div#menu';
this.delegateEvents();
return this;
},
new_friend: function(){
alert('clicked');
}
});
I've tried changing the el to the div#menu after populating the nav, but that isn't working. I've also tried populating the div#menu directly, but that doesn't seem to work either.
Any suggestions? I'm assuming the issue is that the elements are being moved, but it could be something else, and maybe I'm not sure how to debug the other case.

Resources