Template Driven forms con ngModel

Intermedio
Angular
Angular
Actualizado: 24/09/2025

Template-driven forms

Los formularios template-driven representan la forma más sencilla y directa de trabajar con formularios en Angular. Este enfoque utiliza directivas en el template HTML para gestionar el estado y comportamiento del formulario, permitiendo una experiencia de desarrollo similar a los formularios HTML tradicionales pero con toda la funcionalidad reactiva de Angular.

¿Qué son los formularios template-driven?

Los formularios template-driven se basan en el uso de directivas que se aplican directamente en el HTML del template. Angular se encarga automáticamente de crear y gestionar el modelo del formulario en segundo plano, sin que necesitemos definir explícitamente la estructura en el componente TypeScript.

Este enfoque es especialmente útil para formularios simples a medianos, donde la lógica de validación no es extremadamente compleja y preferimos mantener la configuración cerca del template visual.

Configuración de FormsModule

Para utilizar formularios template-driven, necesitamos importar FormsModule en nuestro componente standalone:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  template: `
    <form>
      <input type="text" name="username" placeholder="Nombre de usuario">
      <button type="submit">Enviar</button>
    </form>
  `,
  imports: [FormsModule]
})
export class UserFormComponent {
  // Lógica del componente
}

El FormsModule proporciona todas las directivas necesarias para trabajar con formularios template-driven, incluyendo ngModel, ngForm, y las funcionalidades de validación básicas.

La directiva ngForm automática

Una característica fundamental de los formularios template-driven es que Angular aplica automáticamente la directiva ngForm a cualquier elemento <form> en el template. Esto significa que no necesitamos declararla explícitamente:

@Component({
  template: `
    <!-- Angular aplica ngForm automáticamente -->
    <form>
      <input type="email" name="email" placeholder="Email">
      <input type="password" name="password" placeholder="Contraseña">
      <button type="submit">Iniciar sesión</button>
    </form>
  `,
  imports: [FormsModule]
})
export class LoginFormComponent { }

La directiva ngForm se encarga de:

  • Crear el modelo del formulario automáticamente
  • Rastrear el estado de validación y modificaciones
  • Gestionar el envío del formulario
  • Coordinar todos los controles dentro del formulario

Variables de plantilla para acceso al formulario

Podemos acceder al estado y propiedades del formulario utilizando variables de plantilla. Esto nos permite interactuar con el formulario directamente desde el template:

@Component({
  template: `
    <form #contactForm="ngForm">
      <input type="text" name="name" placeholder="Nombre">
      <input type="email" name="email" placeholder="Email">
      <button type="submit">Contactar</button>
      
      <!-- Información de depuración -->
      <div>
        <p>Formulario válido: {{ contactForm.valid }}</p>
        <p>Formulario modificado: {{ contactForm.dirty }}</p>
        <p>Valores: {{ contactForm.value | json }}</p>
      </div>
    </form>
  `,
  imports: [FormsModule]
})
export class ContactFormComponent { }

La variable #contactForm="ngForm" nos da acceso a propiedades importantes como:

  • valid/invalid: Estado de validación del formulario
  • dirty/pristine: Si el formulario ha sido modificado
  • touched/untouched: Si el usuario ha interactuado con el formulario
  • value: Objeto con todos los valores de los controles

Diferencias con formularios reactivos

Los formularios template-driven se diferencian de los formularios reactivos en varios aspectos clave:

  • 1. Definición: Los template-driven se definen en el template, los reactivos en el componente
  • 2. Complejidad: Los template-driven son ideales para casos simples, los reactivos para lógica compleja
  • 3. Validación: Los template-driven usan validadores HTML y directivas, los reactivos usan funciones TypeScript
  • 4. Testing: Los reactivos son más fáciles de testear unitariamente
// Template-driven: configuración en el template
@Component({
  template: `
    <form #form="ngForm">
      <input type="text" name="username" required>
    </form>
  `
})
export class SimpleFormComponent { }

Para este curso de iniciación, nos enfocaremos en los formularios template-driven por su simplicidad y curva de aprendizaje más suave.

Estructura básica de un formulario

Un formulario template-driven típico sigue esta estructura:

@Component({
  selector: 'app-product-form',
  template: `
    <form #productForm="ngForm">
      <div>
        <label for="name">Nombre del producto:</label>
        <input 
          type="text" 
          id="name" 
          name="name" 
          placeholder="Ingrese el nombre">
      </div>
      
      <div>
        <label for="price">Precio:</label>
        <input 
          type="number" 
          id="price" 
          name="price" 
          placeholder="0.00">
      </div>
      
      <button type="submit">Guardar producto</button>
    </form>
  `,
  imports: [FormsModule]
})
export class ProductFormComponent { }

Cada control del formulario debe tener un atributo name único, que Angular utiliza para identificar y rastrear el control dentro del modelo del formulario.

Los formularios template-driven proporcionan una base sólida para la mayoría de casos de uso en aplicaciones web, combinando la familiaridad de HTML con la reactividad de Angular.

Two-way binding con ngModel

El two-way binding con ngModel es la característica más distintiva de los formularios template-driven. Esta funcionalidad permite sincronizar automáticamente los datos entre el modelo del componente y los controles del formulario, creando un flujo bidireccional de información que simplifica enormemente la gestión del estado.

¿Qué es ngModel?

La directiva ngModel combina property binding y event binding en una sola sintaxis elegante. Utilizando la notación [(ngModel)], conocida como "banana in a box", establecemos una conexión bidireccional entre una propiedad del componente y un control de formulario.

@Component({
  selector: 'app-user-profile',
  template: `
    <form #profileForm="ngForm">
      <input 
        type="text" 
        name="username" 
        [(ngModel)]="user.username"
        placeholder="Nombre de usuario">
      
      <p>Hola, {{ user.username }}!</p>
    </form>
  `,
  imports: [FormsModule]
})
export class UserProfileComponent {
  user = {
    username: 'usuario123'
  };
}

En este ejemplo, cualquier cambio en el input se refleja inmediatamente en user.username, y viceversa.

Sintaxis y funcionamiento

El two-way binding con [(ngModel)] es equivalente a combinar property binding y event binding manualmente:

// Estas dos sintaxis son equivalentes:

// Sintaxis corta con ngModel
<input [(ngModel)]="nombre" name="nombre">

// Sintaxis expandida (equivalente)
<input 
  [ngModel]="nombre" 
  (ngModelChange)="nombre = $event" 
  name="nombre">

La sintaxis corta es mucho más limpia y expresiva, por lo que es la forma recomendada de trabajar con formularios template-driven.

Diferentes tipos de inputs

La directiva ngModel funciona con todos los tipos de controles de formulario HTML. Veamos ejemplos prácticos con cada tipo:

Input de texto:

@Component({
  template: `
    <form #form="ngForm">
      <input 
        type="text" 
        name="firstName" 
        [(ngModel)]="person.firstName"
        placeholder="Nombre">
      
      <input 
        type="email" 
        name="email" 
        [(ngModel)]="person.email"
        placeholder="correo@ejemplo.com">
    </form>
  `,
  imports: [FormsModule]
})
export class PersonFormComponent {
  person = {
    firstName: '',
    email: ''
  };
}

Input numérico:

@Component({
  template: `
    <form #productForm="ngForm">
      <input 
        type="number" 
        name="price" 
        [(ngModel)]="product.price"
        placeholder="Precio"
        min="0"
        step="0.01">
      
      <input 
        type="range" 
        name="rating" 
        [(ngModel)]="product.rating"
        min="1"
        max="5">
      
      <p>Precio: ${{ product.price }}</p>
      <p>Calificación: {{ product.rating }}/5</p>
    </form>
  `,
  imports: [FormsModule]
})
export class ProductFormComponent {
  product = {
    price: 0,
    rating: 3
  };
}

Checkbox:

@Component({
  template: `
    <form #preferencesForm="ngForm">
      <label>
        <input 
          type="checkbox" 
          name="notifications" 
          [(ngModel)]="settings.notifications">
        Recibir notificaciones
      </label>
      
      <label>
        <input 
          type="checkbox" 
          name="newsletter" 
          [(ngModel)]="settings.newsletter">
        Suscribirse al boletín
      </label>
      
      <div>
        <p>Notificaciones: {{ settings.notifications ? 'Activadas' : 'Desactivadas' }}</p>
        <p>Boletín: {{ settings.newsletter ? 'Suscrito' : 'No suscrito' }}</p>
      </div>
    </form>
  `,
  imports: [FormsModule]
})
export class SettingsComponent {
  settings = {
    notifications: true,
    newsletter: false
  };
}

Select dropdown:

@Component({
  template: `
    <form #orderForm="ngForm">
      <select name="country" [(ngModel)]="order.country">
        <option value="">Selecciona un país</option>
        <option value="es">España</option>
        <option value="mx">México</option>
        <option value="ar">Argentina</option>
        <option value="co">Colombia</option>
      </select>
      
      <select name="priority" [(ngModel)]="order.priority">
        <option value="low">Baja</option>
        <option value="medium">Media</option>
        <option value="high">Alta</option>
      </select>
      
      <p>País seleccionado: {{ order.country }}</p>
      <p>Prioridad: {{ order.priority }}</p>
    </form>
  `,
  imports: [FormsModule]
})
export class OrderFormComponent {
  order = {
    country: 'es',
    priority: 'medium'
  };
}

Radio buttons:

@Component({
  template: `
    <form #surveyForm="ngForm">
      <fieldset>
        <legend>¿Cómo nos conociste?</legend>
        
        <label>
          <input 
            type="radio" 
            name="source" 
            value="search"
            [(ngModel)]="survey.source">
          Motor de búsqueda
        </label>
        
        <label>
          <input 
            type="radio" 
            name="source" 
            value="social"
            [(ngModel)]="survey.source">
          Redes sociales
        </label>
        
        <label>
          <input 
            type="radio" 
            name="source" 
            value="friend"
            [(ngModel)]="survey.source">
          Recomendación
        </label>
      </fieldset>
      
      <p>Fuente: {{ survey.source }}</p>
    </form>
  `,
  imports: [FormsModule]
})
export class SurveyComponent {
  survey = {
    source: 'search'
  };
}

Textarea y controles de texto multilínea

@Component({
  template: `
    <form #messageForm="ngForm">
      <textarea 
        name="message" 
        [(ngModel)]="feedback.message"
        placeholder="Escribe tu mensaje aquí..."
        rows="4"
        cols="50">
      </textarea>
      
      <p>Caracteres: {{ feedback.message.length }}</p>
    </form>
  `,
  imports: [FormsModule]
})
export class FeedbackComponent {
  feedback = {
    message: ''
  };
}

Consideraciones importantes

Atributo name obligatorio:

Todos los controles que usan ngModel deben tener un atributo name único. Angular utiliza este nombre para registrar el control en el formulario:

<!-- ✅ Correcto -->
<input type="text" name="username" [(ngModel)]="user.username">

<!-- ❌ Error: falta el atributo name -->
<input type="text" [(ngModel)]="user.username">

Inicialización de propiedades:

Es importante inicializar las propiedades que vas a enlazar con ngModel para evitar errores:

export class FormComponent {
  // ✅ Correcto: propiedades inicializadas
  user = {
    name: '',
    email: '',
    age: 0,
    active: false
  };
  
  // ❌ Problemático: propiedades no inicializadas
  // user: any;
}

Actualización en tiempo real

Una de las ventajas más evidentes del two-way binding es la sincronización inmediata. Cualquier cambio en el input se refleja instantáneamente en el modelo y en otras partes del template:

@Component({
  template: `
    <form #calculatorForm="ngForm">
      <input 
        type="number" 
        name="quantity" 
        [(ngModel)]="order.quantity"
        placeholder="Cantidad">
      
      <input 
        type="number" 
        name="price" 
        [(ngModel)]="order.price"
        placeholder="Precio unitario">
      
      <div class="summary">
        <p>Cantidad: {{ order.quantity }}</p>
        <p>Precio unitario: ${{ order.price }}</p>
        <p><strong>Total: ${{ order.quantity * order.price }}</strong></p>
      </div>
    </form>
  `,
  imports: [FormsModule]
})
export class CalculatorComponent {
  order = {
    quantity: 1,
    price: 0
  };
}

El two-way binding con ngModel proporciona una experiencia de desarrollo fluida y reactiva, permitiendo crear interfaces de usuario dinámicas con muy poco código. Esta funcionalidad es la base sobre la cual construiremos validaciones y funcionalidades más avanzadas en las siguientes lecciones.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Angular

Documentación oficial de Angular
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender qué son los formularios template-driven y su funcionamiento básico.
  • Aprender a configurar FormsModule para usar formularios en Angular.
  • Utilizar la directiva ngForm y variables de plantilla para gestionar el estado del formulario.
  • Implementar el two-way binding con ngModel para sincronizar datos entre el modelo y la vista.
  • Manejar diferentes tipos de controles de formulario con ngModel y entender la importancia del atributo name.