Cheatsheet Angular
Framework frontend da Google baseado em TypeScript
Angular
Setup e CLI
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
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 nativaViewEncapsulation.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
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
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
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
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 mudaChangeDetectionStrategy.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-basedZoneless 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
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.90Pipes 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 imutabilidadePipes 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
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 nullFormBuilder.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
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 → toObservable → debounceTime(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
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
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 resumountracked() 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 recalculalinkedSignal() (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
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.