angular

Especialista em Angular moderno (v20+) com conhecimento profundo de Signals, Componentes Standalone, aplicações Zoneless, SSR/Hydration e padrões reativos.

SKILL.md

GitHub

Especialista em Angular

Domine o desenvolvimento moderno de Angular com Signals, Componentes Standalone, aplicações Zoneless, SSR/Hydration e os últimos padrões reativos.

Quando Usar Esta Habilidade

  • Construir novas aplicações Angular (v20+)
  • Implementar padrões reativos baseados em Signals
  • Criar Componentes Standalone e migrar de NgModules
  • Configurar aplicações Angular Zoneless
  • Implementar SSR, pré-renderização e hidratação
  • Otimizar performance do Angular
  • Adotar padrões e melhores práticas modernas do Angular

Não Use Esta Habilidade Quando

  • Migrar de AngularJS (1.x) → use a habilidade angular-migration
  • Trabalhar com apps Angular legados que não podem ser atualizados
  • Problemas gerais de TypeScript → use a habilidade typescript-expert

Instruções

  1. Avalie a versão do Angular e estrutura do projeto
  2. Aplique padrões modernos (Signals, Standalone, Zoneless)
  3. Implemente com tipagem apropriada e reatividade
  4. Valide com build e testes

Segurança

  • Sempre teste mudanças em desenvolvimento antes de produção
  • Migração gradual para apps existentes (evite big-bang refactor)
  • Mantenha compatibilidade retroativa durante transições

Timeline das Versões do Angular

Versão Lançamento Recursos-chave
Angular 20 Q2 2025 Signals estável, Zoneless estável, hidratação incremental
Angular 21 Q4 2025 Signals-first padrão, SSR aprimorado
Angular 22 Q2 2026 Signal Forms, componentes sem seletor

1. Signals: A Nova Primitiva Reativa

Signals são o sistema de reatividade fine-grained do Angular, substituindo a detecção de mudanças baseada em zone.js.

Conceitos Principais

import { signal, computed, effect } from "@angular/core";

// Sinal gravável
const count = signal(0);

// Ler valor
console.log(count()); // 0

// Atualizar valor
count.set(5); // Set direto
count.update((v) => v + 1); // Atualização funcional

// Sinal computado (derivado)
const doubled = computed(() => count() * 2);

// Effect (efeitos colaterais)
effect(() => {
  console.log(`Count alterado para: ${count()}`);
});

Inputs e Outputs Baseados em Signals

import { Component, input, output, model } from "@angular/core";

@Component({
  selector: "app-user-card",
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ name() }}</h3>
      <span>{{ role() }}</span>
      <button (click)="select.emit(id())">Selecionar</button>
    </div>
  `,
})
export class UserCardComponent {
  // Inputs de sinal (somente leitura)
  id = input.required<string>();
  name = input.required<string>();
  role = input<string>("Usuário"); // Com padrão

  // Output
  select = output<string>();

  // Binding bidirecional (model)
  isSelected = model(false);
}

// Uso:
// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />

Consultas de Signal (ViewChild/ContentChild)

import {
  Component,
  viewChild,
  viewChildren,
  contentChild,
} from "@angular/core";

@Component({
  selector: "app-container",
  standalone: true,
  template: `
    <input #searchInput />
    <app-item *ngFor="let item of items()" />
  `,
})
export class ContainerComponent {
  // Consultas baseadas em Signal
  searchInput = viewChild<ElementRef>("searchInput");
  items = viewChildren(ItemComponent);
  projectedContent = contentChild(HeaderDirective);

  focusSearch() {
    this.searchInput()?.nativeElement.focus();
  }
}

Quando Usar Signals vs RxJS

Caso de Uso Signals RxJS
Estado local do componente ✅ Preferido Excessivo
Valores derivados/computados ✅ computed() combineLatest funciona
Efeitos colaterais ✅ effect() operador tap
Requisições HTTP ❌ ✅ HttpClient retorna Observable
Fluxos de eventos ❌ ✅ fromEvent, operadores
Fluxos assíncronos complexos ❌ ✅ switchMap, mergeMap

2. Componentes Standalone

Componentes standalone são auto-contidos e não exigem declarações NgModule.

Criar Componentes Standalone

import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterLink } from "@angular/router";

@Component({
  selector: "app-header",
  standalone: true,
  imports: [CommonModule, RouterLink], // Imports diretos
  template: `
    <header>
      <a routerLink="/">Início</a>
      <a routerLink="/about">Sobre</a>
    </header>
  `,
})
export class HeaderComponent {}

Bootstrap Sem NgModule

// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideHttpClient()],
});

Lazy Loading de Componentes Standalone

// app.routes.ts
import { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "dashboard",
    loadComponent: () =>
      import("./dashboard/dashboard.component").then(
        (m) => m.DashboardComponent,
      ),
  },
  {
    path: "admin",
    loadChildren: () =>
      import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
  },
];

3. Angular Zoneless

Aplicações zoneless não usam zone.js, melhorando performance e debugging.

Habilitando Modo Zoneless

// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

Padrões de Componente Zoneless

import { Component, signal, ChangeDetectionStrategy } from "@angular/core";

@Component({
  selector: "app-counter",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>Count: {{ count() }}</div>
    <button (click)="increment()">+</button>
  `,
})
export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update((v) => v + 1);
    // Sem zone.js necessário - Signal dispara detecção de mudanças
  }
}

Principais Benefícios do Zoneless

  • Performance: Sem patches zone.js em APIs assíncronas
  • Debugging: Stack traces limpos sem wrappers de zone
  • Tamanho do bundle: Menor sem zone.js (~15KB de economia)
  • Interoperabilidade: Melhor com Web Components e micro-frontends

4. Server-Side Rendering & Hidratação

Configuração de SSR com Angular CLI

ng add @angular/ssr

Configuração de Hidratação

// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
  provideClientHydration,
  withEventReplay,
} from "@angular/platform-browser";

export const appConfig: ApplicationConfig = {
  providers: [provideClientHydration(withEventReplay())],
};

Hidratação Incremental (v20+)

import { Component } from "@angular/core";

@Component({
  selector: "app-page",
  standalone: true,
  template: `
    <app-hero />

    @defer (hydrate on viewport) {
      <app-comments />
    }

    @defer (hydrate on interaction) {
      <app-chat-widget />
    }
  `,
})
export class PageComponent {}

Triggers de Hidratação

Trigger Quando Usar
on idle Baixa prioridade, hidratar quando navegador inativo
on viewport Hidratar quando elemento entra na viewport
on interaction Hidratar na primeira interação do usuário
on hover Hidratar quando usuário passa o mouse
on timer(ms) Hidratar após atraso especificado

5. Padrões Modernos de Roteamento

Guards de Rota Funcionais

// auth.guard.ts
import { inject } from "@angular/core";
import { Router, CanActivateFn } from "@angular/router";
import { AuthService } from "./auth.service";

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

  if (auth.isAuthenticated()) {
    return true;
  }

  return router.createUrlTree(["/login"], {
    queryParams: { returnUrl: state.url },
  });
};

// Uso em rotas
export const routes: Routes = [
  {
    path: "dashboard",
    loadComponent: () => import("./dashboard.component"),
    canActivate: [authGuard],
  },
];

Resolvedores de Dados em Nível de Rota

import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';

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

// Em rotas
{
  path: 'user/:id',
  loadComponent: () => import('./user.component'),
  resolve: { user: userResolver }
}

// No componente
export class UserComponent {
  private route = inject(ActivatedRoute);
  user = toSignal(this.route.data.pipe(map(d => d['user'])));
}

6. Padrões de Injeção de Dependência

Função inject() Moderna

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';

@Component({...})
export class UserComponent {
  // inject() moderno - sem construtor necessário
  private http = inject(HttpClient);
  private userService = inject(UserService);

  // Funciona em qualquer contexto de injeção
  users = toSignal(this.userService.getUsers());
}

Tokens de Injeção para Configuração

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

// Definir token
export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL");

// Fornecer na config
bootstrapApplication(AppComponent, {
  providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }],
});

// Injetar no serviço
@Injectable({ providedIn: "root" })
export class ApiService {
  private baseUrl = inject(API_BASE_URL);

  get(endpoint: string) {
    return this.http.get(`${this.baseUrl}/${endpoint}`);
  }
}

7. Composição de Componentes & Reusabilidade

Projeção de Conteúdo (Slots)

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="header">
        <!-- Selecionar por atributo -->
        <ng-content select="[card-header]"></ng-content>
      </div>
      <div class="body">
        <!-- Slot padrão -->
        <ng-content></ng-content>
      </div>
    </div>
  `
})
export class CardComponent {}

// Uso
<app-card>
  <h3 card-header>Título</h3>
  <p>Conteúdo do corpo</p>
</app-card>

Host Directives (Composição)

// Comportamentos reutilizáveis sem herança
@Directive({
  standalone: true,
  selector: '[appTooltip]',
  inputs: ['tooltip'] // Alias de input Signal
})
export class TooltipDirective { ... }

@Component({
  selector: 'app-button',
  standalone: true,
  hostDirectives: [
    {
      directive: TooltipDirective,
      inputs: ['tooltip: title'] // Mapear input
    }
  ],
  template: `<ng-content />`
})
export class ButtonComponent {}

8. Padrões de Gerenciamento de Estado

Serviço de Estado Baseado em Signal

import { Injectable, signal, computed } from "@angular/core";

interface AppState {
  user: User | null;
  theme: "light" | "dark";
  notifications: Notification[];
}

@Injectable({ providedIn: "root" })
export class StateService {
  // Sinais graváveis privados
  private _user = signal<User | null>(null);
  private _theme = signal<"light" | "dark">("light");
  private _notifications = signal<Notification[]>([]);

  // Computados públicos somente leitura
  readonly user = computed(() => this._user());
  readonly theme = computed(() => this._theme());
  readonly notifications = computed(() => this._notifications());
  readonly unreadCount = computed(
    () => this._notifications().filter((n) => !n.read).length,
  );

  // Ações
  setUser(user: User | null) {
    this._user.set(user);
  }

  toggleTheme() {
    this._theme.update((t) => (t === "light" ? "dark" : "light"));
  }

  addNotification(notification: Notification) {
    this._notifications.update((n) => [...n, notification]);
  }
}

Padrão Component Store com Signals

import { Injectable, signal, computed, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";

@Injectable()
export class ProductStore {
  private http = inject(HttpClient);

  // Estado
  private _products = signal<Product[]>([]);
  private _loading = signal(false);
  private _filter = signal("");

  // Seletores
  readonly products = computed(() => this._products());
  readonly loading = computed(() => this._loading());
  readonly filteredProducts = computed(() => {
    const filter = this._filter().toLowerCase();
    return this._products().filter((p) =>
      p.name.toLowerCase().includes(filter),
    );
  });

  // Ações
  loadProducts() {
    this._loading.set(true);
    this.http.get<Product[]>("/api/products").subscribe({
      next: (products) => {
        this._products.set(products);
        this._loading.set(false);
      },
      error: () => this._loading.set(false),
    });
  }

  setFilter(filter: string) {
    this._filter.set(filter);
  }
}

9. Formulários com Signals (Chegando em v22+)

Formulários Reativos Atuais

import { Component, inject } from "@angular/core";
import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-user-form",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="name" placeholder="Nome" />
      <input formControlName="email" type="email" placeholder="Email" />
      <button [disabled]="form.invalid">Enviar</button>
    </form>
  `,
})
export class UserFormComponent {
  private fb = inject(FormBuilder);

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

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

Padrões de Formulário Conscientes de Signal (Preview)

// API futura de Signal Forms (experimental)
import { Component, signal } from '@angular/core';

@Component({...})
export class SignalFormComponent {
  name = signal('');
  email = signal('');

  // Validação computada
  isValid = computed(() =>
    this.name().length > 0 &&
    this.email().includes('@')
  );

  submit() {
    if (this.isValid()) {
      console.log({ name: this.name(), email: this.email() });
    }
  }
}

10. Otimização de Performance

Estratégias de Detecção de Mudanças

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // Verifica apenas quando:
  // 1. Input signal/referência muda
  // 2. Handler de evento é executado
  // 3. Async pipe emite
  // 4. Valor de Signal muda
})

Blocos Defer para Carregamento Preguiçoso

@Component({
  template: `
    <!-- Carregamento imediato -->
    <app-header />

    <!-- Lazy load quando visível -->
    @defer (on viewport) {
      <app-heavy-chart />
    } @placeholder {
      <div class="skeleton" />
    } @loading (minimum 200ms) {
      <app-spinner />
    } @error {
      <p>Falha ao carregar gráfico</p>
    }
  `
})

NgOptimizedImage

import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img
      ngSrc="hero.jpg"
      width="800"
      height="600"
      priority
    />

    <img
      ngSrc="thumbnail.jpg"
      width="200"
      height="150"
      loading="lazy"
      placeholder="blur"
    />
  `
})

11. Testando Angular Moderno

Testando Componentes com Signal

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { CounterComponent } from "./counter.component";

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

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

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

  it("deve incrementar count", () => {
    expect(component.count()).toBe(0);

    component.increment();

    expect(component.count()).toBe(1);
  });

  it("deve atualizar DOM ao mudar signal", () => {
    component.count.set(5);
    fixture.detectChanges();

    const el = fixture.nativeElement.querySelector(".count");
    expect(el.textContent).toContain("5");
  });
});

Testando com Signal Inputs

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentRef } from "@angular/core";
import { UserCardComponent } from "./user-card.component";

describe("UserCardComponent", () => {
  let fixture: ComponentFixture<UserCardComponent>;
  let componentRef: ComponentRef<UserCardComponent>;

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

    fixture = TestBed.createComponent(UserCardComponent);
    componentRef = fixture.componentRef;

    // Definir signal inputs via setInput
    componentRef.setInput("id", "123");
    componentRef.setInput("name", "John Doe");

    fixture.detectChanges();
  });

  it("deve exibir nome do usuário", () => {
    const el = fixture.nativeElement.querySelector("h3");
    expect(el.textContent).toContain("John Doe");
  });
});

Resumo de Melhores Práticas

Padrão ✅ Faça ❌ Não Faça
Estado Use Signals para estado local Abuse de RxJS para estado simples
Componentes Standalone com imports diretos SharedModules inchados
Detecção de Mudanças OnPush + Signals CD padrão em tudo
Lazy Loading @defer e loadComponent Carregar tudo com urgência
DI Função inject() Injeção em construtor (verbosa)
Inputs Função signal input() Decorador @Input() (legado)
Zoneless Habilitar em novos projetos Forçar em apps legados sem testes

Recursos


Troubleshooting Comum

Problema Solução
Signal não atualiza UI Garanta OnPush + chame signal como função count()
Incompatibilidade de hidratação Verifique consistência entre conteúdo servidor/cliente
Dependência circular Use inject() com forwardRef
Zoneless não detecta mudanças Dispare via atualizações de signal, não mutações
Falha de fetch do SSR Use TransferState ou withFetch()