<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Guías prácticas de Angular: Tailwind CSS, chart.js, Signals, ngrx y formas reactivas avanzadas

Guías prácticas de Angular: Tailwind CSS, Chart.js, Signals, NgRx y formas reactivas avanzadas

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 pendiente de 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 tocar o 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 track en 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.

Conclusión

Angular ofrece herramientas poderosas para crear aplicaciones modernas, pero la diferencia es cómo se combinan. Tailwind CSS simplifica la interfaz de usuario, Chart.js le permite crear paneles de control efectivos, las animaciones mejoran la experiencia, Las señales facilitan la reactividad, NgRx estructura el estado empresarial y Reactive Forms maneja formularios complejos con fuerza.

La mejor opción no es utilizar siempre la tecnología más avanzada, sino elegir la herramienta que mejor se adapte al problema. Comience de manera simple, agregue complejidad solo cuando sea necesario y mantenga el código legible.

💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!