What I'm trying to do:
stackblitz link -> stackblitz example
The component hierarchy is as such
in the 'resize.directive'.
event.stopPropagation()
& event.preventDefault()
are used to prevent event bubbling the child.component
the parent component
@HostListener('click', ['$event'])
as you mouse over the inner child component, you can click to drag and resize the borders of the element. what I'm struggling with is the event propagation of the 'click' handler in the parent component.
My issue is that the @HostListener
click event is being fired from the parent component, when I'm not expecting it to be. i.e. if you drag the borders of the child component. on mouse up, the click event of the ParentComponent
is fired.
My understanding is that
stopPropagation()
yet the parent component is still pickup the click event.
There is a lot of comments around using return false
. however, based on other issues, that doesn't seem to be the cause, as indicated here.
that being said, even if I add return false
to the directive, the event still bubbles up.
Any insights into what I might be missing is appreciated.
To clarify, the event listeners will be fired in the following order, starting with the element in which the event originated
directive -> child -> parent -> document
As far as I can see, the issue you're having boils down to a combination of things:
When you drag the border, the mouse ends up being outside the child element, so you will end up triggering parent.mouseup -> document.mouseup
. To capture the event in directive, you have to listen to document.mouseup
.
Your directive is listening to document.mouseup
. The parent.mouseup
is triggered before document.mouseup
so you can't stop the propagation in your directive.
Let's see what happens when you drag the border down:
You press on the border. This triggers directive.mousedown -> child.mousedown -> ...
. You stop propagation in the directive, so all good.
You drag the mouse to the bottom, the cursor is now outside the child element.
You release the mouse. Note that we're not in the child element anymore, so this triggers parent.mouseup -> document.mouseup
. Parent handler will run BEFORE the document handler, so you can't stop it.
Since the down
and up
happened within the parent controller, click is now triggered. Again, we're out of the child element, so the triggers are parent.click -> document.click
. You can't really prevent this from happening, as the parent is the FIRST handler.
I would propose the following solution, although it might seem a bit hacky:
HostListener('click')
, we can roll our own: we will listen to parent.mousedown
and parent.mouseup
. If they happen in quick succession, the assume it's a click, so we "select" the parent.// parent.component.ts
export class ParentComponent {
parentClicked: boolean = false;
down?: number;
manualClick(): void {
this.parentClicked = true;
console.log(`parent manual click`);
}
@HostListener('mousedown', ['$event'])
onMouseDown( event: MouseEvent): void {
console.log('parent down', event.target);
this.down = Date.now();
}
@HostListener('mouseup', ['$event'])
onMouseUp( event: MouseEvent): void {
console.log('parent up');
// Assume that if it's released within 100 ms, it's a click.
if (this.down && Date.now() - this.down < 100) {
this.manualClick();
this.down = undefined;
}
}
}
Since we don't listen to the click event, it's fine now:
parent.mousedown
NEVER HAPPENS.parent.mouseup -> document.mouseup
. Since parent.mousedown
wasn't triggered, down = undefined
, so our "click logic" doesn't count this as a click.document.mouseup
, where you finally handle it in your directive.Now, there's a small potential issue here: when you click in the child element, you will trigger our parent's click logic, because
child.mousedown-> parent.mousedown
is followed by child.mouseup -> parent.mouseup
. If you don't want this to happen, you can stop propagation of the mousedown
event in the child:
// child.component.ts
@HostListener('mousedown', ['$event'])
onMouseDown( event: MouseEvent): void {
console.log('child down');
event.stopPropagation();
event.preventDefault();
}