I may just be thinking about this wrong because I'm doing it in Angular and over complicating, but what I'm trying to do is setup my click event so it only triggers when an element is clicked, but not it's child. I'm trying to setup a modal, where if you click the background overlay it closes, but obviously I don't want it closing if the user interacts with the modal.
<div class="overlay">
<div class="modal">
</div>
</div>
So far, I've created this:
#HostListener('document:click', ['$event.target']) public onClick(targetElement: HTMLElement) {
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside && targetElement.class.indexOf('overlay') && targetElement.parentElement.tagName === 'GP-MODAL') {
//do stuff
}
}
Where _elementRef is the Angular ElementRef. The problem is it feels like an inefficient way of doing it: trigger on any click, only continue on certain elements. It feels more ideal to trigger a click on .overlay and then somehow not have it go off in .wrapper, but I can't think of how to do it. Any advice?
I think what you are looking for is the stopPropagation method, which resides in the event object
(function () {
var app = angular.module('app', []);
app.controller('MainCtrl', MainController);
MainController.$inject = [];
function MainController () {
var vm = this;
vm.onClick = function (event) {
event.stopPropagation();
alert('Child clicked');
};
vm.parentOnClick = function (event) {
alert('Parent clicked');
};
}
}) ();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
<body ng-app="app" ng-controller="MainCtrl as vm">
<div ng-click="vm.parentOnClick($event)" style="height: 400px; width: 400px; background: red;">
<div ng-click="vm.onClick($event)" style="position: absolute; top: 100px; left: 100px; background: blue; height: 200px; width: 200px;">
</div>
</div>
</body>
I came up with two possible methods, depending on selection criteria.
The first is just Javascript:
If you know a specific attribute of the clicked element you can check on, create a click event on the element you want to track, bound to:
clickedOverlay(event) {
if (event.target.parentElement.tagName === 'GP-MODAL') {
// Do stuff
}
}
A more Angular focused answer:
#ViewChild('target') targetRef: ElementRef;
clickedOverlay(event: MouseEvent) {
if (this.targetRef.nativeElement === event.target) {
// Do stuff
}
}
It's more explicit, and less prone to false positives, but more verbose.
Not suggesting these are the best answers, but my preference right now.
Related
My question in the title probably looks vague. And I sketched an example for the question:
container.onclick = () => {
alert(0);
};
content.onclick = () => {
alert("how can I prevent here appearance alert(0) from parent element event?");
//how can I prevent parent event by content clicked?
};
#container{
height: 100px;
background-color: gray;
}
#content{
height: 50px;
width: 150px;
background-color: green;
}
<div id="container">
<div id="content"></div>
</div>k
This is a simple example. In a real project, I can't combine these two events into one, because the first one is programmatically assigned somewhere in the bowels of my framework and it shouldn't be removed from the EventListener after clicking on content
In General, is it possible to somehow interrupt the execution of the call chain event by clicking in the DOM layers? I tried to do this:
content.onclick = () => {
alert("how can I prevent here appearance alert(0) from parent element event?");
e.preventDefault();
return false;
//how can I prevent parent event by content clicked?
};
But this, of course, was not successful
You should pass the event by dependency injection to the specific method (content.onclick) and then stop the propagation of it.
container.onclick = () => {
alert(0);
};
content.onclick = (e) => {
e.stopPropagation();
alert("VoilĂ , this prevent that appears alert(0) from parent element event.");
};
#container{
height: 100px;
background-color: gray;
}
#content{
height: 50px;
width: 150px;
background-color: green;
}
<div id="container">
<div id="content"></div>
</div>
For this, you can use stop propogation of js like this
<div id="container">
<div id="content" onclick="event.stopPropagation();">
</div>
</div>
So when you click on content it will not trigger container event only.
There have been some attempts to answer this question:
here, here and here. However none of the answers are giving a solid response. I'm not referring to the event phases capture, bubble and target and how stopPropagation() affects the overall event. I'm looking for a case where adding stopPropagation() to a DOM node will benefit the overall code?
This really shouldn't be an answer, but there is only so much you can write in a single comment
I don't think you're doing your question justice by having the words "good practice" in its title. This sort of implies that in most cases, stopPropagation is bad practice. This is similar to saying that eval is evil. It completely brushes off any legitimate use cases of it with misplaced dogmatism.
I never found myself in a situation where using stopPropagation didn't feel like a workaround to avoid fixing the real issue.
In an ideal world, applications are built out of smaller components that do very little on their own but are highly reusable and combinable. For this to work, the recipe is simple yet very difficult to execute: each component must know nothing about the outside world.
Therefore if a component needs to use stopPropagation(), it can only be because it knows that something further up the chain will break or that it will put your application into an undesirable state.
In this case you should be asking yourself whether that is not a symptom of a design issue. Perhaps you need a component that orchestrates and manages the events of its children?
You should also consider the fact that preventing the propagation of events can cause other components to misbehave. The classic example is a drop-down that closes when you click outside of it. If that click is stopped, your drop-down may never close.
Think of events as sources of data. You don't want to lose data. Au contraire! Let it go, let it free ;)
While I don't see using stopPropagation as bad or evil practice, I just don't think it is ever needed.
Example: how to avoid using stopPropagation
In this example we're building a very simple game: if you click on a red area you lose, on a green area you win. The game is over once a click is made.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', () => console.log('you lost'));
onClick('#green', () => console.log('you won'));
#red, #green { width: 50px; height: 50px; display: inline-block; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red"></div>
<div id="green"></div>
</div>
Now let's imagine that there are different levels in which red and green blocks are arranged randomly. In level #42, the red block contains the green one.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', () => console.log('you lost'));
onClick('#green', () => console.log('you won'));
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
As you can see when you click on the green area, you both win and lose at the same time! And if you were to put a stopPropagation() call in the green handler, there will be no way to win this game since the click won't bubble up to the game handler to signal the end of the game!
Solution 1: identify the origin of the click
const filter = handler => ev =>
ev.target === ev.currentTarget ? handler(ev) : null;
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', () => console.log('game over'));
onClick('#red', filter(() => console.log('you lost')));
onClick('#green', () => console.log('you won'));
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
The key function is filter. It will make sure that handler will only execute if the click actually originated from the node itself and not from one of its children.
The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred.
https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
Solution 2: use event delegation
You don't actually need three events handlers. Just set up one on the #game node.
const onClick = (selector, handler) => {
document.querySelector(selector).addEventListener('click', handler);
};
onClick('#game', (ev) => {
if (ev.target.id === 'red') {
console.log('you lost');
} else if (ev.target.id === 'green') {
console.log('you won');
}
console.log('game over');
});
#red, #green { max-width: 100px; padding: 10px; }
#red { background-color: orangered; }
#green { background-color: yellowgreen; }
<div id="game">
<div id="red">
<div id="green"></div>
</div>
</div>
Intro
I am extending photoswipe with my own button & modal dialog, similar to built in share dialog.
I already made code that worked, but then followed these modifications to photoswipe:
https://github.com/dimsemenov/PhotoSwipe/issues/1209
Now it doesn't work anymore. Issue is that photoswipe's event handlers get called before mine, so it appears as if user clicked on photoswipe controls and photoswipe hides image, controls & everything and only my modal is visible.
Diagnostics
I have modified onControlsTap and onGlobalTap and my button click to log to console and I see they are fired in this order:
onControlsTap
onGlobalTap
Settings button click
Html on the other hand looks like this:
<div id="globalTapContainer">
<div id="controlTapContainer">
<button id="myButton"></button>
</div>
</div>
And events are registered using addEventListener(..., false)
Code
This is my code which binds to click event
$("#pswp__settings__dropdown_background, .pswp__button--settings")
.click(function(ev) {
console.log('Settings button click');
ev.stopPropagation();
toggleSettings();
});
This is photoswipe code that binds events.
_controls = framework.getChildByClass(pswp.scrollWrap, 'pswp__ui');
// ...
framework.bind(_controls, 'pswpTap click', _onControlsTap);
framework.bind(pswp.scrollWrap, 'pswpTap', ui.onGlobalTap);
var framework = {
// ...
bind: function(target, type, listener, unbind) {
var methodName = (unbind ? 'remove' : 'add') + 'EventListener';
type = type.split(' ');
for(var i = 0; i < type.length; i++) {
if(type[i]) {
target[methodName]( type[i], listener, false);
}
}
}
}
My button and modal are one of child nodes of pswp__ui.
Question
How is it possible that their events are called before mine when I have registered click event to a specific button?
What to do to make photoswipe events not fire when you click on my controls?
I'm not familiar with photoswipe, but its events use a custom event called pswpTap, not click. Presumably this fires when an element is tapped or when the mouse button is pressed. click events don't fire until the mouse button is released, so that would explain why their events are firing before yours.
Example:
$('#outerdiv').on('mousedown', function() {
console.log('outer mousedown');
});
$('#innerdiv').on('click', function() {
console.log('inner click');
});
#outerdiv {
width: 100px;
height: 100px;
background-color: blue;
}
#innerdiv {
width: 40px;
height: 40px;
background-color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="outerdiv">
<div id="innerdiv"></div>
</div>
You should presumably be able to prevent this by having your element handle and cancel the mousedown event. You may also need to add an event handler for tap events if they work differently from mousedown (I'm not sure whether they are).
$('#outerdiv').on('mousedown', function() {
console.log('outer mousedown');
});
$('#innerdiv').on('mousedown', function(event) {
console.log('inner mousedown');
event.stopPropagation();
});
$('#innerdiv').on('click', function() {
console.log('inner click');
});
#outerdiv {
width: 100px;
height: 100px;
background-color: blue;
}
#innerdiv {
width: 40px;
height: 40px;
background-color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="outerdiv">
<div id="innerdiv"></div>
</div>
I'm having a bit of trouble handling multiple, nestled, angularjs mousebutton directives.
I have a template that looks like this:
<div class="project-wrap">
<div class="workspace-wrap">
<div id="workspace" class="workspace"
oncontextmenu="return false;"
ng-mousedown="project.checkBtnDown($event)"
ng-mouseup="project.checkBtnUp($event)"
ng-mousemove="project.mouseMoved($event)">
</div>
</div>
<button class="logoutBtn" ng-click="project.logout()">
Logout
</button>
</div>
What checkBtnDown() does, is simply check which of the mousebuttons was pressed and then processes it.
The problem I'm having is, when the left mousebutton is pushed down on the "workspace" (within my ProjectCtrl's template), it places a SVG element inside the "workspace" div. This SVG element is bound with an custom angular directive, which has a ng-click on it's template.
So what's happening is, I create the SVG element as planned but, when I click on the portion of the SVG element that I want to call a function on scope. It's still calling checkBtnDown(), because the new SVG element is inside the project template.
How can I get the SVG element ng-click to "peek through" and not fire checkBtnDown() simultaneously?
Hi, i don't know if you meaning this or not..hope this helps you.
in fact you just need to detect if your mouse clicked or not, for that we need to something to detect this for second time.
var app = angular.module("app", []);
app.controller("ctrl", function ($scope) {
$scope.alreadyClicked = false;
$scope.action = function() {
console.log("action");
}
$scope.mousedown = function (event) {
if ($scope.alreadyClicked) {
$scope.action();
} else {
console.log("mousedown");
$scope.alreadyClicked = true;
}
}
});
.box {
position: relative;
background: #eee;
border: solid 1px #ccc;
float: left;
width: 100px;
height: 100px;
cursor:pointer
}
<!DOCTYPE html>
<html ng-app="app" ng-controller="ctrl">
<head>
<title></title>
</head>
<body>
<div class="box" ng-mousedown="mousedown($event)">
{{alreadyClicked ? "click to call action":"click to call mousedown"}}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</body>
</html>
What I decided to do using your suggestion Mayer,
"In fact you just need to detect if your mouse clicked or not, for that we need to [do] something to detect this for second time."
was to measure the maximum time between the first and second mouse-events, then use a setTimeout().
Here's some pseudo-code to illustrate my idea.
// First mouse-down event
if (firstEvent) {
var timerID = setTimeout(function() {
// handle first mouse-event
}, dTimeBetween);
}
// Second mouse-down event
if (secondEvent) {
clearTimeout(timerID);
function() {
// handle second mouse-event
}
}
When I click on the icon, I want it to be switched on/off.
In my html file I have:
<div class="startSharing"></div>
And in my js:
$('.startSharing').click(function() {
$(this).removeClass('startSharing');
$(this).addClass('stopSharing');
});
$('.stopSharing').click(function() {
$(this).removeClass('stopSharing');
$(this).addClass('startSharing');
});
It works fine when switching from on to off - but I don't understand why it still goes into "startSharing" section when it is turned off.
Thank you for help.
In this case, you're setting the handlers to the particular elements. Once they're set - they're set, regardless of the selector changing.
You could just define it on the .startSharing element, if they all start like that. Then, you could use .toggleClass() to easily switch them on/off:
$('.startSharing').click(function() {
$(this).toggleClass('startSharing stopSharing');
});
You may also delegate the click event like this:
$('body').on('click', '.startSharing', function() {
$(this).removeClass('startSharing').addClass('stopSharing');
});
$('body').on('click', '.stopSharing', function() {
$(this).removeClass('stopSharing').addClass('startSharing');
});
Doing it this way will also allow dynamically-generated to trigger events. In your case, .stopSharing was generated dynamically.
use event delegation
$('#container').on('click',".startSharing,.stopSharing",function(){
$(this).toggleClass('startSharing').toggleClass('stopSharing');
});
#container div {
width: 250px;
height: 100px;
}
.startSharing {
background-color: green;
}
.startSharing::after {
content: "start sharing";
}
.stopSharing {
background-color: red;
}
.stopSharing::after {
content: "stop sharing";
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
<div class="startSharing"></div>
</div>