Angular – Handling Events

June 12, 20263 min readUpdated 8/21/2026

An event binding runs a method when the element does something. The syntax is the event name in round brackets, and the name is the plain DOM event with no on prefix: click, not onclick.

<button type="button" class="btn btn-sm btn-primary" (click)="selected.emit(product())">
  {{ isPizza() ? 'Build it' : 'Add' }}
</button>

Anything the element emits can be bound this way — click, input, focus, submit, scroll. There is no allow-list, because Angular is not wrapping the DOM's events the way React's synthetic events do. If the browser fires it, you can bind it.

$event

$event is the event object, available inside the binding expression. For a DOM event it is the real DOM event:

<input
  id="menu-search"
  type="search"
  class="form-control"
  placeholder="Search — try 'pepperoni' or 'pepsi'"
  autocomplete="off"
  [value]="searchTerm()"
  (input)="searchTerm.set($any($event.target).value)"
/>

The $any(…) is there for the type checker. Templates are type checked, and EventTarget has no value property — plenty of things that fire input are not inputs. $any is the template's escape hatch, and using it here is a deliberate trade: the alternative is a handler in the class taking an Event and narrowing it properly, which is more correct and more noise for a search box.

When a child component emits, $event is whatever it emitted — not a DOM event:

<app-product-card [product]="product" (selected)="selectedProduct.set($event)" />

selected is an output<Product>(), so $event is a Product, fully typed.

Key events

Angular can filter a keyboard event by key in the binding itself, which saves the if (e.key === 'Escape') that would otherwise open every such handler:

<div
  class="modal fade show"
  tabindex="-1"
  role="dialog"
  aria-modal="true"
  [attr.aria-label]="ariaLabel()"
  (keydown.escape)="closed.emit()"
>

The suffix is the key name lowercased — (keydown.enter), (keydown.arrowdown), (keyup.space) — and modifiers combine with dots: (keydown.control.enter), (keydown.shift.tab).

Forms: use (ngSubmit), not (submit)

<form (ngSubmit)="submit()">

ngSubmit is an output on Angular's form directive rather than the raw DOM event. It fires on the same occasions — a click on the submit button, or Enter in a field — and it does not reload the page, so you do not have to remember $event.preventDefault(). Binding (submit) instead works right up until someone presses Enter and the whole application restarts.

Every form in the pizza app binds ngSubmit, template-driven and reactive alike.

Where the logic goes

The expression in an event binding is allowed to do more than a normal template expression — assignment is legal there, which is what makes [(x)] possible. That freedom is worth using sparingly.

These are fine, because there is nothing to test and nothing to get wrong:

<app-cart-drawer [open]="cartOpen()" (closed)="cartOpen.set(false)" />

Anything with a branch, a calculation, or a call to a service belongs in a method. The delete button on the admin users table binds remove(row), and the confirmation, the request and the error handling all live in the class:

<button
  type="button"
  class="btn btn-sm btn-outline-danger"
  [disabled]="self"
  [attr.title]="self ? 'You cannot delete your own account' : null"
  (click)="remove(row)"
>
  Delete
</button>

The rule is the same one that applies to templates generally: the template says what happens, the class says how. A template is not a place your tests can reach.

What you do not need

Two habits from React have no equivalent here, and looking for them is a common way to waste an afternoon.

There is no useCallback. Binding (selected)="selectedProduct.set($event)" creates no identity problem, because a handler is not an input and cannot invalidate a child. OnPush works without any cooperation from the parent.

You rarely need $event.preventDefault(). Use (ngSubmit) for forms and routerLink for navigation, and the two cases that usually call for it do not arise.

What is next

Templates so far have been straight-line markup. Next: @if, @for and @switch — the block syntax that replaced *ngIf and *ngFor.