Backbone delegateEvents not bubbling / propagating - backbone.js

Weird problem with event propagation in Backbone. Most people ask how to stop event propagation, but I'm struggling with getting my events to propagate!!
Here I have two View objects. The MainView which contains Item views and listens to click events to call run():
var MainView = Backbone.View.extend({
...
events: {
"click .item": "run" // works only if no click event in Item
},
render: {
// Item View object children
},
run: function() {
//run :)
}
});
Item view objects also listen to click events on themselves to toggle on/off behaviour:
var Item = Backbone.View.extend({
...
events: {
"click" : "toggle" // MainView click event works when this is removed
},
toggle: function() {
this.model.toggle();
}
});
The problem being that MainView.run() is not fired when the Item is clicked, while it has a click event for Item.toggle().
However, MainView.run() DOES fire if I remove the Item.toggle() click event. Leading me to the conclusion that the event is somehow forced to stop propagating, outside of my control.
How can I solve this problem? Am I missing something obvious, or is this unavoidable?
Thank you for any and all suggestions and answers :).

It appears that the click event in your item view isn't bound to a specific DOM object. It's possible that listening for a generic click event is overriding Backbone from listening for your specific .item click event. Try adding an ID or class name to your item view click event to remove any ambiguity.
var Item = Backbone.View.extend({
...
events: {
"click .some-class" : "toggle" // This should fix your problem
},
...

Jay B. Martin answered the question.
The problem is that the View calls this.model.toggle();
The toggle() function sets some variables which the MainView is listening for, causing a render() event to fire.
When MainView calls render(), the Item views are in turn removed, rendered and added to the DOM. This loses the bound event to the DOM element using events: {}.
Instead _.bind() or _.bindAll() should have been used to permanently bind the events, regardless of the context / state of the element bound to in the DOM.
Original comment answer:
#Dan0, sorry I'm a little confused about how toggle could be the root of your issue. I think it's a symptom of the context ambiguity created by binding to an implicit DOM element in a nested view. Once toggle is called, the click event loses the context to which it was initially bound (i.e., this.el). The idiomatic way of solving this is to either a) pass an explicit element so that it can rebind on subsequent events, or b) use _.bind or _.bindAll, so that the click event is permanently bound to the itemview as the context changes. – Jay B. Martin Aug 10 at 23:46

Related

Backbone event propagation clarification

With the below code we are able to prevent handler1 from firing when clicking the second button.
var See = Backbone.View.extend({
events: {
'click': 'handler1',
'click .c2': 'handler2'
},
handler1: () => console.log('First handler'),
handler2: (e) => {
e.stopPropagation();
console.log('Second handler');
},
render: function() {
this.$el.html('<button>One</button><button class="c2">Two</button>');
return this;
}
});
How can this be? What path does the propagation take? Any insight would be much appreciated. Thanks!
In backbone, the events registered via view's event's hash are delegated to the respective view's el, except events which do not specify a selector which is registered directly on the view's el.
What path does the propagation take?
The propagation always takes the normal path - from the child to it's parents.
In this case the event bubbles from the button to the view element, when it reaches the view element the delegated click handler for the button is fired first 1 which prevents further propagation 2, So the direct handler on the view element is not invoked.
1 According to order-of-event-handling-in-jquery
In jQuery, delegates are privileged event handlers and are allowed to jump the queue. In fact, they are always stored in the front of the queue. This mechanism allows delegates to fire before direct events.
2 jQuery probably (because I didn't bother to check the source code to confirm, that's the way it should be) checks whether e.stopPropagation() have been called before invoking each handler to mimic normal bubbling.
propagation will flow from child to parent element if propagation is not stopped.
The First event you are listening to, is basically any click made inside the view area and is caught when it propagates to the view's root element.
The second event is caught whenever a click is propagated to the .c2 element, all DOM events propagate from the source element up to the parent.
If you stop the propagation in .c2, then the click will never reach the parent.
events: {
'click': 'handler1', // This catch clicks propagated to the view root
'click .c2': 'handler2 // this catch clicks in propagated to .c2 elem
},

event Ext.view.ViewView after Refresh

I need an event which will be fired when items in DataView are ready as DOM elements, when refresh() is fireing the items are still not ready
Thanks
viewready may not be used becouse it fires only onse, I need handle event after each refresh
I think viewready is what you are after. If this doesn't work, afterrender should.
If you need an event that fires AFTER refresh has completed then you can add one. Within the view component of your choice, or within a dataview override that would apply to all dataviews, override the refresh function, call its parent and fire a custom event when it's complete:
refresh : function () {
this.callParent(arguments);
this.fireEvent('afterrefresh')
}
You can listen for that event like any other e.g.
me.on({
afterrefresh : me.doSomethingAfterRefresh,
scope : me
});

How can I trigger two events with a "double" action?

I've got a view Foo, with an input field, and a wrapper view Bar, with a button.
When I write something in the input and then press the button, the 'change' event on Foo's model should be triggered, and then the 'click' button on Bar's, yet only the first do happen. Is there a way to trigger both, preferably at the correct order?
Thanks!
To reiterate, and confirm your question/setup. Based on your question, this is the setup you have. You want to change the Foo.model THEN trigger the click event on the Bar.
<div id="Bar">
<div id="Foo">
<input type="text">
</div>
<button id="barButton">Update</button>
</div>
It's a little bit of an odd setup. #alexanderb illustrates it the way I also think of the whole problem but I'll explain what I'd do in your scenario assuming you know something I don't about the context of the situation! :-)
If you want the change to be triggered on the Foo.model followed by the click event on the Bar button, all you need to do manually trigger the button click from your Foo view. To do this, you can do it one of several ways. The first is to directly trigger the event of your parent (Bar) view.
// When you create your Foo subview, you pass in the parent view so you can access it
var foo = new Foo({
'parent': this // this = the barView assuming you're creating FooView inside BarView
});
// In Foo after you set your model attribute, you access the parentView
this.options.parent.$el.find('#barButton').click(); // Manually trigger the click event
// or
this.options.parent.onButtonClick() // Manually run the event handler bypassing the event
This satisfies your request to have the change event followed by the button click event in the DOM context you provided. (Not sure why in this case but I can imagine some scenarios where something similar might be desired.)
The second way of doing it is to use an event aggregator pattern which allows your views to send and receive events to one another. In this scenario, what happens is your Foo view, after updating the model triggers an event that is listened to by your parent Bar view, upon which it executes some handler.
You can read more about event aggregator pattern in Backbone here:
Event Aggregator Explanation
To be honest, I really think that alexanderb's answer is the best one for this situation unless there is something more we don't know about your special setup. But, I hope you can see and compare the two and get an idea of when and why one setup might be appropriate given a particular context.
Hm, I'm a little confused. If you don't use any backbone plugis, the workflow is different.
If you press something on a screen, an event occures on view, not in model. You have to listen to that event and update the model accordingly. If you set new value in model, change model event will occur.
Say, you have a view Foo it's subview Bar and model Boo. Both views are using the same model objects.
In Bar view,
events: {
'click .button': 'onButtonClicked';
}
onButtonClicked: function () {
var value = this.$el('.input').val();
this.model.set({ someValue: value});
}
In Foo view, you can listen for model changes
initialize: function () {
this.model.on('change:someValue', this.onSomeValueChanged);
}
In this.onSomeValueChanged you can do, what ever you like.

Backbone click event not firing

So I have a Backbone view in which I declare it's className. I'm trying to bind a click event to that class. So something like this:
className: "question"
events:
"click .question": -> console.log("clicked")
This doesn't seem to work. It seems to be because the element isn't inside the view itself. So if I create an element within a template, I can bind to that just fine. I should be able to bind to the view itself right? Any help is appreciated. Thanks!
From the fine manual:
delegateEvents delegateEvents([events])
[...] Events are written in the format {"event selector": "callback"}. The callback may be either the name of a method on the view, or a direct function body. Omitting the selector causes the event to be bound to the view's root element (this.el).
So you want your events to look like this:
events:
'click': -> console.log('clicked')
Demo: http://jsfiddle.net/6W6QE/

Removing multiple views after a model has been deleted

My application is structured like this: There is a Sidebar which contains many items and is generated by a SidebarView. The SidebarView invokes an ItemView for every item in the sidebar:
render: ->
view = new ItemView({model: the_item})
$(#el).append(view.render().el)
Then there is a ShowView which displays the item in the main div. There is also a button, which is used to delete the item.
events:
"click #destroy-button" : "destroy"
destroy: () ->
#model.destroy()
this.remove()
return false
It removes the ShowView from the DOM tree and sends a DELETE request to the server. But what is the best way to remove the ItemView from the sidebar? Adding IDs like <div class="item" data-index="123"></div> and then remove the items via the data-index? I have seen somebody using jQuery.data to bind data to the DOM tree. But both solutions look a bit smelly. Is there an elegant way to accomplish this?
Your ItemView should handle the "remove" button. The sequence goes like this:
You hit the remove button.
That triggers an event on the appropriate ItemView.
The ItemView destroys the model.
Destroying the model triggers a 'destroy' event from the model.
The ItemView listens for the 'destroy' event and removes itself when it happens.
So, your ItemView would look something like this:
class ItemView extends Backbone.View
events:
'click .del': -> #model.destroy()
initialize: ->
#model.on('destroy', #remove)
render: ->
# ...
#
remove: =>
$(#el).remove()
# And whatever other cleanup tasks you have...
That way your views will respond appropriately if one of your Item models is destroyed by someone else.
Demo: http://jsfiddle.net/ambiguous/KMT74/1/
And if someone else renders the delete button then you'd just need to call destroy on the appropriate model instance and the ItemView would remove itself. See the kill first button in the demo for an example. You could use a data-id attribute on the ItemView's el to associate models with their views and then do something like:
your_destroy_button_handler: (ev) ->
item = #collection.get($(ev.target).data('id'))
item.destroy()
but it would be cleaner for the ItemView to render its own delete button.
Also, this:
events:
"click #destroy-button" : "destroy"
is going to be a problem as you'll have duplicate id attributes, use a class for the button instead:
events:
"click .destroy-button" : "destroy"

Resources