skip to content

In an Angular template, how does an (event)="statement" binding work, and what does the $event variable hold?

level: juniorimportance: must knowfreq 72%

answer

  1. parentheses mean target to source
  2. statement runs on every occurrence
  3. native event or emitted value
  4. returning false has a side effect

basics

~20 s

Parentheses around an event name make Angular listen for that event and run the quoted template statement each time it fires. $event is the native DOM event object, or the value a component output emitted.

solid answer

~40 s

`(click)="save()"` tells Angular to register a listener for `click` on that element and evaluate the statement `save()` in the component's context whenever it fires. Inside the statement Angular exposes `$event`: for a native DOM event it is the browser's event object (a `PointerEvent`, `KeyboardEvent`, `InputEvent` and so on), and for a binding to a child component's output it is whatever value the child emitted with `output()` or an `EventEmitter`. The statement usually calls a method, but it may also assign a field. Two details catch people out: a handler whose statement evaluates to `false` makes Angular call `preventDefault()`, and Angular removes the listener itself when the view is destroyed, so template listeners never need manual cleanup.

code

ts · 30 lines
ts
import { Component, output, signal } from '@angular/core';

@Component({
  selector: 'app-rating',
  template: `
    @for (n of [1, 2, 3, 4, 5]; track n) {
      <button type="button" (click)="rated.emit(n)">{{ n }}</button>
    }
  `,
})
export class Rating {
  rated = output<number>();
}

@Component({
  selector: 'app-review',
  imports: [Rating],
  template: `
    <input (keyup)="onKey($event)" />
    <app-rating (rated)="score.set($event)" />
    <p>Score: {{ score() }}</p>
  `,
})
export class Review {
  score = signal(0);

  onKey(event: KeyboardEvent) {
    console.log('pressed', event.key);
  }
}

go deeper

for a junior

Recall the direction (DOM to component), that the statement runs on every event, and what $event holds for a DOM event versus an output.

for a middle

Explain the wrapper Angular registers: it marks the view dirty, runs the statement, and calls preventDefault on an explicit false result.

for a senior

Point out the traps you have seen in reviews: uninvoked method references, boolean-returning handlers cancelling defaults, and outputs named after DOM events.

for a principal

Tie listener ownership to view lifetime when setting team rules on template listeners versus imperative addEventListener code.

## What an event binding is An Angular template has three binding directions. `[prop]="expr"` pushes data **from the component into the DOM**, `(event)="statement"` sends a signal **from the DOM back to the component**, and `[(x)]` combines the two. An **event binding** is the parenthesised form: ```html <button type="button" (click)="save()">Save</button> <input (keyup)="onKey($event)" /> <app-rating (rated)="setScore($event)" /> ``` The text inside the parentheses is the **event name**; the quoted text is a **template statement**. The compiler turns the statement into a small function, registers it as a listener, and runs it in the component's context every time the event fires. Unlike a binding expression, a statement is allowed to have side effects: calling a method is the normal case, and assigning a field (`(click)="open = true"`) is also legal. ## What `$event` contains Angular gives every event statement one extra variable, `$event`. What it holds depends on what the name resolves to: | Binding target | `$event` is | Example | |---|---|---| | A native DOM event on an element | The browser's event object | `(click)` gives a `PointerEvent`, `(keydown)` a `KeyboardEvent` | | An output of a component or directive | The value passed to `emit()` | `(rated)` on a child that calls `this.rated.emit(4)` gives `4` | | A global target such as `window:resize` | The event fired on that global object | the `resize` event dispatched on `window` | So `$event` is **not** always an `Event`. For an output declared as `rated = output<number>()`, `$event` is a plain `number`, and code that reads `$event.target` there is a bug. ## What happens when the event fires The listener Angular registers is a wrapper around the compiled statement. On each event it: 1. **Marks the view that declared the listener dirty**, along with its ancestors, so the next change detection pass refreshes it. This is why a click handler that changes a plain field still updates an `OnPush` component. 2. **Runs the statement** with `$event` bound, catching any error and passing it to Angular's error handling instead of letting it escape. 3. **Checks the result.** If the statement evaluates to exactly `false`, Angular calls `preventDefault()` on the event. Any other value, including `undefined`, is ignored. The third step matters in practice. A method such as `validate(): boolean` used as `(submit)="validate()"` silently blocks the default action whenever it returns `false`. The Angular docs recommend calling `event.preventDefault()` explicitly so the intent is visible. ## Lifetime and cleanup Template listeners belong to the view that declares them. When that view is destroyed (the component is removed, an `@if` block turns false, a route changes) Angular removes every listener it registered for that view. That includes listeners on global targets such as `(window:resize)` and `(document:keydown)`. You never call `removeEventListener` for a template binding, which is one practical reason to prefer template listeners over `addEventListener` calls in component code. ## Walking through one click Take `<button (click)="count.set(count() + 1)">` in a component. At creation, Angular registers one native `click` listener on the button. When the user clicks, the browser calls that listener with a `PointerEvent`; Angular's wrapper marks the button's view dirty, evaluates the statement with `$event` available (unused here), sees that the result is not `false`, and returns. The signal write and the dirty mark both lead to a refresh, so the new count appears on the next change detection pass. Nothing in the component had to subscribe, unsubscribe or ask for a re-render. ## Common mistakes - Writing `(click)="save"` without the call parentheses. The statement just reads the method reference and nothing runs. With `strictTemplates`, the extended diagnostic `uninvokedFunctionInEventBinding` (NG8111) flags it. - Treating `$event` as a DOM event on a component output, or as a string on an input field. It is the event object, and the text lives on `$event.target.value`. - Returning a boolean from a click handler for an unrelated reason and accidentally cancelling the browser's default action. - Adding manual `removeEventListener` code for template listeners, or worrying that `(window:resize)` leaks after the component is destroyed. ## Current style In Angular 22 new components are standalone, declare outputs with `output()`, and are `OnPush` by default. None of that changes the event-binding syntax, which has been the same since Angular 2. What has changed is what a handler's dirty-marking means: in a zoneless app (the default since v21) the listener is also one of the signals that schedule change detection.

  • What happens if the statement in an Angular event binding evaluates to false?
    Angular calls `preventDefault()` on the event. Only an explicit `false` counts; `undefined` or other falsy values are ignored. It mirrors the old inline `onclick` attribute behaviour, but the Angular docs recommend calling `event.preventDefault()` yourself so the intent is readable and a method that happens to return a boolean does not cancel the default action by accident.
  • Do you need to remove an Angular template listener such as (window:resize) when the component is destroyed?
    No. Angular stores a cleanup function for every listener it registers, including those on `window`, `document` and `body`, and runs them when the declaring view is destroyed. Manual cleanup is only needed for listeners you add yourself with `addEventListener` or `Renderer2.listen` in component code.
  • In Angular, what is $event when you bind to a child component's output?
    It is exactly the value the child passed to `emit()`, typed from the output's declared type. For `saved = output<User>()` it is a `User`; for `output<void>()` it is `undefined`. It is not a DOM event, so reading `$event.target` there is a mistake.

An event binding is like a doorbell wired to a note on the fridge: each ring runs the instruction on the note, and the visitor at the door ($event) is handed to whoever reads it.

saying these in an interview costs you the question

  • $event is always a DOM Event object, even for component outputs
  • (click)="save" without parentheses still calls save
  • Template listeners must be removed manually in ngOnDestroy
  • Returning false from a handler is harmless and ignored
  • Event bindings push data from the component into the DOM