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, @switch ao invés de *ngIf, *ngFor
  • Lazy Loading: Carregue rotas com loadComponent para reduzir o bundle inicial
  • OnPush: Use changeDetection: ChangeDetectionStrategy.OnPush com signals
  • DestroyRef: Prefira takeUntilDestroyed ao invés de gerenciar subscriptions manualmente
  • Typed Forms: Use FormBuilder com generics para formulários tipados
  • Functional Guards/Interceptors: Prefira funções ao invés de classes
  • afterNextRender: Use ao invés de AfterViewInit para acesso ao DOM em componentes com SSR