Handling taps and events

Lynx elements fire native events: taps, touches, scrolls and text input. You bind them with Angular's (event) syntax, using Lynx names such as (bindtap) in place of (click).

Handle a tap

src/app/color-picker.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-color-picker',
  imports: [LYNX_ELEMENTS],
  template: `
    @for (color of colors; track color) {
      <view class="rounded-lg bg-card p-4" (bindtap)="selected.set(color)">
        <text class="text-foreground">{{ color }}</text>
      </view>
    }
    <text class="text-muted-foreground">Selected: {{ selected() }}</text>
  `,
})
export class ColorPicker {
  readonly colors = ['Red', 'Green', 'Blue'];
  readonly selected = signal('none');
}
  • bindtap is Lynx's click. It fires when a finger presses and lifts without moving far. In the web preview, a mouse click fires it too.
  • (click) does nothing. AngularLynx ignores event names that don't start with a Lynx prefix such as bind or catch, and it doesn't warn you.
  • Pass values as arguments. (bindtap)="select(item)" is simpler than reading the item back from the event.
  • Handlers run on the main thread, inside the native event. Changing signals there is always safe. See Threads.

Defer navigation and new views

src/app/order-row.ts
import { Component, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-order-row',
  imports: [LYNX_ELEMENTS],
  template: `
    <view (bindtap)="openDetails()">
      <text>{{ opening() ? 'Opening…' : 'Order #1042' }}</text>
    </view>
  `,
})
export class OrderRow {
  readonly opening = signal(false);
  readonly #router = inject(Router);

  openDetails(): void {
    // Safe right away: Angular re-renders after the handler returns.
    this.opening.set(true);

    // Deferred: navigating creates and destroys views.
    setTimeout(() => this.#router.navigate(['/orders', 1042]));
  }
}

Wrap router navigation, LynxPortal.open() and element.animate() in setTimeout when you call them from a handler. A handler runs inside the native event, and creating or destroying views there can crash the app or leave it blank. See the setTimeout rule.

Stop an event from bubbling

<view class="h-full w-full bg-black/50" (bindtap)="close()">
  <!-- catchtap keeps taps on the panel from reaching the backdrop -->
  <view class="rounded-lg bg-background p-6" (catchtap)="noop()">
    <text>Tapping here doesn't close the dialog.</text>
  </view>
</view>

The prefix on the event name decides how the event travels:

PrefixBehaviorExample
bindHandles the event and lets it bubble up(bindtap)="onTap()"
catchHandles the event and stops it there(catchtap)="onTap()"
capture-bindHandles it on the way down, then bubbles(capture-bindtap)="onTap()"
capture-catchHandles it on the way down, then stops(capture-catchtap)="onTap()"
  • Use catch, not $event.stopPropagation(). Lynx sets propagation by the prefix. AngularLynx turns stopPropagation() and preventDefault() into harmless no-ops so web-style code doesn't crash, but they don't stop anything.
  • A catch handler can be empty. The prefix alone stops the event, so (catchtap)="noop()" with an empty method is a common pattern.

Read the event object

src/app/touch-pad.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import type { TouchEvent } from '@lynx-js/types';

@Component({
  selector: 'app-touch-pad',
  imports: [LYNX_ELEMENTS],
  template: `
    <view class="h-[200px] bg-muted" (bindtouchmove)="onMove($event)">
      <text>{{ x() }}, {{ y() }}</text>
    </view>
  `,
})
export class TouchPad {
  readonly x = signal(0);
  readonly y = signal(0);

  onMove(event: TouchEvent): void {
    const touch = event.touches[0];
    if (!touch) return;
    this.x.set(Math.round(touch.x));
    this.y.set(Math.round(touch.y));
  }
}

Every event has the same outer shape. ng add installs @lynx-js/types, which has a type for each one.

FieldTypeWhat it holds
typestringThe event name without its prefix, such as 'tap'
timestampnumberWhen the event fired
target{ id, uid, dataset }The element the event started on: its id attribute, an internal number, and its data-* attributes
currentTarget{ id, uid, dataset }The element whose handler is running
detailobjectThe event's own data. See Common events
touchesTouch[]Touch and tap events only: the fingers on the screen
changedTouchesTouch[]Touch and tap events only: the fingers that changed in this event

Each Touch has x and y relative to the element, pageX and pageY relative to the page, clientX and clientY relative to the screen, and an identifier.

target and currentTarget are plain data, not elements, so you can't call methods on them. To call an element method, such as scrolling a <scroll-view>, see Call element methods.

Type the parameter

  • Tap, long-press and touch events: $event is Lynx's TouchEvent. Import it with import type { TouchEvent } from '@lynx-js/types', as above.
  • Every other event: Angular types $event as the DOM Event. Accept Event, then cast it to read detail: (event as CustomEvent<{ value: string }>).detail.value.

Common events

EventElementsFires whendetail
bindtapAllA finger presses and lifts{ x, y }, page position
bindlongpressAllA finger is held down{ x, y }
bindtouchstart, bindtouchmove, bindtouchend, bindtouchcancelAllA finger goes down, moves, lifts or is interrupted{ x, y }, plus touches
bindlayoutchangeAllThe element's size or position changes{ id, width, height, top, right, bottom, left, dataset }
bindscroll<scroll-view>, <list>The content scrolls{ scrollTop, scrollLeft, scrollHeight, scrollWidth, deltaX, deltaY }
bindscrolltolower<scroll-view>, <list>Scrolling nears the endSame as bindscroll
bindinput<input>, <textarea>The text changes{ value, selectionStart, selectionEnd, isComposing }
bindfocus, bindblur, bindconfirm<input>, <textarea>The field gains or loses focus, or the keyboard's action key is pressed{ value }
bindload<image>The image finishes loading{ width, height }

Each element's page under API → Elements lists all its events.

  • bindtap and bindlongpress exclude each other. With both on one element, a long press fires only bindlongpress.
  • Touch events don't fire from a mouse. In the web preview, a mouse fires tap but no touchstart or touchend. If you build press feedback from touch events, also bind bindmousedown and bindmouseup.

Choose taps, gestures or touch events

UseFor
bindtap and bindlongpressButtons, list rows, anything you press. Start here.
Gestures ([lynxGesture])Drags and swipes, or several interactions on one element that must not clash, such as a drag that shouldn't count as a tap
Touch events (bindtouchstart…)Raw finger positions, such as a pressed state or a custom drawing pad
Main-thread scriptsMoving an element on every frame with no lag