Cheatsheet Angular: Guia Completo do Básico ao Avançado
Referência definitiva com tudo sobre Angular moderno: CLI, componentes standalone, signals, templates, diretivas, pipes, services, roteamento, formulários, HTTP, SSR e boas práticas.
Angular CLI
npm install -g @angular/cli
Instala o Angular CLI globalmente.
ng new meu-projeto --style=scss --ssr
Cria um novo projeto com SCSS e Server-Side Rendering.
ng serve
Inicia o servidor de desenvolvimento com hot reload na porta 4200.
ng serve --port 3000 --open
Inicia na porta 3000 e abre o navegador automaticamente.
ng build
Compila o projeto para produção com otimizações.
ng generate component components/header --standalone --style=scss
Gera um componente standalone com SCSS.
ng generate service services/auth
Gera um service injetável.
ng generate directive directives/highlight
Gera uma diretiva customizada.
ng generate pipe pipes/currency-br
Gera um pipe customizado.
ng generate guard guards/auth --implements CanActivate
Gera um guard de rota.
ng test
Executa os testes unitários.
ng update @angular/core @angular/cli
Atualiza o Angular para a versão mais recente.
Componentes Standalone
Desde o Angular 17, standalone é o padrão. Sem NgModules:
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class=\"card\">
<ng-content />
</div>
`,
styles: [`
.card {
padding: 24px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
}
`]
})
export class Card {}
Importando outros componentes diretamente:
import { Component } from '@angular/core';
import { Card } from './card';
import { Button } from './button';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [Card, Button],
template: `
<app-card>
<h2>Dashboard</h2>
<app-button label=\"Ação\" />
</app-card>
`,
})
export class Dashboard {}
Signals
O sistema reativo moderno do Angular para gerenciar estado:
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
count(); // Ler: 0
count.set(5); // Setar valor absoluto
count.update(v => v + 1); // Atualizar baseado no valor anterior
Signals computados derivam valores reativamente:
const firstName = signal('Ivan');
const lastName = signal('Reis');
const fullName = computed(() => `${firstName()} ${lastName()}`);
fullName(); // 'Ivan Reis'
Effects executam side effects quando signals mudam:
effect(() => {
console.log(`Nome atualizado: ${fullName()}`);
});
Signal Inputs
Substituto moderno do @Input():
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class=\"user-card\">
<h3>{{ displayName() }}</h3>
<span>{{ role() }}</span>
</div>
`,
})
export class UserCard {
name = input.required<string>();
role = input<string>('user');
displayName = computed(() => this.name().toUpperCase());
}
Usando no template pai:
<app-user-card [name]=\"'Ivan'\" [role]=\"'admin'\" />
<app-user-card name=\"Ivan\" /> <!-- string literal -->
Signal Outputs
Substituto moderno do @Output():
import { Component, output } from '@angular/core';
@Component({
selector: 'app-search',
standalone: true,
template: `
<input
type=\"text\"
(input)=\"onSearch($event)\"
placeholder=\"Buscar...\"
/>
`,
})
export class Search {
searchChange = output<string>();
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searchChange.emit(value);
}
}
Ouvindo no pai:
<app-search (searchChange)=\"onSearchChange($event)\" />
model() - Two-Way Binding com Signals
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `
<button (click)=\"checked.set(!checked())\">
{{ checked() ? 'Ativo' : 'Inativo' }}
</button>
`,
})
export class Toggle {
checked = model(false);
}
Usando two-way binding:
<app-toggle [(checked)]=\"isActive\" />
viewChild e contentChild com Signals
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-canvas',
standalone: true,
template: `<canvas #myCanvas width=\"800\" height=\"600\"></canvas>`,
})
export class CanvasComponent {
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('myCanvas');
constructor() {
afterNextRender(() => {
const ctx = this.canvas().nativeElement.getContext('2d');
});
}
}
Para múltiplos elementos:
import { viewChildren } from '@angular/core';
items = viewChildren<ElementRef>('itemRef');
Template Syntax
Interpolação e property binding:
<h1>{{ title }}</h1>
<img [src]=\"imageUrl\" [alt]=\"imageAlt\" />
<button [disabled]=\"isLoading()\">Enviar</button>
<div [class.active]=\"isActive()\">Menu</div>
<div [style.opacity]=\"isVisible() ? 1 : 0\">Conteúdo</div>
Event binding:
<button (click)=\"handleClick()\">Clique</button>
<input (keyup.enter)=\"submit()\" />
<form (ngSubmit)=\"onSubmit()\">...</form>
<div (mouseenter)=\"onHover()\" (mouseleave)=\"onLeave()\">Hover</div>
Template reference variables:
<input #emailInput type=\"email\" />
<button (click)=\"send(emailInput.value)\">Enviar</button>
Control Flow (@if, @for, @switch)
Sintaxe moderna que substitui *ngIf, *ngFor e ngSwitch:
@if (user(); as u) {
<h1>Bem-vindo, {{ u.name }}</h1>
} @else if (isLoading()) {
<p>Carregando...</p>
} @else {
<p>Faça login para continuar</p>
}
Loop com @for (requer track):
@for (item of items(); track item.id) {
<div class=\"card\">{{ item.name }}</div>
} @empty {
<p>Nenhum item encontrado</p>
}
Switch:
@switch (status()) {
@case ('active') {
<span class=\"badge-active\">Ativo</span>
}
@case ('inactive') {
<span class=\"badge-inactive\">Inativo</span>
}
@default {
<span>Desconhecido</span>
}
}
@defer - Lazy Loading de Templates
Carregamento sob demanda de partes do template:
@defer (on viewport) {
<app-comments [postId]=\"post().id\" />
} @placeholder {
<p>Scroll para ver comentários...</p>
} @loading (minimum 300ms) {
<app-skeleton />
} @error {
<p>Erro ao carregar comentários</p>
}
Triggers disponíveis:
@defer (on idle) { ... } <!-- Quando o browser estiver idle -->
@defer (on viewport) { ... } <!-- Quando entrar no viewport -->
@defer (on interaction) { ... } <!-- No primeiro click/focus -->
@defer (on hover) { ... } <!-- No hover -->
@defer (on timer(3s)) { ... } <!-- Após 3 segundos -->
@defer (when condition()) { ... } <!-- Quando condição for true -->
Diretivas Built-in
ngClass e ngStyle:
<div [ngClass]="{'active': isActive(), 'disabled': isDisabled()}">...</div>
<div [ngClass]=\"dynamicClasses()\">...</div>
<div [ngStyle]=\"{'color': textColor(), 'font-size': fontSize() + 'px'}\">...</div>
ngTemplateOutlet:
<ng-template #loading>
<div class=\"spinner\">Carregando...</div>
</ng-template>
<ng-container *ngTemplateOutlet=\"loading\"></ng-container>
Diretivas Customizadas
import { Directive, ElementRef, inject, input, effect } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class Highlight {
private readonly el = inject(ElementRef);
color = input<string>('yellow', { alias: 'appHighlight' });
constructor() {
effect(() => {
this.el.nativeElement.style.backgroundColor = this.color();
});
}
}
Usando:
<p [appHighlight]=\"'#43a3be'\">Texto destacado</p>
<p appHighlight>Texto com highlight padrão amarelo</p>
Pipes
Pipes built-in:
{{ nome | uppercase }} <!-- IVAN REIS -->
{{ nome | lowercase }} <!-- ivan reis -->
{{ nome | titlecase }} <!-- Ivan Reis -->
{{ preco | currency:'BRL' }} <!-- R$1.500,00 -->
{{ data | date:'dd/MM/yyyy' }} <!-- 16/02/2026 -->
{{ data | date:'fullDate':'':'pt' }} <!-- domingo, 16 de fevereiro de 2026 -->
{{ valor | percent }} <!-- 75% -->
{{ objeto | json }} <!-- JSON formatado -->
{{ lista | slice:0:5 }} <!-- Primeiros 5 itens -->
Pipe customizado:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'readTime',
standalone: true,
})
export class ReadTimePipe implements PipeTransform {
transform(text: string): string {
const wordsPerMinute = 200;
const wordCount = text.split(/\\s+/).length;
const minutes = Math.ceil(wordCount / wordsPerMinute);
return `${minutes} min de leitura`;
}
}
Usando:
<span>{{ post.content | readTime }}</span>
Services e Injeção de Dependência
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly total = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
readonly count = computed(() =>
this._items().reduce((sum, item) => sum + item.qty, 0)
);
addItem(product: Product) {
this._items.update(items => {
const existing = items.find(i => i.id === product.id);
if (existing) {
return items.map(i =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i
);
}
return [...items, { ...product, qty: 1 }];
});
}
removeItem(id: string) {
this._items.update(items => items.filter(i => i.id !== id));
}
clear() {
this._items.set([]);
}
}
Injetando em componentes:
import { Component, inject } from '@angular/core';
import { CartService } from '../services/cart.service';
@Component({
selector: 'app-cart',
standalone: true,
template: `
<div class=\"cart\">
<h2>Carrinho ({{ cart.count() }})</h2>
@for (item of cart.items(); track item.id) {
<div>{{ item.name }} - {{ item.qty }}x</div>
}
<p>Total: R$ {{ cart.total() }}</p>
</div>
`,
})
export class Cart {
protected readonly cart = inject(CartService);
}
Roteamento
Configuração de rotas com lazy loading:
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', loadComponent: () => import('./pages/home').then(m => m.Home) },
{ path: 'blog', loadComponent: () => import('./pages/blog/blog-list').then(m => m.BlogList) },
{ path: 'blog/:slug', loadComponent: () => import('./pages/blog/blog-post').then(m => m.BlogPost) },
{
path: 'dashboard',
loadComponent: () => import('./pages/dashboard/layout').then(m => m.DashboardLayout),
canActivate: [authGuard],
children: [
{ path: '', loadComponent: () => import('./pages/dashboard/home').then(m => m.DashboardHome) },
{ path: 'users', loadComponent: () => import('./pages/dashboard/users').then(m => m.UsersList) },
]
},
{ path: '**', redirectTo: '' }
];
Configuração do provider:
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding(),
withViewTransitions(),
),
]
};
Navegação no template:
<a routerLink=\"/blog\">Blog</a>
<a [routerLink]=\"['/blog', post.slug]\">{{ post.title }}</a>
<a routerLink=\"/blog\" routerLinkActive=\"active\">Blog</a>
Navegação programática:
import { Router } from '@angular/router';
const router = inject(Router);
router.navigate(['/blog', slug]);
router.navigate(['/'], { fragment: 'contato' });
router.navigate(['/blog'], { queryParams: { page: 2 } });
Lendo parâmetros de rota com signals:
import { Component, input } from '@angular/core';
@Component({ ... })
export class BlogPost {
slug = input.required<string>();
}
Ou via ActivatedRoute:
import { ActivatedRoute } from '@angular/router';
const route = inject(ActivatedRoute);
const slug = route.snapshot.paramMap.get('slug');
Guards Funcionais
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login']);
};
Guard com verificação de role:
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
return () => {
const auth = inject(AuthService);
return allowedRoles.includes(auth.userRole());
};
};
{ path: 'admin', canActivate: [roleGuard(['admin'])] }
Interceptors Funcionais
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(cloned);
}
return next(req);
};
Registrando:
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor])
)
HttpClient
Configuração:
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch()),
]
};
Service com HttpClient:
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api';
getAll<T>(endpoint: string, params?: Record<string, string>): Observable<T[]> {
let httpParams = new HttpParams();
if (params) {
Object.entries(params).forEach(([key, value]) => {
httpParams = httpParams.set(key, value);
});
}
return this.http.get<T[]>(`${this.baseUrl}/${endpoint}`, { params: httpParams });
}
getOne<T>(endpoint: string, id: string): Observable<T> {
return this.http.get<T>(`${this.baseUrl}/${endpoint}/${id}`);
}
create<T>(endpoint: string, body: Partial<T>): Observable<T> {
return this.http.post<T>(`${this.baseUrl}/${endpoint}`, body);
}
update<T>(endpoint: string, id: string, body: Partial<T>): Observable<T> {
return this.http.patch<T>(`${this.baseUrl}/${endpoint}/${id}`, body);
}
delete(endpoint: string, id: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${endpoint}/${id}`);
}
}
Reactive Forms
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-register',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">
<input formControlName=\"name\" placeholder=\"Nome\" />
@if (form.get('name')?.hasError('required') && form.get('name')?.touched) {
<span class=\"error\">Nome obrigatório</span>
}
<input formControlName=\"email\" type=\"email\" placeholder=\"E-mail\" />
@if (form.get('email')?.hasError('email') && form.get('email')?.touched) {
<span class=\"error\">E-mail inválido</span>
}
<input formControlName=\"password\" type=\"password\" placeholder=\"Senha\" />
@if (form.get('password')?.hasError('minlength') && form.get('password')?.touched) {
<span class=\"error\">Mínimo 8 caracteres</span>
}
<button type=\"submit\" [disabled]=\"form.invalid\">Cadastrar</button>
</form>
`,
})
export class Register {
private readonly fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.getRawValue());
}
}
}
Validador customizado:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function matchFieldsValidator(field1: string, field2: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value1 = control.get(field1)?.value;
const value2 = control.get(field2)?.value;
return value1 === value2 ? null : { fieldsMismatch: true };
};
}
this.fb.group({
password: ['', Validators.required],
confirmPassword: ['', Validators.required],
}, { validators: matchFieldsValidator('password', 'confirmPassword') });
Lifecycle Hooks
import {
Component,
OnInit,
OnDestroy,
AfterViewInit,
OnChanges,
SimpleChanges,
afterNextRender,
afterRender,
} from '@angular/core';
@Component({ ... })
export class MyComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges {
ngOnChanges(changes: SimpleChanges) {
// Executado quando inputs mudam (antes de OnInit na primeira vez)
}
ngOnInit() {
// Executado uma vez após a criação do componente
// Ideal para buscar dados iniciais
}
ngAfterViewInit() {
// Executado após a view e child views serem inicializadas
// Ideal para manipulação de DOM
}
ngOnDestroy() {
// Executado antes da destruição do componente
// Ideal para limpar subscriptions e event listeners
}
constructor() {
afterNextRender(() => {
// Executado uma vez após o próximo render (substitui AfterViewInit em muitos casos)
// Seguro para acessar DOM e APIs do browser
});
afterRender(() => {
// Executado após cada ciclo de render
});
}
}
DestroyRef e takeUntilDestroyed
Gerenciamento automático de subscriptions:
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ ... })
export class Polling {
private readonly destroyRef = inject(DestroyRef);
constructor() {
interval(5000).pipe(
takeUntilDestroyed()
).subscribe(() => {
console.log('Polling a cada 5s - cancela automaticamente no destroy');
});
}
startLater() {
interval(1000).pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
}
Content Projection
Projeção simples:
@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class=\"overlay\">
<div class=\"modal\">
<header><ng-content select=\"[modal-header]\" /></header>
<main><ng-content /></main>
<footer><ng-content select=\"[modal-footer]\" /></footer>
</div>
</div>
`,
})
export class Modal {}
Usando:
<app-modal>
<h2 modal-header>Confirmar Ação</h2>
<p>Deseja realmente excluir este item?</p>
<div modal-footer>
<button (click)=\"cancel()\">Cancelar</button>
<button (click)=\"confirm()\">Confirmar</button>
</div>
</app-modal>
SSR - Server-Side Rendering
Configuração do provedor de hydration:
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
]
};
Usando TransferState para evitar requisições duplicadas:
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { TransferState, makeStateKey } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class DataService {
private readonly transferState = inject(TransferState);
private readonly platformId = inject(PLATFORM_ID);
getData(key: string) {
const stateKey = makeStateKey<any>(key);
if (isPlatformBrowser(this.platformId)) {
const cached = this.transferState.get(stateKey, null);
if (cached) {
this.transferState.remove(stateKey);
return of(cached);
}
}
return this.http.get(`/api/${key}`).pipe(
tap(data => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(stateKey, data);
}
})
);
}
}
Prerender de rotas parametrizadas:
import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'blog/:slug',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const fs = await import('node:fs/promises');
const data = JSON.parse(await fs.readFile('public/data/posts.json', 'utf-8'));
return data.posts.map((p: { slug: string }) => ({ slug: p.slug }));
},
fallback: PrerenderFallback.Server
},
{ path: '**', renderMode: RenderMode.Prerender }
];
Animations
import { trigger, transition, style, animate, query, stagger } from '@angular/animations';
@Component({
selector: 'app-list',
standalone: true,
animations: [
trigger('listAnimation', [
transition('* => *', [
query(':enter', [
style({ opacity: 0, transform: 'translateY(20px)' }),
stagger(50, [
animate('300ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
])
], { optional: true }),
])
]),
trigger('fadeIn', [
transition(':enter', [
style({ opacity: 0 }),
animate('200ms ease-in', style({ opacity: 1 }))
]),
transition(':leave', [
animate('200ms ease-out', style({ opacity: 0 }))
])
])
],
template: `
<div [@listAnimation]=\"items().length\">
@for (item of items(); track item.id) {
<div [@fadeIn]>{{ item.name }}</div>
}
</div>
`,
})
export class AnimatedList {}
Configuração do provider:
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
providers: [provideAnimationsAsync()]
Boas Práticas
- Standalone: Use componentes standalone sempre. NgModules são legado
- Signals: Prefira signals sobre BehaviorSubject para estado local
- inject(): Use a função
inject()ao invés de constructor injection - Control Flow: Use
@if,@for,@switchao invés de*ngIf,*ngFor - Lazy Loading: Carregue rotas com
loadComponentpara reduzir o bundle inicial - OnPush: Use
changeDetection: ChangeDetectionStrategy.OnPushcom signals - DestroyRef: Prefira
takeUntilDestroyedao invés de gerenciar subscriptions manualmente - Typed Forms: Use
FormBuildercom generics para formulários tipados - Functional Guards/Interceptors: Prefira funções ao invés de classes
- afterNextRender: Use ao invés de
AfterViewInitpara acesso ao DOM em componentes com SSR



