Explicación de Inyección de dependencias, formularios y validaciones

7. Crear componente de formulario validado

Creamos el componente

ng generate component componentes/form-validado --standalone --skip-tests

Retocamos src/app/componentes/form-validado/form-validado.component.ts.

Más un validador custom que devuelve null si todo va bien o un objeto con el mensaje de error si no.
Angular añade clases CSS como ng-invalid, ng-valid, ng-touched, etc

ng-invalid -> campo inválido

ng-valid -> campo válido

ng-touched -> campo tocado

ng-pristine -> no modificado

import { Component } from '@angular/core';
import {
  ReactiveFormsModule,
  FormGroup,
  FormControl,
  Validators,
  AbstractControl,
  ValidationErrors
} from '@angular/forms';

@Component({
  selector: 'app-form-validado',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <h2>Formulario Reactivo con Validaciones</h2>

    <form [formGroup]="form" (ngSubmit)="onSubmit()">

      <!-- NOMBRE -->
      <div>
        <label>Nombre</label>
        <input type="text" formControlName="nombre">

        @if (nombre.invalid && nombre.touched) {
          @if (nombre.errors?.['required']) {
            <small style="color:red">El nombre es obligatorio.</small>
          }
          @if (nombre.errors?.['pattern']) {
            <small style="color:red">El nombre contiene caracteres no válidos.</small>
          }
        }
      </div>

      <!-- EMAIL -->
      <div>
        <label>Email</label>
        <input type="email" formControlName="email">

        @if (email.invalid && email.touched) {
          @if (email.errors?.['required']) {
            <small style="color:red">El email es obligatorio.</small>
          }
          @if (email.errors?.['email']) {
            <small style="color:red">El email no tiene formato válido.</small>
          }
        }
      </div>

      <!-- EDAD (validador personalizado) -->
      <div>
        <label>Edad</label>
        <input type="number" formControlName="edad">

        @if (edad.invalid && edad.touched) {
          @if (edad.errors?.['required']) {
            <small style="color:red">La edad es obligatoria.</small>
          }
          @if (edad.errors?.['rangoEdad']) {
            <small style="color:red">
              {{ edad.errors['rangoEdad'].message }}
            </small>
          }
        }
      </div>

      <button type="submit" [disabled]="form.invalid">
        Enviar
      </button>
    </form>

    <hr>

    <h3>Estado del control "nombre"</h3>
    <pre>
dirty: {{ nombre.dirty }}
pristine: {{ nombre.pristine }}
touched: {{ nombre.touched }}
untouched: {{ nombre.untouched }}
valid: {{ nombre.valid }}
invalid: {{ nombre.invalid }}
    </pre>
  `
})
export class FormValidadoComponent {
  form = new FormGroup({
    nombre: new FormControl('', [
      Validators.required,
      Validators.pattern('[\\w\\-\\s\\/]+')
    ]),
    email: new FormControl('', [
      Validators.required,
      Validators.email
    ]),
    edad: new FormControl('', [
      Validators.required,
      this.rangoEdadValidator(18, 65)
    ])
  });

  get nombre() { return this.form.controls.nombre; }
  get email() { return this.form.controls.email; }
  get edad() { return this.form.controls.edad; }

  // Validador personalizado (igual idea que en el PDF)
  rangoEdadValidator(min: number, max: number) {
    return (control: AbstractControl): ValidationErrors | null => {
      const raw = control.value;
      if (raw === null || raw === '') return null; // lo gestiona required
      const edad = Number(raw);

      if (Number.isNaN(edad)) {
        return { rangoEdad: { message: 'La edad debe ser un número' } };
      }

      if (edad >= min && edad <= max) {
        return null;
      } else {
        return { rangoEdad: { message: `La edad debe estar entre ${min} y ${max}` } };
      }
    };
  }

  onSubmit() {
    if (this.form.valid) {
      console.log('Formulario válido:', this.form.value);
    } else {
      console.log('Formulario inválido');
    }
  }
}