DevTools

Cheatsheet Angular

Framework frontend da Google baseado em TypeScript

Back to languages
Angular
152 cards found
Categories:
Versions:

Setup e CLI


14 cards
Global Installation
npm install -g @angular/cli
ng version
ng new my-project
cd my-project
ng serve --open

Install @angular/cli globally with npm install -g. The ng new command creates the project with all the configuration. ng serve --open starts the dev server on port 4200 and opens the browser automatically.

Project Structure
my-project/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   ├── app.config.ts
│   │   └── app.routes.ts
│   ├── index.html
│   ├── main.ts
│   └── styles.css
├── angular.json
├── package.json
└── tsconfig.json

The modern structure (v17+) has app.config.ts (providers) and app.routes.ts (routes) instead of app.module.ts. main.ts bootstraps with bootstrapApplication(). angular.json configures builds, assets and budgets.

Environments
// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api'
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com'
};

// Usage: import { environment } from '../environments/environment';

environments allow different settings per environment. ng build uses the production file automatically. In development, ng serve uses the base file. Access via environment.apiUrl.

Global Styles
/* src/styles.css — global */
@import 'normalize.css';

:root {
  --primary: #DD0031;
  --spacing: 1rem;
}

body {
  font-family: 'Inter', sans-serif;
  margin: 0;
}

styles.css (defined in angular.json) contains global CSS. Component styles are encapsulated by ViewEncapsulation.Emulated (default) — they do not affect other components. Use ViewEncapsulation.None for global styles in a component.

ng new — Options
ng new app --routing
ng new app --skip-tests
ng new app --ssr
ng new app --style=scss
ng new app --standalone

ng new accepts flags: --routing adds routing, --style=scss sets the CSS preprocessor, --ssr enables Server-Side Rendering with Hydration. Since Angular 17, projects are standalone by default.

main.ts — Bootstrap
import { bootstrapApplication }
  from '@angular/platform-browser';
import { AppComponent }
  from './app/app.component';
import { appConfig }
  from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

main.ts is the entry point. bootstrapApplication() initializes the root component with the global configuration. It replaces the old platformBrowserDynamic().bootstrapModule() from NgModule projects.

Useful CLI Commands
ng serve --port 3000
ng serve --proxy-config proxy.conf.json
ng lint
ng test
ng e2e
ng update @angular/core
ng add @angular/material
ng deploy

The CLI has commands for everyday work: ng serve --port changes the port, ng add installs and configures libraries (e.g. @angular/material), ng update updates dependencies with automatic migrations.

Assets and Static Files
// angular.json
"assets": [
  "src/favicon.ico",
  "src/assets",
  { "glob": "**/*", "input": "src/i18n", "output": "/i18n" }
]

// Access in the template
<img src="assets/logo.png">
<img [src]="'assets/' + image">

The src/assets/ folder is copied into the build. Configure it in angular.json in the assets array. You can add objects with glob, input and output to map external folders. In the template, use paths relative to assets/.

ng generate
ng generate component components/header
ng g c components/header
ng g s services/api
ng g d directives/highlight
ng g p pipes/truncate
ng g g guards/auth
ng g interceptor interceptors/token

ng generate (shortcut ng g) creates files with boilerplate. The path defines the folder: ng g c components/header creates it in src/app/components/header/. All artifacts are standalone by default since Angular 17.

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ]
};

app.config.ts defines the global providers using provide* functions: provideRouter() for routes, provideHttpClient() for HTTP, provideAnimations() for animations. It replaces the imports array of the old NgModule.

TypeScript Config
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ES2022",
    "experimentalDecorators": true,
    "paths": {
      "@app/*": ["src/app/*"],
      "@env/*": ["src/environments/*"]
    }
  }
}

tsconfig.json configures TypeScript. strict: true enables all checks. experimentalDecorators is required for the @Component, @Injectable, etc. decorators. paths allows import aliases like @app/.

Build and Deploy
ng build
ng build --watch
ng build --configuration production
# Output in dist/
# npx serve dist/my-project/browser

ng build compiles to dist/. In production it applies AOT compilation, tree-shaking, minification and budget checks (warnings if the bundle exceeds the limits defined in angular.json).

angular.json — Configuration
{
  "projects": {
    "my-project": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/my-project",
            "index": "src/index.html",
            "main": "src/main.ts",
            "styles": ["src/styles.css"],
            "assets": ["src/favicon.ico", "src/assets"]
          }
        }
      }
    }
  }
}

angular.json configures builds, serve, tests and assets. In build.options set styles (global CSS), assets (static files) and budgets (size limits). serve inherits from the build configuration.

Path Aliases
// tsconfig.json
"paths": {
  "@components/*": ["src/app/components/*"],
  "@services/*": ["src/app/services/*"],
  "@models/*": ["src/app/models/*"]
}

// Usage in imports
import { HeaderComponent } from '@components/header/header.component';
import { ApiService } from '@services/api.service';

Path aliases simplify long imports. Define them in tsconfig.json under compilerOptions.paths. Instead of ../../services/api.service, use @services/api.service. The Angular CLI resolves the aliases automatically at build time.

Components


14 cards
Standalone Component
import { Component } from '@angular/core';

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [],
  templateUrl: './header.component.html',
  styleUrl: './header.component.css'
})
export class HeaderComponent {
  title = 'My App';
}

A standalone component does not need an NgModule. The @Component decorator defines the selector (HTML tag), the template and the styles. The imports property lists the component's dependencies (other components, pipes, directives).

Lifecycle
import { OnInit, OnChanges, OnDestroy, SimpleChanges }
  from '@angular/core';

export class UserComponent
  implements OnInit, OnChanges, OnDestroy {

  ngOnChanges(changes: SimpleChanges) {
    console.log(changes['userId']);
  }

  ngOnInit() {
    this.loadData();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

The lifecycle hooks: ngOnChanges (inputs change), ngOnInit (initialization), ngDoCheck (manual detection), ngAfterViewInit (view ready), ngOnDestroy (cleanup). Implement the corresponding interfaces.

Host Binding and Host Listener
import { Component, HostBinding, HostListener } from '@angular/core';

@Component({
  selector: 'app-clickable',
  template: `<ng-content />`
})
export class ClickableComponent {
  @HostBinding('class.active') active = false;
  @HostBinding('attr.role') role = 'button';

  @HostListener('click')
  onClick() { this.active = !this.active; }

  @HostListener('document:keydown.escape')
  onEscape() { this.active = false; }
}

@HostBinding binds a component property to an attribute/class of the host element. @HostListener listens to events on the host or the document (document:keydown.escape). Modern alternative: use the host property in the @Component decorator.

model() — Two-Way Binding
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-toggle',
  template: `
    <button (click)="active.update(v => !v)">
      {{ active() ? 'ON' : 'OFF' }}
    </button>
  `
})
export class ToggleComponent {
  active = model(false);
}

// Parent: <app-toggle [(active)]="on" />

model() creates a signal with automatic two-way binding. The child reads with active() and updates with active.set() or active.update(). The parent uses [(active)]="variable" — the "banana in a box" syntax works automatically with model().

Inline Template and Style
@Component({
  selector: 'app-badge',
  standalone: true,
  template: `
    <span class="badge" [class.active]="active">
      {{ text }}
    </span>
  `,
  styles: `
    .badge { padding: 4px 8px; border-radius: 4px; }
    .active { background: #DD0031; color: white; }
  `
})
export class BadgeComponent {
  text = '';
  active = false;
}

For small components, use inline template and styles (with template literals). Avoids separate files. styleUrl (singular) is as modern form; styleUrls (array) still works but is deprecated.

ng-content — Projection
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <header><ng-content select="h2" /></header>
      <div class="body"><ng-content /></div>
      <footer><ng-content select=".actions" /></footer>
    </div>
  `
})

// Usage:
// <app-card>
//   <h2>Title</h2>
//   <p>Main content</p>
//   <div class="actions"><button>OK</button></div>
// </app-card>

<ng-content> projects content from the parent into the child (slot). The select attribute filters by CSS selector: select="h2" captures headings, select=".actions" captures elements with that class. Without select, it captures everything not yet distributed.

Host Metadata (modern)
@Component({
  selector: 'app-btn',
  template: `<ng-content />`,
  host: {
    'class': 'btn',
    '[class.disabled]': 'disabled',
    '[attr.aria-disabled]': 'disabled',
    '(click)': 'onClick()',
    '(mouseenter)': 'hover = true',
    '(mouseleave)': 'hover = false'
  }
})
export class BtnComponent {
  disabled = false;
  hover = false;
  onClick() { /* ... */ }
}

The host property in the decorator replaces @HostBinding and @HostListener. Use [class.x] for classes, [attr.x] for attributes and (event) for listeners. More declarative and without extra decorators.

Query with viewChild()
import { Component, viewChild, viewChildren, ElementRef }
  from '@angular/core';

@Component({
  selector: 'app-list',
  template: `<input #field /><p *ngFor="let i of items">{{ i }}</p>`
})
export class ListComponent {
  field = viewChild.required<ElementRef>('field');
  paragraphs = viewChildren<ElementRef>('p');

  focus() {
    this.field().nativeElement.focus();
  }
}

viewChild() and viewChildren() are the signal-based alternative to @ViewChild. They return signals: read with field(). viewChild.required() throws an error if not found. No need for ngAfterViewInit — the signal updates automatically.

@Input — Receive Data
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-card',
  template: `<h2>{{ title }}</h2><p>{{ description }}</p>`
})
export class CardComponent {
  @Input() title = '';
  @Input() description = '';
  @Input({ required: true }) id!: number;
}

// Usage: <app-card [title]="t" [id]="1" />

@Input() declares properties the parent can bind with [property]="value". @Input({ required: true }) makes the input mandatory — compile error if not provided. The ! (non-null assertion) indicates it will be initialized externally.

@ViewChild and @ContentChild
import { ViewChild, ContentChild, ElementRef, AfterViewInit }
  from '@angular/core';

export class ParentComponent implements AfterViewInit {
  @ViewChild('inputRef') input!: ElementRef;
  @ViewChild(ChildComponent) child!: ChildComponent;
  @ContentChild('projected') projected!: ElementRef;

  ngAfterViewInit() {
    this.input.nativeElement.focus();
    this.child.publicMethod();
  }
}

@ViewChild accesses elements/components in the component's own template. @ContentChild accesses content projected via ng-content. Use #ref in the template the a reference. They are only available after ngAfterViewInit / ngAfterContentInit.

Dynamic Components
import { ViewChild, ViewContainerRef } from '@angular/core';
import { AlertComponent } from './alert.component';

@Component({ template: `<ng-container #container />` })
export class HostComponent {
  @ViewChild('container', { read: ViewContainerRef })
  container!: ViewContainerRef;

  creupTolert(message: string) {
    const ref = this.container.createComponent(AlertComponent);
    ref.setInput('message', message);
    ref.instance.closed.subscribe(() => ref.destroy());
  }
}

ViewContainerRef allows creating components dynamically with createComponent(). Use setInput() to set inputs and instance to access outputs. ref.destroy() removes the component. Useful for modals, tooltips and notifications.

@Output — Emit Events
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-search',
  template: `<input (input)="onInput($event)">`
})
export class SearchComponent {
  @Output() searched = new EventEmitter<string>();

  onInput(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.searched.emit(value);
  }
}

// Parent: <app-search (searched)="search($event)" />

@Output() exposes events with EventEmitter. The child calls .emit(value) and the parent listens with (event)="handler($event)". $event contains the emitted value. Combine with @Input for two-way communication.

Signal Inputs (v17.1+)
import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-user',
  template: `
    <p>{{ name() }} ({{ age() }})</p>
    <button (click)="removed.emit()">X</button>
  `
})
export class UserComponent {
  name = input.required<string>();
  age = input(0);
  removed = output<void>();
}

// Usage: <app-user [name]="'Anna'" [age]="25" />

Signal inputs (input()) are the modern alternative to @Input(). They are signals: read with name(). input.required<T>() makes it mandatory. output() replaces @Output() + EventEmitter. More type-safe and decorator-free.

Style Encapsulation
import { ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-global',
  template: `...`,
  styles: `p { color: red; }`,
  encapsulation: ViewEncapsulation.None
})

// Options:
// Emulated (default) — styles isolated with _ngcontent
// None — global styles (no isolation)
// ShadowDom — uses native Shadow DOM

ViewEncapsulation.Emulated (default) adds _ngcontent-xxx attributes to isolate styles. None makes the styles global. ShadowDom uses the browser's native Shadow DOM. To style projected content, use :host ::ng-deep (deprecated).

Template and Syntax


14 cards
Interpolation
<p>{{ title }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ user?.name }}</p>
<p>{{ items.length }}</p>
<p>{{ active ? 'Yes' : 'No' }}</p>
<p>{{ name | uppercase }}</p>

Interpolation {{ }} renders expressions in the template. It supports properties, ternary operators, optional chaining (?.) and pipes. It does not allow variable declarations, access to window/document or side effects.

@if — Conditional (v17+)
@if (user) {
  <p>Hello, {{ user.name }}</p>
} @else if (loading) {
  <p>Loading...</p>
} @else {
  <p>No user</p>
}

@if (data$ | async; the data) {
  <ul>
    @for (item of data; track item.id) {
      <li>{{ item.name }}</li>
    }
  </ul>
}

The new @if syntax (v17+) replaces *ngIf. It supports @else if and @else. The @if (expr; the var) form creates a local variable with the value. No <ng-container> needed — the block does not create an extra DOM element.

Template Reference Variables
<input #nameField>
<button (click)="greet(nameField.value)">Greet</button>

<app-form #form>
  <button (click)="form.validate()">Validate</button>
</app-form>

<input #email="ngModel" [(ngModel)]="mail" required>
<p *ngIf="email.invalid">Invalid email</p>

Reference variables #name give direct access to elements, components or directives in the template. #field references the HTMLElement. #comp references the component instance. #x="ngModel" references the NgModel directive with its validation state.

Events with $event
<input (input)="onInput($event)">
// onInput(e: Event) {
//   const v = (e.target as HTMLInputElement).value;
// }

<div (click)="onClick($event)">
// onClick(e: MouseEvent) {
//   e.preventDefault();
// }

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

$event contains the native event object. For (input) it is Event, for (click) it is MouseEvent, for (keydown) it is KeyboardEvent. If you do not need the event, omit $event. For custom outputs, $event is the value emitted by the EventEmitter.

Property Binding
<img [src]="imageUrl">
<button [disabled]="!form.valid">
<div [innerHTML]="safeHtml">
<td [attr.colspan]="columnCount">
<button [attr.aria-label]="description">
<div [class.active]="isActive">
<div [style.color]="textColor">
<div [style.width.px]="width">

[property]="expression" binds a DOM property to an expression. For HTML attributes that are not properties, use [attr.x]. For classes: [class.name]="boolean". For styles: [style.prop]="value" with an optional unit (.px, .%).

@for — Iteration (v17+)
@for (item of items; track item.id;
      let i = $index, first = $first,
      last = $last, count = $count) {
  <li [class.first]="first">
    {{ i + 1 }}/{{ count }}: {{ item.name }}
  </li>
} @empty {
  <li>Empty list</li>
}

@for replaces *ngFor. The track is mandatory (better performance than trackBy). The @empty block renders when the list is empty. Implicit variables: $index, $first, $last, $even, $odd, $count.

ng-container
<ng-container *ngIf="user">
  <h2>{{ user.name }}</h2>
  <p>{{ user.email }}</p>
</ng-container>

@if (user) {
  <h2>{{ user.name }}</h2>
  <p>{{ user.email }}</p>
}

<ng-container *ngTemplateOutlet="headerTmpl">
</ng-container>

<ng-container> groups elements without rendering an extra DOM element. Useful with *ngIf and *ngFor when you need to apply them to multiple elements. With the new @if/@for syntax, ng-container is less necessary.

Old vs New Syntax
<!-- *ngIf (old) -->
<p *ngIf="active">Visible</p>

<!-- @if (new, v17+) -->
@if (active) { <p>Visible</p> }

<!-- *ngFor (old) -->
<li *ngFor="let x of items; let i = index">

<!-- @for (new) -->
@for (x of items; track x.id; let i = $index) {
  <li>{{ x }}</li>
}

The new control flow syntax (@if, @for, @switch) is more readable, does not need <ng-container> and has better performance. The track is mandatory in @for. The old syntax (*ngIf, *ngFor) still works but is discouraged in new projects.

Event Binding
<button (click)="save()">Save</button>
<input (input)="onInput($event)">
<form (ngSubmit)="submit()">
<input (keydown.enter)="search()">
<input (keydown.control.s)="save($event)">
<div (scroll)="onScroll($event)">
<input (blur)="validate()">
<select (change)="onChange($event)">

(event)="handler()" listens to DOM events. $event passes the event object. For specific keys: (keydown.enter), (keydown.escape). Modifiers: (keydown.control.s). (ngSubmit) prevents the page reload.

@switch — Multiple Conditions
@switch (status) {
  @case ('active') {
    <span class="green">Active</span>
  }
  @case ('pending') {
    <span class="yellow">Pending</span>
  }
  @case ('inactive') {
    <span class="red">Inactive</span>
  }
  @default {
    <span class="gray">Unknown</span>
  }
}

@switch replaces multiple *ngIf / ngSwitch. Each @case compares against the expression value. @default is the fallback. More readable and performant than @if / @else if chains for discrete values.

ng-template and ngTemplateOutlet
<ng-template #loading>
  <p>Loading...</p>
</ng-template>

<ng-template #itemTmpl let-item="data">
  <li>{{ item.name }}</li>
</ng-template>

<ng-container
  *ngTemplateOutlet="isLoading ? loading : null">
</ng-container>

<ng-container *ngTemplateOutlet="itemTmpl;
  context: { data: product }">
</ng-container>

<ng-template> defines a template that is not rendered until instantiated. *ngTemplateOutlet renders the template with an optional context. let-x="key" declares local variables that receive values from the context. Essential for customizable templates in libraries.

Two-Way Binding
<!-- With ngModel (FormsModule) -->
<input [(ngModel)]="user.name">

<!-- Expanded equivalent: -->
<input [ngModel]="user.name"
       (ngModelChange)="user.name = $event">

<!-- With signal model() (v17.1+): -->
<app-toggle [(active)]="on" />

<!-- Equivalent: -->
<app-toggle [active]="on"
            (activeChange)="on = $event" />

Two-way binding [(x)]="var" combines property binding [x] with event binding (xChange). With ngModel it requires FormsModule. With model() signals, it works automatically without modules. The convention is: input x + output xChange.

ngClass and ngStyle
<div [ngClass]="{
  'active': isActive,
  'highlight': focused,
  'error': hasError
}">

<div [ngClass]="['base', theme, size]">

<div [ngStyle]="{
  'color': textColor,
  'font-size.px': fontSize,
  'background-color': background
}">

[ngClass] applies multiple classes conditionally (object, array or string). [ngStyle] applies multiple inline styles. For a single class/style, prefer [class.x] and [style.x] — they are more performant.

Safe Navigation and Non-Null
<p>{{ user?.address?.city }}</p>
<p>{{ user!.name }}</p>
<p>{{ user?.name ?? 'Anonymous' }}</p>
<p>{{ (user$ | async)?.name }}</p>

The ?. operator (safe navigation) prevents errors when the property can be null/undefined. The ! (non-null assertion) tells TypeScript the value exists. Combine with ?? (nullish coalescing) for default values. With the async pipe, use parentheses: (x$ | async)?.prop.

Utilities and DI


12 cards
Create a Service
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserService {
  private users = ['Anna', 'Brian'];

  getUsers(): string[] {
    return [...this.users];
  }

  addUser(name: string): void {
    this.users.push(name);
  }
}

@Injectable({ providedIn: 'root' }) creates a global singleton — one instance shared by the whole app. The service encapsulates reusable logic (API, state, utilities). providedIn: 'root' allows tree-shaking: if no component uses it, it is removed from the bundle.

InjectionToken
import { InjectionToken, inject } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL');

// Provider
providers: [
  { provide: API_URL, useValue: 'https://api.com' }
]

// Consumption
export class ApiService {
  private apiUrl = inject(API_URL);
}

InjectionToken creates tokens for values that are not classes (strings, configs, objects). Define with new InjectionToken<T>('NAME'). Provide with { provide: TOKEN, useValue: value }. Consume with inject(TOKEN). Essential for configurable libraries.

toSignal and toObservable
import { toSignal, toObservable } from '@angular/rxjs-interop';

users$ = this.http.get<User[]>('/api/users');
users = toSignal(this.users$, { initialValue: [] });

filter = signal('');
filter$ = toObservable(this.filter);

results$ = this.filter$.pipe(
  debounceTime(300),
  switchMap(f => this.search(f))
);

toSignal() converts an Observable into a signal (with optional initialValue). toObservable() converts a signal into an Observable — useful for using RxJS operators with signals. Both come from @angular/rxjs-interop. They ease the gradual migration from RxJS to signals.

inject() — Modern Injection
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);
  private logger = inject(LoggerService);
}

// In components:
// private userService = inject(UserService);

inject() is the modern form of dependency injection. It works in components, services, guards, interceptors and functions. It replaces injection via constructor. Advantages: less code, works in standalone functions and simplifies inheritance (no super()).

useFactory and useExisting
{ provide: ApiService, useFactory: () => {
    const env = inject(EnvironmentService);
    return new ApiService(env.apiUrl, env.debug);
  }
}

{ provide: AbstractLogger, useExisting: ConsoleLogger }

{ provide: LoggerService, useClass: ProductionLogger }

Providers can use: useValue (static value), useClass (replace implementation), useFactory (create dynamically with logic), useExisting (alias). useFactory can use inject() internally to access other dependencies.

takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/rxjs-interop';

@Component({ ... })
export class ListComponent {
  users = toSignal(
    this.http.get<User[]>('/api/users')
      .pipe(takeUntilDestroyed()),
    { initialValue: [] }
  );

  ngOnInit() {
    this.service.data$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(d => this.data = d);
  }
}

takeUntilDestroyed() unsubscribes automatically when the component/service is destroyed. It replaces the manual Subject + takeUntil + ngOnDestroy pattern. Outside the injection context, pass DestroyRef the an argument. From @angular/rxjs-interop.

Constructor Injection
import { Component } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({ selector: 'app-list', template: `...` })
export class ListComponent {
  constructor(
    private userService: UserService,
    private http: HttpClient
  ) {}

  ngOnInit() {
    this.users = this.userService.getUsers();
  }
}

Injection via constructor is the classic form. Angular resolves the dependencies automatically by type. private creates the property automatically. It still works, but inject() is preferred in new code for being more flexible and working outside classes.

Stateful Service (Signal Store)
@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<CartItem[]>([]);

  readonly cartItems = this.items.asReadonly();
  readonly total = computed(() =>
    this.items().reduce((s, i) => s + i.price, 0)
  );
  readonly count = computed(() => this.items().length);

  addItem(item: CartItem) {
    this.items.update(list => [...list, item]);
  }

  clear() { this.items.set([]); }
}

A modern pattern is using signals the a store: private state with signal(), exposed the asReadonly(). computed() derives values. Public methods modify with .set() / .update(). Replaces BehaviorSubject and NgRx for simple state.

Resolver (Route Data)
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';

export const userResolver: ResolveFn<User> = (route) => {
  const service = inject(UserService);
  const id = Number(route.paramMap.get('id'));
  return service.getUser(id);
};

// app.routes.ts
{ path: 'user/:id', component: UserComponent,
  resolve: { user: userResolver } }

A ResolveFn preloads data before activating the route. The component accesses it via route.data['user']. The functional form with inject() replaces the class with Resolve. If the resolver returns an Observable, Angular waits until it completes before navigating.

Hierarchical Providers
@Injectable({ providedIn: 'root' })

@Component({ providers: [FormService] })

@Injectable({ providedIn: 'platform' })

constructor(@Self() private svc: MyService) {}
constructor(@Optional() private svc: MyService) {}
constructor(@SkipSelf() private svc: MyService) {}

Angular has hierarchical injection: providedIn: 'root' creates a global singleton. providers on the component creates one instance per component. @Self() restricts to the current injector. @Optional() does not throw if not found. @SkipSelf() looks in the parent.

Complete HTTP Service
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = inject(API_URL);

  getUsers() {
    return this.http.get<User[]>(`${this.apiUrl}/users`);
  }

  createUser(data: CreateUserDto) {
    return this.http.post<User>(`${this.apiUrl}/users`, data);
  }

  deleteUser(id: number) {
    return this.http.delete<void>(`${this.apiUrl}/users/${id}`);
  }
}

An HTTP service encapsulates all API calls. Use inject(HttpClient) and inject(API_URL). Each method returns a typed Observable<T>. The component subscribes or uses the async pipe. It centralizes URLs, error handling and interceptors.

APP_INITIALIZER
import { APP_INITIALIZER, inject } from '@angular/core';

export function initApp() {
  const config = inject(ConfigService);
  return () => config.load();
}

providers: [
  { provide: APP_INITIALIZER, useFactory: initApp, multi: true }
]

APP_INITIALIZER runs code before the app starts. Useful for loading remote configuration, checking auth or initializing SDKs. multi: true allows multiple initializers. If the factory returns a Promise or Observable, the bootstrap waits until it resolves.

HTTP e Interceptors


12 cards
provideHttpClient
import { provideHttpClient, withInterceptors, withFetch }
  from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
      withFetch()
    )
  ]
};

provideHttpClient() enables HttpClient in the app. withInterceptors() registers functional interceptors. withFetch() (v15+) uses the native Fetch API instead of XMLHttpRequest. Without this, injecting HttpClient throws "No provider for HttpClient".

Error Interceptor
import { HttpInterceptorFn, HttpErrorResponse }
  from '@angular/common/http';
import { catchError, throwError } from 'rxjs';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) inject(Router).navigate(['/login']);
      else if (error.status === 0) console.error('No connection');
      else console.error(`Error ${error.status}:`, error.message);
      return throwError(() => error);
    })
  );
};

An error interceptor uses catchError in the pipe. HttpErrorResponse contains status, message and error. Status 0 indicates a network/CORS failure. Always re-throw with throwError(() => error) so the subscriber can handle it.

File Download
download(id: number) {
  this.http.get(`/api/files/${id}`, {
    responseType: 'blob', observe: 'response'
  }).subscribe(res => {
    const blob = res.body!;
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'download.pdf';
    a.click();
    URL.revokeObjectURL(url);
  });
}

For downloads, use responseType: 'blob'. Create a URL.createObjectURL() and a temporary link with a.click(). The file name comes from the Content-Disposition header. URL.revokeObjectURL() frees the memory. Works for PDFs, images, Excel, etc.

Typed GET
import { HttpClient, HttpParams } from '@angular/common/http';

export class UserService {
  private http = inject(HttpClient);

  getUsers(filter?: string) {
    let params = new HttpParams();
    if (filter) params = params.set('q', filter);
    return this.http.get<User[]>('/api/users', { params });
  }

  getUser(id: number) {
    return this.http.get<User>(`/api/users/${id}`);
  }
}

http.get<T>() returns a typed Observable<T>. HttpParams builds query strings immutably (.set() returns a new instance). The type is compile-time only — Angular does not validate the response at runtime. Use interfaces for the data models.

Error Handling
getUsers() {
  return this.http.get<User[]>('/api/users').pipe(
    retry(3),
    catchError(this.handleError)
  );
}

private handleError(error: HttpErrorResponse) {
  if (error.error?.message)
    return throwError(() => new Error(error.error.message));
  return throwError(() => new Error('Server error'));
}

// In the component:
this.service.getUsers().subscribe({
  next: users => this.users = users,
  error: err => this.error = err.message
});

retry(n) retries on error. catchError transforms the error. In the component, use the observer with next and error. Access the API message with error.error?.message. For global errors, use an error interceptor.

Simple Cache
@Injectable({ providedIn: 'root' })
export class CachedService {
  private http = inject(HttpClient);
  private cache = new Map<string, any>();

  get<T>(url: string): Observable<T> {
    if (this.cache.has(url)) return of(this.cache.get(url));
    return this.http.get<T>(url).pipe(
      tap(data => this.cache.set(url, data))
    );
  }

  invalidate(url?: string) {
    if (url) this.cache.delete(url);
    else this.cache.clear();
  }
}

A simple cache uses a Map to store responses. If the URL is already cached, it returns of(value) (synchronous Observable). tap() stores the response after the first request. invalidate() clears the cache. For advanced caches, use HttpInterceptorFn or libraries like @ngneat/cashew.

POST, PUT, DELETE
createUser(data: CreateUserDto) {
  return this.http.post<User>('/api/users', data);
}

updateUser(id: number, data: User) {
  return this.http.put<User>(`/api/users/${id}`, data);
}

patchUser(id: number, data: Partial<User>) {
  return this.http.patch<User>(`/api/users/${id}`, data);
}

deleteUser(id: number) {
  return this.http.delete<void>(`/api/users/${id}`);
}

http.post() sends data in the body. http.put() replaces the whole resource. http.patch() updates partially. http.delete() removes. All accept a generic type for the response. The body is serialized the JSON automatically.

File Upload
upload(file: File) {
  const formData = new FormData();
  formData.append('file', file, file.name);
  return this.http.post<{ url: string }>('/api/upload', formData);
}

uploadWithProgress(file: File) {
  const req = new HttpRequest('POST', '/api/upload', formData,
    { reportProgress: true });
  return this.http.request(req).pipe(
    filter(e => e.type === HttpEventType.UploadProgress),
    map(e => Math.round(100 * e.loaded / e.total!))
  );
}

For uploads, use FormData — Angular sets Content-Type: multipart/form-data automatically. For progress, create an HttpRequest with reportProgress: true and filter HttpEventType.UploadProgress events.

Parallel Requests
import { forkJoin } from 'rxjs';

loadDashboard() {
  return forkJoin({
    users: this.http.get<User[]>('/api/users'),
    posts: this.http.get<Post[]>('/api/posts'),
    stats: this.http.get<Stats>('/api/stats')
  });
}

this.service.loadDashboard()
  .subscribe(({ users, posts, stats }) => {
    this.users = users;
    this.posts = posts;
    this.stats = stats;
  });

forkJoin runs multiple Observables in parallel and emits when all complete. It accepts an object with named keys — the response has the same structure. If one fails, the error propagates (use catchError on each for resilience). Ideal for dashboards and pages with multiple sources.

Functional Interceptor
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.getToken();

  if (token) {
    const cloned = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
    return next(cloned);
  }
  return next(req);
};

Functional interceptors (HttpInterceptorFn) replace classes with HTTP_INTERCEPTORS. They receive req and next. req.clone() creates a modified copy (requests are immutable). next(req) passes to the next interceptor. Register with withInterceptors([]).

Headers and Options
import { HttpHeaders } from '@angular/common/http';

const headers = new HttpHeaders()
  .set('X-Api-Key', 'abc123')
  .set('Accept-Language', 'en-US');

this.http.get('/api/data', { headers });

this.http.get('/api/data', { observe: 'response' })
  .subscribe(res => {
    console.log(res.status);
    console.log(res.headers.get('X-Total'));
    console.log(res.body);
  });

this.http.get('/api/csv', { responseType: 'text' });

HttpHeaders is immutable — .set() returns a new instance. observe: 'response' gives access to the full response (status, headers, body). responseType: 'text' returns a string instead of JSON. responseType: 'blob' for binary files.

Base URL and Proxy
// Option 1: environment
this.http.get(`${environment.apiUrl}/users`)

// Option 2: proxy.conf.json (dev)
{ "/api": { "target": "http://localhost:3000",
  "secure": false, "changeOrigin": true } }
// ng serve --proxy-config proxy.conf.json

// Option 3: Interceptor with baseUrl
const cloned = req.clone({ url: `${baseUrl}${req.url}` });

For the base URL: use environment.apiUrl (simple), proxy.conf.json in dev (avoids CORS), or an interceptor that prefixes all URLs. The proxy only works with ng serve — in production, configure the web server (nginx, Apache) or use CORS in the backend.

Advanced and Performance


12 cards
Change Detection — OnPush
import { ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class ListComponent {
  @Input() items: Item[] = [];
}

// OnPush only checks when:
// 1. An input changes (reference)
// 2. A component event fires (click, etc.)
// 3. An Observable with async pipe emits
// 4. A signal changes

ChangeDetectionStrategy.OnPush optimizes change detection: it only checks when inputs change (new reference), component events fire, the async pipe emits or signals change. With signals, OnPush is the implicit default. Always use OnPush in new components.

ContentChildren and QueryList
import { ContentChildren, QueryList,
  AfterContentInit } from '@angular/core';

@Component({ selector: 'app-tabs' })
export class TabsComponent implements AfterContentInit {
  @ContentChildren(TabComponent)
  tabs!: QueryList<TabComponent>;

  ngAfterContentInit() {
    this.tabs.first.active = true;
    this.tabs.changes.subscribe(() => {
      // Tabs were added/removed
    });
  }
}

// <app-tabs>
//   <app-tab title="A">...</app-tab>
//   <app-tab title="B">...</app-tab>
// </app-tabs>

@ContentChildren queries all components/directives projected via ng-content. QueryList is a reactive collection — .changes emits when the list changes. .first, .last, .toArray() access the items. Useful for composite components (tabs, accordion, steps).

Best Practices
// 1. Small, focused components
// 2. Services for business logic
// 3. OnPush + signals (no mutations)
// 4. takeUntilDestroyed() on subscriptions
// 5. Lazy loading for large sections
// 6. @defer for heavy components
// 7. track required in @for
// 8. inject() instead of constructor
// 9. input()/output() instead of decorators
// 10. Reactive Forms for complex forms

Angular best practices: small components with OnPush, services for logic, signals for state, takeUntilDestroyed() for subscriptions, lazy loading and @defer for performance, inject() and signal inputs/outputs for modern code. Avoid mutations and any.

@defer — Lazy Load Components
@defer (on viewport) {
  <app-chart [data]="data" />
} @placeholder {
  <p>Loading chart...</p>
} @loading (after 200ms) {
  <app-spinner />
} @error {
  <p>Error loading</p>
}

// Triggers:
// on viewport — when visible
// on idle — when the browser is idle
// on timer(3s) — after 3 seconds
// on hover — on mouse hover
// when condition — when the condition is true

@defer (v17+) lazy loads components in the template. The @placeholder block shows before, @loading during the load (with after 200ms to avoid flicker), @error on failure. Triggers: on viewport, on idle, on timer(), on hover, when. Reduces the initial bundle.

ngZone and Performance
import { NgZone } from '@angular/core';

export class ChartComponent {
  private zone = inject(NgZone);

  initChart() {
    // Outside the Angular zone (no change detection)
    this.zone.runOutsideAngular(() => {
      this.chart = new Chart(this.canvas, {
        // frequent callbacks (mousemove, etc.)
        onHover: () => { /* does not trigger CD */ }
      });
    });
  }

  update(data: any) {
    // Back into the zone to update the template
    this.zone.run(() => {
      this.data = data;
    });
  }
}

NgZone controls change detection. runOutsideAngular() runs code without triggering CD — ideal for frequent listeners (scroll, mousemove, WebSocket). run() goes back into the zone to update the template. With signals, the need for NgZone decreases. Use for external libraries (charts, maps, editors).

Folder Structure (Feature-Based)
src/app/
├── core/           # singleton services, guards
│   ├── services/
│   └── interceptors/
├── shared/         # reusable components/pipes/directives
│   ├── components/
│   ├── pipes/
│   └── directives/
├── features/       # modules per feature
│   ├── users/
│   │   ├── users.routes.ts
│   │   ├── users.component.ts
│   │   └── components/
│   └── dashboard/
└── app.routes.ts

Feature-based structure: core/ for singletons (services, guards, interceptors), shared/ for reusable components, features/ for isolated features with their own routes. Each feature is lazy-loaded. Avoids the flat by-type structure (components/, services/) that does not scale.

SSR and Hydration
// Create a project with SSR:
ng new app --ssr

// app.config.ts
import { provideClientHydration }
  from '@angular/platform-browser';

providers: [
  provideClientHydration()
]

// app.config.server.ts
import { provideServerRendering }
  from '@angular/platform-server';

// The HTML is rendered on the server
// and hydrated on the client (no re-render)

SSR (Server-Side Rendering) renders HTML on the server for better SEO and First Contentful Paint. provideClientHydration() enables hydration — Angular reuses the server DOM instead of re-rendering. --ssr in ng new sets everything up. Requires Node.js on the server.

Unit Tests
import { TestBed } from '@angular/core/testing';

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserService,
        { provide: HttpClient, useValue: httpMock }
      ]
    });
    service = TestBed.inject(UserService);
  });

  it('should return users', () => {
    const users = service.getUsers();
    expect(users.length).toBe(2);
  });

  it('should add user', () => {
    service.addUser('Carlos');
    expect(service.getUsers()).toContain('Carlos');
  });
});

TestBed configures the testing module. TestBed.configureTestingModule() defines providers and imports. TestBed.inject() gets services. Use useValue for mocks. ng test runs with Karma/Jasmine. For components, use TestBed.createComponent() and fixture.detectChanges().

Security (DomSanitizer)
import { DomSanitizer, SafeHtml, SafeUrl }
  from '@angular/platform-browser';

export class SafeContentComponent {
  private sanitizer = inject(DomSanitizer);

  safeHtml: SafeHtml;
  safeUrl: SafeUrl;

  constructor() {
    this.safeHtml = this.sanitizer
      .bypassSecurityTrustHtml('<b>Trusted HTML</b>');
    this.safeUrl = this.sanitizer
      .bypassSecurityTrustUrl('https://example.com');
  }
}

// <div [innerHTML]="safeHtml"></div>

Angular sanitizes HTML automatically to prevent XSS. For trusted HTML, use DomSanitizer.bypassSecurityTrustHtml(). Methods: bypassSecurityTrustHtml, bypassSecurityTrustUrl, bypassSecurityTrustResourceUrl, bypassSecurityTrustScript. Use with extreme care — only for 100% trusted content.

Animations
import { trigger, state, style,
  transition, animate } from '@angular/animations';

@Component({
  animations: [
    trigger('fade', [
      state('visible', style({ opacity: 1 })),
      state('hidden', style({ opacity: 0 })),
      transition('visible <=> hidden',
        animate('300ms ease-in-out'))
    ])
  ]
})
export class FadeComponent {
  state = 'visible';
}

// <div [@fade]="state">Content</div>

Angular animations use trigger, state and transition. state defines styles per state. transition defines as animation between states (=>, <=>). Apply with [@trigger]="state". Requires provideAnimations() or provideAnimationsAsync().

Component Tests
import { ComponentFixture, TestBed } from '@angular/core/testing';

describe('CounterComponent', () => {
  let fixture: ComponentFixture<CounterComponent>;
  let component: CounterComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CounterComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
  });

  it('should increment', () => {
    component.increment();
    fixture.detectChanges();
    const el = fixture.nativeElement.querySelector('span');
    expect(el.textContent).toContain('1');
  });
});

ComponentFixture gives access to the component and the DOM. fixture.componentInstance is the instance. fixture.detectChanges() triggers change detection. fixture.nativeElement accesses the DOM. compileComponents() compiles external templates. With standalone, use imports instead of declarations.

Zoneless (v18+ experimental)
import { provideExperimentalZonelessChangeDetection }
  from '@angular/core';

// app.config.ts
providers: [
  provideExperimentalZonelessChangeDetection()
]

// Without Zone.js:
// - Change detection only with signals
// - Less overhead (no monkey-patching)
// - Better performance
// - Requires 100% signal-based code

Zoneless change detection (v18+ experimental) removes Zone.js. Change detection is triggered only by signals — no event monkey-patching. Requires 100% signal-based code (no setTimeout, manual addEventListener). Better performance and a smaller bundle. Enable with provideExperimentalZonelessChangeDetection().

Pipes


10 cards
Text Pipes
{{ name | uppercase }}
{{ name | lowercase }}
{{ name | titlecase }}
{{ text | slice:0:100 }}
{{ list | slice:0:5 }}
{{ obj | json }}

Text pipes transform strings: uppercase, lowercase, titlecase. The slice pipe works with strings and arrays (start:end). The json pipe serializes objects — useful for debugging in the template.

Chaining Pipes
{{ name | lowercase | titlecase }}
{{ date | date:'dd/MM' | uppercase }}
{{ text | slice:0:50 | uppercase }}
{{ price | currency:'EUR':'symbol':'1.2-2' }}
<p>{{ (items | slice:0:3).length }} of {{ items.length }}</p>

Pipes can be chained with | — each pipe receives the output of the previous one. Order matters: lowercase | titlecase is different from titlecase | lowercase. Parameters are separated by :. Use parentheses to apply pipes to sub-expressions.

Pipes with Parameters
@Pipe({ name: 'money', standalone: true })
export class MoneyPipe implements PipeTransform {
  transform(
    value: number,
    symbol = '€',
    decimals = 2,
    position: 'before' | 'after' = 'after'
  ): string {
    const formatted = value.toFixed(decimals);
    return position === 'before'
      ? `${symbol}${formatted}`
      : `${formatted}${symbol}`;
  }
}
// {{ 49.9 | money:'R$':2:'before' }} → R$49.90

Pipes accept multiple parameters separated by :. Set default values in the transform() parameters. Use union types to restrict values. The pipe is reusable across the whole app — add it to the imports array of the components that use it.

Date and Number Pipes
{{ today | date:'dd/MM/yyyy' }}
{{ today | date:'fullDate' }}
{{ today | date:'HH:mm:ss' }}
{{ 3.14159 | number:'1.2-2' }}
{{ 1234567 | number }}
{{ 0.75 | percent }}
{{ 49.99 | currency:'EUR' }}
{{ 49.99 | currency:'BRL':'R$' }}

The date pipe formats dates with patterns (dd/MM/yyyy, fullDate). The number pipe formats with the minInt.minDec-maxDec format. percent multiplies by 100 and adds %. currency formats currencies with a symbol.

KeyValue Pipe
config = { theme: 'dark', language: 'en', version: 2 };

@for (item of config | keyvalue; track item.key) {
  <p>{{ item.key }}: {{ item.value }}</p>
}

@for (item of config | keyvalue: originalOrder; track item.key) {
  <p>{{ item.key }} = {{ item.value }}</p>
}
// originalOrder = () => 0;

The keyvalue pipe iterates over objects and Maps, returning { key, value } pairs. By default it sorts alphabetically by key. Pass a comparison function the an argument for custom sorting. Useful for rendering dynamic objects without knowing the keys.

Decimal and Locale
{{ 3.14159 | number:'1.2-4' }}
{{ 42 | number:'3.0-0' }}
{{ 1234.5 | number:'1.0-0' }}
{{ 0.1234 | percent:'1.2-2' }}
{{ 1234.56 | number:'1.2-2':'pt-PT' }}

The minInt.minDec-maxDec format controls digits: 1.2-4 = minimum 1 integer, 2 to 4 decimals. The third parameter of the number pipe is the locale (pt-PT uses a decimal comma and space the thousands separator). Register locales with registerLocaleData().

Async Pipe
<p>{{ user$ | async }}</p>

@if (users$ | async; the users) {
  <ul>
    @for (u of users; track u.id) {
      <li>{{ u.name }}</li>
    }
  </ul>
}

// With signal: users = toSignal(this.http.get(url));
// <p>{{ users()?.length }}</p>

The async pipe subscribes to an Observable or Promise and returns the latest value. It unsubscribes automatically when the component is destroyed — prevents memory leaks. With @if (x$ | async; the x), it avoids multiple subscriptions and gives access to the value.

i18n
<p i18n="@@greeting">Hello, world!</p>
<img i18n-alt alt="Company logo">

<!-- ng extract-i18n -->

<p i18n>
  {counter, plural,
    =0 {No items}
    =1 {One item}
    other {{{counter}} items}
  }
</p>

Angular's i18n system uses the i18n attribute to mark texts for translation. @@id defines a custom ID. ng extract-i18n generates XLIFF files. Supports pluralization and gender selection. For runtime translation, use @angular/localize.

Custom Pipe
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 50, suffix = '...'): string {
    if (value.length <= limit) return value;
    return value.slice(0, limit) + suffix;
  }
}

// Usage: {{ text | truncate:100:' [more]' }}

A custom pipe implements PipeTransform with the transform() method. The first parameter is the value; the following ones are arguments (| truncate:100). Declare it the standalone: true and add it to the imports array of the component that uses it.

Pure vs Impure Pipes
// Pure (default) — only recalculates if the input changes
@Pipe({ name: 'sort', pure: true })

// Impure — recalculates on every change detection
@Pipe({ name: 'filter', pure: false })

// Pure: efficient, but does not detect mutations
// Impure: detects mutations, but less performant
// Prefer signals or immutability

Pure pipes (default) only run when the input reference changes. Impure pipes (pure: false) run on every change detection cycle. Prefer pure pipes with immutable data. To filter/sort arrays, prefer transforming in the component with computed() signals.

Forms


14 cards
Reactive Forms — Setup
import { ReactiveFormsModule, FormControl } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="name">
    <p>{{ name.value }}</p>
  `
})
export class FormComponent {
  name = new FormControl('');
}

Reactive Forms requires importing ReactiveFormsModule in the component's imports array. FormControl represents an individual field. The value is accessed with .value and updated with .setValue(). More testable and scalable than Template-Driven Forms.

Custom Validator
import { AbstractControl, ValidationErrors, ValidatorFn }
  from '@angular/forms';

export function forbiddenWord(word: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const has = control.value?.toLowerCase()
      .includes(word.toLowerCase());
    return has ? { forbiddenWord: { value: control.value } } : null;
  };
}

// Usage: name: ['', [Validators.required, forbiddenWord('admin')]]

A custom validator is a function that returns a ValidatorFn. It receives the AbstractControl and returns null (valid) or a ValidationErrors object. For validators with parameters, use a factory function. Check errors with hasError('forbiddenWord').

setValue vs patchValue
this.form.setValue({
  name: 'Anna', email: 'ana@mail.com',
  age: 25, terms: true
});

this.form.patchValue({ name: 'Anna' });

this.form.get('name')?.setValue('Brian');

this.form.reset();
this.form.reset({ name: '' });

setValue() requires all fields of the group (error if one is missing). patchValue() accepts partial fields — ideal for updating only some. reset() clears the form and marks it the pristine and untouched. Use patchValue to fill from an API.

Template-Driven Forms
import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="submit(f)">
      <input name="name" [(ngModel)]="user.name"
             required minlength="3" #name="ngModel">
      @if (name.invalid && name.touched) {
        <span>Invalid name</span>
      }
      <button [disabled]="f.invalid">Submit</button>
    </form>
  `
})
export class FormComponent {
  user = { name: '', email: '' };
}

Template-Driven Forms uses FormsModule and [(ngModel)]. Validation is declarative in the template (required, minlength). #f="ngForm" gives access to the form. Simpler for small forms, but less testable and scalable than Reactive Forms.

FormBuilder
import { FormBuilder, Validators } from '@angular/forms';

export class RegisterComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(3)]],
    email: ['', [Validators.required, Validators.email]],
    age: [18, [Validators.min(18)]],
    terms: [false, [Validators.requiredTrue]]
  });

  submit() {
    if (this.form.valid) console.log(this.form.value);
  }
}

FormBuilder simplifies form creation. this.fb.group({}) creates a FormGroup. Each field is [initialValue, [validators]]. inject(FormBuilder) injects the service. form.value returns the object with all values. form.valid checks whether all fields are valid.

Cross-Field Validation
export function passwordsMatch(): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get('password')?.value;
    const confirm = group.get('confirm')?.value;
    return password === confirm ? null : { passwordsMismatch: true };
  };
}

form = this.fb.group({
  password: ['', Validators.required],
  confirm: ['', Validators.required]
}, { validators: passwordsMatch() });

Cross-field validation compares multiple fields. The validator receives the FormGroup (the AbstractControl). Register it in the second argument of fb.group() with { validators: [...] }. The error lives on the group: form.hasError('passwordsMismatch').

Form State
form.valid
form.invalid
form.pristine
form.dirty
form.touched
form.untouched
form.pending

form.get('name')?.errors
form.get('name')?.hasError('required')
form.get('name')?.touched

form.markAllAsTouched();

Each control has states: valid/invalid, pristine/dirty, touched/untouched, pending. Use touched to show errors only after interaction. markAllAsTouched() forces error display (e.g. on submit).

Form with NonNullable
private fb = inject(FormBuilder).nonNullable;

form = this.fb.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  active: true
});

// form.value.name → string (not string | null)
// this.form.reset(); // name = '', not null

FormBuilder.nonNullable (v14+) removes null from the types. form.value.name is string instead of string | null. reset() restores the initial values instead of null. Reduces null checks and improves type-safety.

Form Template
<form [formGroup]="form" (ngSubmit)="submit()">
  <input formControlName="name"
         [class.error]="nameCtrl.invalid && nameCtrl.touched">

  @if (nameCtrl.hasError('required')) {
    <span class="error">Required field</span>
  }

  <button [disabled]="form.invalid">Submit</button>
</form>

get nameCtrl() { return this.form.get('name')!; }

In the template: [formGroup]="form" binds the form, formControlName="name" binds each field. (ngSubmit) prevents reload and calls the handler. Use getters to access controls. Show errors with hasError() and condition them with touched (only after interaction).

FormArray
import { FormArray } from '@angular/forms';

form = this.fb.group({
  emails: this.fb.array<string>([''])
});

get emails(): FormArray {
  return this.form.get('emails') as FormArray;
}

addEmail() {
  this.emails.push(this.fb.control('', Validators.email));
}

removeEmail(index: number) {
  this.emails.removeAt(index);
}

FormArray manages a dynamic list of controls. Add with .push(), remove with .removeAt(). In the template, iterate with @for and use [formControlName]="$index". Useful for lists of emails, phones, dynamic form items.

Async Validation
import { AsyncValidatorFn } from '@angular/forms';

export function uniqueEmail(service: UserService): AsyncValidatorFn {
  return (control) => {
    return timer(500).pipe(
      switchMap(() => service.checkEmail(control.value)),
      map(exists => exists ? { emailExists: true } : null),
      catchError(() => of(null))
    );
  };
}

// email: ['', [Validators.email], [uniqueEmail(this.userService)]]

Async validation uses AsyncValidatorFn — it returns an Observable. The third argument of fb.control() accepts async validators. Use timer(500) + switchMap for debounce. The pending state is true while validating. Ideal for checking uniqueness.

Built-in Validators
import { Validators } from '@angular/forms';

form = this.fb.group({
  name: ['', [Validators.required, Validators.minLength(3),
              Validators.maxLength(100)]],
  email: ['', [Validators.required, Validators.email]],
  age: [null, [Validators.min(18), Validators.max(120)]],
  website: ['', Validators.pattern('https?://.+')],
  terms: [false, Validators.requiredTrue]
});

Built-in validators: required, email, minLength(n), maxLength(n), min(n), max(n), pattern(regex), requiredTrue (for checkboxes). Combine multiple ones in an array. All return null (valid) or an error object.

Nested FormGroup
form = this.fb.group({
  name: ['', Validators.required],
  address: this.fb.group({
    street: ['', Validators.required],
    city: ['', Validators.required],
    zipCode: ['', Validators.pattern('\d{4}-\d{3}')]
  })
});

// Template: <div formGroupName="address">
//   <input formControlName="street">
// </div>

Nested FormGroups organize complex forms. In the template, use formGroupName="address" to create the context. Access values with form.value.address.street or form.get('address.street'). form.value returns the complete nested structure.

valueChanges
this.form.valueChanges
  .pipe(
    debounceTime(300),
    distinctUntilChanged(),
    takeUntilDestroyed()
  )
  .subscribe(value => this.search(value.query));

this.form.get('country')?.valueChanges
  .pipe(takeUntilDestroyed())
  .subscribe(country => this.updateCities(country));

valueChanges is an Observable that emits on every change. Combine it with debounceTime for real-time search, distinctUntilChanged to avoid duplicate emissions. Use takeUntilDestroyed() for auto-unsubscribe. Ideal for filters, autocomplete and side effects.

RxJS e Observables


14 cards
Observable — Basics
import { Observable } from 'rxjs';

const data$ = new Observable<string>(subscriber => {
  subscriber.next('First');
  subscriber.next('Second');
  setTimeout(() => {
    subscriber.next('Third');
    subscriber.complete();
  }, 1000);
});

data$.subscribe({
  next: value => console.log(value),
  error: err => console.error(err),
  complete: () => console.log('Complete')
});

An Observable is a lazy data stream — it only runs when someone subscribes. subscriber.next() emits values, .complete() ends, .error() throws an error. The $ suffix is a convention for Observables. The subscriber receives next, error and complete.

Subject and BehaviorSubject
import { Subject, BehaviorSubject } from 'rxjs';

const events$ = new Subject<string>();
events$.next('click');

const counter$ = new BehaviorSubject(0);
counter$.value;
counter$.next(1);

// Pattern: expose the Observable (read-only)
private _data$ = new BehaviorSubject<User[]>([]);
readonly data$ = this._data$.asObservable();

update(data: User[]) { this._data$.next(data); }

Subject is an Observable that can also emit (.next()). BehaviorSubject has an initial value and keeps the last value (.value). Expose it the asObservable() for read-only. Replaced by signal() in most cases (v17+).

takeUntilDestroyed (Modern)
import { takeUntilDestroyed } from '@angular/rxjs-interop';
import { DestroyRef, inject } from '@angular/core';

export class ListComponent {
  data$ = this.service.data$.pipe(takeUntilDestroyed());

  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.otherService.stream$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(d => this.data = d);
  }
}

takeUntilDestroyed() replaces the Subject + takeUntil + ngOnDestroy pattern. In the injection context (constructor, field initializer), no arguments are needed. Outside it, pass DestroyRef. From @angular/rxjs-interop. Prevents memory leaks automatically.

Pattern: Search with Debounce
import { toSignal, toObservable } from '@angular/rxjs-interop';

export class SearchComponent {
  private http = inject(HttpClient);
  term = signal('');

  results = toSignal(
    toObservable(this.term).pipe(
      debounceTime(300),
      distinctUntilChanged(),
      filter(t => t.length >= 2),
      switchMap(term =>
        this.http.get<Result[]>(`/api/search?q=${term}`)
          .pipe(catchError(() => of([])))
      )
    ),
    { initialValue: [] }
  );
}

Search pattern: signal → toObservabledebounceTime(300) (wait) → distinctUntilChanged (skip duplicates) → filter (minimum 2 chars) → switchMap (cancels previous) → catchError (resilience). toSignal converts back for template use.

Transformation Operators
import { map, scan, startWith } from 'rxjs';

this.http.get<User[]>('/api/users').pipe(
  map(users => users.map(u => u.name))
);

this.clicks$.pipe(
  scan((acc, _) => acc + 1, 0)
);

this.search$.pipe(
  startWith(''),
  switchMap(q => this.search(q))
);

map transforms each emitted value. scan accumulates values (counter, sum). startWith emits an initial value before the stream. All return a new Observable — the original stream is not modified.

combineLatest and zip
import { combineLatest, zip } from 'rxjs';

combineLatest([filter$, sorting$]).pipe(
  map(([filter, sorting]) =>
    this.applyFilter(filter, sorting)
  )
);

zip(
  this.http.get('/api/names'),
  this.http.get('/api/ages')
).pipe(
  map(([names, ages]) =>
    names.map((n, i) => ({ name: n, age: ages[i] }))
  )
);

combineLatest emits whenever any Observable emits (with the latest values of all). Ideal for combining filters. zip pairs emissions in order — waits for all to emit. Use combineLatest for reactivity and zip for synchronization.

fromEvent and interval
import { fromEvent, interval, timer } from 'rxjs';
import { map, throttleTime } from 'rxjs/operators';

const clicks$ = fromEvent(document, 'click');
const resize$ = fromEvent(window, 'resize').pipe(
  throttleTime(200),
  map(() => window.innerWidth)
);

const tick$ = interval(1000);
const delay$ = timer(3000);
const periodic$ = timer(1000, 5000);

fromEvent converts DOM events into an Observable. Combine with throttleTime for performance. interval(ms) emits incrementsl numbers every X ms. timer(delay) emits once after the delay; timer(delay, period) emits periodically. All need unsubscribe.

RxJS vs Signals
// RxJS: streams, events, complex async
this.search$.pipe(
  debounceTime(300),
  switchMap(q => this.http.get(q))
);

// Signals: synchronous state, computed
counter = signal(0);
double = computed(() => this.counter() * 2);

// Interop
import { toSignal, toObservable } from '@angular/rxjs-interop';
const search$ = toObservable(this.term);
const data = toSignal(this.http.get(url));

Use signals for synchronous state and computed values. Use RxJS for event streams, debounce, HTTP requests and complex operations. toObservable() and toSignal() bridge the two. The trend is signals for state and RxJS for async flows.

Filtering Operators
import { filter, distinctUntilChanged, debounceTime,
  take, first } from 'rxjs';

this.data$.pipe(filter(d => d.active === true));
this.input$.pipe(distinctUntilChanged());
this.search$.pipe(debounceTime(300));
this.data$.pipe(take(5));
this.data$.pipe(first());

filter only lets through values that satisfy the condition. distinctUntilChanged ignores emissions equal to the previous one. debounceTime(ms) waits for silence before emitting — ideal for search. take(n) limits to N emissions. first() emits the first and completes.

tap and finalize
import { tap, finalize } from 'rxjs';

this.http.get<User[]>('/api/users').pipe(
  tap(users => {
    console.log(`${users.length} users`);
    this.loading.set(false);
  }),
  finalize(() => {
    this.loading.set(false);
    this.spinner.hide();
  })
);

tap runs side effects without modifying the stream (logs, state updates, cache). finalize runs when the stream completes or errors — ideal for hiding spinners. Neither changes the emitted value. Use finalize instead of repeating logic in next and error.

shareReplay
import { shareReplay } from 'rxjs';

private users$ = this.http.get<User[]>('/api/users')
  .pipe(shareReplay({ bufferSize: 1, refCount: true }));

getUsers() { return this.users$; }
getCount() {
  return this.users$.pipe(map(u => u.length));
}

shareReplay shares an Observable among multiple subscribers and replays the last value. bufferSize: 1 keeps the last value. refCount: true unsubscribes from the source when there are no subscribers. Avoids duplicate HTTP requests. Ideal for data consumed by multiple components.

switchMap, mergeMap, concatMap
import { switchMap, mergeMap, concatMap, exhaustMap }
  from 'rxjs';

// switchMap: cancels the previous request
this.search$.pipe(switchMap(q => this.http.get(`/api?q=${q}`)));

// mergeMap: runs in parallel
this.ids$.pipe(mergeMap(id => this.http.get(`/api/${id}`)));

// concatMap: runs in series (order)
this.actions$.pipe(concatMap(action => this.save(action)));

// exhaustMap: ignores while processing
this.clicks$.pipe(exhaustMap(() => this.http.post('/api', data)));

switchMap: cancels the previous Observable (search). mergeMap: runs in parallel (independent requests). concatMap: runs in series, keeps order (sequential operations). exhaustMap: ignores new emissions while processing (prevent double-submit).

catchError and retry
import { catchError, retry, throwError } from 'rxjs';

this.http.get('/api/data').pipe(retry(3));

this.http.get('/api/data').pipe(
  retry({ count: 3, delay: 1000 })
);

this.http.get('/api/data').pipe(
  catchError(err => {
    if (err.status === 404) return of([]);
    return throwError(() => err);
  })
);

retry(n) re-subscribes on error. retry({ count, delay }) adds a delay between attempts. catchError allows recovering with a default value (of([])) or re-throwing with throwError. Combine: retry(3) before catchError.

of, from and throwError
import { of, from, throwError, EMPTY } from 'rxjs';

of(1, 2, 3);
of({ name: 'Anna' });

from([1, 2, 3]);
from(fetch('/api/data'));

throwError(() => new Error('Failure'));

EMPTY;

catchError(err => {
  if (err.status === 404) return of(null);
  return throwError(() => err);
});

of() creates an Observable that emits values and completes. from() converts arrays, Promises or iterables. throwError() creates an Observable that emits an error. EMPTY completes immediately without emitting. Useful the returns in catchError, mocks and tests.

Routing and Navigation


12 cards
Define Routes
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'users', component: UsersComponent },
  { path: 'users/:id', component: UserDetailComponent },
  { path: '**', component: NotFoundComponent }
];

// app.config.ts
providers: [provideRouter(routes)]

// Template: <router-outlet />

Define routes in app.routes.ts the an array of Routes. path is the URL segment. :id is a dynamic parameter. ** is the wildcard (404). provideRouter(routes) enables routing. <router-outlet /> renders the active route's component.

Functional Guards
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) return true;
  return router.createUrlTree(['/login'], {
    queryParams: { redirect: state.url }
  });
};

// Usage: { path: 'admin', canActivate: [authGuard] }

Functional guards (CanActivateFn) replace classes with CanActivate. They return true (allow), false (block) or UrlTree (redirect). Use inject() for services. canActivate protects individual routes. Register it in the route array.

Named Outlets
{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }

<router-outlet />
<router-outlet name="sidebar" />

<a [routerLink]="[{ outlets: {
  primary: ['home'],
  sidebar: ['chat']
}}]">Home + Chat</a>

// URL: /home(sidebar:chat)

<router-outlet name="sidebar"> creates a named outlet. Routes with outlet: 'sidebar' render in it. Navigate with { outlets: { primary: [...], sidebar: [...] } }. The URL uses parentheses: /home(sidebar:chat). Allows multiple independent views on the same page.

RouterLink and Navigation
<a routerLink="/">Home</a>
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">Detail</a>

<a [routerLink]="['/users']"
   [queryParams]="{ page: 1, filter: 'active' }"
   fragment="top">Users</a>

<a routerLink="/about" routerLinkActive="active">About</a>

this.router.navigate(['/users', id]);
this.router.navigateByUrl('/login?redirect=/home');

routerLink navigates without a reload. [routerLink]="['/users', id]" for dynamic parameters. [queryParams] adds a query string. routerLinkActive applies a class when the route is active. router.navigate() for navigation via code (e.g. after login).

Guard Types
import { CanActivateFn, CanActivateChildFn,
  CanDeactivateFn, CanMatchFn, ResolveFn }
  from '@angular/router';

canActivate: [authGuard]
canActivateChild: [adminGuard]
canDeactivate: [unsavedGuard]
canMatch: [featureFlagGuard]
resolve: { user: userResolver }

Guard types: canActivate (can it enter?), canActivateChild (can it enter the children?), canDeactivate (can it leave? — e.g. unsaved form), canMatch (does the route exist? — e.g. feature flags), resolve (preload data). All have a functional version.

Route Data and Title
{
  path: 'users',
  component: UsersComponent,
  title: 'Users',
  data: { breadcrumb: 'Users', roles: ['admin'] }
}

@Injectable({ providedIn: 'root' })
export class AppTitleStrategy extends TitleStrategy {
  override updateTitle(snapshot: RouterStateSnapshot) {
    const title = this.buildTitle(snapshot);
    document.title = title ? `${title} | My App` : 'My App';
  }
}

The title property on the route sets the page title automatically. data stores metadata (breadcrumbs, roles). For dynamic titles, extend TitleStrategy. Access data with route.data or route.snapshot.data.

Route Parameters
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/rxjs-interop';

export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  userId = toSignal(
    this.route.paramMap.pipe(
      map(params => Number(params.get('id')))
    )
  );

  id = Number(this.route.snapshot.paramMap.get('id'));

  page = toSignal(
    this.route.queryParamMap.pipe(
      map(params => Number(params.get('page') ?? 1))
    )
  );
}

ActivatedRoute gives access to parameters. paramMap is an Observable (reacts to changes). snapshot.paramMap is the current value (does not react). queryParamMap for query params. Use toSignal to convert to a signal. Prefer paramMap if the route can change without destroying the component.

CanDeactivate (Unsaved Form)
import { CanDeactivateFn } from '@angular/router';

export interface FormGuard {
  hasUnsavedChanges(): boolean;
}

export const unsavedGuard: CanDeactivateFn<FormGuard> =
  (component) => {
    if (component.hasUnsavedChanges()) {
      return confirm('You have unsaved changes. Leave?');
    }
    return true;
  };

// In the component:
hasUnsavedChanges() { return this.form.dirty; }

CanDeactivateFn checks whether the user can leave the route. It receives the component instance. If the form has changes (form.dirty), it shows a confirmation. Return true (leave), false (stay) or Observable<boolean> for an async dialog.

Router Events and Scroll
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';

provideRouter(routes,
  withInMemoryScrolling({
    scrollPositionRestoration: 'top',
    anchorScrolling: 'enabled'
  }),
  withRouterConfig({ onSameUrlNavigation: 'reload' })
);

inject(Router).events.pipe(
  filter(e => e instanceof NavigationEnd)
).subscribe(e => console.log('Navigated to:', e.url));

withInMemoryScrolling configures scrolling: scrollPositionRestoration: 'top' returns to the top, anchorScrolling: 'enabled' enables fragments. Router.events emits navigation events (NavigationStart, NavigationEnd). Filter with instanceof.

Lazy Loading
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
  },
  {
    path: 'settings',
    loadComponent: () =>
      import('./settings/settings.component')
        .then(m => m.SettingsComponent)
  }
];

export const ADMIN_ROUTES: Routes = [
  { path: '', component: AdminDashboardComponent },
  { path: 'users', component: AdminUsersComponent }
];

loadChildren loads a routes module on demand (lazy). loadComponent loads a standalone component. The file is only downloaded when the user navigates to the route. Reduces the initial bundle. Use for large sections (admin, settings, reports).

Nested Routes (Children)
export const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    children: [
      { path: '', component: OverviewComponent },
      { path: 'stats', component: StatsComponent },
      { path: 'reports', component: ReportsComponent }
    ]
  }
];

// dashboard.component.html
<a routerLink="./">Overview</a>
<a routerLink="stats">Statistics</a>
<router-outlet />

children defines nested routes. The parent component has its own <router-outlet />. routerLink="./" is the default child route. The URL becomes /dashboard/stats. Use for layouts with sidebar/tabs where the content changes but the wrapper remains.

Preload Strategies
import { PreloadAllModules, withPreloading }
  from '@angular/router';

provideRouter(routes, withPreloading(PreloadAllModules));

@Injectable({ providedIn: 'root' })
export class SelectivePreload implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>) {
    if (route.data?.['preload']) return load();
    return of(null);
  }
}

// On the route: data: { preload: true }

PreloadAllModules loads all lazy modules in the background after the initial load. For fine-grained control, implement PreloadingStrategy with custom logic (e.g. only routes with data.preload). Improves perceived speed without increasing the initial bundle.

Signals


12 cards
signal() — Reactive State
import { signal } from '@angular/core';

counter = signal(0);
name = signal('Anna');
items = signal<string[]>([]);
user = signal<User | null>(null);

console.log(this.counter());

this.counter.set(5);
this.counter.update(v => v + 1);
this.items.update(list => [...list, 'new']);
this.user.update(u => u ? { ...u, name: 'Brian' } : u);

signal() creates a reactive value. Read it by calling it the a function: counter(). Update with .set(value) or .update(fn). For arrays/objects, create a new reference (immutability). Angular updates the template automatically when the signal changes.

input() — Signal Inputs
import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-product',
  template: `<h3>{{ name() }}</h3><p>{{ formattedPrice() }}</p>`
})
export class ProductComponent {
  name = input.required<string>();
  price = input(0);
  currency = input('EUR');

  formattedPrice = computed(() =>
    `${this.price().toFixed(2)} ${this.currency()}`
  );
}

input() creates signal inputs (v17.1+). input.required<T>() is mandatory. input(default) has a default value. Read the a function: name(). Combine with computed() for derived values. Replaces @Input() — more type-safe and works with signals.

untracked()
import { signal, computed, untracked } from '@angular/core';

counter = signal(0);
name = signal('Anna');

summary = computed(() => {
  const c = this.counter();
  const n = untracked(() => this.name());
  return `${n}: ${c}`;
});

// Only counter() is tracked
// Changing name() does NOT recalculate summary

untracked() reads a signal without registering it the a dependency. Inside computed or effect, a signal read with untracked does not trigger recalculation. Useful for reading secondary values that should not cause reactivity. Use with care — it can cause inconsistencies.

computed() — Derived Values
import { signal, computed } from '@angular/core';

price = signal(100);
quantity = signal(2);
discount = signal(0.1);

subtotal = computed(() => this.price() * this.quantity());
total = computed(() => this.subtotal() * (1 - this.discount()));
formatted = computed(() => `${this.total().toFixed(2)} €`);

// In the template: {{ total() }} → 180
// Read-only: cannot do total.set()

computed() creates a signal derived from other signals. It recalculates automatically when dependencies change (lazy — only when read). It is read-only. It can depend on other computed. Ideal for totals, filters, formatting and any derived value.

output() — Signal Outputs
import { Component, output } from '@angular/core';

@Component({
  selector: 'app-item',
  template: `
    <span>{{ name() }}</span>
    <button (click)="removed.emit(this.id())">X</button>
  `
})
export class ItemComponent {
  id = input.required<number>();
  name = input.required<string>();
  removed = output<number>();
}

// Parent: <app-item (removed)="onRemove($event)" />

output() creates signal outputs (v17.1+). Emit with .emit(value). The parent listens with (event)="handler($event)". Replaces @Output() + new EventEmitter(). More concise and type-safe. Combine with input() for fully signal-based components.

Signal with Arrays and Objects
items = signal<string[]>([]);
user = signal<User>({ name: 'Anna', age: 25 });

// Add (immutable)
this.items.update(list => [...list, 'new']);

// Remove
this.items.update(list => list.filter(i => i !== 'x'));

// Update object
this.user.update(u => ({ ...u, age: 26 }));

// Does NOT work (mutation):
// this.items().push('x');  ← does not notify!

Signals with arrays/objects require immutability. .update() with spread ([...list, x]) or filter creates a new reference. Direct mutation (.push(), .name = x) does not notify Angular because the reference does not change. Always create new arrays/objects.

effect() — Side Effects
import { signal, effect } from '@angular/core';

export class SearchComponent {
  term = signal('');

  constructor() {
    effect(() => {
      const t = this.term();
      console.log('Searched:', t);
      this.saveHistory(t);
    });
  }
}

effect() runs code when the signals read inside it change. It tracks dependencies automatically. Use for side effects: logs, analytics, persistence, API calls. Do not use it to derive state (use computed). The effect is destroyed with the component.

toSignal/toObservable (Interop)
import { toSignal, toObservable } from '@angular/rxjs-interop';

users = toSignal(
  this.http.get<User[]>('/api/users'),
  { initialValue: [] }
);

filter = signal('');
filter$ = toObservable(this.filter);

results$ = this.filter$.pipe(
  debounceTime(300),
  switchMap(f => this.search(f))
);

results = toSignal(this.results$, { initialValue: [] });

toSignal() converts an Observable into a signal (with initialValue). toObservable() converts a signal into an Observable to use RxJS operators. Pattern: signal → toObservable → RxJS operators → toSignal for the template. From @angular/rxjs-interop.

effect() with Cleanup
import { effect, signal, EffectRef } from '@angular/core';

export class PollingComponent {
  interval = signal(5000);
  private effectRef!: EffectRef;

  constructor() {
    this.effectRef = effect((onCleanup) => {
      const ms = this.interval();
      const id = setInterval(() => this.poll(), ms);

      onCleanup(() => clearInterval(id));
    });
  }

  // Destroy manually (optional)
  stop() { this.effectRef.destroy(); }
}

effect() accepts an onCleanup callback that registers cleanup. The cleanup runs before the next effect or when the effect is destroyed. Useful for clearInterval, removeEventListener, cancelling requests. EffectRef.destroy() destroys manually.

model() — Two-Way Signal
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-volume',
  template: `
    <input type="range" [value]="volume()"
      (input)="volume.set(+$any($event.target).value)">
    <span>{{ volume() }}%</span>
  `
})
export class VolumeComponent {
  volume = model(50);
}

// Parent: <app-volume [(volume)]="appVolume" />

model() creates a signal with two-way binding. The child reads with volume() and updates with volume.set(). The parent uses [(volume)]="var". It replaces @Input + @Output + EventEmitter for values the child can modify. Requires Angular 17.1+.

asReadonly()
@Injectable({ providedIn: 'root' })
export class AuthService {
  private _user = signal<User | null>(null);
  private _loading = signal(false);

  readonly user = this._user.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly isLoggedIn = computed(() => this._user() !== null);

  login(email: string, password: string) {
    this._loading.set(true);
    this.http.post('/login', { email, password }).subscribe({
      next: user => {
        this._user.set(user);
        this._loading.set(false);
      }
    });
  }
}

asReadonly() exposes a signal without .set() / .update(). The pattern: private state with signal(), exposed the asReadonly(). Public methods control mutation. Ensures only the service modifies the state — components only read.

linkedSignal (v19+)
import { signal, linkedSignal } from '@angular/core';

userId = signal(1);

// Recalculates when userId changes, but is editable
userName = linkedSignal(() => {
  const id = this.userId();
  return this.getUser(id)?.name ?? '';
});

// Can be modified manually:
this.userName.set('Custom Name');

// But recalculates if userId() changes
this.userId.set(2); // userName recalculates

linkedSignal() (v19+) is a signal that recalculates when dependencies change (like computed), but can also be modified manually with .set(). Ideal for forms that fill from an API but the user can edit. Combines reactivity with mutability.

Directives


12 cards
Attribute Directive
import { Directive, ElementRef, HostListener,
  input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  color = input('yellow', { alias: 'appHighlight' });
  private el = inject(ElementRef);

  @HostListener('mouseenter')
  onEnter() {
    this.el.nativeElement.style.backgroundColor = this.color();
  }

  @HostListener('mouseleave')
  onLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

// <p [appHighlight]="'lightblue'">Text</p>

An attribute directive uses @Directive with a selector in square brackets ([appHighlight]). It modifies the behavior of an existing element. input() with alias allows passing values. @HostListener reacts to element events.

Directive Composition (hostDirectives)
import { Component } from '@angular/core';
import { TooltipDirective } from './tooltip.directive';
import { TrackClickDirective } from './track-click.directive';

@Component({
  selector: 'app-btn',
  template: `<button><ng-content /></button>`,
  hostDirectives: [
    TooltipDirective,
    { directive: TrackClickDirective,
      inputs: ['trackLabel'] }
  ]
})
export class BtnComponent {}

// <app-btn appTooltip="Click here" trackLabel="CTA">

hostDirectives (v15+) applies directives to a component without inheritance. The directives are composed on the host. inputs exposes directive inputs on the component. Removes the need to apply directives manually on every use. Ideal for cross-cutting behaviors (tooltip, tracking, accessibility).

Debounce Directive
@Directive({
  selector: '[appDebounceInput]',
  standalone: true
})
export class DebounceInputDirective {
  debounceMs = input(300, { alias: 'appDebounceInput' });
  debouncedValue = output<string>();
  private el = inject(ElementRef);

  constructor() {
    fromEvent(this.el.nativeElement, 'input').pipe(
      debounceTime(this.debounceMs()),
      map((e: any) => e.target.value),
      distinctUntilChanged(),
      takeUntilDestroyed()
    ).subscribe(v => this.debouncedValue.emit(v));
  }
}

// <input [appDebounceInput]="500"
//        (debouncedValue)="search($event)">

The debounce directive uses fromEvent + debounceTime + distinctUntilChanged to emit values with a delay. input() with alias lets you configure the milliseconds. output() emits the debounced value. Useful for real-time search without overloading the API.

Structural Directive
import { Directive, TemplateRef, ViewContainerRef,
  input, effect } from '@angular/core';

@Directive({
  selector: '[appRepeat]',
  standalone: true
})
export class RepeatDirective {
  private templateRef = inject(TemplateRef);
  private viewContainer = inject(ViewContainerRef);

  appRepeat = input.required<number>();

  constructor() {
    effect(() => {
      const count = this.appRepeat();
      this.viewContainer.clear();
      for (let i = 0; i < count; i++) {
        this.viewContainer.createEmbeddedView(
          this.templateRef, { $implicit: i }
        );
      }
    });
  }
}

// <p *appRepeat="3; let i">Item {{ i }}</p>

Structural directives manipulate the DOM with TemplateRef and ViewContainerRef. createEmbeddedView() renders the template. $implicit is the default variable of let. The * prefix in the template is syntactic sugar for ng-template. Examples: *ngIf, *ngFor.

Tooltip Directive
@Directive({
  selector: '[appTooltip]',
  standalone: true,
  host: {
    '(mouseenter)': 'show()',
    '(mouseleave)': 'hide()'
  }
})
export class TooltipDirective {
  appTooltip = input.required<string>();
  private overlayRef: OverlayRef | null = null;
  private overlay = inject(Overlay);

  show() {
    this.overlayRef = this.overlay.create({
      positionStrategy: this.overlay.position()
        .flexibleConnectedTo(this.el)
        .withPositions([{ originX: 'center', originY: 'top',
          overlayX: 'center', overlayY: 'bottom' }])
    });
    this.overlayRef.attach(
      new TemplatePortal(this.tooltipTmpl, this.vcr)
    );
  }

  hide() { this.overlayRef?.dispose(); }
}

A tooltip directive uses the Angular CDK Overlay to position floating elements. host in the decorator registers mouseenter/mouseleave. flexibleConnectedTo positions relative to the element. TemplatePortal renders the template in the overlay. dispose() removes it on leave.

Permission Directive
@Directive({
  selector: '[appHasPermission]',
  standalone: true
})
export class HasPermissionDirective {
  private templateRef = inject(TemplateRef);
  private viewContainer = inject(ViewContainerRef);
  private auth = inject(AuthService);

  appHasPermission = input.required<string>();

  constructor() {
    effect(() => {
      const perm = this.appHasPermission();
      this.viewContainer.clear();
      if (this.auth.hasPermission(perm)) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}

// <button *appHasPermission="'user:delete'">Delete</button>

The permission directive is a structural directive that shows/hides elements based on permissions. effect() reacts to permission changes. viewContainer.clear() removes, createEmbeddedView() renders. Use * in the template. Essential for role/permission-based UI.

Directive with ngTemplateOutlet
@Directive({
  selector: '[appIfRole]',
  standalone: true
})
export class IfRoleDirective {
  private templateRef = inject(TemplateRef);
  private viewContainer = inject(ViewContainerRef);
  private auth = inject(AuthService);

  appIfRole = input.required<string>();

  constructor() {
    effect(() => {
      const role = this.appIfRole();
      this.viewContainer.clear();
      if (this.auth.hasRole(role)) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}

// <div *appIfRole="'admin'">Admin Panel</div>

A conditional structural directive checks permissions with inject(AuthService). effect() reacts to changes in the appIfRole() signal. viewContainer.clear() removes previous views. createEmbeddedView() renders if the condition is true. Pattern for role-based access control.

Click Outside Directive
import { Directive, output, ElementRef,
  HostListener } from '@angular/core';

@Directive({
  selector: '[appClickOutside]',
  standalone: true
})
export class ClickOutsideDirective {
  clickOutside = output<void>();
  private el = inject(ElementRef);

  @HostListener('document:click', ['$event'])
  onClick(event: MouseEvent) {
    if (!this.el.nativeElement.contains(event.target)) {
      this.clickOutside.emit();
    }
  }
}

// <div (appClickOutside)="closeMenu()">

The ClickOutside directive listens for clicks on the document and checks whether the target is outside the element with contains(). If so, it emits clickOutside. Useful for closing dropdowns, modals and menus. Use output() for the event. The [appClickOutside] selector applies the an attribute.

TrackBy Directive
@Directive({
  selector: '[appTrackById]',
  standalone: true
})
export class TrackByIdDirective {
  // Used with @for:
  // @for (item of items; track item.id) { ... }

  // Or with the old ngFor:
  // <li *ngFor="let item of items; trackBy: trackById">
  trackById(index: number, item: any): any {
    return item.id;
  }
}

// With @for (v17+), track is required:
// @for (item of items; track item.id) { ... }

trackBy helps Angular identify list items by a unique key (e.g. id) instead of the index. With @for (v17+), track is required: track item.id. Improves performance in large lists — Angular only re-renders items that changed, not the whole list.

Validation Directive
import { Directive, input } from '@angular/core';
import { NG_VALIDATORS, Validator,
  AbstractControl, ValidationErrors } from '@angular/forms';

@Directive({
  selector: '[appForbiddenWord]',
  standalone: true,
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: ForbiddenWordDirective,
    multi: true
  }]
})
export class ForbiddenWordDirective implements Validator {
  appForbiddenWord = input.required<string>();

  validate(control: AbstractControl): ValidationErrors | null {
    const has = control.value?.includes(this.appForbiddenWord());
    return has ? { forbidden: true } : null;
  }
}

A validation directive implements Validator and registers itself the NG_VALIDATORS with multi: true. The validate() method receives the AbstractControl. Use input() for the parameter. Apply in the template: <input appForbiddenWord="admin">.

Lazy Load Directive (IntersectionObserver)
@Directive({
  selector: '[appLazyLoad]',
  standalone: true
})
export class LazyLoadDirective {
  appLazyLoad = input.required<string>();
  private el = inject(ElementRef);

  constructor() {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          this.el.nativeElement.src = this.appLazyLoad();
          observer.disconnect();
        }
      },
      { rootMargin: '200px' }
    );
    observer.observe(this.el.nativeElement);
  }
}

// <img appLazyLoad="assets/photo.jpg" src="placeholder.jpg">

The lazy load directive uses IntersectionObserver to load images when they enter the viewport. rootMargin: '200px' preloads 200px earlier. observer.disconnect() stops observing after loading. Replaces lazy loading libraries for simple cases.

Accessibility Directive
@Directive({
  selector: '[appA11y]',
  standalone: true,
  host: {
    'role': 'button',
    'tabindex': '0',
    '[attr.aria-label]': 'appA11y()',
    '(keydown.enter)': 'onClick()',
    '(keydown.space)': 'onClick()'
  }
})
export class A11yDirective {
  appA11y = input.required<string>();
  clicked = output<void>();

  onClick() { this.clicked.emit(); }
}

// <div [appA11y]="'Close modal'" (clicked)="close()">

The accessibility directive adds role, tabindex, aria-label and keyboard handlers (Enter, Space) to non-interactive elements. Use host in the decorator for declarative binding. input() receives the label. Essential to make divs/spans accessible the buttons.