Guías prácticas Angular: integraciones prácticas, gestión de estados y formularios reactivos avanzados
Las guías paso a paso son algunos de los contenidos más útiles para los desarrolladores porque resuelven problemas reales: instalar una biblioteca, configurar correctamente un proyecto, elegir una estrategia de gestión estatal, cree formularios complejos y evite errores difíciles de detectar.
En este artículo veremos un camino práctico para trabajar mejor con Angular moderno: Integración Tailwind CSS, usando Chart.js, animaciones, gestión de estado con Signals, NgRx y Akita, y creación de formularios responsivos avanzados con validaciones sincrónicas y asincrónicas.
1. Requisitos previos del proyecto Angular
Antes de integrar bibliotecas o implementar arquitecturas complejas, es mejor comenzar desde una base limpia. Usaremos un proyecto Angular moderno, preferiblemente con componentes independientes, Reactive Forms y TypeScript.
Creando el proyecto
npm install -g @angular/cli
ng new angular-how-to-guide
cd angular-how-to-guide
ng serve
Si está trabajando en un proyecto existente, al menos asegúrese de tener:
- Angular actualizado a una versión reciente.
- Node.js en una versión compatible con Angular.
- TypeScript configurado correctamente.
- Una estructura clara para componentes, servicios, modelos y tiendas.
Estructura recomendada
src/
app/
core/
services/
guards/
interceptors/
shared/
components/
pipes/
validators/
features/
dashboard/
products/
profile/
state/
signals/
ngrx/
app.config.ts
app.routes.ts
Una estructura ordenada ayuda especialmente cuando el proyecto crece. Separar las funciones principales, compartidas y el estado le permite evitar dependencias confusas y componentes demasiado grandes.
2. Cómo integrar Tailwind CSS en Angular
Tailwind CSS es una biblioteca de utilidades que le permite crear interfaces modernas sin escribir grandes cantidades de CSS personalizado. En Angular es muy útil para paneles de control, paneles de administración, páginas de destino y componentes reutilizables.
Instalación
npm install tailwindcss @tailwindcss/postcss postcss --save-dev
Cree o actualice el archivo de configuración PostCSS.
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
Luego importe Tailwind al archivo de estilos globales, por ejemplo src/styles.css.
@import "tailwindcss";
Ejemplo de componente Angular con 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 {}
Mejores prácticas con Tailwind en Angular
- Evita plantillas enormes llenas de clases repetidas.
- Extrae componentes reutilizables cuando se repite una interfaz de usuario.
- Utilice clases condicionales con
[class.class-name]o[ngClass]. - Defina diseños de tokens consistentes para colores, espaciado y tipografía.
- No uses Tailwind como excusa para ignorar la accesibilidad.
Ejemplo con clases condicionales
<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 es útil para pestañas, botones activos, filtros, elementos seleccionados y estados de validación.
3. Cómo integrar Chart.js en Angular
Chart.js es una biblioteca ampliamente utilizada para gráficos de líneas, barras, radar, anillos y paneles estadísticos. En Angular puedes usarlo directamente o mediante contenedores dedicados. Aquí vemos una integración directa, sencillo y controlable.
Instalación
npm install chart.js
Creación del componente gráfico
ng generate component features/dashboard/revenue-chart --standalone
Código TypeScript del 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();
}
}
¿Por qué destruir el gráfico?
Cuando se elimina un componente Angular de la página, Chart.js puede dejar oyentes u objetos en la memoria.
Llamar a destroy() en ngOnDestroy() evita pérdidas de memoria, especialmente en paneles con muchos gráficos.
Actualizar datos dinámicamente
updateChart(values: number[]): void {
if (!this.chart) {
return;
}
this.chart.data.datasets[0].data = values;
this.chart.update();
}
En un proyecto real, los valores suelen provenir de un servicio HTTP, un almacén global o una señal.
Consejos prácticos
- Utilice componentes separados para cada gráfico complejo.
- No inicialice Chart.js antes de que el lienzo esté disponible.
- Destruir siempre el gráfico en
ngOnDestroy(). - Para SSR o Angular Universal, ejecute Chart.js solo en el navegador.
- Normaliza los datos antes de pasarlos al gráfico.
4. Cómo agregar animaciones en Angular
Las animaciones mejoran la percepción de la interfaz, pero deben utilizarse con prudencia. El objetivo no es "decorar" la página, sino ayudar al usuario a comprender lo que está sucediendo: abrir paneles, aparecer notificaciones, transiciones entre estados y comentarios sobre formularios.
Instalación del paquete de animaciones angulares
npm install @angular/animations
Configuración en app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations()
]
};
Ejemplo de desvanecimiento + animación de diapositivas
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;
}
Cuándo usar una biblioteca externa como GSAP
Angular Animations es suficiente para muchas interfaces de aplicaciones. Una biblioteca como GSAP resulta útil cuando necesitas líneas de tiempo complejas, animaciones coordinadas, páginas de destino muy dinámicas o microinteracciones sofisticadas.
npm install gsap
Regla importante: evite animar propiedades pesadas como width, height o top
cuando puedes usar transform y opacity, que generalmente son más eficientes.
5. Gestión de estados en Angular: Signals, NgRx y Akita
La gestión del estado es uno de los temas más importantes en Angular. Al principio un servicio con algunas variables puede parecer suficiente, pero a medida que la app crece se hace necesario elegir un enfoque más estructurado.
Comparación rápida
| Solución | Cuándo usarlo | Ventajas | Desventajas |
|---|---|---|---|
| Señales | Simple/medio local o estado compartido | Sintaxis moderna, capacidad de respuesta simple, menos repetitivo | Requiere disciplina arquitectónica en grandes proyectos |
| NgRx | Aplicaciones empresariales, flujos complejos, auditabilidad | Patrones predecibles, DevTools, efectos, selectores | Más texto repetitivo y curva de aprendizaje |
| Akita | Tienda más eficiente, tienda de entidad, equipos que quieren menos texto estándar | API pragmática, gestión conveniente de entidades | Ecosistema menos central que NgRx |
Regla general
- Utilice señales para el estado local, el estado de la interfaz de usuario y funciones pequeñas y medianas.
- Utilice NgRx cuando tenga muchas transmisiones asincrónicas, roles, permisos, almacenamiento en caché y depuración avanzada.
- Utiliza Akita si quieres una tienda estructurada pero menos detallada que NgRx.
- No introduzca una tienda global si el problema se puede resolver con un servicio sencillo.
6. Tutorial práctico: tienda con Angular Signals
Las señales permiten representar valores reactivos de forma sencilla. Son particularmente convenientes para crear pequeños almacenes de funciones sin introducir bibliotecas externas.
Creemos una tienda de todo
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 utiliza la tienda
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 = '';
}
}
Ventajas del enfoque de Señales
- El código es legible.
- No hay necesidad de acción, reductor o efecto para casos simples.
- Los valores derivados con
computed()son muy convenientes. - El Estado queda encapsulado en el servicio.
- Es fácil probar los métodos de la tienda.
Límite para recordar
Señales no significa "arquitectura automática". Si el proyecto crece mucho, aún necesitas definir convenciones: denominación, separación de servicios, persistencia, almacenamiento en caché, manejo de errores y comunicación con API.
7. Tutorial Práctico: Gestión de Estados con NgRx
NgRx implementa un patrón inspirado en Redux. Es adecuado cuando se desea un estado predecible, rastreable y fácilmente depurable. Es ampliamente utilizado en aplicaciones empresariales.
Instalación
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Definición del 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
};
Acciones
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 }>()
}
});
Reductor
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
}))
);
Selectores
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
);
Efectos
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.'
})
);
})
);
})
);
});
}
Configuración de la tienda
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 con 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());
}
}
Cuando NgRx es la elección correcta
- Tienes muchas funciones que comparten datos.
- Tienes flujos asincrónicos complejos.
- Necesitas DevTools y depuración de viajes en el tiempo.
- El equipo debe seguir patrones muy claros.
- El Estado debe ser predecible y centralizado.
8. Cuándo usar Akita
Akita es una solución de gestión de estado más pragmática y menos detallada que NgRx. Es útil cuando quieres una tienda estructurada, pero sin escribir muchas acciones y reductores.
Instalación
npm install @datorama/akita
Ejemplo conceptual de tienda
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();
}
}
Servicio para cambiar el 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();
}
}
Elección práctica
Si su equipo ya está familiarizado con NgRx y trabaja en aplicaciones empresariales, NgRx sigue siendo una opción muy sólida. Sin embargo, si desea una tienda que sea más rápida de implementar y menos ceremoniosa, Akita puede ser una buena alternativa. Para aplicaciones nuevas y pequeñas y medianas, considere primero las señales.
9. Formas reactivas avanzadas en Angular
Los formularios reactivos son una de las herramientas más poderosas de Angular. Le permiten crear formularios complejos, validar entradas, administrar estados dinámicos, crear FormArrays, validaciones entre campos y comprobaciones asincrónicas vinculadas a API.
Escenario práctico
Creemos un formulario de perfil con:
- datos de cuenta;
- correo electrónico con validación asincrónica;
- contraseña con validador personalizado;
- confirmar contraseña;
- lista dinámica de habilidades con
FormArray; - borrar mensajes de error en la plantilla.
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);
})
);
};
}
Ejemplo de servicio API
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 con forma reactiva compleja
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());
}
}
Plantilla de formulario HTML
<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>
Errores comunes en formas reactivas
- Poniendo demasiada lógica en la plantilla.
- No utilice
markAllAsTouched()al enviar. - Realice llamadas asincrónicas con cada pulsación de tecla sin
updateOn: 'desenfoque'o antirrebote. - No separe los validadores en archivos dedicados.
- No manejar el estado
pendientede validaciones asincrónicas. - No escriba formularios correctamente en proyectos grandes.
Mejores prácticas
- Utilice formularios reactivos para formularios complejos y dinámicos.
- Utilice formularios basados en plantillas solo para formularios muy simples.
- Extraer validadores personalizados.
- Mostrar mensajes de error solo después de
tocaro enviar. - Desactivar el envío mientras el formulario esté
pendiente. - Normalice la carga útil antes de enviarla al backend.
10. Tienda de prueba y formulario
Un tutorial práctico no está completo sin pruebas. No es necesario probar cada detalle del marco, pero es importante probar su lógica personalizada: almacenes, validadores y transformación de datos.
Probando un validador de contraseñas
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();
});
});
Prueba de tienda con señales
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. Rendimiento y mantenimiento
En Angular, el rendimiento y el mantenimiento están vinculados. Una aplicación rápida pero caótica se vuelve difícil de evolucionar. Por otro lado, una aplicación bien organizada también tiende a ser más fácil de optimizar.
Consejos prácticos
- Dividir funciones en componentes pequeños.
- Utilice carga diferida para páginas y áreas pesadas.
- Evite la lógica compleja dentro de las plantillas.
- Utilice
tracken bucles@for. - Destruye oyentes, gráficos y suscripciones que no se administran automáticamente.
- No pongas todo en la tienda global.
- Utilice valores calculados en lugar de volver a calcular los datos en la plantilla.
- Evite validaciones asincrónicas demasiado agresivas.
12. Lista de verificación final para un proyecto Angular sólido
- ¿Tiene el proyecto una estructura clara?
- ¿Los componentes son pequeños y enfocados?
- ¿Las bibliotecas externas están encapsuladas en componentes o servicios?
- ¿Se ha destruido Chart.js correctamente?
- ¿Se utiliza Tailwind de forma constante?
- ¿El Estado es local cuando es posible y global sólo cuando es necesario?
- ¿Los formularios complejos utilizan formularios reactivos?
- ¿Se prueban los validadores personalizados?
- ¿Los errores del formulario son comprensibles para el usuario?
- ¿Las llamadas asíncronas tienen manejo de errores?
- ¿El código es legible incluso para un nuevo desarrollador?
Preguntas frecuentes
¿Mejores señales o NgRx?
Depende de la complejidad. Signals es ideal para estados simples o medios. NgRx se adapta mejor a aplicaciones grandes con flujos complejos, depuración avanzada y equipos grandes.
¿Puedo usar Signals y NgRx juntos?
Sí. Puede usar NgRx para el estado global y Signals para el estado de la interfaz de usuario local, filtros, paneles, pestaña y valores derivados en componentes.
¿Tailwind CSS es adecuado para aplicaciones empresariales?
Sí, si se usa con convenciones claras. Es importante crear componentes compartidos y no duplicar clases en todas partes sin criterios.
¿Chart.js es suficiente para paneles complejos?
Para muchos paneles, sí. Si necesita gráficos extremadamente avanzados, interacciones personalizadas o grandes cantidades de datos, Podrías considerar bibliotecas más especializadas.
¿Cuándo utilizar formularios reactivos?
Utilice formularios reactivos cuando tenga validaciones complejas, formularios dinámicos, FormArrays, controles mutuamente dependientes o validaciones asincrónicas.
¿Cómo manejar errores en formularios?
Muestre los errores solo después de que se haya tocado el campo o después de enviarlo. Evite mostrar demasiados errores mientras el usuario todavía está escribiendo.
¿Las validaciones asíncronas ralentizan la aplicación?
Pueden ralentizarlo si hacen todos los personajes.
Utilice updateOn: 'blur', rebote y almacenamiento en caché cuando sea necesario.
¿Akita sigue siendo una buena opción?
Puede ser en proyectos que ya lo utilizan o en equipos que prefieren un enfoque más ligero. Para proyectos nuevos, evalúe cuidadosamente Signals y NgRx antes de introducir una biblioteca externa.
¿Es mejor usar bibliotecas contenedoras para Chart.js?
Depende. Un contenedor puede simplificar la integración, pero usarlo directamente ofrece más control. Para casos simples, la integración directa suele ser suficiente.
¿Cómo evitar componentes angulares que son demasiado grandes?
Extraiga lógica en servicios, tiendas o validadores. Divida la interfaz de usuario en componentes más pequeños y utilice entrada/salida o servicios compartidos cuando sea necesario.