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.