How can I stop event propagation with Backbone.js? - javascript

Using a Backbone.js View, say I want to include the following events:
events: {
'click a': 'link',
'click': 'openPanel'
}
How can I avoid openPanel to be fired when I click on a link. What I want is to have a clickable box which will trigger an action, but this box can have elements which should trigger other actions, and not the parent action. Think for example Twitter.com, and links in Tweets/right hand panel.

I've been using e.stopImmediatePropagation(); in order to keep the event from propagating. I wish there was a shorter way to do this. I would like return false; but that is due to my familiarity with jQuery

The JQuery preventDefault method would also be a good option.
window.LocationViewLI = Backbone.View.extend({
tagName: "li",
template: _.template('<%= name %>'),
events: {
"click a": "handleClick"
},
handleClick: function(event) {
event.preventDefault();
console.log("LocationViewLI handleClick", this.model.escape("name") );
// debugger;
},
...

Each of your event handlers will be passed an event object when it's triggered. Inside your handler, you need to leverage jQuery's event.stopPropagation() method. For example:
link: function(event) {
//do some stuff here
event.stopPropagation();
}

Two other methods that might work for you:
1
events: {
'click a': 'link',
'click *:not(a, a *)': 'openPanel'
}
Then openPanel will not capture click events on any <a> or child of an <a> (in case you have an icon in your <a> tag).
2
At the top of the openPanel method, make sure the event target wasn't an <a>:
openPanel: function(event) {
// Don't open the panel if the event target (the element that was
// clicked) is an <a> or any element within an <a>
if (event && event.target && $(event.target).is('a, a *')) return;
// otherwise it's safe to open the panel as usual
}
Note that both of these methods still allow the openPanel function to be called from elsewhere (from a parent view or another function on this view, for example). Just don't pass an event argument and it'll be fine. You also don't have to do anything special in your link function -- just handle the click event and move on. Although you'll probably still want to call event.preventDefault().

Return "false" in your "link" function.

Related

Template event selector 'not' isn't working

In this Meteor template code, when the canvas is clicked, it prints out the canvas element to the console but it is expected not to fire the event.
How can it be made so that it fires if the element which is clicked is not a canvas?
Template.myTemp.events({
'click *:not(canvas)': function(e) {
e.stopPropagation();
console.log(e.target);
});
Definitely an interesting problem as the :not selector is supported by Blaze, and works properly with other HTML elements. You might want to open an issue about this in the Blaze repo.
The above being said, there are a few different ways you can work around this. You could add a check in your event handler to make sure you don't do anything with canvas related events:
Template.myTemp.events({
'click *'(event, instance) {
if (event.target.nodeName.toLowerCase() !== 'canvas') {
// Handle non-canvas events ...
}
},
});
Another option involves chaining your event handlers, if you want to be able to filter out canvas events specifically. For example:
Template.myTemp.events({
'click canvas'(event, instance) {
event.stopImmediatePropagation();
// Handle canvas click events only ...
},
'click *'(event, instance) {
// Handle all click events except canvas click events, since they're
// captured and handled above ...
},
});

Turning Off Specific Click Event

JSFIDDLE: http://jsfiddle.net/w55usyqk/
When you click the div there are two separate click events fired. They print out "click" and "clicked" respectively in the console log.
$("div").on("click", function() { console.log("click"); });
$("div").on("click", function() { console.log("clicked"); });
If you tap on the button it will remove both event declarations from the div object
$("button").on("click", function() { $("div").off("click"); });
However, what if I just needed to remove a single click event? Is this stored in some sort of event array where I could do something along the lines of $("div").off("click")[1]; or is it impossible to turn off one without turning off the other as well?
I did try looking for the answer if it's been posted before. I think this is one of those questions that's hard to word, so though there may be an answer out there, it's difficult to pin down.
You can use namespaces to easily do this. When you create your event handlers, add the namespace after the event. Ex:
$("div").on("click.namespace1", function() { console.log("click"); });
$("div").on("click.namespace2", function() { console.log("clicked"); });
then for your button, use the namespace of the event to remove:
// remove only the event for namespace2
$("button").on("click", function() { $("div").off(".namespace2"); });
jsFiddle example
Some more on namespaces for events:
An event name can be qualified by event namespaces that simplify
removing or triggering the event. For example, "click.myPlugin.simple"
defines both the myPlugin and simple namespaces for this particular
click event. A click event handler attached via that string could be
removed with .off("click.myPlugin") or .off("click.simple") without
disturbing other click handlers attached to the elements. Namespaces
are similar to CSS classes in that they are not hierarchical; only one
name needs to match. Namespaces beginning with an underscore are
reserved for jQuery's use.
Use named functions as event handlers, so you can then reference what handler you want to unbind:
function clicOne() {console.log("click");};
function clicTwo() {console.log("clicked");};
$("div").on("click", clickOne);
$("div").on("click", clicTwo);
$("button").on("click", function() { $("div").off("click", clickOne); });

Backbone click event: Trigger only on element actually clicked on

In a Backbone application, I instantiate a view for every model in a collection.
If one of these views is clicked, I want to call a function.
Unfortunately, the function is called n times, where n is the number of models/view instantiated. I’ve managed to get around this by finding out what element has been clicked on, but I still don’t feel comfortable knowing that one event might be triggered 200+ times in the very same moment.
The event is bound like this:
var Item = Backbone.View.extend({
events: {
'click .list-group-item': function(event) { this.doSomething(event); },
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});
In the code above you can also see my workaround using event.currentTarget, but how can I avoid this? Is there a way to distinguish the .list-group-item elements without resorting to event.currentTarget, so preferable right in the moment an element is clicked?
Another approach would be to bind the event to the parent element, so it is only triggered once and then using event.currentTarget, but that also seems kind of fishy to me.
Since you want to attach to a click anywhere in the view, you don't need to specify .list-group-item. Also, you only need to specify the name of the event callback function:
var Item = Backbone.View.extend({
events: {
'click': 'doSomething'
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});

Capture only hashchange events not resulting from anchor clicks

I am trying to use Javascript to emulate the CSS :target pseudo-class so as to capture all events that result in an element on page being targeted. I've identified 3 trigger events:
window.location.hash already targets an element of the same ID on initialisation
An anchor targeting the element is clicked
The hashchange event is fired independently of the above (for example via the window.history API)
Scenario 2 is important as a distinct case since I would want to invoke the click event's preventDefault. The simplified code for this scenario follows:
$('body').on('click', 'a[href*=#]', function filterTarget(clickEvent){
$(this.hash).trigger('target', [clickEvent]);
});
The problem comes when trying to implement scenario 3:
$(window).on('hashchange', function filterTarget(hashChangeEvent){
$(this.hash).trigger('target', [hashChangeEvent]);
});
If a target handler doesn't cancel the native behaviour for scenario 2, it will be triggered again when the native behaviour causes the resulting hashchange event. How can I filter out these edge cases?
POST-SOLUTION EDIT:
roasted's answer held the key — handle a namespaced hashchange event, then unbind and rebind the handler based on logic handled inside the click handler and its preventDefault. I wrote up the full plugin here.
If i understand it, you don't want the hashchange event to be fired if an anchor tag is clicked. You could then set your logic using namespaced events:
DEMO
$('body').on('click', 'a[href*=#]', function (clickEvent) {
filterTarget(clickEvent,this);
$(window).off('hashchange.filter').on('hashchange.tmp', function () {
$(this).off('hashchange.tmp').on('hashchange.filter', filterTarget);
});
});
$(window).on('hashchange.filter', filterTarget);
function filterTarget(event,elem) {
$(elem?elem.hash:window.location.hash).trigger('target', [event]);
//you could filter depending event.type
alert(event.type + '::'+ (elem?elem.hash:window.location.hash));
}
if the click is setting the hash with the fragment anyway, just throw away duplicates in the hash change event:
onhashchange=function(e){
if(e.newURL == e.oldURL ){return; }
//do your normal hashchange event stuff below:
};
ref: https://developer.mozilla.org/en-US/docs/Web/API/window.onhashchange
this fixes cascade issues no matter what invoked the change.
Seems like you could use mousedown instead of click, if you're going to be calling preventDefault on it. Then presumably the hashchange would not be triggered.

How do I specify children only in the Backbone.js event handler?

I have a view which has a view inside it (the same view in fact, its recursive). I want only the internal view to handle the event from an 'a' onclick event. I tried to do this by specifying only the direct children in the selector, but it doesnt work.
iv tried:
events: {
'click >a': 'toggle'
},
and
events: {
'click > a': 'toggle'
},
but they dont work, any suggestions? (Note: doing things like using tags and classes wont work because the view is recursive (meaning that both the inner and outer have the same event definitions)
I have run into the same issue, and have solved it by stopping the propagation of the event in my child view. For example...
events: {
'click a': 'toggle'
},
toggle: function (e) {
// Stop event here.
e.stopImmediatePropagation();
// Do more stuff...
}
This does not answer your question of how to specify specific child selectors, but it keeps the event from propagating up to other handlers.
e.stopImmediatePropagation() is handy if you have no other events firing when you click that specific element. What you should do otherwise is compare the e.currentTarget with the element you want to select. I.e.
events: {
'click a': 'toggle'
}
toggle: function(e) {
if (e.currentTarget == this.$el.find('a')[0]) {
...
}
}
That way, if you have a parent view also firing an event when that link is clicked, it won't be stopped by e.stopImmediatePropagation();
You may have to specify the starting selector...meaning I'm not sure the base selector is assumed.
events: {
'click #parent > a': 'toggle'
}
Haven't tried this, but it may work.

Categories