Tap event bubble in parent and child components

I have a problem with tap events, that doesn’t happen with click events, and makes it apparently impossible to stop the event bubbling from an inner component (child) to an outer one (parent) (at least without hacks).

To simulate the problem I’ve created a sidemenu starter project:

ionic start --v2 myApp sidemenu

Then I’ve changed the ion-list inside the app.html to:

	<ion-list>
		<ion-item-group>
			<ion-item-divider>
				Click
			</ion-item-divider>

			<button menuClose ion-item *ngFor="let p of pages" (click)="onOuterClick($event)">
				{{p.title}}
				<button item-right clear (click)="onInnerClick($event)">
					<ion-icon name="close"></ion-icon>
				</button>
			</button>
		</ion-item-group>

		<ion-item-group>
			<ion-item-divider>
				Tap
			</ion-item-divider>

			<button menuClose ion-item *ngFor="let p of pages" (tap)="onOuterClick($event)">
				{{p.title}}
				<button item-right clear (tap)="onInnerClick($event)">
					<ion-icon name="close"></ion-icon>
				</button>
			</button>
		</ion-item-group>
	</ion-list>

And added the following methods to the app.component.ts:

onOuterClick(event: Event) {
	console.log('onOuterClick');
}

onInnerClick(event: Event) {
	if (event && event.stopPropagation) {
		console.log('onInnerClick - stopPropagation');
		event.stopPropagation();
	}

	console.log('onInnerClick');
}

Then I run ionic serve and click both the areas (one at a time) in the menus outside the inner button and the inner button (icon) itself, to simulate the behaviour:

Logs - Click Event - Outer Area:

onOuterClick

Logs - Click Event - Inner Area (Icon):

onInnerClick - stopPropagation
onInnerClick

Logs - Tap Event - Outer Area:

onOuterClick

Logs - Tap Event - Inner Area (Icon):

onOuterClick
onInnerClick

The click events behaves the expected way, stoping the propagation of the event when the inner component is clicked, but the tap events have 2 problems:

  1. The event is called first in the outer element and after in the inner one, that is, the bubbling order is inverted.

  2. The event doesn’t have a stopPropagation method, as opposite to all (or most) normal HTML DOM events, so I can’t stop its propagation.

Is there a way to make the tap event behaves like the click event, concerning the bubble behaviour?

I’ve solved it in a relatively simple way, although a bit hack, doing the following:

In app.html I’ve added a pointerup event handler to the inner button with the tap event:

<button item-right clear (pointerup)="onInnerPointerUp($event)" (tap)="onInnerClick($event)">
	<ion-icon name="close"></ion-icon>
</button>

In app.component.html:

onOuterClick(event: { srcEvent: Event }) {
	let srcEvent = event && event.srcEvent;
	let fire = (!srcEvent) || (!srcEvent.defaultPrevented);

	if (fire) {
		console.log('onOuterClick');
	}
}

onInnerClick(event: Event) {
	if (event && event.stopPropagation) {
		console.log('onInnerClick - stopPropagation');
		event.stopPropagation();
	}

	console.log('onInnerClick');
}

onInnerPointerUp(event: PointerEvent) {
	if (event && event.preventDefault) {
		event.preventDefault();
	}

	console.log('onInnerPointerUp');
}

Logs - Tap Event - Inner Area (Icon):

onInnerPointerUp
onInnerClick

It solves the issue, because the onInnerPointerUp event handler is the first one called, and its event is the srcEvent property of the tap event, so I’ve called event.preventDefault() in it and in the outer button event I check if the srcEvent is not defined or if the defaultPrevented property is false, and only in these cases I execute the other instructions (in this test case, just the console.log()).

PS: I could have set an inexistent property in the event (any name that is improbable that the event would have as a property name) to some value (instead of calling preventDefault()) in the onInnerPointerUp method, and checking it in the onOuterClick method (instead of srcEvent.defaultPrevented, it would be srcEvent['myPropertyName']), but I think that using preventDefault() is cleanier because it uses the Event interface properties.

This worked great for me! Until I got it onto a physical iOS device. Works on android, but not on iOS for me.