Guias How-To de Angular: integrações práticas, gestão do estado e Reactive Forms avançados
Os guias passo a passo estão entre os conteúdos mais úteis para programadores porque resolvem problemas reais: instalar uma biblioteca, configurar corretamente um projeto, escolher uma estratégia de gestão do estado, criar formulários complexos e prevenir bugs difíceis de encontrar.
Neste artigo vamos seguir um percurso prático para trabalhar melhor com o Angular moderno: integração do Tailwind CSS, uso do Chart.js, animações, gestão do estado com Signals, NgRx e Akita, e construção de formulários reativos avançados com validações síncronas e assíncronas.
1. Pré-requisitos do projeto Angular
Antes de integrar bibliotecas ou implementar arquiteturas complexas, convém partir de uma base limpa. Vamos usar um projeto Angular moderno, de preferência com componentes standalone, Reactive Forms e TypeScript.
Criação do projeto
npm install -g @angular/cli
ng new angular-how-to-guide
cd angular-how-to-guide
ng serve
Se estás a trabalhar num projeto existente, garante pelo menos que tens:
- Angular atualizado para uma versão recente.
- Node.js numa versão compatível com o Angular.
- TypeScript configurado corretamente.
- Uma estrutura clara para componentes, serviços, modelos e store.
Estrutura recomendada
src/
app/
core/
services/
guards/
interceptors/
shared/
components/
pipes/
validators/
features/
dashboard/
products/
profile/
state/
signals/
ngrx/
app.config.ts
app.routes.ts
Uma estrutura organizada ajuda sobretudo quando o projeto cresce. Separar core, shared, features e state permite evitar dependências confusas e componentes demasiado grandes.
2. Como integrar o Tailwind CSS no Angular
O Tailwind CSS é uma biblioteca utility-first que permite criar interfaces modernas sem escrever grandes quantidades de CSS personalizado. No Angular é muito útil para dashboards, painéis de administração, landing pages e componentes reutilizáveis.
Instalação
npm install tailwindcss @tailwindcss/postcss postcss --save-dev
Cria ou atualiza o ficheiro de configuração do PostCSS.
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
Depois importa o Tailwind no ficheiro de estilos global, por exemplo src/styles.css.
@import "tailwindcss";
Exemplo de componente Angular com Tailwind
import { Component } from '@angular/core';
@Component({
selector: 'app-pricing-card',
standalone: true,
template: `
<section class="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium uppercase tracking-wide text-indigo-600">Piano Pro</p>
<h2 class="mt-2 text-3xl font-bold text-slate-900">€29/mese</h2>
<p class="mt-3 text-slate-600">
Ideale per team che vogliono creare applicazioni Angular più velocemente.
</p>
<ul class="mt-6 space-y-2 text-sm text-slate-700">
<li>Componenti riutilizzabili</li>
<li>Dashboard responsive</li>
<li>Supporto a form complessi</li>
</ul>
<button class="mt-6 w-full rounded-xl bg-indigo-600 px-4 py-3 font-semibold text-white hover:bg-indigo-700">
Inizia ora
</button>
</section>
`
})
export class PricingCardComponent {}
Boas práticas com Tailwind no Angular
- Evita templates enormes cheios de classes repetidas.
- Extrai componentes reutilizáveis quando uma parte da UI se repete.
- Usa classes condicionais com
[class.nome-classe]ou[ngClass]. - Define design tokens coerentes para cores, espaçamentos e tipografia.
- Não uses o Tailwind como desculpa para ignorar a acessibilidade.
Exemplo com classes condicionais
<button
type="button"
class="rounded-lg px-4 py-2 font-semibold transition"
[class.bg-green-600]="isActive"
[class.text-white]="isActive"
[class.bg-slate-200]="!isActive"
[class.text-slate-700]="!isActive">
Stato elemento
</button>
Esta técnica é útil para separadores, botões ativos, filtros, elementos selecionados e estados de validação.
3. Como integrar o Chart.js no Angular
O Chart.js é uma biblioteca muito usada para gráficos de linhas, barras, radar, doughnut e dashboards estatísticos. No Angular podes usá-la diretamente ou através de wrappers dedicados. Aqui vemos uma integração direta, simples e controlável.
Instalação
npm install chart.js
Criação do componente do gráfico
ng generate component features/dashboard/revenue-chart --standalone
Código TypeScript do componente
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
ViewChild
} from '@angular/core';
import {
Chart,
ChartConfiguration,
registerables
} from 'chart.js';
Chart.register(...registerables);
@Component({
selector: 'app-revenue-chart',
standalone: true,
template: `
<div class="h-80 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-slate-900">Ricavi mensili</h2>
<canvas #canvas></canvas>
</div>
`
})
export class RevenueChartComponent implements AfterViewInit, OnDestroy {
@ViewChild('canvas')
canvas!: ElementRef<HTMLCanvasElement>;
private chart?: Chart;
ngAfterViewInit(): void {
const config: ChartConfiguration = {
type: 'line',
data: {
labels: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu'],
datasets: [
{
label: 'Ricavi',
data: [12000, 15000, 14000, 21000, 26000, 31000],
borderColor: '#4f46e5',
backgroundColor: 'rgba(79, 70, 229, 0.15)',
tension: 0.35,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true
}
},
scales: {
y: {
beginAtZero: true
}
}
}
};
this.chart = new Chart(this.canvas.nativeElement, config);
}
ngOnDestroy(): void {
this.chart?.destroy();
}
}
Porquê destruir o gráfico?
Quando um componente Angular é removido da página, o Chart.js pode deixar listeners ou objetos em memória. Chamar destroy() em ngOnDestroy() evita memory leaks, sobretudo em dashboards com muitos gráficos.
Atualizar os dados dinamicamente
updateChart(values: number[]): void {
if (!this.chart) {
return;
}
this.chart.data.datasets[0].data = values;
this.chart.update();
}
Num projeto real, os valores chegam muitas vezes de um serviço HTTP, de uma store global ou de um signal.
Conselhos práticos
- Usa componentes separados para cada gráfico complexo.
- Não inicializes o Chart.js antes de o canvas estar disponível.
- Destrói sempre o gráfico em
ngOnDestroy(). - Para SSR ou Angular Universal, executa o Chart.js apenas no browser.
- Normaliza os dados antes de os passar ao gráfico.
4. Como adicionar animações no Angular
As animações melhoram a perceção da interface, mas devem ser usadas com critério. O objetivo não é "decorar" a página, mas ajudar o utilizador a perceber o que está a acontecer: abertura de painéis, aparecimento de notificações, transições entre estados e feedback nos formulários.
Instalação do pacote Angular Animations
npm install @angular/animations
Configuração em app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations()
]
};
Exemplo de animação fade + slide
import { Component } from '@angular/core';
import {
animate,
style,
transition,
trigger
} from '@angular/animations';
@Component({
selector: 'app-notification',
standalone: true,
animations: [
trigger('fadeSlide', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateY(12px)'
}),
animate(
'180ms ease-out',
style({
opacity: 1,
transform: 'translateY(0)'
})
)
]),
transition(':leave', [
animate(
'140ms ease-in',
style({
opacity: 0,
transform: 'translateY(12px)'
})
)
])
])
],
template: `
@if (visible) {
<div
@fadeSlide
class="rounded-xl border border-green-200 bg-green-50 p-4 text-green-800">
Operazione completata con successo.
</div>
}
<button
class="mt-4 rounded-lg bg-green-600 px-4 py-2 text-white"
type="button"
(click)="visible = !visible">
Mostra/Nascondi
</button>
`
})
export class NotificationComponent {
visible = true;
}
Quando usar uma biblioteca externa como o GSAP
O Angular Animations é suficiente para muitas interfaces aplicacionais. Uma biblioteca como o GSAP torna-se útil quando são precisas timelines complexas, animações coordenadas, landing pages muito dinâmicas ou micro-interações sofisticadas.
npm install gsap
Regra importante: evita animar propriedades pesadas como width, height ou top quando podes usar transform e opacity, que são geralmente mais eficientes.
5. Gestão do estado no Angular: Signals, NgRx e Akita
A gestão do estado é um dos temas mais importantes no Angular. No início, um serviço com algumas variáveis pode parecer suficiente, mas quando a aplicação cresce é preciso escolher uma abordagem mais estruturada.
Comparação rápida
| Solução | Quando usar | Vantagens | Desvantagens |
|---|---|---|---|
| Signals | Estado local ou partilhado simples/médio | Sintaxe moderna, reatividade simples, menos boilerplate | Exige disciplina de arquitetura em projetos grandes |
| NgRx | Aplicações empresariais, fluxos complexos, auditabilidade | Padrão previsível, DevTools, effects, selectors | Mais boilerplate e curva de aprendizagem |
| Akita | Store mais leve, entity store, equipas que querem menos boilerplate | API pragmática, gestão de entidades cómoda | Ecossistema menos central do que o NgRx |
Regra prática
- Usa Signals para estado local, estado de UI e funcionalidades pequenas a médias.
- Usa NgRx quando tens muitos fluxos assíncronos, papéis, permissões, cache e debugging avançado.
- Usa Akita se queres uma store estruturada mas menos verbosa do que o NgRx.
- Não introduzas uma store global se o problema se resolve com um simples serviço.
6. Tutorial prático: store com Angular Signals
Os Signals permitem representar valores reativos de forma simples. São particularmente práticos para criar pequenas stores de funcionalidade sem introduzir bibliotecas externas.
Vamos criar uma Todo Store
import { Injectable, computed, signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
done: boolean;
}
export type TodoFilter = 'all' | 'active' | 'done';
@Injectable({
providedIn: 'root'
})
export class TodoStore {
private readonly todosState = signal<Todo[]>([]);
readonly filter = signal<TodoFilter>('all');
readonly todos = this.todosState.asReadonly();
readonly activeCount = computed(() => {
return this.todosState().filter(todo => !todo.done).length;
});
readonly completedCount = computed(() => {
return this.todosState().filter(todo => todo.done).length;
});
readonly visibleTodos = computed(() => {
const filter = this.filter();
const todos = this.todosState();
if (filter === 'active') {
return todos.filter(todo => !todo.done);
}
if (filter === 'done') {
return todos.filter(todo => todo.done);
}
return todos;
});
addTodo(title: string): void {
const cleanTitle = title.trim();
if (!cleanTitle) {
return;
}
const todo: Todo = {
id: Date.now(),
title: cleanTitle,
done: false
};
this.todosState.update(current => [todo, ...current]);
}
toggleTodo(id: number): void {
this.todosState.update(current => {
return current.map(todo => {
if (todo.id !== id) {
return todo;
}
return {
...todo,
done: !todo.done
};
});
});
}
removeTodo(id: number): void {
this.todosState.update(current => {
return current.filter(todo => todo.id !== id);
});
}
clearCompleted(): void {
this.todosState.update(current => {
return current.filter(todo => !todo.done);
});
}
}
Componente que usa a store
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TodoStore } from './todo.store';
@Component({
selector: 'app-todos',
standalone: true,
imports: [FormsModule],
template: `
<section class="mx-auto max-w-2xl rounded-2xl bg-white p-6 shadow-sm">
<h2 class="text-2xl font-bold text-slate-900">Todo con Signals</h2>
<div class="mt-4 flex gap-2">
<input
class="flex-1 rounded-lg border border-slate-300 px-3 py-2"
placeholder="Nuova attività"
[(ngModel)]="title">
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="add()">
Aggiungi
</button>
</div>
<div class="mt-4 flex gap-2">
<button type="button" (click)="store.filter.set('all')">Tutti</button>
<button type="button" (click)="store.filter.set('active')">Attivi</button>
<button type="button" (click)="store.filter.set('done')">Completati</button>
</div>
<ul class="mt-6 space-y-2">
@for (todo of store.visibleTodos(); track todo.id) {
<li class="flex items-center justify-between rounded-lg border border-slate-200 p-3">
<label class="flex items-center gap-2">
<input
type="checkbox"
[checked]="todo.done"
(change)="store.toggleTodo(todo.id)">
<span [class.line-through]="todo.done">
{{ todo.title }}
</span>
</label>
<button
type="button"
class="text-red-600"
(click)="store.removeTodo(todo.id)">
Elimina
</button>
</li>
}
</ul>
<p class="mt-4 text-sm text-slate-600">
Attivi: {{ store.activeCount() }} -
Completati: {{ store.completedCount() }}
</p>
</section>
`
})
export class TodosComponent {
readonly store = inject(TodoStore);
title = '';
add(): void {
this.store.addTodo(this.title);
this.title = '';
}
}
Vantagens da abordagem com Signals
- O código é legível.
- Não são precisos actions, reducers ou effects para casos simples.
- Os valores derivados com
computed()são muito práticos. - O estado fica encapsulado no serviço.
- É fácil testar os métodos da store.
Um limite a lembrar
Signals não significa "arquitetura automática". Se o projeto crescer muito, continuas a ter de definir convenções: nomes, separação dos serviços, persistência, cache, tratamento de erros e comunicação com as APIs.
7. Tutorial prático: gestão do estado com NgRx
O NgRx implementa um padrão inspirado no Redux. É indicado quando queres um estado previsível, rastreável e fácil de depurar. É muito usado em aplicações empresariais.
Instalação
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Definição do modelo
export interface Product {
id: number;
name: string;
price: number;
}
export interface ProductsState {
items: Product[];
loading: boolean;
error: string | null;
}
export const initialProductsState: ProductsState = {
items: [],
loading: false,
error: null
};
Actions
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Product } from './product.model';
export const ProductsActions = createActionGroup({
source: 'Products',
events: {
'Load Products': emptyProps(),
'Load Products Success': props<{ products: Product[] }>(),
'Load Products Failure': props<{ error: string }>()
}
});
Reducer
import { createReducer, on } from '@ngrx/store';
import { ProductsActions } from './products.actions';
import { initialProductsState } from './product.model';
export const productsReducer = createReducer(
initialProductsState,
on(ProductsActions.loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
...state,
items: products,
loading: false
})),
on(ProductsActions.loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsState } from './product.model';
export const selectProductsState =
createFeatureSelector<ProductsState>('products');
export const selectProducts =
createSelector(
selectProductsState,
state => state.items
);
export const selectProductsLoading =
createSelector(
selectProductsState,
state => state.loading
);
export const selectProductsError =
createSelector(
selectProductsState,
state => state.error
);
Effects
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { ProductsActions } from './products.actions';
import { ProductsApiService } from './products-api.service';
@Injectable()
export class ProductsEffects {
private readonly actions$ = inject(Actions);
private readonly api = inject(ProductsApiService);
loadProducts$ = createEffect(() => {
return this.actions$.pipe(
ofType(ProductsActions.loadProducts),
switchMap(() => {
return this.api.getProducts().pipe(
map(products => ProductsActions.loadProductsSuccess({ products })),
catchError(() => {
return of(
ProductsActions.loadProductsFailure({
error: 'Errore durante il caricamento dei prodotti.'
})
);
})
);
})
);
});
}
Configuração da store
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsReducer } from './state/products.reducer';
import { ProductsEffects } from './state/products.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({
products: productsReducer
}),
provideEffects([
ProductsEffects
]),
provideStoreDevtools({
maxAge: 25
})
]
};
Componente Angular com NgRx
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { ProductsActions } from './state/products.actions';
import {
selectProducts,
selectProductsError,
selectProductsLoading
} from './state/products.selectors';
@Component({
selector: 'app-products-page',
standalone: true,
template: `
<section class="space-y-4">
<h2 class="text-2xl font-bold">Prodotti</h2>
@if (loading()) {
<p>Caricamento...</p>
}
@if (error()) {
<p class="text-red-600">{{ error() }}</p>
}
<ul class="grid gap-4 md:grid-cols-3">
@for (product of products(); track product.id) {
<li class="rounded-xl border border-slate-200 p-4">
<h3 class="font-semibold">{{ product.name }}</h3>
<p>€ {{ product.price }}</p>
</li>
}
</ul>
</section>
`
})
export class ProductsPageComponent {
private readonly store = inject(Store);
readonly products = this.store.selectSignal(selectProducts);
readonly loading = this.store.selectSignal(selectProductsLoading);
readonly error = this.store.selectSignal(selectProductsError);
constructor() {
this.store.dispatch(ProductsActions.loadProducts());
}
}
Quando o NgRx é a escolha certa
- Tens muitas funcionalidades que partilham dados.
- Tens fluxos assíncronos complexos.
- Precisas de DevTools e de time-travel debugging.
- A equipa tem de seguir padrões muito claros.
- O estado tem de ser previsível e centralizado.
8. Quando usar o Akita
O Akita é uma solução de gestão do estado mais pragmática e menos verbosa do que o NgRx. É útil quando queres uma store estruturada sem escrever muitos actions e reducers.
Instalação
npm install @datorama/akita
Exemplo conceptual de store
import { Injectable } from '@angular/core';
import {
EntityState,
EntityStore,
StoreConfig
} from '@datorama/akita';
export interface CartItem {
id: number;
name: string;
quantity: number;
}
export interface CartState extends EntityState<CartItem, number> {}
@Injectable({
providedIn: 'root'
})
@StoreConfig({
name: 'cart'
})
export class CartStore extends EntityStore<CartState> {
constructor() {
super();
}
}
Serviço para modificar o estado
import { Injectable, inject } from '@angular/core';
import { CartStore, CartItem } from './cart.store';
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly cartStore = inject(CartStore);
addItem(item: CartItem): void {
this.cartStore.add(item);
}
updateQuantity(id: number, quantity: number): void {
this.cartStore.update(id, {
quantity
});
}
removeItem(id: number): void {
this.cartStore.remove(id);
}
clearCart(): void {
this.cartStore.reset();
}
}
Escolha prática
Se a tua equipa já conhece o NgRx e trabalha em aplicações empresariais, o NgRx continua a ser uma escolha muito sólida. Se, pelo contrário, queres uma store mais rápida de implementar e menos cerimoniosa, o Akita pode ser uma boa alternativa. Para aplicações novas e de pequena ou média dimensão, avalia primeiro os Signals.
9. Reactive Forms avançados no Angular
Os Reactive Forms são uma das ferramentas mais poderosas do Angular. Permitem construir formulários complexos, validar input, gerir estados dinâmicos, criar FormArrays, validações entre campos e verificações assíncronas ligadas a APIs.
Cenário prático
Vamos criar um formulário de perfil com:
- dados da conta;
- email com validação assíncrona;
- palavra-passe com validador personalizado;
- confirmação da palavra-passe;
- lista dinâmica de competências com
FormArray; - mensagens de erro claras no template.
Validadores personalizados
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
ValidatorFn
} from '@angular/forms';
import {
catchError,
map,
of,
switchMap,
timer
} from 'rxjs';
import { UsersApiService } from './users-api.service';
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = String(control.value || '');
const hasMinLength = value.length >= 8;
const hasUppercase = /[A-Z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[^A-Za-z0-9]/.test(value);
const valid =
hasMinLength &&
hasUppercase &&
hasNumber &&
hasSpecial;
if (valid) {
return null;
}
return {
weakPassword: {
hasMinLength,
hasUppercase,
hasNumber,
hasSpecial
}
};
};
}
export function passwordsMatchValidator(
passwordKey: string,
confirmPasswordKey: string
): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get(passwordKey)?.value;
const confirmPassword = group.get(confirmPasswordKey)?.value;
if (password === confirmPassword) {
return null;
}
return {
passwordsMismatch: true
};
};
}
export function uniqueEmailValidator(
usersApi: UsersApiService
): AsyncValidatorFn {
return (control: AbstractControl) => {
const email = String(control.value || '').trim();
if (!email) {
return of(null);
}
return timer(400).pipe(
switchMap(() => usersApi.emailExists(email)),
map(exists => {
return exists ? { emailTaken: true } : null;
}),
catchError(() => {
return of(null);
})
);
};
}
Serviço de API de exemplo
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UsersApiService {
private readonly http = inject(HttpClient);
emailExists(email: string): Observable<boolean> {
return this.http.get<boolean>(
`/api/users/email-exists?email=${encodeURIComponent(email)}`
);
}
}
Componente com Reactive Form complexo
import { Component, inject } from '@angular/core';
import {
FormArray,
NonNullableFormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import {
passwordStrengthValidator,
passwordsMatchValidator,
uniqueEmailValidator
} from './profile.validators';
import { UsersApiService } from './users-api.service';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [
ReactiveFormsModule
],
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
private readonly fb = inject(NonNullableFormBuilder);
private readonly usersApi = inject(UsersApiService);
readonly form = this.fb.group({
account: this.fb.group(
{
name: [
'',
[
Validators.required,
Validators.minLength(2)
]
],
email: [
'',
{
validators: [
Validators.required,
Validators.email
],
asyncValidators: [
uniqueEmailValidator(this.usersApi)
],
updateOn: 'blur'
}
],
password: [
'',
[
Validators.required,
passwordStrengthValidator()
]
],
confirmPassword: [
'',
[
Validators.required
]
]
},
{
validators: [
passwordsMatchValidator('password', 'confirmPassword')
]
}
),
profile: this.fb.group({
role: ['frontend'],
bio: ['']
}),
skills: this.fb.array([
this.createSkill()
])
});
get skills(): FormArray {
return this.form.controls.skills;
}
createSkill() {
return this.fb.group({
name: [
'',
[
Validators.required
]
],
years: [
0,
[
Validators.required,
Validators.min(0),
Validators.max(40)
]
]
});
}
addSkill(): void {
this.skills.push(this.createSkill());
}
removeSkill(index: number): void {
if (this.skills.length === 1) {
return;
}
this.skills.removeAt(index);
}
submit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('Payload form:', this.form.getRawValue());
}
}
Template HTML do formulário
<form
class="mx-auto max-w-3xl space-y-8 rounded-2xl bg-white p-6 shadow-sm"
[formGroup]="form"
(ngSubmit)="submit()">
<section formGroupName="account" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Account</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Nome
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="text"
formControlName="name">
@if (form.controls.account.controls.name.touched &&
form.controls.account.controls.name.hasError('required')) {
<p class="mt-1 text-sm text-red-600">
Il nome è obbligatorio.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Email
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="email"
formControlName="email">
@if (form.controls.account.controls.email.pending) {
<p class="mt-1 text-sm text-slate-500">
Verifica email in corso...
</p>
}
@if (form.controls.account.controls.email.hasError('emailTaken')) {
<p class="mt-1 text-sm text-red-600">
Questa email è già registrata.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="password">
@if (form.controls.account.controls.password.hasError('weakPassword')) {
<p class="mt-1 text-sm text-red-600">
La password deve avere almeno 8 caratteri, una maiuscola,
un numero e un carattere speciale.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Conferma password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="confirmPassword">
@if (form.controls.account.hasError('passwordsMismatch')) {
<p class="mt-1 text-sm text-red-600">
Le password non coincidono.
</p>
}
</div>
</section>
<section formGroupName="profile" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Profilo</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Ruolo
</label>
<select
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
formControlName="role">
<option value="frontend">Frontend Developer</option>
<option value="backend">Backend Developer</option>
<option value="fullstack">Full Stack Developer</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Bio
</label>
<textarea
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
rows="4"
formControlName="bio"></textarea>
</div>
</section>
<section class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-slate-900">
Competenze
</h2>
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="addSkill()">
Aggiungi competenza
</button>
</div>
<div formArrayName="skills" class="space-y-3">
@for (skill of skills.controls; track $index) {
<div
class="grid gap-3 rounded-xl border border-slate-200 p-4 md:grid-cols-[1fr_140px_auto]"
[formGroupName]="$index">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="text"
placeholder="Competenza"
formControlName="name">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="number"
placeholder="Anni"
formControlName="years">
<button
type="button"
class="rounded-lg border border-red-300 px-4 py-2 text-red-600"
(click)="removeSkill($index)">
Rimuovi
</button>
</div>
}
</div>
</section>
<button
type="submit"
class="w-full rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white disabled:opacity-50"
[disabled]="form.pending">
Salva profilo
</button>
</form>
Erros comuns nos Reactive Forms
- Pôr demasiada lógica no template.
- Não usar
markAllAsTouched()no submit. - Fazer chamadas assíncronas a cada tecla sem
updateOn: 'blur'ou debounce. - Não separar os validadores em ficheiros dedicados.
- Não gerir o estado
pendingdas validações assíncronas. - Não tipar corretamente os formulários em projetos grandes.
Boas práticas
- Usa Reactive Forms para formulários complexos e dinâmicos.
- Usa template-driven forms só para formulários muito simples.
- Extrai os validadores personalizados.
- Mostra mensagens de erro só depois de
touchedou do submit. - Desativa o submit enquanto o formulário está
pending. - Normaliza o payload antes de o enviar para o backend.
10. Testar stores e formulários
Um tutorial prático não está completo sem testes. Não é preciso testar todos os detalhes do framework, mas é importante verificar a lógica personalizada: stores, validadores e transformação de dados.
Teste de um validador de palavra-passe
import { FormControl } from '@angular/forms';
import { passwordStrengthValidator } from './profile.validators';
describe('passwordStrengthValidator', () => {
it('dovrebbe invalidare una password debole', () => {
const control = new FormControl('abc');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toEqual(
jasmine.objectContaining({
weakPassword: jasmine.any(Object)
})
);
});
it('dovrebbe accettare una password forte', () => {
const control = new FormControl('Angular2026!');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toBeNull();
});
});
Teste da store com Signals
import { TodoStore } from './todo.store';
describe('TodoStore', () => {
it('dovrebbe aggiungere una todo', () => {
const store = new TodoStore();
store.addTodo('Studiare Angular Signals');
expect(store.todos().length).toBe(1);
expect(store.todos()[0].title).toBe('Studiare Angular Signals');
});
it('dovrebbe filtrare le todo completate', () => {
const store = new TodoStore();
store.addTodo('Prima attività');
const id = store.todos()[0].id;
store.toggleTodo(id);
store.filter.set('done');
expect(store.visibleTodos().length).toBe(1);
expect(store.visibleTodos()[0].done).toBeTrue();
});
});
11. Desempenho e manutenção
No Angular, desempenho e manutenção andam juntos. Uma aplicação rápida mas caótica torna-se difícil de evoluir. Uma aplicação bem organizada, pelo contrário, tende também a ser mais fácil de otimizar.
Conselhos práticos
- Divide as funcionalidades em componentes pequenos.
- Usa lazy loading para páginas e áreas pesadas.
- Evita lógica complexa dentro dos templates.
- Usa
tracknos ciclos@for. - Destrói listeners, gráficos e subscrições que não são geridos automaticamente.
- Não ponhas tudo na store global.
- Usa computed values em vez de recalcular dados no template.
- Evita validações assíncronas demasiado agressivas.
12. Checklist final para um projeto Angular sólido
- O projeto tem uma estrutura clara?
- Os componentes são pequenos e focados?
- As bibliotecas externas estão encapsuladas em componentes ou serviços?
- O Chart.js é destruído corretamente?
- O Tailwind é usado com coerência?
- O estado é local sempre que possível e global só quando é preciso?
- Os formulários complexos usam Reactive Forms?
- Os validadores personalizados estão testados?
- Os erros do formulário são compreensíveis para o utilizador?
- As chamadas assíncronas têm tratamento de erros?
- O código é legível também para um novo programador?
FAQ
Melhor Signals ou NgRx?
Depende da complexidade. Signals é ótimo para estado simples ou médio. O NgRx é mais adequado a grandes aplicações com fluxos complexos, debugging avançado e equipas numerosas.
Posso usar Signals e NgRx em conjunto?
Sim. Podes usar o NgRx para o estado global e os Signals para o estado local de UI, filtros, painéis, separadores e valores derivados nos componentes.
O Tailwind CSS é adequado a aplicações empresariais?
Sim, se for usado com convenções claras. É importante criar componentes partilhados e não duplicar classes por todo o lado sem critério.
O Chart.js chega para dashboards complexos?
Para muitos dashboards, sim. Se precisares de gráficos extremamente avançados, interações personalizadas ou grandes quantidades de dados, podes considerar bibliotecas mais especializadas.
Quando usar Reactive Forms?
Usa Reactive Forms quando tens validações complexas, formulários dinâmicos, FormArrays, controlos dependentes entre si ou validações assíncronas.
Como gerir os erros nos formulários?
Mostra os erros só depois de o campo ter sido tocado ou depois do submit. Evita mostrar demasiados erros enquanto o utilizador ainda está a escrever.
As validações assíncronas tornam a aplicação mais lenta?
Podem torná-la mais lenta se forem executadas a cada carácter. Usa updateOn: 'blur', debounce e cache quando for preciso.
O Akita ainda é uma escolha válida?
Pode ser, em projetos que já o usam ou em equipas que preferem uma abordagem mais leve. Para projetos novos, avalia com atenção Signals e NgRx antes de introduzir uma biblioteca externa.
É melhor usar bibliotecas wrapper para o Chart.js?
Depende. Um wrapper pode simplificar a integração, mas o uso direto dá mais controlo. Para casos simples, a integração direta é muitas vezes suficiente.
Como evitar componentes Angular demasiado grandes?
Extrai a lógica para serviços, stores ou validadores. Divide a UI em componentes mais pequenos e usa inputs/outputs ou serviços partilhados quando for preciso.