Skip to content
Aprende a hacer apps móviles con Ionic 2 

Formularios en Angular 2

Punto de partida

Este artículo es un tutorial para que aprendas a usar los nuevos formularios de Angular2.

Tomaré como punto de partida el código de los artículos de introducción a Angular 2 que puedes encontrar en este repositorio. Es la base de una aplicación en Angular 2 tipo lista de tareas muy sencilla, de la que iré destacando los puntos importantes a lo largo del código.

Una vez descargado el repositorio, deberás ejecutar npm install para descargar las dependencias del proyecto.

Bootstrap

En primer lugar, para usar los nuevos formularios de Angular 2 (es la segunda versión de formularios que sacan), hay que eliminar los formularios antiguos (que se mantienen por retrocompatibilidad) y añadir los nuevos durante la fase de bootstrap de la app.

Veamos como hacerlo, recordemos que la fase de bootstrap de la app la hacemos por defecto en el archivo main.ts:

//main.ts
import { bootstrap }    from '@angular/platform-browser-dynamic';
import { disableDeprecatedForms, provideForms } from '@angular/forms';
import { AppComponent } from './app.component';

bootstrap(AppComponent, [
  disableDeprecatedForms(),
  provideForms()
 ])
 .catch((err: any) => console.error(err));

Estamos modificando el bootstrap básico, para pasarle como providers los métodos disableDeprecatedForms() para desabilitar la versión anterior de formularios y provideForms() para activar la versión más reciente.

Modelo

En el código de inicio que hay en el repositorio, ya hay creado un modelo para el objeto “tarea”, que tiene esta forma:

//app/todos/shared/todo.model.ts
export class TodoModel {
    constructor(
        public subject: string,
        public content: string,
        public isDone: boolean = false,
        public isImportant: boolean = false
    ){}
}

Así, vemos que nuestras tareas tendrán los campos:

  • subject
  • content
  • isDone
  • important

Componente formulario

Ahora que ya sabemos los campos que queremos incorporar a nuestro formulario para crear tareas, vamos a crear un componente que se llamará mediante los tags <todo-form></todo-form>. Para ello creamos una carpeta todo-form, y añadimos un nuevo archivo todo-form.component.ts:

//app/todos/todo-form/todo-form.component.ts
import { Component } from '@angular/core';
import { NgForm }    from '@angular/forms';
import { TodoModel }    from '../shared/todo.model';


@Component({
  selector: 'todo-form',
  templateUrl: 'app/todos/todo-form/todo-form.component.html'
})

export class TodoFormComponent {
  model = new TodoModel('new subject', 'new content', false, false);
  submitted = false;
  onSubmit() { this.submitted = true; }
}

De momento puedes ver como creamos un modelo a partir de la clase TodoModel y creamos un método para hacer submit del formulario. También indicamos gracias a los metadatos, la URL del template que irá asociado a este componente.

Template del formulario

Para ir directo al grano no perderé tiempo en temas de diseño. Creo un template bien simple con los campos que quiero que el usuario introduzca. En la misma carpeta de antes, añado también el archivo todo-form.component.html:

<div class="container">
    <h2>Add task</h2>

    <form>
      <div class="form-group">
        <label for="subject">Subject</label>
        <input type="text" class="form-control" required>
      </div>

      <div class="form-group">
        <label for="content">Content</label>
        <input type="text" class="form-control" required>
      </div>

      <div class="form-group">
        <div class="checkbox">
          <label for="isImportant">
            <input type="checkbox"> Is important
          </label>
        </div>
      </div>   

      <button type="submit" class="btn btn-default">Submit</button>
    </form>

</div>

Como puedes ver, de momento, este template es HTML5 puro y duro, sin Angular por ningún lado. El atributo required que hemos añadido a algunas etiquetas <input> forma parte de HTML5 para indicar que ese campo es necesario para poder hacer submit.

Los templates de formularios en Angular 2 funcionan con HTML5 puro

Algo de estilo

Las clases container, form-group, form-control, y btn que aparecen son simples clases de Twitter Bootstrap que añado al formulario por cuestiones estéticas, pero son completamente innecesarias.

Para poder utilizar estos estilos necesitarás descargar esta librería y añadirla al index.html:

$ npm install bootstrap --save
<!--index.html-->

<head>
<!-- ...some stuff... -->
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>

<!-- ...some stuff... -->

Usando el formulario en una vista

Seguro que quieres ver como queda el formulario (aún no funcional) que acabas de crear. Vamos a meterlo en la vista principal, es muy sencillo.

Modifica el archivo app/app.component.ts para que quede así:

//app/app.component.ts
import {Component} from '@angular/core';
import {TodoListComponent} from './todos/todos-list/todo-list.component';
import {TodoFormComponent} from './todos/todo-form/todo-form.component';
import { Logger } from './shared/logger.service';

@Component({
    selector: 'my-app',
    template: `<h1>ToDo List example</h1>
                <todo-form></todo-form>
                <todo-list></todo-list>`,
    directives: [TodoListComponent, TodoFormComponent],
    providers: [Logger]
})
export class AppComponent { }

Básicamente lo que has hecho es:

  • Importar el nuevo componente TodoFormComponent
  • Meter el componente en el template con <todo-form></todo-form>
  • Inyectar el componente para poderlo usar desde este componente a través del metadato directives

Si has seguido bien todos los pasos, al ejecutar el servidor de desarrollo con npm start deberías ver la siguiente imagen:

todo list

Two way data binding

¿Recuerdas que habíamos creado un modelo en el componente? No estamos viendo sus valores, sino que el formulario aparece vacío.

Esto lo vamos a solucionar con un binding bi-direccional, gracias a [(ngModel)].

Abrimos el template del componente todo-form.component.html y añadimos a todos los inputs los atributos [(ngModel)] y name, como te enseño para el caso de subject:

<label for="subject">Subject</label>
        <input type="text" class="form-control" required
          [(ngModel)]="model.subject" name="subject">

Al refrescar la página, verás que ahora se muestra en el formulario el contenido del modelo de prueba que habíamos creado en el componente. Pero no solo eso: si editas el contenido, el modelo se actualiza en el lado Javascript. Esto es lo que se conoce por two-way data binding.

NOTA: Fíjate que también hemos añadido el atributo name. Esto es un requisito cuando se usa [(ngModel)] en un formulario, para poder referirnos con facilidad a esta propiedad desde el formulario y para validar su estado.

Seguramente me creerás cuando digo que el contenido se modifica, pero ¿no estaría bien verlo con tus propios ojos?

Voy a actualizar el template del componente para ver el binding bi-direccional en acción. Los cambios son simples: muevo todo el contenido del formulario a una columna que ocupe la mitad de la pantalla, y en la otra mitad, muestro el contenido de la variable model por interpolación. Te copio el resultado final:

<div class="container">
    <h2>Add task</h2>

    <div class="col-xs-6">
      <form>
        <div class="form-group">
          <label for="subject">Subject</label>
          <input type="text" class="form-control" required
            [(ngModel)]="model.subject" name="subject">
        </div>

        <div class="form-group">
          <label for="content">Content</label>
          <input type="text" class="form-control" required
            [(ngModel)]="model.content" name="content">
        </div>

        <div class="form-group">
          <div class="checkbox">
            <label for="isImportant">
              <input type="checkbox"
                [(ngModel)]="model.isImportant" name="isImportant">
                Is important
            </label>
          </div>
        </div>   

        <button type="submit" class="btn btn-default">Submit</button>
      </form>
    </div>

    <div class="col-xs-6">
      <h3>Subject:</h3>
      {{model.subject}}

      <h3>Content</h3>
      {{model.content}}

      <h3>is important</h3>
      {{model.isImportant}}
    </div>

</div>

Si recargas la página, verás como a la derecha del formulario aparece un resumen de la tarea que se está editando, y todos los cambios que haces en el formulario de la izquierda afectan al contenido de la derecha, como te muestro en la imagen.

two way data binding todo list

Cambio de estado y validación con ngModel

Una característica muy potente de usar ngModel en tus formularios Angular 2 es que además del two-way data binding, automáticamente añade clases al elemento para indicarnos si el control se ha tocado, si el valor ha cambiado y si es inválido.

A continuación tienes el resumen de clases que aplica ngModel al elemento en función del estado:

Estado Clase si es cierto Clase si es falso
El control ha sido visitado ng-touched ng-untouched
El valor ha sido modificado ng-dirty ng-pristine
El valor es válido ng-valid ng-invalid

Modificar el propio elemento según su estado

Aprovechando esta ventaja, vamos a añadir algunos estilos para comprobar que realmente ngModel está haciendo un seguimiento del estado.

Creamos el archivo styles.css, y añadimos este contenido:

.ng-valid[required] {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid {
  border-left: 5px solid #a94442 !important; /* red */
  border-color:  #a94442 !important; /* red */
  box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(169,68,66,.6) !important;
}

De este modo, cuando el valor de un elemento del formulario sea correcto, se mostrará una linea verde a la izquierda del mismo, mientras que si es incorrecto, se mostrará con una linea roja, y todo un borde rojo alrededor.

Modificar otros elementos

¿Y qué pasa si quieres manipular un elemento distinto a el que tiene el problema de validación? Tranquilo, puedes obtener el estado del input a través de una template reference variable para utilizarlo desde otro lado del template.

Para ejemplificar esta situación, lo que haré es mostrar un mensaje de error debajo del elemento que tiene el problema de validación, de forma que se muestre únicamente en esa situación. Lo detallo con el campo subject del ejemplo:

    <input type="text" class="form-control" required
        [(ngModel)]="model.subject" name="subject" #subjectState="ngModel">

    <div [hidden]="subjectState.valid || subjectState.pristine" class="alert alert-danger">
        Subject is required
    </div>

¿Qué he cambiado?

  • #subjectState: Con la almoadilla declaro una template reference variable, variable que vinculo en este caso a la directiva ngModel. A partir de este momento, tengo un objeto llamado subjectState con las propiedades de validación que proporciona ngModel para utilizarlo en el resto de template.
  • <div [hidden]=...: Creo un nuevo div a continuación con las clases alert y alert-danger de Bootstrap. Modifico la propiedad HTML5 hidden del elemento en función de las propiedades de la variable subjectState que acabo de crear.

Veamos de nuevo el resultado

todo list ngModel validation

Completar submit

Para completar el proceso de submit del formulario me falta añadir un nuevo objeto to do y limpiar los datos que se muestran.

Limpiando el formulario

Para limpiar el formulario básicamente tienes que asignar un nuevo TodoModel a la propiedad model del componente. Lo que pasa es que como tenemos el tema de validación por medio, después de haber modificado el formulario, al mostrar sus campos vacíos saldría un mensaje de error.

Para evitarlo ahí tienes un pequeño truco: creas una variable que pondrás un momento a false después de generar un nuevo modelo, y lo único que tienes que hacer entonces es aplicar la directiva *ngIf al formulario, asignada a esta variable.

Fíjate en el código:

//app/todos/todo-form/todo-form.component.ts
export class TodoFormComponent {
  //...some stuff...

  active = true;
  newTodo(){
    this.model = new TodoModel('', '', false, false);
    this.active = false;
    setTimeout(()=>{this.active = true;});
  }
}
<!--app/todos/todo-form/todo-form.component.html-->
<!-- ...some stuff... -->
<form *ngIf="active">
    <!-- ...some stuff... -->
</form>

Haciendo submit

Si te fijas, el botón de submit del formulario no llama a ninguna acción, simplemente es del tipo “submit”.

Lo que necesitas para que se llame a la función onSubmit que hay creada en el componente, es vincularla al evento (ngSubmit) del formulario:

<!--app/todos/todo-form/todo-form.component.html-->
<!-- ...some stuff... -->
<form *ngIf="active" (ngSubmit)="onSubmit()">
    <!-- ...some stuff... -->
</form>

Por otro lado, quieres que al hacer click, no solo se borre el formulario sino que además se añada el nuevo item al listado de tareas. Para eso tendrás que inyectar el servicio TodoService, y utilizar su método addTodo. Deja que te enseñe los cambios relevantes en el componente:

//app/todos/todo-form/todo-form.component.ts

//...some imports...
import { TodoService } from '../shared/todo.service';

@Component({
  //...some stuff...
  providers:   [TodoService]
})
export class TodoFormComponent {
  //...some stuff...

  constructor(private todoService:TodoService){}

  onSubmit() { 
    this.submitted = true;
    this.todoService.addTodo(this.model);
    this.newTodo();
  }
}

Como puedes observar, ahora cuando haces click en el botón de submit, se borra el contenido del formulario, y además si te fijas en la salida por consola, verás que el array de tareas ha aumentado.

Además, los campos vacíos se marcan con el estilo en rojo, pero no aparece el mensaje de error por el pequeño truco que hemos aplicado antes.

En cuanto al listado de tareas, verás que no se ha modificado. Eso es por que este tutorial se centraba en los formularios, y no en el patrón Observable que es lo que habría que aplicar en el componente todo-list para que actualice su lista con el nuevo array de tareas. ¿Suena muy interesante verdad? Pués tendré que dejarlo para otro tutorial 😉

Encontrarás el código final de este artículo en la rama formsTutorial del repositorio. ¡Recuerda compartirlo si te ha gustado!

¡Saludos!

Published inAngular 2ES6JavascriptTypeScript

4 Comments

  1. andres andres

    Que versión del node , npm tienes’ y cual es la mínima recomendada?.

    De ante mano darte felicitaciones por los excelentes contenidos que se encuentran en un tu blog, felicidades!!

    • Enrique Oriol Enrique Oriol

      ¡¡Gracias!!

      Actualmente estoy con la versión 6.3.1 de Node y la 3.10.3 de npm. Yo siempre recomiendo utilizar la última versión estable 😉

      Por cierto, no sé si lo estás utilizando, pero desde mi punto de vista lo más cómodo para instalar y actualizarte node es el Node Version Manager (nvm)

  2. Muchas gracias por tu tutorial, me está sirviendo de gran ayuda. No he encontrado ningún sitio donde esté mejor explicado el glosario de Angular 2 que la verdad me estaba costando bastante entender.

    En el formulario del ejemplo que has puesto en este post me da un error: “Template parse errors: There is no directive with “exportAs” set to “ngModel” ”

    Y una recomendación para revisar los forms con este enlace: https://docs.google.com/document/u/1/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/pub

    Lo he cambiado así:

    Y ya funciona perfectamente. Creo que estos de Angular se están haciendo un lío con los formularios y nos lo están poniendo difícil para seguirles la pista.

    Enhorabuena por los post y gracias.

    • Enrique Oriol Enrique Oriol

      Gracias Cristina,

      La verdad es que tengo pendiente actualizar todo el contenido de Angular 2, han hecho algunos cambios estructurales (@NgModule) y también con respecto a los forms. Espero publicarlos en breve.

      Un abrazo

Deja un comentario