formularios angularangular template vs reactiveform template vs reactive
angular formularios

Angular 7 - Formularios (Template vs Reactive)

Publicado: por

Tutorial para crear de 0 los dos tipos de formulario que hay en Angular. Ver demo.

Para descargar el código fuente, código github.

1. Setup: Crear nuevo proyecto

Lo primero de todo es crear un proyecto angular, en este caso, angular 7, funciona en todas las versiones anteriores.

$ ng new formularios

2. Bootstrap: Un poco de estilo…

Para no partir de 0 en los estilos, vamos a trabajar con bootstrap 4. Así nos centramos en el código y no en la maquetación.

Instala bootstrap y configura.

$ npm install bootstrap popper.js jquery --save

angular.json

...
"styles": [
  "src/styles.css",
  "node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
    "node_modules/jquery/dist/jquery.min.js",
    "node_modules/bootstrap/dist/js/bootstrap.min.js",
    "node_modules/popper.js/dist/umd/popper.min.js"
]
...

3. FormsModule

Importa este módulo, necesario para usar formularios e inputs en angular

app.module.ts

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

4. Tipos de formulario

Hay dos maneras de hacer formularios en Angular, template y reactive. Las diferencias son:

Template Driven

  • fácil de usar
  • adecuado para escenarios simples.
  • similar a AngularJS
  • two way data binding (usando [(NgModel)] syntax)
  • código de componente mínimo
  • seguimiento automático del formulario y sus datos (manejado por Angular)
  • test unitarios difíciles de implementar

Reactive Forms

  • más flexible, pero necesita mucha práctica.
  • adecuado en escenarios complejos
  • No se realiza ningún data binding (immutable data model preferido por la mayoría de los desarrolladores)
  • Más código de componente y menos marcado HTML
  • Se pueden hacer transformaciones reactivas tales como
    • Manejando un evento basado en debounce time.
    • Manejo de eventos cuando los componentes son distintos hasta que se modifican.
  • Adición de elementos dinámicamente
  • test unitarios fáciles de usar

5. Template Driven forms (1ª forma)

Lo primero de todo es crear el modelo del formulario, contiene todos los elementos (input, select, radio,…) necesarios.

src/app/models/user.ts

 export class Hero {

  constructor(
    public id: number,
    public name: string,
    public color: string,
    public sex: string,
    public isOk: boolean,
    public email?: string
  ) {  }

}

Crea la página donde estará el formulario:

$ ng g c pages/templateDrivenForms

5.1 Estado de control y validez con ngModel

Estos son los 3 estados posibles.

                                      true           false
El control ha sido visitado  ----->  ng-touched - ng-untouched
El valor del control ha cambiado ->  ng-dirty --- ng-pristine
El valor del  control es válido -->  ng-valid --- ng-invalid

src/app/pages/template-driven-forms/template-driven-forms.component.html

5.2. Form

<form (ngSubmit)="onSubmit(userForm)" #userForm="ngForm" novalidate="">
  • novalidate: para que no valide por html5. De esta manera podremos hacerlo por angular.
  • #userForm="ngForm": asignamos el formulario en la variable userForm. A través de el podremos ver si el formulario es válido, y si no lo es, que campos están con errores, así como el valor de cada uno.
  • (ngSubmit)="onSubmit(userForm)": El submit de angular. Se le pasa una función con el formulario para comprobar su estado. Se evalua con: userForm.form.valid.

5.3. Input text

<div class="form-group">
  <label for="name">Nombre *</label>
  <input type="text" class="form-control" id="name" required [(ngModel)]="myUser.name" name="name" #name="ngModel"
    minlength="5">
  <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
    <div *ngIf="name.errors?.required">
      Este campo es requerido.
    </div>
    <div *ngIf="name.errors?.minlength">
      Por lo menos  caracteres.
    </div>
  </div>
</div>
  • required: Se le proporciona a un campo para que sea obligatorio su completado
  • minlength: Obliga a que el campo tenga un mínimo de caracteres.
  • maxlength: Obliga a que el campo tenga un máximo de caracteres.
  • [(ngModel)]="myUser.name" name="name" #name="ngModel": Es obligatorio poner un name, en ngModel guardamos el valor en el objeto myUser. El tag #name sirve para poder realizar el control de errores por mensaje.
  • <div *ngIf="name.errors?.required">: si name contiene errores, en este caso, si hay error porque es requerido y está vacío. Cuando no hay errores, errors es null, de ahí el ?.
  • pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$": patrón que debe serguir un input para que no de error en cada pulsación. En este caso se trata de un patrón para emails.

5.4. Select

<div class="form-group">
  <label for="color">Color *</label>
  <select class="form-control" id="color" required [(ngModel)]="myUser.color" name="color" #color="ngModel">
    <option *ngFor="let color of colors" [value]="color"></option>
  </select>
  <div [hidden]="color.valid || color.pristine" class="alert alert-danger">
    Color es obligatorio
  </div>
</div>
  • [(ngModel)]="myUser.color" name="color" #color="ngModel": El mismo caso que en input.

5.5. Input radio

<div class="form-group">
  <div> <label>Sexo</label></div>
  <div class="custom-control custom-radio custom-control-inline" *ngFor="let item of sexKeys2">
    <input type="radio" id="sexo" [(ngModel)]="myUser.sex" [value]="item" name="sex" class="custom-control-input">
    <label class="custom-control-label" for="sexo"></label>
  </div>
</div>
  • [(ngModel)]="myUser.sex" [value]="item" name="sex": El mismo caso que en input, salvo que no se le asigna #, ya que no tendrá contror de errores, ya tiene un valor por defecto.

5.6. Checkbox

<div class="form-group">
  <div> <label>Acepta</label></div>
  <div class="custom-control custom-checkbox">
    <input type="checkbox" required [(ngModel)]="myUser.isOk" name="isOk" class="custom-control-input" id="customCheck1">
    <label class="custom-control-label" for="customCheck1">Acepta las condiciones</label>
  </div>
</div>

6. Reactive forms (2ª forma)

Crea la página donde estará el formulario:

$ ng g c pages/reactiveForms

6.1 Importaciones necesarias

Importa este módulo, necesario para usar formularios e inputs en angular

app.module.ts

import {ReactiveFormsModule} from '@angular/forms';

src/app/pages/reactive-forms/reactive-forms.component.ts

import { FormGroup, FormControl, Validators } from '@angular/forms';

6.2 Formulario básico reactivo

miFormulario: FormGroup;

constructor() {
  this.miFormulario = new FormGroup({
    'nombre': new FormControl('Jose'),
    'apellido': new FormControl(),
    'correo': new FormControl()
  });
}

miSubmit() {
  console.log(this.miFormulario.value);
  console.log(this.miFormulario );
}
...
 <form [formGroup]="miFormulario" (ngSubmit)="guardarCambios()" novalidate="">
...

 <input class="form-control" type="text" placeholder="Nombre" formControlName="nombre">
 <input class="form-control" type="text" placeholder="Apellido" formControlName="apellido">
 <input class="form-control" type="email" placeholder="Correo electrónico" formControlName="correo">

...

6.3 Validaciones

Tiene dos pasos, por la parte del código, se añade mediante corchetes las validaciones que se requiera.

this.miFormulario = new FormGroup({
      'nombre': new FormControl('Jose', [
        Validators.required,
        Validators.minLength(3)
      ]),
      'apellido': new FormControl('', [
        Validators.required
      ]),
      'correo': new FormControl('', [
        Validators.required,
        Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')
      ])
    });

Solo queda añadir unos divs, igual que el form por template.

 <input class="form-control" 
  [ngClass]="{'is-invalid': !miFormulario.get('nombre').valid}"
  type="text" placeholder="Nombre" formControlName="nombre">
  <div *ngIf="miFormulario.controls['nombre'].errors?.required" class="invalid-feedback">
      El nombre es necesario
  </div>
  <div *ngIf="miFormulario.controls['nombre'].errors?.minlength" class="invalid-feedback">
      Por lo menos 3 caracteres
  </div>

6.4 Objetos complejos

Supongamos que nombre y apellido lo queremos englobar en un objeto llamado nombreCompleto.

Quedaría así:

  miFormulario: FormGroup;
  usuario: Object = {
    nombreCompleto: {
      nombre: 'José Luisote',
      apellido: 'Garcia'
    },
    correo: 'joseluis@jolugama.com'
  };

  constructor() {
    this.miFormulario = new FormGroup({
      'nombreCompleto': new FormGroup({
        'nombre': new FormControl('Jose', [
          Validators.required,
          Validators.minLength(3)
        ]),
        'apellido': new FormControl('', [
          Validators.required
        ]),
      }),
      'correo': new FormControl('', [
        Validators.required,
        Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')
      ])
    });
  }
...
<form [formGroup]="miFormulario" (ngSubmit)="guardarCambios()" novalidate="">
  ...
   <input class="form-control" type="text" placeholder="Nombre" formControlName="nombre" [ngClass]="{'is-invalid': !miFormulario.get('nombreCompleto.nombre').valid}">
  <div *ngIf="miFormulario.get('nombreCompleto.nombre').errors?.required" class="invalid-feedback">
    El nombre es necesario
  </div>
  <div *ngIf="miFormulario.get('nombreCompleto.nombre').errors?.minlength" class="invalid-feedback">
    Por lo menos 3 caracteres
  </div>
  ...

6.5 Reset y valores por defecto

Para añadir valores por defecto a los campos:

this.miFormulario.setValue(this.usuario);

Para borrar los valores y dejar los estados por defecto

this.usuarioVacio: Object = {
  nombreCompleto: {
    nombre: '',
    apellido: ''
  },
  correo: ''
};
this.miFormulario.reset(this.usuarioVacio);

6.6 Controles dinámicos utilizando arrays

Crear dinámicamente input text mediante FormArray.

import { FormArray } from '@angular/forms';
...
  usuario: Object = {
    ...
    aficciones: ['tocar la armónica']
  };

  agregarAficciones() {
    (<FormArray>this.miFormulario.controls['aficciones']).push(
      new FormControl('', Validators.required)
    );
  }
<div class="form-group row">
  <label class="col-2 col-form-label">Aficciones</label>
  <div class="col-md-8" formArrayName="aficciones">
    <input class="form-control" type="text" *ngFor="let item of miFormulario.controls['aficciones'].controls; let i=index"
      [formControlName]="i">
  </div>
  <button type="button" (click)="agregarAficciones()" class="btn btn-primary">Nuevo </button>
</div>

6.7 Suscripción en cambios de estado y valor

Cuando se escribe, o se cambia el estado de un input, se puede controlar de la siguiente manera:

this.miFormulario.controls['username'].valueChanges.subscribe(data => {
  console.log(data);
});

this.miFormulario.controls['username'].statusChanges.subscribe(data => {
  console.log(data);
});

6.8 Validaciones asíncronos

Mediante este ejemplo, simulamos con una función con setTimeout, una validación asíncrona de un posible servicio que tenga que validar un campo.

...
 'username': new FormControl('', [Validators.required],
        [this.UserExist]
      ),
...

UserExist(control: FormControl): Promise<any> | Observable<any> {
  return new Promise((resolv, reject) => {
    setTimeout(() => {
      if (control.value === 'admin') {
        resolv({ existe: true });
      } else {
        resolv(null);
      }
    }, 3000);
  });
}
<div class="form-group row">
  <label class="col-3 col-form-label">Username</label>
  <div class="col-8">
    <input class="form-control" type="text" placeholder="username" formControlName="username">
  </div>
</div>

6.9 Validaciones personalizadas

  ...
  'password2': new FormControl('', [
        Validators.required,
        this.match('password1')
      ])
  ...


  /** control.value debe ser igual a <FormGroup>.controls[controlKey].value */
  match(controlKey: string) {
    return (control: AbstractControl): { [s: string]: boolean } => {
      // control.parent es el FormGroup
      if (control.parent) { // en las primeras llamadas control.parent es undefined
        const checkValue = control.parent.controls[controlKey].value;
        if (control.value !== checkValue) {
          return {
            match: true
          };
        }
      }
      return null;
    };
  }

6.10 formBuilder

La creación manual de instancias de control de formulario puede volverse repetitiva cuando se trata de formularios múltiples. El servicio FormBuilder proporciona métodos convenientes para generar controles.

import { FormBuilder } from '@angular/forms';

...

constructor(private fb: FormBuilder) { 
  this.miFormulario = this.fb.group({
  'nombreCompleto': this.fb.group({
    'nombre': this.fb.control('Jose', [
      Validators.required,
      Validators.minLength(3)
    ]),
  ...
}

 
... y finalmente... Si te ha gustado, difúndelo. Es solo un momento. escríbe algo en comentarios 😉 Gracias.