Angular 7 - Formularios (Template vs Reactive)
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
- 2. Bootstrap: Un poco de estilo…
- 3. FormsModule
- 4. Tipos de formulario
- 5. Template Driven forms (1ª forma)
- 6. Reactive forms (2ª forma)
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.
- Manejando un evento basado en
- 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)
]),
...
}
escríbe algo en comentarios
😉 Gracias.