AngularAtlased. 22.2

Ch. 02Modern fundamentals06 / 75

Data binding & events

Interpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.

~0 min read Official docs

A template isn't static HTML: Angular keeps it connected to the component class. The brackets tell you which way the data goes — {{ }} and [ ] carry values from the class into the DOM, ( ) brings events back into the class, and [( )] does both.

Most beginner bugs here aren't syntax errors. They come from three questions: which way is the data flowing, am I setting a DOM property or an HTML attribute, and which element really received the click?

Why it matters
Every screen you build is made of these four bindings. Once you know exactly what each one writes and when, a field that won't reset, a button that stays disabled or a click that opens the wrong thing stop being mysteries.

Four bindings, two directions

Data goes down with {{ }} and [ ], events come up with ( ). Angular re-evaluates the expressions on the right when the signals they read change, and writes to the DOM only if the result is different from what it wrote last time.

SyntaxDirectionWhat happens
{{ expr }}class → DOMsets the text of a node (always text, never HTML)
[prop]="expr"class → DOMsets a property of the DOM element
[attr.x]="expr"class → DOMsets or removes an HTML attribute
(event)="stmt"DOM → classruns a statement when the event fires
[(x)]="sig"both waysa property binding plus a change event, in one
<!-- class → DOM -->
<h2>{{ title() }}</h2>                          <!-- interpolation: text -->
<img [src]="avatarUrl()" [alt]="userName()" />  <!-- property binding -->
<td [attr.colspan]="wide() ? 2 : null"></td>    <!-- attribute binding -->

<!-- DOM → class -->
<button (click)="save()">Save</button>          <!-- event binding -->

<!-- both ways -->
<app-rating [(value)]="stars" />                <!-- two-way binding -->

Interpolation: always text

{{ }} turns the value into a string and puts it in a text node. HTML inside the value is shown as characters, not parsed — that's what protects you from XSS. Signals are read by calling them: {{ count() }}. Keep the expression cheap and free of side effects: it may run on every change detection pass.

<!-- the value becomes text: '<b>Ana</b>' is shown with its brackets, not in bold -->
<p>Hello, {{ userName() }}!</p>

<!-- any expression without side effects; signals are called -->
<p>{{ items().length }} items · {{ total() | currency: 'EUR' }}</p>
<p>{{ user()?.address?.city ?? 'no city yet' }}</p>

<!-- ✗ the text of the signal function, not its value -->
<p>{{ count }}</p>

Property binding sets a DOM property

[value]="name()" doesn't touch the HTML. It sets the value property of the input element. Attributes are the starting values written in the HTML; properties are the element's live state. When the user types, the property changes and the attribute stays as it was. And Angular writes the property only when name() changes — so if the class still holds the same value, a reset won't change what the user typed.

@Component({
  selector: 'app-profile',
  template: `
    <!-- sets input.value; does not change the value="" attribute -->
    <input [value]="name()" (input)="name.set(box.value)" #box />
    <button [disabled]="saving()" (click)="save()">Save</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class Profile {
  readonly name = signal('Ana');
  readonly saving = signal(false);

  reset() {
    // works only because (input) keeps name() up to date;
    // with [value] alone, name() is still 'Ana' and set('Ana') changes nothing
    this.name.set('Ana');
  }
}
LiveProperty or attribute, and a reset that doesn't reset
name()in the class'Ana'
input.valueDOM property'Ana'
getAttribute('value')HTML attributenull

in syncThe field and the signal both say 'Ana'.

Static attribute or property binding
clicks: 0

The first button is disabled: the attribute exists, and its text value doesn't matter.

In [value] mode: type a surname after “Ana”, then press name.set('Ana'). Repeat in the second mode, and compare the table and the two buttons below it.

In the first mode, the field shows text the class knows nothing about. name.set('Ana') gives the signal the value it already had, so nothing changes and Angular doesn't write to the input. Reading the input back with (input) — or binding both ways with [( )] or a form — keeps the class and the DOM in step. Notice that getAttribute('value') stays null throughout: Angular never set the attribute.

When there's no property: [attr.]

Some attributes have no matching property: aria-*, colspan, data-*, attributes on SVG. For those, use [attr.name]. A value of null or undefined removes the attribute, which is how you switch aria-current on and off. Classes and styles have their own bindings, covered in [class] and [style].

Boolean attributes are the classic trap: the browser looks only at whether the attribute exists. disabled="false" is a string, the attribute is present, and the button is disabled. [disabled]="false" sets the property to the boolean false.

<!-- no DOM property for these: bind the attribute -->
<td [attr.colspan]="span()">…</td>
<svg><circle [attr.r]="radius()" /></svg>
<div [attr.data-state]="state()"></div>

<!-- null removes the attribute: aria-current exists only on the active link -->
<a [attr.aria-current]="active() ? 'page' : null" href="/orders">Orders</a>

<!-- ✗ the attribute exists, so the button is disabled -->
<button disabled="false">Save</button>
<!-- ✓ the property gets the boolean false -->
<button [disabled]="false">Save</button>

Event binding: ( )

(click)="save()" adds a listener to the element and runs the statement when the event fires. $event is the DOM event object: a MouseEvent for clicks, a KeyboardEvent for keys, or the emitted value for a component's output(). For reading a field, a template reference such as #box is simpler and fully typed: (input)="name.set(box.value)".

<button (click)="save()">Save</button>

<!-- $event is the DOM event; for an output() it's the emitted value -->
<div (pointermove)="track($event.clientX, $event.clientY)"></div>
<app-rating (valueChange)="stars.set($event)" />

<!-- a template reference is typed as HTMLInputElement: no casts -->
<input #box (input)="query.set(box.value)" />

<!-- ✗ in strict mode: 'value' does not exist on type 'EventTarget' -->
<input (input)="query.set($event.target.value)" />

Events bubble up

A click starts on the deepest element and then travels up through every ancestor, running their (click) handlers too. That's how a Delete button inside a clickable row also opens the row — the row's handler runs right after the button's.

LiveA Delete button inside a clickable row
  • Order #1042
  • Order #1043
  • Order #1044
Handlers that ran, in order

Nothing clicked yet.

Press Delete on an order and look at Details and at the list of handlers. Then switch to $event.stopPropagation() and try again.

$event.target is the element where the click started, and it's the same in every handler. $event.currentTarget is the element whose handler is running. stopPropagation() in the inner handler is the quick fix. A better structure is often not to nest them at all: make the row's action a real link or button next to Delete, not a click handler on the whole row — that also gives keyboard users a way to open it.

<ul>
  @for (order of orders(); track order.id) {
    <!-- the row opens the order… -->
    <li (click)="open(order)">
      Order #{{ order.id }}
      <!-- …and without stopPropagation(), a click here reaches the row too -->
      <button (click)="remove(order); $event.stopPropagation()">Delete</button>
    </li>
  }
</ul>

<!-- often better: two separate actions, no nesting, and reachable from the keyboard -->
<li>
  <a [routerLink]="['/orders', order.id]">Order #{{ order.id }}</a>
  <button (click)="remove(order)">Delete</button>
</li>

Key filters and the browser's default action

Angular lets you filter keyboard events right in the binding: (keydown.enter), (keydown.escape), (keydown.shift.enter), (keydown.control.s). The modifiers must match exactly: (keydown.enter) doesn't fire for Shift+Enter. Your handler runs before the browser's default action — inserting a line break, submitting the form, following a link — and $event.preventDefault() cancels that action.

<textarea
  #msg
  [value]="draft()"
  (input)="draft.set(msg.value)"
  (keydown.enter)="send($event)"
  (keydown.escape)="draft.set('')"
></textarea>
<!-- (keydown.enter) does not fire for Shift+Enter: the browser adds a line, as usual -->
LiveA chat box: Enter sends, Shift+Enter adds a line
  • No messages yet.
(keydown)—
(keydown.enter)—
(keydown.shift.enter)—
(keydown.escape)—

Press a key in the message box.

Type a message and press Enter in each mode. Then try Shift+Enter and Escape, and watch which bindings run.

Without preventDefault(), the browser inserts the line break after your handler has emptied draft, and the (input) event writes the text back. One more detail most chat boxes get wrong: while someone types Japanese or Chinese with an input method, Enter confirms the characters. Check $event.isComposing and ignore the key in that case.

[( )] is [ ] plus ( )

The “banana in a box” is shorthand. [(value)]="name" means a property binding on value plus a listener for the valueChange event that writes back. When you pass a WritableSignal, Angular sets the signal for you. On your own components, model() creates both halves; for form fields, use Signal Forms.

<!-- the shorthand… -->
<app-rating [(value)]="stars" />

<!-- …means exactly this -->
<app-rating [value]="stars()" (valueChange)="stars.set($event)" />

<!-- with a WritableSignal, Angular calls set() for you;
     the child declares it with: value = model(0) -->

Template reference variables

#name on an element gives you that element in the rest of the template; on a component, it gives you the component instance. It's the quickest way to read a field or call focus() without going through $event.target, whose type is only EventTarget. In the class, the same reference is reached with viewChild().

<!-- on an element: the element itself -->
<input #search placeholder="Search" />
<button (click)="search.focus()">Focus</button>

<!-- on a component: the component instance -->
<app-video-player #player />
<button (click)="player.play()">Play</button>

<!-- in the class: readonly search = viewChild<ElementRef<HTMLInputElement>>('search') -->

Good habits / avoid

Good habits
  • Keep the source of truth in the class (a signal) and read every change back from the DOM.
  • Use template references (#box) instead of $event.target when you need a value.
  • Use [attr.] for aria-* and data-*, and null to remove an attribute.
Avoid
  • Calling functions that do heavy work in {{ }} or [ ] — they run again on every pass.
  • Static boolean attributes with a value (disabled="false"): the attribute's presence is all that counts.
  • stopPropagation() as a habit: it also hides the event from analytics, from menus that close when you click outside, and from your colleagues.

Common pitfalls

  • A reset that doesn't reset. The field was bound one way, the user typed, and the class still holds the old value. Setting it again changes nothing.
  • Forgetting the parentheses. {{ count }} prints the signal function itself, not its value. The compiler warns about it; don't ignore the warning.
  • The row opens when you click Delete. The click bubbled from the button to the row. Stop it, or don't nest actions.
  • A line break left behind after sending. Enter's default action ran after the handler. Call $event.preventDefault().
  • Typing $event.target.value. target is an EventTarget, so this fails to compile in strict mode. Use a template reference, or read it in the class with a cast.

Cheat sheet

{{ expr }}             text into the DOM; HTML is escaped; call signals: {{ count() }}
[prop]="expr"          a DOM property (value, disabled, src, checked) or a component input
[attr.x]="expr"        an HTML attribute (aria-*, data-*, colspan, svg); null removes it
[class.x] / [style.x]  one class or one style; see the [class] and [style] page
(event)="stmt"         a listener; $event = the DOM event or the emitted value
(keydown.enter)        key filters; modifiers must match: shift.enter, control.s, escape
[(x)]="sig"            [x]="sig()" + (xChange)="sig.set($event)"
#ref                   the element, or the component instance, in the template

when writes happen     only when the value differs from the last one Angular wrote
attribute vs property  attribute = starting value in the HTML; property = live state
bubbling               a click runs the handlers of every ancestor; target stays the same
default actions        run after your handler; $event.preventDefault() cancels them

Ch. 02 · 0 of 8 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Standalone & bootstrap
  2. Data binding & events
  3. Control flow: @if, @for, @switch
  4. @let and the new template expressions
  5. @defer — lazy loading in templates
  6. Class & style bindings
  7. Component styles & encapsulation
  8. Pipes: when and how
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation