DevTools

Cheatsheet Angular

Framework frontend da Google baseado em TypeScript

Voltar às linguagens
Angular
152 cards encontrados
Categorias:
Versões:

Setup e CLI


14 cards
Instalação Global
npm install -g @angular/cli
ng version
ng new meu-projeto
cd meu-projeto
ng serve --open

Instale o @angular/cli globalmente com npm install -g. O comando ng new cria o projeto com todas as configurações. O ng serve --open inicia o dev server na porta 4200 e abre o browser automaticamente.

Estrutura do Projeto
meu-projeto/
├── 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

A estrutura moderna (v17+) tem app.config.ts (providers) e app.routes.ts (rotas) em vez de app.module.ts. O main.ts faz o bootstrap com bootstrapApplication(). O angular.json configura builds, assets e 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.exemplo.com'
};

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

Os environments permitem configurações diferentes por ambiente. O ng build usa o ficheiro de produção automaticamente. Em desenvolvimento, o ng serve usa o ficheiro base. Aceda via environment.apiUrl.

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

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

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

O styles.css (definido em angular.json) contém CSS global. Os estilos dos componentes são encapsulados por ViewEncapsulation.Emulated (default) — não afetam outros componentes. Use ViewEncapsulation.None para estilos globais num componente.

ng new — Opções
ng new app --routing
ng new app --skip-tests
ng new app --ssr
ng new app --style=scss
ng new app --standalone

O ng new aceita flags: --routing adiciona routing, --style=scss define o pré-processador CSS, --ssr ativa Server-Side Rendering com Hydration. Desde o Angular 17, projetos são standalone por padrão.

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));

O main.ts é o ponto de entrada. bootstrapApplication() inicializa o componente raiz com a configuração global. Substitui o antigo platformBrowserDynamic().bootstrapModule() dos projetos com NgModule.

Comandos CLI Úteis
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

O CLI tem comandos para o dia a dia: ng serve --port muda a porta, ng add instala e configura libraries (ex: @angular/material), ng update atualiza dependências com migrations automáticas.

Assets e Ficheiros Estáticos
// angular.json
"assets": [
  "src/favicon.ico",
  "src/assets",
  { "glob": "**/*", "input": "src/i18n", "output": "/i18n" }
]

// Acesso no template
<img src="assets/logo.png">
<img [src]="'assets/' + imagem">

A pasta src/assets/ é copiada para o build. Configure em angular.json no array assets. Pode adicionar objetos com glob, input e output para mapear pastas externas. No template, use caminhos relativos a 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

O ng generate (atalho ng g) cria ficheiros com boilerplate. O caminho define a pasta: ng g c components/header cria em src/app/components/header/. Todos os artefactos são standalone por padrão desde o 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(),
  ]
};

O app.config.ts define os providers globais usando funções provide*: provideRouter() para rotas, provideHttpClient() para HTTP, provideAnimations() para animações. Substitui o array imports do antigo NgModule.

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

O tsconfig.json configura o TypeScript. strict: true ativa todas as verificações. experimentalDecorators é necessário para os decorators @Component, @Injectable, etc. paths permite aliases de import como @app/.

Build e Deploy
ng build
ng build --watch
ng build --configuration production
# Output em dist/
# npx serve dist/meu-projeto/browser

O ng build compila para dist/. Em produção, aplica AOT compilation, tree-shaking, minificação e budget checks (avisos se o bundle exceder limites definidos no angular.json).

angular.json — Configuração
{
  "projects": {
    "meu-projeto": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/meu-projeto",
            "index": "src/index.html",
            "main": "src/main.ts",
            "styles": ["src/styles.css"],
            "assets": ["src/favicon.ico", "src/assets"]
          }
        }
      }
    }
  }
}

O angular.json configura builds, serve, testes e assets. Em build.options defina styles (CSS global), assets (ficheiros estáticos) e budgets (limites de tamanho). O serve herda da configuração de build.

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

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

Path aliases simplificam imports longos. Defina em tsconfig.json na secção compilerOptions.paths. Em vez de ../../services/api.service, use @services/api.service. O Angular CLI resolve os aliases automaticamente no build.

Componentes


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

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

Um componente standalone não precisa de NgModule. O decorator @Component define o selector (tag HTML), o template e os estilos. A propriedade imports lista as dependências do componente (outros componentes, pipes, diretivas).

Ciclo de Vida
import { OnInit, OnChanges, OnDestroy, SimpleChanges }
  from '@angular/core';

export class UserComponent
  implements OnInit, OnChanges, OnDestroy {

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

  ngOnInit() {
    this.carregarDados();
  }

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

Os hooks de ciclo de vida: ngOnChanges (inputs mudam), ngOnInit (inicialização), ngDoCheck (detecção manual), ngAfterViewInit (view pronta), ngOnDestroy (limpeza). Implemente as interfaces correspondentes.

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

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

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

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

@HostBinding liga uma propriedade do componente a um atributo/classe do elemento host. @HostListener ouve eventos no host ou no documento (document:keydown.escape). Alternativa moderna: usar a propriedade host no decorator @Component.

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

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

// Pai: <app-toggle [(ativo)]="ligado" />

model() cria um signal com two-way binding automático. O filho lê com ativo() e atualiza com ativo.set() ou ativo.update(). O pai usa [(ativo)]="variavel" — a sintaxe "banana in a box" funciona automaticamente com model().

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

Para componentes pequenos, use template e styles inline (com template literals). Evita ficheiros separados. O styleUrl (singular) é a forma moderna; styleUrls (array) ainda funciona mas está deprecated.

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

// Uso:
// <app-card>
//   <h2>Título</h2>
//   <p>Conteúdo principal</p>
//   <div class="acoes"><button>OK</button></div>
// </app-card>

<ng-content> projeta conteúdo do pai para o filho (slot). O atributo select filtra por seletor CSS: select="h2" capta headings, select=".acoes" capta elementos com essa classe. Sem select, capta tudo o que não foi distribuído.

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

A propriedade host no decorator substitui @HostBinding e @HostListener. Use [class.x] para classes, [attr.x] para atributos e (evento) para listeners. Mais declarativo e sem decorators extra.

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

@Component({
  selector: 'app-lista',
  template: `<input #campo /><p *ngFor="let i of itens">{{ i }}</p>`
})
export class ListaComponent {
  campo = viewChild.required<ElementRef>('campo');
  paragrafos = viewChildren<ElementRef>('p');

  focar() {
    this.campo().nativeElement.focus();
  }
}

viewChild() e viewChildren() são a alternativa signal-based a @ViewChild. Retornam signals: leia com campo(). viewChild.required() lança erro se não encontrar. Não precisa de ngAfterViewInit — o signal atualiza automaticamente.

@Input — Receber Dados
import { Component, Input } from '@angular/core';

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

// Uso: <app-card [titulo]="t" [id]="1" />

@Input() declara propriedades que o pai pode ligar com [propriedade]="valor". @Input({ required: true }) torna o input obrigatório — erro de compilação se não for fornecido. O ! (non-null assertion) indica que será inicializado externamente.

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

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

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

@ViewChild acede a elementos/componentes do próprio template. @ContentChild acede a conteúdo projetado via ng-content. Use #ref no template como referência. Só estão disponíveis após ngAfterViewInit / ngAfterContentInit.

Componentes Dinâmicos
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;

  criarAlerta(mensagem: string) {
    const ref = this.container.createComponent(AlertComponent);
    ref.setInput('mensagem', mensagem);
    ref.instance.fechado.subscribe(() => ref.destroy());
  }
}

ViewContainerRef permite criar componentes dinamicamente com createComponent(). Use setInput() para definir inputs e instance para aceder a outputs. ref.destroy() remove o componente. Útil para modais, tooltips e notificações.

@Output — Emitir Eventos
import { Component, Output, EventEmitter } from '@angular/core';

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

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

// Pai: <app-search (pesquisado)="buscar($event)" />

@Output() expõe eventos com EventEmitter. O filho chama .emit(valor) e o pai ouve com (evento)="handler($event)". O $event contém o valor emitido. Combine com @Input para comunicação bidirecional.

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

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

// Uso: <app-user [nome]="'Ana'" [idade]="25" />

Signal inputs (input()) são a alternativa moderna a @Input(). São signals: leia com nome(). input.required<T>() torna obrigatório. output() substitui @Output() + EventEmitter. Mais type-safe e sem decorators.

Encapsulamento de Estilos
import { ViewEncapsulation } from '@angular/core';

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

// Opções:
// Emulated (default) — estilos isolados com _ngcontent
// None — estilos globais (sem isolamento)
// ShadowDom — usa Shadow DOM nativa

ViewEncapsulation.Emulated (default) adiciona atributos _ngcontent-xxx para isolar estilos. None torna os estilos globais. ShadowDom usa a Shadow DOM nativa do browser. Para estilizar conteúdo projetado, use :host ::ng-deep (deprecated).

Template e Sintaxe


14 cards
Interpolação
<p>{{ titulo }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ user?.nome }}</p>
<p>{{ itens.length }}</p>
<p>{{ ativo ? 'Sim' : 'Não' }}</p>
<p>{{ nome | uppercase }}</p>

A interpolação {{ }} renderiza expressões no template. Suporta propriedades, operadores ternários, optional chaining (?.) e pipes. Não permite declarações de variáveis, acesso a window/document ou efeitos colaterais.

@if — Condicional (v17+)
@if (user) {
  <p>Olá, {{ user.nome }}</p>
} @else if (aCarregar) {
  <p>A carregar...</p>
} @else {
  <p>Sem utilizador</p>
}

@if (dados$ | async; as dados) {
  <ul>
    @for (item of dados; track item.id) {
      <li>{{ item.nome }}</li>
    }
  </ul>
}

A nova sintaxe @if (v17+) substitui *ngIf. Suporta @else if e @else. A forma @if (expr; as var) cria uma variável local com o valor. Não precisa de <ng-container> — o bloco não cria elemento extra no DOM.

Template Reference Variables
<input #campoNome>
<button (click)="saudar(campoNome.value)">Saudar</button>

<app-form #formulario>
  <button (click)="formulario.validar()">Validar</button>
</app-form>

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

Variáveis de referência #nome dão acesso direto a elementos, componentes ou diretivas no template. #campo referencia o HTMLElement. #comp referencia a instância do componente. #x="ngModel" referencia a diretiva NgModel com o seu estado de validação.

Eventos com $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)="salvar()">Salvar</button>

O $event contém o objeto do evento nativo. Para (input) é Event, para (click) é MouseEvent, para (keydown) é KeyboardEvent. Se não precisa do evento, omita o $event. Para outputs customizados, $event é o valor emitido pelo EventEmitter.

Property Binding
<img [src]="urlImagem">
<button [disabled]="!formulario.valido">
<div [innerHTML]="htmlSeguro">
<td [attr.colspan]="numeroColunas">
<button [attr.aria-label]="descricao">
<div [class.ativo]="estaAtivo">
<div [style.color]="corTexto">
<div [style.width.px]="largura">

[propriedade]="expressao" liga uma propriedade DOM a uma expressão. Para atributos HTML que não são propriedades, use [attr.x]. Para classes: [class.nome]="booleano". Para estilos: [style.prop]="valor" com unidade opcional (.px, .%).

@for — Iteração (v17+)
@for (item of itens; track item.id;
      let i = $index, first = $first,
      last = $last, count = $count) {
  <li [class.primeiro]="first">
    {{ i + 1 }}/{{ count }}: {{ item.nome }}
  </li>
} @empty {
  <li>Lista vazia</li>
}

@for substitui *ngFor. O track é obrigatório (melhor performance que trackBy). O bloco @empty renderiza quando a lista está vazia. Variáveis implícitas: $index, $first, $last, $even, $odd, $count.

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

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

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

<ng-container> agrupa elementos sem renderizar um elemento extra no DOM. Útil com *ngIf e *ngFor quando precisa de aplicar a múltiplos elementos. Com a nova sintaxe @if/@for, o ng-container é menos necessário.

Sintaxe Antiga vs Nova
<!-- *ngIf (antigo) -->
<p *ngIf="ativo">Visível</p>

<!-- @if (novo, v17+) -->
@if (ativo) { <p>Visível</p> }

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

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

A nova sintaxe de controlo (@if, @for, @switch) é mais legível, não precisa de <ng-container> e tem melhor performance. O track é obrigatório no @for. A sintaxe antiga (*ngIf, *ngFor) ainda funciona mas é desencorajada em projetos novos.

Event Binding
<button (click)="salvar()">Salvar</button>
<input (input)="onInput($event)">
<form (ngSubmit)="enviar()">
<input (keydown.enter)="pesquisar()">
<input (keydown.control.s)="guardar($event)">
<div (scroll)="onScroll($event)">
<input (blur)="validar()">
<select (change)="onMudar($event)">

(evento)="handler()" ouve eventos do DOM. O $event passa o objeto do evento. Para teclas específicas: (keydown.enter), (keydown.escape). Modificadores: (keydown.control.s). O (ngSubmit) previne o reload da página.

@switch — Múltiplas Condições
@switch (estado) {
  @case ('ativo') {
    <span class="verde">Ativo</span>
  }
  @case ('pendente') {
    <span class="amarelo">Pendente</span>
  }
  @case ('inativo') {
    <span class="vermelho">Inativo</span>
  }
  @default {
    <span class="cinza">Desconhecido</span>
  }
}

@switch substitui múltiplos *ngIf / ngSwitch. Cada @case compara com o valor da expressão. @default é o fallback. Mais legível e performático que cadeias de @if / @else if para valores discretos.

ng-template e ngTemplateOutlet
<ng-template #loading>
  <p>A carregar...</p>
</ng-template>

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

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

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

<ng-template> define um template que não é renderizado até ser instanciado. *ngTemplateOutlet renderiza o template com um context opcional. let-x="chave" declara variáveis locais que recebem valores do contexto. Essencial para templates customizáveis em bibliotecas.

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

<!-- Equivalente expandido: -->
<input [ngModel]="user.nome"
       (ngModelChange)="user.nome = $event">

<!-- Com signal model() (v17.1+): -->
<app-toggle [(ativo)]="ligado" />

<!-- Equivalente: -->
<app-toggle [ativo]="ligado"
            (ativoChange)="ligado = $event" />

O two-way binding [(x)]="var" combina property binding [x] com event binding (xChange). Com ngModel requer FormsModule. Com model() signals, funciona automaticamente sem módulos. A convenção é: input x + output xChange.

ngClass e ngStyle
<div [ngClass]="{
  'ativo': estaAtivo,
  'destaque': emFoco,
  'erro': temErro
}">

<div [ngClass]="['base', tema, tamanho]">

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

[ngClass] aplica múltiplas classes condicionalmente (objeto, array ou string). [ngStyle] aplica múltiplos estilos inline. Para uma única classe/estilo, prefira [class.x] e [style.x] — são mais performáticos.

Safe Navigation e Non-Null
<p>{{ user?.endereco?.cidade }}</p>
<p>{{ user!.nome }}</p>
<p>{{ user?.nome ?? 'Anónimo' }}</p>
<p>{{ (user$ | async)?.nome }}</p>

O operador ?. (safe navigation) previne erros quando a propriedade pode ser null/undefined. O ! (non-null assertion) diz ao TypeScript que o valor existe. Combine com ?? (nullish coalescing) para valores default. Com async pipe, use parênteses: (x$ | async)?.prop.

Serviços e DI


12 cards
Criar Serviço
import { Injectable } from '@angular/core';

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

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

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

@Injectable({ providedIn: 'root' }) cria um singleton global — uma instância partilhada por toda a app. O serviço encapsula lógica reutilizável (API, estado, utilitários). O providedIn: 'root' permite tree-shaking: se nenhum componente o usar, é removido do 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' }
]

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

InjectionToken cria tokens para valores que não são classes (strings, configs, objetos). Defina com new InjectionToken<T>('NOME'). Forneça com { provide: TOKEN, useValue: valor }. Consuma com inject(TOKEN). Essencial para bibliotecas configuráveis.

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

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

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

resultados$ = this.filtro$.pipe(
  debounceTime(300),
  switchMap(f => this.pesquisar(f))
);

toSignal() converte um Observable em signal (com initialValue opcional). toObservable() converte um signal em Observable — útil para usar operadores RxJS com signals. Ambos são de @angular/rxjs-interop. Facilitam a migração gradual de RxJS para signals.

inject() — Injeção Moderna
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);
}

// Em componentes:
// private userService = inject(UserService);

inject() é a forma moderna de injeção de dependências. Funciona em componentes, serviços, guards, interceptors e funções. Substitui a injeção via constructor. Vantagens: menos código, funciona em funções standalone e simplifica herança (sem super()).

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

{ provide: AbstractLogger, useExisting: ConsoleLogger }

{ provide: LoggerService, useClass: ProductionLogger }

Providers podem usar: useValue (valor estático), useClass (substituir implementação), useFactory (criar dinamicamente com lógica), useExisting (alias). useFactory pode usar inject() internamente para aceder a outras dependências.

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

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

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

takeUntilDestroyed() faz unsubscribe automaticamente quando o componente/serviço é destruído. Substitui o padrão manual Subject + takeUntil + ngOnDestroy. Fora do contexto de injeção, passe DestroyRef como argumento. De @angular/rxjs-interop.

Injeção via Constructor
import { Component } from '@angular/core';
import { UserService } from '../services/user.service';

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

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

A injeção via constructor é a forma clássica. O Angular resolve as dependências automaticamente pelo tipo. private cria a propriedade automaticamente. Ainda funciona, mas inject() é preferido em código novo por ser mais flexível e funcionar fora de classes.

Serviço com Estado (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.preco, 0)
  );
  readonly count = computed(() => this.items().length);

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

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

Um padrão moderno é usar signals como store: estado privado com signal(), exposto como asReadonly(). computed() deriva valores. Métodos públicos modificam com .set() / .update(). Substitui BehaviorSubject e NgRx para estado simples.

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 } }

Um ResolveFn pré-carrega dados antes de ativar a rota. O componente acede via route.data['user']. A forma funcional com inject() substitui a classe com Resolve. Se o resolver retornar um Observable, o Angular espera até completar antes de navegar.

Providers Hierárquicos
@Injectable({ providedIn: 'root' })

@Component({ providers: [FormService] })

@Injectable({ providedIn: 'platform' })

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

O Angular tem injeção hierárquica: providedIn: 'root' cria um singleton global. providers no componente cria uma instância por componente. @Self() restringe ao injector atual. @Optional() não lança erro se não encontrar. @SkipSelf() procura no pai.

Serviço HTTP Completo
@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}`);
  }
}

Um serviço HTTP encapsula todas as chamadas à API. Use inject(HttpClient) e inject(API_URL). Cada método retorna um Observable<T> tipado. O componente subscreve ou usa async pipe. Centraliza URLs, tratamento de erros e 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 executa código antes da app arrancar. Útil para carregar configurações remotas, verificar auth ou inicializar SDKs. multi: true permite múltiplos initializers. Se a factory retornar uma Promise ou Observable, o bootstrap espera até resolver.

HTTP e Interceptors


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

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

provideHttpClient() ativa o HttpClient na app. withInterceptors() regista interceptors funcionais. withFetch() (v15+) usa a Fetch API nativa em vez de XMLHttpRequest. Sem isto, injetar HttpClient lança erro "No provider for HttpClient".

Interceptor de Erros
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('Sem conexão');
      else console.error(`Erro ${error.status}:`, error.message);
      return throwError(() => error);
    })
  );
};

Um interceptor de erros usa catchError no pipe. HttpErrorResponse contém status, message e error. Status 0 indica falha de rede/CORS. Sempre re-lance com throwError(() => error) para o subscriber tratar.

Download de Ficheiros
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);
  });
}

Para download, use responseType: 'blob'. Crie um URL.createObjectURL() e um link temporário com a.click(). O nome do ficheiro vem do header Content-Disposition. URL.revokeObjectURL() liberta a memória. Funciona para PDFs, imagens, Excel, etc.

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

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

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

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

http.get<T>() retorna Observable<T> tipado. HttpParams constrói query strings de forma imutável (.set() retorna nova instância). O tipo é apenas compile-time — o Angular não valida a resposta em runtime. Use interfaces para os modelos de dados.

Tratamento de Erros
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('Erro no servidor'));
}

// No componente:
this.service.getUsers().subscribe({
  next: users => this.users = users,
  error: err => this.erro = err.message
});

retry(n) tenta novamente em caso de erro. catchError transforma o erro. No componente, use o observer com next e error. Aceda à mensagem da API com error.error?.message. Para erros globais, use um interceptor de erros.

Cache Simples
@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(dados => this.cache.set(url, dados))
    );
  }

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

Um cache simples usa um Map para armazenar respostas. Se a URL já está em cache, retorna of(valor) (Observable síncrono). tap() armazena a resposta após o primeiro request. invalidate() limpa o cache. Para caches avançados, use HttpInterceptorFn ou bibliotecas como @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() envia dados no body. http.put() substitui o recurso completo. http.patch() atualiza parcialmente. http.delete() remove. Todos aceitam um tipo genérico para a resposta. O body é serializado como JSON automaticamente.

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

uploadComProgresso(ficheiro: 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!))
  );
}

Para upload, use FormData — o Angular define o Content-Type: multipart/form-data automaticamente. Para progresso, crie um HttpRequest com reportProgress: true e filtre eventos HttpEventType.UploadProgress.

Requests Paralelos
import { forkJoin } from 'rxjs';

carregarDashboard() {
  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.carregarDashboard()
  .subscribe(({ users, posts, stats }) => {
    this.users = users;
    this.posts = posts;
    this.stats = stats;
  });

forkJoin executa múltiplos Observables em paralelo e emite quando todos completam. Aceita um objeto com chaves nomeadas — a resposta tem a mesma estrutura. Se um falhar, o erro propaga (use catchError em cada um para resiliência). Ideal para dashboards e páginas com múltiplas fontes.

Interceptor Funcional
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);
};

Interceptors funcionais (HttpInterceptorFn) substituem classes com HTTP_INTERCEPTORS. Recebem req e next. req.clone() cria uma cópia modificada (requests são imutáveis). next(req) passa ao próximo interceptor. Registe com withInterceptors([]).

Headers e Opções
import { HttpHeaders } from '@angular/common/http';

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

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

this.http.get('/api/dados', { 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 é imutável — .set() retorna nova instância. observe: 'response' dá acesso à resposta completa (status, headers, body). responseType: 'text' retorna string em vez de JSON. responseType: 'blob' para ficheiros binários.

Base URL e Proxy
// Opção 1: environment
this.http.get(`${environment.apiUrl}/users`)

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

// Opção 3: Interceptor com baseUrl
const cloned = req.clone({ url: `${baseUrl}${req.url}` });

Para a base URL: use environment.apiUrl (simples), proxy.conf.json em dev (evita CORS), ou um interceptor que prefixa todas as URLs. O proxy só funciona com ng serve — em produção, configure o servidor web (nginx, Apache) ou use CORS no backend.

Avançado e Performance


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

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

// OnPush só verifica quando:
// 1. Input muda (referência)
// 2. Evento do componente (click, etc.)
// 3. Observable com async pipe emite
// 4. Signal muda

ChangeDetectionStrategy.OnPush otimiza o change detection: só verifica quando inputs mudam (nova referência), eventos do componente disparam, async pipe emite ou signals mudam. Com signals, OnPush é o default implícito. Sempre use OnPush em componentes novos.

ContentChildren e 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.ativo = true;
    this.tabs.changes.subscribe(() => {
      // Tabs foram adicionadas/removidas
    });
  }
}

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

@ContentChildren consulta todos os componentes/diretivas projetados via ng-content. QueryList é uma coleção reativa — .changes emite quando a lista muda. .first, .last, .toArray() acedem aos itens. Útil para componentes compostos (tabs, accordion, steps).

Boas Práticas
// 1. Componentes pequenos e focados
// 2. Serviços para lógica de negócio
// 3. OnPush + signals (sem mutações)
// 4. takeUntilDestroyed() em subscriptions
// 5. Lazy loading para secções grandes
// 6. @defer para componentes pesados
// 7. track obrigatório em @for
// 8. inject() em vez de constructor
// 9. input()/output() em vez de decorators
// 10. Reactive Forms para forms complexos

Boas práticas Angular: componentes pequenos com OnPush, serviços para lógica, signals para estado, takeUntilDestroyed() para subscriptions, lazy loading e @defer para performance, inject() e signal inputs/outputs para código moderno. Evite mutações e any.

@defer — Lazy Load de Componentes
@defer (on viewport) {
  <app-grafico [dados]="dados" />
} @placeholder {
  <p>A carregar gráfico...</p>
} @loading (after 200ms) {
  <app-spinner />
} @error {
  <p>Erro ao carregar</p>
}

// Triggers:
// on viewport — quando visível
// on idle — quando o browser está livre
// on timer(3s) — após 3 segundos
// on hover — ao passar o rato
// when condicao — quando condição é true

@defer (v17+) faz lazy load de componentes no template. O bloco @placeholder mostra antes, @loading durante o load (com after 200ms para evitar flicker), @error em caso de falha. Triggers: on viewport, on idle, on timer(), on hover, when. Reduz o bundle inicial.

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

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

  initChart() {
    // Fora do Angular zone (sem change detection)
    this.zone.runOutsideAngular(() => {
      this.chart = new Chart(this.canvas, {
        // callbacks frequentes (mousemove, etc.)
        onHover: () => { /* não dispara CD */ }
      });
    });
  }

  atualizar(dados: any) {
    // Volta ao zone para atualizar o template
    this.zone.run(() => {
      this.dados = dados;
    });
  }
}

NgZone controla o change detection. runOutsideAngular() executa código sem disparar CD — ideal para listeners frequentes (scroll, mousemove, WebSocket). run() volta ao zone para atualizar o template. Com signals, a necessidade de NgZone diminui. Use para bibliotecas externas (charts, maps, editors).

Estrutura de Pastas (Feature-Based)
src/app/
├── core/           # singleton services, guards
│   ├── services/
│   └── interceptors/
├── shared/         # componentes/pipes/diretivas reutilizáveis
│   ├── components/
│   ├── pipes/
│   └── directives/
├── features/       # módulos por funcionalidade
│   ├── users/
│   │   ├── users.routes.ts
│   │   ├── users.component.ts
│   │   └── components/
│   └── dashboard/
└── app.routes.ts

Estrutura feature-based: core/ para singletons (serviços, guards, interceptors), shared/ para componentes reutilizáveis, features/ para funcionalidades isoladas com as suas rotas. Cada feature é lazy-loaded. Evita a estrutura plana por tipo (components/, services/) que não escala.

SSR e Hydration
// Criar projeto com 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';

// O HTML é renderizado no servidor
// e hidratado no cliente (sem re-render)

SSR (Server-Side Rendering) renderiza HTML no servidor para melhor SEO e First Contentful Paint. provideClientHydration() ativa hydration — o Angular reutiliza o DOM do servidor em vez de re-renderizar. --ssr no ng new configura tudo. Requer Node.js no servidor.

Testes Unitários
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('deve retornar users', () => {
    const users = service.getUsers();
    expect(users.length).toBe(2);
  });

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

TestBed configura o módulo de testes. TestBed.configureTestingModule() define providers e imports. TestBed.inject() obtém serviços. Use useValue para mocks. ng test executa com Karma/Jasmine. Para componentes, use TestBed.createComponent() e fixture.detectChanges().

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

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

  htmlSeguro: SafeHtml;
  urlSegura: SafeUrl;

  constructor() {
    this.htmlSeguro = this.sanitizer
      .bypassSecurityTrustHtml('<b>HTML confiável</b>');
    this.urlSegura = this.sanitizer
      .bypassSecurityTrustUrl('https://exemplo.com');
  }
}

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

O Angular sanitiza HTML automaticamente para prevenir XSS. Para HTML confiável, use DomSanitizer.bypassSecurityTrustHtml(). Métodos: bypassSecurityTrustHtml, bypassSecurityTrustUrl, bypassSecurityTrustResourceUrl, bypassSecurityTrustScript. Use com extremo cuidado — só para conteúdo 100% confiável.

Animações
import { trigger, state, style,
  transition, animate } from '@angular/animations';

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

// <div [@fade]="estado">Conteúdo</div>

As animações do Angular usam trigger, state e transition. state define estilos por estado. transition define a animação entre estados (=>, <=>). Aplique com [@trigger]="estado". Requer provideAnimations() ou provideAnimationsAsync().

Testes de Componente
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('deve incrementar', () => {
    component.incrementar();
    fixture.detectChanges();
    const el = fixture.nativeElement.querySelector('span');
    expect(el.textContent).toContain('1');
  });
});

ComponentFixture dá acesso ao componente e ao DOM. fixture.componentInstance é a instância. fixture.detectChanges() dispara change detection. fixture.nativeElement acede ao DOM. compileComponents() compila templates externos. Com standalone, use imports em vez de declarations.

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

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

// Sem Zone.js:
// - Change detection só com signals
// - Menos overhead (sem monkey-patching)
// - Melhor performance
// - Requer código 100% signal-based

Zoneless change detection (v18+ experimental) remove o Zone.js. O change detection é disparado apenas por signals — sem monkey-patching de eventos. Requer código 100% signal-based (sem setTimeout, addEventListener manual). Melhor performance e bundle menor. Ative com provideExperimentalZonelessChangeDetection().

Pipes


10 cards
Pipes de Texto
{{ nome | uppercase }}
{{ nome | lowercase }}
{{ nome | titlecase }}
{{ texto | slice:0:100 }}
{{ lista | slice:0:5 }}
{{ objeto | json }}

Pipes de texto transformam strings: uppercase, lowercase, titlecase. O pipe slice funciona com strings e arrays (início:fim). O pipe json serializa objetos — útil para debug no template.

Encadear Pipes
{{ nome | lowercase | titlecase }}
{{ data | date:'dd/MM' | uppercase }}
{{ texto | slice:0:50 | uppercase }}
{{ preco | currency:'EUR':'symbol':'1.2-2' }}
<p>{{ (itens | slice:0:3).length }} de {{ itens.length }}</p>

Pipes podem ser encadeados com | — cada pipe recebe o output do anterior. A ordem importa: lowercase | titlecase é diferente de titlecase | lowercase. Parâmetros são separados por :. Use parênteses para aplicar pipes a sub-expressões.

Pipes com Parâmetros
@Pipe({ name: 'moeda', standalone: true })
export class MoedaPipe implements PipeTransform {
  transform(
    valor: number,
    simbolo = '€',
    decimais = 2,
    posicao: 'antes' | 'depois' = 'depois'
  ): string {
    const formatado = valor.toFixed(decimais);
    return posicao === 'antes'
      ? `${simbolo}${formatado}`
      : `${formatado}${simbolo}`;
  }
}
// {{ 49.9 | moeda:'R$':2:'antes' }} → R$49.90

Pipes aceitam múltiplos parâmetros separados por :. Defina valores default nos parâmetros do transform(). Use union types para restringir valores. O pipe é reutilizável em toda a app — adicione ao array imports dos componentes que o usam.

Pipes de Data e Número
{{ hoje | date:'dd/MM/yyyy' }}
{{ hoje | date:'fullDate' }}
{{ hoje | date:'HH:mm:ss' }}
{{ 3.14159 | number:'1.2-2' }}
{{ 1234567 | number }}
{{ 0.75 | percent }}
{{ 49.99 | currency:'EUR' }}
{{ 49.99 | currency:'BRL':'R$' }}

O pipe date formata datas com padrões (dd/MM/yyyy, fullDate). O pipe number formata com o formato minInt.minDec-maxDec. percent multiplica por 100 e adiciona %. currency formata moedas com símbolo.

KeyValue Pipe
config = { tema: 'escuro', lingua: 'pt', versao: 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;

O pipe keyvalue itera sobre objetos e Maps, retornando pares { key, value }. Por padrão ordena alfabeticamente por chave. Passe uma função de comparação como argumento para ordenação customizada. Útil para renderizar objetos dinâmicos sem conhecer as chaves.

Decimal e 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' }}

O formato minInt.minDec-maxDec controla dígitos: 1.2-4 = mínimo 1 inteiro, 2 a 4 decimais. O terceiro parâmetro do number pipe é o locale (pt-PT usa vírgula decimal e espaço como separador de milhares). Registe locales com registerLocaleData().

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

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

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

O async pipe subscreve um Observable ou Promise e retorna o último valor. Faz unsubscribe automaticamente quando o componente é destruído — previne memory leaks. Com @if (x$ | async; as x), evita múltiplas subscrições e dá acesso ao valor.

i18n
<p i18n="@@saudacao">Olá, mundo!</p>
<img i18n-alt alt="Logótipo da empresa">

<!-- ng extract-i18n -->

<p i18n>
  {contador, plural,
    =0 {Sem itens}
    =1 {Um item}
    other {{{contador}} itens}
  }
</p>

O sistema i18n do Angular usa o atributo i18n para marcar textos para tradução. @@id define um ID customizado. ng extract-i18n gera ficheiros XLIFF. Suporta pluralização e seleção de género. Para runtime translation, use @angular/localize.

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

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

// Uso: {{ texto | truncate:100:' [mais]' }}

Um pipe customizado implementa PipeTransform com o método transform(). O primeiro parâmetro é o valor; os seguintes são argumentos (| truncate:100). Declare como standalone: true e adicione ao array imports do componente que o usa.

Pure vs Impure Pipes
// Pure (default) — só recalcula se o input mudar
@Pipe({ name: 'ordenar', pure: true })

// Impure — recalcula em cada change detection
@Pipe({ name: 'filtrar', pure: false })

// Pure: eficiente, mas não deteta mutações
// Impure: deteta mutações, mas menos performático
// Prefira signals ou imutabilidade

Pipes pure (default) só executam quando a referência do input muda. Pipes impure (pure: false) executam em cada ciclo de change detection. Prefira pipes pure com dados imutáveis. Para filtrar/ordenar arrays, prefira transformar no componente com computed() signals.

Formulários


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

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

Reactive Forms requer importar ReactiveFormsModule no array imports do componente. FormControl representa um campo individual. O valor é acedido com .value e atualizado com .setValue(). Mais testável e escalável que Template-Driven Forms.

Validador Personalizado
import { AbstractControl, ValidationErrors, ValidatorFn }
  from '@angular/forms';

export function proibidoPalavra(palavra: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const tem = control.value?.toLowerCase()
      .includes(palavra.toLowerCase());
    return tem ? { proibidoPalavra: { valor: control.value } } : null;
  };
}

// Uso: nome: ['', [Validators.required, proibidoPalavra('admin')]]

Um validador customizado é uma função que retorna um ValidatorFn. Recebe o AbstractControl e retorna null (válido) ou um objeto ValidationErrors. Para validadores com parâmetros, use uma factory function. Verifique erros com hasError('proibidoPalavra').

setValue vs patchValue
this.form.setValue({
  nome: 'Ana', email: 'ana@mail.com',
  idade: 25, termos: true
});

this.form.patchValue({ nome: 'Ana' });

this.form.get('nome')?.setValue('Bruno');

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

setValue() requer todos os campos do grupo (erro se faltar). patchValue() aceita campos parciais — ideal para atualizar apenas alguns. reset() limpa o form e marca como pristine e untouched. Use patchValue para preencher de uma API.

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

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

Template-Driven Forms usa FormsModule e [(ngModel)]. A validação é declarativa no template (required, minlength). #f="ngForm" dá acesso ao form. Mais simples para formulários pequenos, mas menos testável e escalável que Reactive Forms.

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

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

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

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

FormBuilder simplifica a criação de forms. this.fb.group({}) cria um FormGroup. Cada campo é [valorInicial, [validadores]]. inject(FormBuilder) injeta o serviço. O form.value retorna o objeto com todos os valores. form.valid verifica se todos os campos são válidos.

Validação Cross-Field
export function passwordsIguais(): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const senha = group.get('senha')?.value;
    const confirmar = group.get('confirmar')?.value;
    return senha === confirmar ? null : { passwordsDiferentes: true };
  };
}

form = this.fb.group({
  senha: ['', Validators.required],
  confirmar: ['', Validators.required]
}, { validators: passwordsIguais() });

Validação cross-field compara múltiplos campos. O validador recebe o FormGroup (como AbstractControl). Registe no segundo argumento de fb.group() com { validators: [...] }. O erro fica no grupo: form.hasError('passwordsDiferentes').

Estado do Formulário
form.valid
form.invalid
form.pristine
form.dirty
form.touched
form.untouched
form.pending

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

form.markAllAsTouched();

Cada controlo tem estados: valid/invalid, pristine/dirty, touched/untouched, pending. Use touched para mostrar erros só após interação. markAllAsTouched() força a exibição de erros (ex: ao submeter).

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

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

// form.value.nome → string (não string | null)
// this.form.reset(); // nome = '', não null

FormBuilder.nonNullable (v14+) elimina null dos tipos. form.value.nome é string em vez de string | null. O reset() restaura os valores iniciais em vez de null. Reduz verificações de null e melhora type-safety.

Template do Formulário
<form [formGroup]="form" (ngSubmit)="enviar()">
  <input formControlName="nome"
         [class.erro]="nomeCtrl.invalid && nomeCtrl.touched">

  @if (nomeCtrl.hasError('required')) {
    <span class="erro">Campo obrigatório</span>
  }

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

get nomeCtrl() { return this.form.get('nome')!; }

No template: [formGroup]="form" liga o form, formControlName="nome" liga cada campo. (ngSubmit) previne reload e chama o handler. Use getters para aceder a controlos. Mostre erros com hasError() e condicione com touched (só após interação).

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;
}

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

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

FormArray gere uma lista dinâmica de controlos. Adicione com .push(), remova com .removeAt(). No template, itere com @for e use [formControlName]="$index". Útil para listas de emails, telefones, itens de formulário dinâmicos.

Validação Assíncrona
import { AsyncValidatorFn } from '@angular/forms';

export function emailUnico(service: UserService): AsyncValidatorFn {
  return (control) => {
    return timer(500).pipe(
      switchMap(() => service.verificarEmail(control.value)),
      map(existe => existe ? { emailExiste: true } : null),
      catchError(() => of(null))
    );
  };
}

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

Validação assíncrona usa AsyncValidatorFn — retorna um Observable. O terceiro argumento de fb.control() aceita validadores async. Use timer(500) + switchMap para debounce. O estado pending é true enquanto valida. Ideal para verificar unicidade.

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

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

Validadores integrados: required, email, minLength(n), maxLength(n), min(n), max(n), pattern(regex), requiredTrue (para checkboxes). Combine múltiplos num array. Todos retornam null (válido) ou um objeto de erro.

FormGroup Aninhado
form = this.fb.group({
  nome: ['', Validators.required],
  endereco: this.fb.group({
    rua: ['', Validators.required],
    cidade: ['', Validators.required],
    codigoPostal: ['', Validators.pattern('\\d{4}-\\d{3}')]
  })
});

// Template: <div formGroupName="endereco">
//   <input formControlName="rua">
// </div>

FormGroup aninhados organizam formulários complexos. No template, use formGroupName="endereco" para criar o contexto. Aceda a valores com form.value.endereco.rua ou form.get('endereco.rua'). O form.value retorna a estrutura aninhada completa.

valueChanges
this.form.valueChanges
  .pipe(
    debounceTime(300),
    distinctUntilChanged(),
    takeUntilDestroyed()
  )
  .subscribe(valor => this.pesquisar(valor.pesquisa));

this.form.get('pais')?.valueChanges
  .pipe(takeUntilDestroyed())
  .subscribe(pais => this.atualizarCidades(pais));

valueChanges é um Observable que emite a cada mudança. Combine com debounceTime para pesquisa em tempo real, distinctUntilChanged para evitar emissões duplicadas. Use takeUntilDestroyed() para auto-unsubscribe. Ideal para filtros, autocompletar e efeitos colaterais.

RxJS e Observables


14 cards
Observable — Básico
import { Observable } from 'rxjs';

const dados$ = new Observable<string>(subscriber => {
  subscriber.next('Primeiro');
  subscriber.next('Segundo');
  setTimeout(() => {
    subscriber.next('Terceiro');
    subscriber.complete();
  }, 1000);
});

dados$.subscribe({
  next: valor => console.log(valor),
  error: err => console.error(err),
  complete: () => console.log('Completo')
});

Um Observable é uma stream de dados lazy — só executa quando alguém subscreve. subscriber.next() emite valores, .complete() termina, .error() lança erro. O sufixo $ é convenção para Observables. O subscriber recebe next, error e complete.

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

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

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

// Padrão: expor como Observable (read-only)
private _dados$ = new BehaviorSubject<User[]>([]);
readonly dados$ = this._dados$.asObservable();

atualizar(dados: User[]) { this._dados$.next(dados); }

Subject é um Observable que também pode emitir (.next()). BehaviorSubject tem valor inicial e guarda o último valor (.value). Exponha como asObservable() para read-only. Substituído por signal() na maioria dos casos (v17+).

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

export class ListaComponent {
  dados$ = this.service.dados$.pipe(takeUntilDestroyed());

  private destroyRef = inject(DestroyRef);

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

takeUntilDestroyed() substitui o padrão Subject + takeUntil + ngOnDestroy. No contexto de injeção (constructor, field initializer), não precisa de argumentos. Fora dele, passe DestroyRef. De @angular/rxjs-interop. Previne memory leaks automaticamente.

Padrão: Pesquisa com Debounce
import { toSignal, toObservable } from '@angular/rxjs-interop';

export class PesquisaComponent {
  private http = inject(HttpClient);
  termo = signal('');

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

Padrão de pesquisa: signal → toObservabledebounceTime(300) (espera) → distinctUntilChanged (ignora iguais) → filter (mínimo 2 chars) → switchMap (cancela anterior) → catchError (resiliência). toSignal converte de volta para uso no template.

Operadores de Transformação
import { map, scan, startWith } from 'rxjs';

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

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

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

map transforma cada valor emitido. scan acumula valores (contador, soma). startWith emite um valor inicial antes da stream. Todos retornam um novo Observable — a stream original não é modificada.

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

combineLatest([filtro$, ordenacao$]).pipe(
  map(([filtro, ordenacao]) =>
    this.aplicarFiltro(filtro, ordenacao)
  )
);

zip(
  this.http.get('/api/nomes'),
  this.http.get('/api/idades')
).pipe(
  map(([nomes, idades]) =>
    nomes.map((n, i) => ({ nome: n, idade: idades[i] }))
  )
);

combineLatest emite sempre que qualquer Observable emite (com os últimos valores de todos). Ideal para combinar filtros. zip emparelha emissões na ordem — espera todos emitirem. Use combineLatest para reatividade e zip para sincronização.

fromEvent e 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 periodico$ = timer(1000, 5000);

fromEvent converte eventos DOM em Observable. Combine com throttleTime para performance. interval(ms) emite números incrementais a cada X ms. timer(delay) emite uma vez após delay; timer(delay, periodo) emite periodicamente. Todos precisam de unsubscribe.

RxJS vs Signals
// RxJS: streams, eventos, async complexo
this.busca$.pipe(
  debounceTime(300),
  switchMap(q => this.http.get(q))
);

// Signals: estado síncrono, computado
contador = signal(0);
dobro = computed(() => this.contador() * 2);

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

Use signals para estado síncrono e valores computados. Use RxJS para streams de eventos, debounce, requests HTTP e operações complexas. toObservable() e toSignal() fazem a ponte. A tendência é signals para estado e RxJS para fluxos assíncronos.

Operadores de Filtragem
import { filter, distinctUntilChanged, debounceTime,
  take, first } from 'rxjs';

this.dados$.pipe(filter(d => d.ativo === true));
this.input$.pipe(distinctUntilChanged());
this.busca$.pipe(debounceTime(300));
this.dados$.pipe(take(5));
this.dados$.pipe(first());

filter só deixa passar valores que satisfazem a condição. distinctUntilChanged ignora emissões iguais à anterior. debounceTime(ms) espera silêncio antes de emitir — ideal para pesquisa. take(n) limita a N emissões. first() emite o primeiro e completa.

tap e 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 executa efeitos colaterais sem modificar a stream (logs, atualizar estado, cache). finalize executa quando a stream completa ou erro — ideal para esconder spinners. Nenhum dos dois altera o valor emitido. Use finalize em vez de repetir lógica no next e 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 partilha um Observable entre múltiplos subscribers e faz replay do último valor. bufferSize: 1 guarda o último valor. refCount: true faz unsubscribe da fonte quando não há subscribers. Evita requests HTTP duplicados. Ideal para dados que múltiplos componentes consomem.

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

// switchMap: cancela o request anterior
this.busca$.pipe(switchMap(q => this.http.get(`/api?q=${q}`)));

// mergeMap: executa em paralelo
this.ids$.pipe(mergeMap(id => this.http.get(`/api/${id}`)));

// concatMap: executa em série (ordem)
this.acoes$.pipe(concatMap(acao => this.salvar(acao)));

// exhaustMap: ignora enquanto processa
this.clicks$.pipe(exhaustMap(() => this.http.post('/api', data)));

switchMap: cancela o Observable anterior (pesquisa). mergeMap: executa em paralelo (requests independentes). concatMap: executa em série, mantém ordem (operações sequenciais). exhaustMap: ignora novas emissões enquanto processa (prevenir double-submit).

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

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

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

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

retry(n) re-subscreve em caso de erro. retry({ count, delay }) adiciona delay entre tentativas. catchError permite recuperar com um valor default (of([])) ou re-lançar com throwError. Combine: retry(3) antes de catchError.

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

of(1, 2, 3);
of({ nome: 'Ana' });

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

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

EMPTY;

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

of() cria um Observable que emite valores e completa. from() converte arrays, Promises ou iteráveis. throwError() cria um Observable que emite erro. EMPTY completa imediatamente sem emitir. Úteis como retornos em catchError, mocks e testes.

Routing e Navegação


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

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

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

// Template: <router-outlet />

Defina rotas em app.routes.ts como array de Routes. path é o segmento da URL. :id é um parâmetro dinâmico. ** é o wildcard (404). provideRouter(routes) ativa o routing. <router-outlet /> renderiza o componente da rota ativa.

Guards Funcionais
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 }
  });
};

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

Guards funcionais (CanActivateFn) substituem classes com CanActivate. Retornam true (permitir), false (bloquear) ou UrlTree (redirecionar). Use inject() para serviços. canActivate protege rotas individuais. Registe no array da rota.

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"> cria um outlet nomeado. Rotas com outlet: 'sidebar' renderizam nele. Navegue com { outlets: { primary: [...], sidebar: [...] } }. A URL usa parênteses: /home(sidebar:chat). Permite múltiplas views independentes na mesma página.

RouterLink e Navegação
<a routerLink="/">Home</a>
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">Detalhe</a>

<a [routerLink]="['/users']"
   [queryParams]="{ pagina: 1, filtro: 'ativo' }"
   fragment="topo">Users</a>

<a routerLink="/sobre" routerLinkActive="ativo">Sobre</a>

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

routerLink navega sem reload. [routerLink]="['/users', id]" para parâmetros dinâmicos. [queryParams] adiciona query string. routerLinkActive aplica classe quando a rota está ativa. router.navigate() para navegação via código (ex: após login).

Tipos de Guards
import { CanActivateFn, CanActivateChildFn,
  CanDeactivateFn, CanMatchFn, ResolveFn }
  from '@angular/router';

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

Tipos de guards: canActivate (pode entrar?), canActivateChild (pode entrar nas filhas?), canDeactivate (pode sair? — ex: form não guardado), canMatch (a rota existe? — ex: feature flags), resolve (pré-carregar dados). Todos têm versão funcional.

Route Data e Title
{
  path: 'users',
  component: UsersComponent,
  title: 'Utilizadores',
  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} | Minha App` : 'Minha App';
  }
}

A propriedade title na rota define o título da página automaticamente. data armazena metadados (breadcrumbs, roles). Para títulos dinâmicos, estenda TitleStrategy. Aceda a data com route.data ou route.snapshot.data.

Parâmetros de Rota
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'));

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

ActivatedRoute dá acesso a parâmetros. paramMap é um Observable (reage a mudanças). snapshot.paramMap é o valor atual (não reage). queryParamMap para query params. Use toSignal para converter em signal. Prefira paramMap se a rota pode mudar sem destruir o componente.

CanDeactivate (Form Não Guardado)
import { CanDeactivateFn } from '@angular/router';

export interface FormGuard {
  temMudancasNaoGuardadas(): boolean;
}

export const unsavedGuard: CanDeactivateFn<FormGuard> =
  (component) => {
    if (component.temMudancasNaoGuardadas()) {
      return confirm('Tem mudanças não guardadas. Sair?');
    }
    return true;
  };

// No componente:
temMudancasNaoGuardadas() { return this.form.dirty; }

CanDeactivateFn verifica se o utilizador pode sair da rota. Recebe a instância do componente. Se o form tem mudanças (form.dirty), mostra confirmação. Retorne true (sair), false (ficar) ou Observable<boolean> para diálogo assíncrono.

Router Events e 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('Navegou para:', e.url));

withInMemoryScrolling configura scroll: scrollPositionRestoration: 'top' volta ao topo, anchorScrolling: 'enabled' ativa fragments. Router.events emite eventos de navegação (NavigationStart, NavigationEnd). Filtre com 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 carrega um módulo de rotas sob demanda (lazy). loadComponent carrega um componente standalone. O ficheiro só é descarregado quando o utilizador navega para a rota. Reduz o bundle inicial. Use para secções grandes (admin, settings, relatórios).

Rotas Aninhadas (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">Estatísticas</a>
<router-outlet />

children define rotas aninhadas. O componente pai tem o seu próprio <router-outlet />. routerLink="./" é a rota filha default. A URL fica /dashboard/stats. Use para layouts com sidebar/tabs onde o conteúdo muda mas o wrapper permanece.

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);
  }
}

// Na rota: data: { preload: true }

PreloadAllModules carrega todos os módulos lazy em background após o load inicial. Para controlo fino, implemente PreloadingStrategy com lógica customizada (ex: só rotas com data.preload). Melhora a perceção de velocidade sem aumentar o bundle inicial.

Signals


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

contador = signal(0);
nome = signal('Ana');
items = signal<string[]>([]);
user = signal<User | null>(null);

console.log(this.contador());

this.contador.set(5);
this.contador.update(v => v + 1);
this.items.update(list => [...list, 'novo']);
this.user.update(u => u ? { ...u, nome: 'Bruno' } : u);

signal() cria um valor reativo. Leia chamando como função: contador(). Atualize com .set(valor) ou .update(fn). Para arrays/objetos, crie nova referência (imutabilidade). O Angular atualiza o template automaticamente quando o signal muda.

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

@Component({
  selector: 'app-product',
  template: `<h3>{{ nome() }}</h3><p>{{ precoFormatado() }}</p>`
})
export class ProductComponent {
  nome = input.required<string>();
  preco = input(0);
  moeda = input('EUR');

  precoFormatado = computed(() =>
    `${this.preco().toFixed(2)} ${this.moeda()}`
  );
}

input() cria signal inputs (v17.1+). input.required<T>() é obrigatório. input(default) tem valor default. Leia como função: nome(). Combine com computed() para valores derivados. Substitui @Input() — mais type-safe e funciona com signals.

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

contador = signal(0);
nome = signal('Ana');

resumo = computed(() => {
  const c = this.contador();
  const n = untracked(() => this.nome());
  return `${n}: ${c}`;
});

// Só contador() é rastreado
// Mudar nome() NÃO recalcula resumo

untracked() lê um signal sem o registar como dependência. Dentro de computed ou effect, o signal lido com untracked não dispara recalculo. Útil para ler valores secundários que não devem causar reatividade. Use com cuidado — pode causar inconsistências.

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

preco = signal(100);
quantidade = signal(2);
desconto = signal(0.1);

subtotal = computed(() => this.preco() * this.quantidade());
total = computed(() => this.subtotal() * (1 - this.desconto()));
formatado = computed(() => `${this.total().toFixed(2)} €`);

// No template: {{ total() }} → 180
// Read-only: não pode fazer total.set()

computed() cria um signal derivado de outros signals. Recalcula automaticamente quando as dependências mudam (lazy — só quando lido). É read-only. Pode depender de outros computed. Ideal para totais, filtros, formatações e qualquer valor derivado.

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

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

// Pai: <app-item (removido)="onRemover($event)" />

output() cria signal outputs (v17.1+). Emite com .emit(valor). O pai ouve com (evento)="handler($event)". Substitui @Output() + new EventEmitter(). Mais conciso e type-safe. Combine com input() para componentes totalmente signal-based.

Signal com Arrays e Objetos
items = signal<string[]>([]);
user = signal<User>({ nome: 'Ana', idade: 25 });

// Adicionar (imutável)
this.items.update(list => [...list, 'novo']);

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

// Atualizar objeto
this.user.update(u => ({ ...u, idade: 26 }));

// NÃO funciona (mutação):
// this.items().push('x');  ← não notifica!

Signals com arrays/objetos requerem imutabilidade. .update() com spread ([...list, x]) ou filter cria nova referência. Mutação direta (.push(), .nome = x) não notifica o Angular porque a referência não muda. Sempre crie novos arrays/objetos.

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

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

  constructor() {
    effect(() => {
      const t = this.termo();
      console.log('Pesquisou:', t);
      this.salvarHistorico(t);
    });
  }
}

effect() executa código quando os signals lidos dentro dele mudam. Rastreia dependências automaticamente. Use para efeitos colaterais: logs, analytics, persistência, chamadas API. Não use para derivar estado (use computed). O effect é destruído com o componente.

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

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

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

resultados$ = this.filtro$.pipe(
  debounceTime(300),
  switchMap(f => this.pesquisar(f))
);

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

toSignal() converte Observable em signal (com initialValue). toObservable() converte signal em Observable para usar operadores RxJS. Padrão: signal → toObservable → operadores RxJS → toSignal para o template. De @angular/rxjs-interop.

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

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

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

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

  // Destruir manualmente (opcional)
  parar() { this.effectRef.destroy(); }
}

effect() aceita um callback onCleanup que regista limpeza. A limpeza executa antes do próximo efeito ou quando o effect é destruído. Útil para clearInterval, removeEventListener, cancelar requests. EffectRef.destroy() destrói manualmente.

model() — Signal Bidirecional
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);
}

// Pai: <app-volume [(volume)]="volumeApp" />

model() cria um signal com two-way binding. O filho lê com volume() e atualiza com volume.set(). O pai usa [(volume)]="var". Substitui @Input + @Output + EventEmitter para valores que o filho pode modificar. Requer 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, senha: string) {
    this._loading.set(true);
    this.http.post('/login', { email, senha }).subscribe({
      next: user => {
        this._user.set(user);
        this._loading.set(false);
      }
    });
  }
}

asReadonly() expõe um signal sem .set() / .update(). O padrão: estado privado com signal(), exposto como asReadonly(). Métodos públicos controlam a mutação. Garante que só o serviço modifica o estado — componentes apenas leem.

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

userId = signal(1);

// Recalcula quando userId muda, mas é editável
userName = linkedSignal(() => {
  const id = this.userId();
  return this.getUser(id)?.nome ?? '';
});

// Pode ser modificado manualmente:
this.userName.set('Nome Custom');

// Mas recalcula se userId() mudar
this.userId.set(2); // userName recalcula

linkedSignal() (v19+) é um signal que recalcula quando as dependências mudam (como computed), mas também pode ser modificado manualmente com .set(). Ideal para formulários que preenchem de uma API mas o utilizador pode editar. Combina reatividade com mutabilidade.

Diretivas


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

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

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

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

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

Uma diretiva de atributo usa @Directive com um selector entre parênteses retos ([appHighlight]). Modifica o comportamento de um elemento existente. input() com alias permite passar valores. @HostListener reage a eventos do elemento.

Diretivas 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="Clique aqui" trackLabel="CTA">

hostDirectives (v15+) aplica diretivas a um componente sem herança. As diretivas são compostas no host. inputs expõe inputs da diretiva no componente. Substitui a necessidade de aplicar diretivas manualmente em cada uso. Ideal para comportamentos transversais (tooltip, tracking, acessibilidade).

Diretiva de Debounce
@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)="pesquisar($event)">

A diretiva de debounce usa fromEvent + debounceTime + distinctUntilChanged para emitir valores com atraso. input() com alias permite configurar os milissegundos. output() emite o valor debounced. Útil para pesquisa em tempo real sem sobrecarregar a API.

Diretiva Estrutural
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>

Diretivas estruturais manipulam o DOM com TemplateRef e ViewContainerRef. createEmbeddedView() renderiza o template. $implicit é a variável default do let. O prefixo * no template é açúcar sintático para ng-template. Exemplos: *ngIf, *ngFor.

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

  mostrar() {
    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)
    );
  }

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

Uma diretiva de tooltip usa Overlay do Angular CDK para posicionar elementos flutuantes. host no decorator regista mouseenter/mouseleave. flexibleConnectedTo posiciona relativo ao elemento. TemplatePortal renderiza o template no overlay. dispose() remove ao sair.

Diretiva de Permissão
@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'">Eliminar</button>

A diretiva de permissão é uma diretiva estrutural que mostra/esconde elementos baseado em permissões. effect() reage a mudanças na permissão. viewContainer.clear() remove, createEmbeddedView() renderiza. Use * no template. Essencial para UI baseada em roles/permissões.

Diretiva com 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'">Painel Admin</div>

Uma diretiva estrutural condicional verifica permissões com inject(AuthService). effect() reage a mudanças no signal appIfRole(). viewContainer.clear() remove views anteriores. createEmbeddedView() renderiza se a condição for verdadeira. Padrão para controlo de acesso baseado em roles.

Diretiva de Click Outside
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)="fecharMenu()">

A diretiva ClickOutside ouve cliques no document e verifica se o alvo está fora do elemento com contains(). Se sim, emite clickOutside. Útil para fechar dropdowns, modais e menus. Use output() para o evento. O selector [appClickOutside] aplica como atributo.

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

  // Ou com ngFor antigo:
  // <li *ngFor="let item of items; trackBy: trackById">
  trackById(index: number, item: any): any {
    return item.id;
  }
}

// Com @for (v17+), track é obrigatório:
// @for (item of items; track item.id) { ... }

trackBy ajuda o Angular a identificar itens em listas por uma chave única (ex: id) em vez do índice. Com @for (v17+), track é obrigatório: track item.id. Melhora performance em listas grandes — o Angular só re-renderiza itens que mudaram, não a lista inteira.

Diretiva de Validação
import { Directive, input } from '@angular/core';
import { NG_VALIDATORS, Validator,
  AbstractControl, ValidationErrors } from '@angular/forms';

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

  validate(control: AbstractControl): ValidationErrors | null {
    const tem = control.value?.includes(this.appProibidoPalavra());
    return tem ? { proibido: true } : null;
  }
}

Uma diretiva de validação implementa Validator e regista-se como NG_VALIDATORS com multi: true. O método validate() recebe o AbstractControl. Use input() para o parâmetro. Aplique no template: <input appProibidoPalavra="admin">.

Diretiva de Lazy Load (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/foto.jpg" src="placeholder.jpg">

A diretiva de lazy load usa IntersectionObserver para carregar imagens quando entram no viewport. rootMargin: '200px' pré-carrega 200px antes. observer.disconnect() para de observar após carregar. Substitui bibliotecas de lazy loading para casos simples.

Diretiva de Acessibilidade
@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]="'Fechar modal'" (clicked)="fechar()">

A diretiva de acessibilidade adiciona role, tabindex, aria-label e handlers de teclado (Enter, Space) a elementos não-interativos. Use host no decorator para binding declarativo. input() recebe o label. Essencial para tornar divs/spans acessíveis como botões.