Tipos de Formularios
Template Forms
Son formularios controlados por modelo
El modelo basado en plantillas
Angular crea modelos como FormGroups y FormControls
Directivas de Angular NgForm y NgModel
Reactive Forms
Mucho mas potentes y mejor rendimiento
Mejora el tipo de validación de los datos
Se usa para Formularios complejos
FormControl
Es la pieza mas atómica de un formulario reactivo.
Para poder utilizarlo se necesita importar el módulo ReactiveFormsModule
, en el modulo principal
Copy import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [ ... ],
imports: [
...
ReactiveFormsModule,
...
],
providers: [ ... ],
bootstrap: [AppComponent]
})
export class AppModule { }
Para usarlo dentro de un componente
Copy export class BasicFormComponent implements OnInit {
// Crear un form control
nameField = new FormControl('soy un control');
ngOnInit() {
// Optiene valores desde la vista por medio de una suscripcion
this.nameField.valueChanges.subscribe((name) => {
console.log(`Name changed to: ${name}`);
});
}
}
Copy // Enlazar el form cotrol con el template
<input type="text" [formControl]="nameField" />
// Obtener el valor
{{ nameField.value }}
// Obtener el objeto value
<code>
<pre> {{ nameField | json }} </pre>
</code>
Manejo y binding de selects y selects múltiples
Copy export class BasicFormComponent {
categoryField = new FormControl('category-3');
array = [
{ key: 'category-1', 'name': 'Category 1' },
{ key: 'category-2', 'name': 'Category 2' },
{ key: 'category-3', 'name': 'Category 3' },
{ key: 'category-4', 'name': 'Category 4' },
{ key: 'category-5', 'name': 'Category 5' },
{ key: 'category-6', 'name': 'Category 6' },
{ key: 'category-7', 'name': 'Category 7' },
{ key: 'category-8', 'name': 'Category 8' },
{ key: 'category-9', 'name': 'Category 9' },
];
getNameValue() {
console.log(this.categoryField.value);
}
}
Copy <select id="category" [formControl]="categoryField">
<option *ngFor="let item of array" [value]="item.key">{{item.name}}</option>
</select>
<!-- o con ngVAlue para objectos -->
<select id="category" [formControl]="categoryField">
<option *ngFor="let item of array" [ngValue]="item">{{item.name}}</option>
</select>
<!-- También se puede enviar todo el objeto y en el componente usar JSON.parse para convertirlo-->
<!-- <option *ngFor="let item of array" [value]="item | json">{{item.name}}</option> -->
Para setear por defecto el valor del select que utiliza ngValue , se puede usar por ejemplo:
Copy this.form.controls['<formName>'].setValue( {...}, {onlySelf: true});
Con múltiple
Copy <label for="category">Category</label>
<select id="category" [formControl]="categoryField" multiple>
<option *ngFor="let item of array" [value]="item.name">{{item.name}}</option>
</select>
Y observable
Copy ...
ngOnInit() {
this.categoryField.valueChanges.subscribe((nameArr: any) => {
console.log(nameArr);
});
}
...
Manejo y binding de inputs radio y checkbox
Copy genderField = new FormControl('');
zoneField = new FormControl('');
Copy <p>
Agree: {{ agreeField.value }}
<input type="checkbox" [formControl]="agreeField" />
</p>
<p>
Gender: {{ genderField.value }}
<label>
<input
name="gender"
value="male"
type="radio"
[formControl]="genderField"
/>
Male
</label>
<label>
<input
name="gender"
value="female"
type="radio"
[formControl]="genderField"
/>
Female
</label>
<label>
<input
name="gender"
value="other"
type="radio"
[formControl]="genderField"
/>
Other
</label>
</p>
Aplica validaciones a un FormControl
La clase FormControl recibe aparte del valor, puede recibir una o varias validaciones sincronas y asíncronas.
Copy new FormControl('', [Validators.required]);
Copy <p>
Name: {{ nameField.value }}
<input type="text" [formControl]="nameField" />
<button [disabled]="nameField.hasError('required')" (click)="getNameValue()">
Get Value
</button>
<!-- ó -->
<button [disabled]="nameField.invalid" (click)="getNameValue()">
Get Value
</button>
</p>
una mala préctica es mostrar el error desde el antes de utilizar el input
para esto podemos usar la propiedad touched
.
https://angular.io/api/forms/Validators
FormGroup
Un FormGroup es un grupo de FormContols y sirve para manejar todo el formulario, al igual que el FormControl tiene validaciones el FormGroup también pero a nivel de todo el form.
Copy ...
form = new FormGroup({
name: new FormControl('', [Validators.required, Validators.maxLength(10)]),
});
get nameField() {
return this.form.get('name');
}
save(e: MouseEvent) {
e.preventDefault()
...
}
...
Copy <form [formGroup]="form" (ngSubmit)="save($event)">
<p>
Name: {{ nameField.value }} {{ nameField.valid }}
<input
[class.is-valid]="isNameFieldValid"
[class.is-invalid]="isNameFieldInvalid"
type="text"
formControlName="name"/>
<button [disabled]="nameField.invalid" type="submit">Get value</button>
</p>
</form>
FormBuilder
Con FormBuilder se tiene una sintaxis un poco mas amigable
Copy ...
form: FormGroup;
constructor(
private formBuilder: FormBuilder
) {
this.buildForm();
}
private buildForm() {
this.form = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(10)]],
...
});
}
get nameField() {
return this.form.get('name');
}
save(e: MouseEvent) {
e.preventDefault()
if(this.form.valid) {
...
} else {
this.form.markAllAsTouched();
}
}
...
Multiples FormGroups
Sirve para agrupar campos de formulario con su validaciones independientes.
Copy <form [formGroup]="form" (ngSubmit)="save($event)">
<p>
Name:
<input [disabled]="true" type="text" formControlName="name" />
</p>
Address:
<div formGroupName="addresses">
<input type="text" formControlName="address1" />
<input type="text" formControlName="address2" />
<span> {{addresses.valid}}</span>
</div>
<button type="submit" [disabled]="form.invalid">
Send
</button>
</form>
Copy ...
form!: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.buildForm();
}
private buildForm() {
this.form = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(10)]],
addresses: this.formBuilder.group({
address1: ['', [Validators.required]],
address2: ['', [Validators.required]],
}),
});
}
get nameField() {
return this.form.get('name')!;
}
get addresses() {
return this.form.get('addresses')!;
}
get address1() {
return this.form.get('addresses')!.get('address1')!;
}
get address2() {
return this.form.get('addresses')!.get('address1')!;
}
save(e: MouseEvent) {
e.preventDefault()
console.log(this.nameField.value);
}
...
Validaciones personalizadas
Copy import { AbstractControl } from '@angular/forms';
export class MyValidators {
static validPassword(control: AbstractControl) {
const value = control.value;
if (!containsNumber(value)) {
return {invalid_password: true};
}
return null;
}
static matchPasswords(control: AbstractControl) {
const password = control.get('password').value;
const confirmPassword = control.get('confirmPassword').value;
if (password !== confirmPassword) {
return {match_password: true};
}
return null;
}
}
function containsNumber(value: string){
return value.split('').find(v => isNumber(v)) !== undefined;
}
function isNumber(value: string){
return !isNaN(parseInt(value, 10));
}
Copy private buildForm() {
this.form = this.formBuilder.group({
email: ['', [Validators.required]],
password: ['', [Validators.required, Validators.minLength(6), MyValidators.validPassword]],
confirmPassword: ['', [Validators.required]],
}, {
validators: MyValidators.matchPasswords // Validación grupal
});
}
Copy <div>
<input placeholder="password" formControlName="confirmPassword" type="password">
<div *ngIf="form.get('confirmPassword').touched && form.errors">
<mat-error *ngIf="form.hasError('match_password')">
No hace match
</mat-error>
</div>
Las validaciones grupales dependen de uno o mas campos del mismo formulario y lo validan todo.
Validaciones asincrónicas
Un ejemplo sería al intentar crear una categoría duplicada
Copy import { AbstractControl } from '@angular/forms';
import { map } from 'rxjs/operators';
import { CategoriesService } from './../core/services/categories.service';
export class MyValidators {
...
static validateCategory(service: CategoriesService) {
return (control: AbstractControl) => {
const value = control.value;
return service.checkCategory(value)
.pipe(
map((response: any) => {
const isAvailable = response.isAvailable;
if (!isAvailable) {
return {not_available: true};
}
return null;
})
);
};
}
...
}
Copy private buildForm() {
this.form = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(4)], MyValidators.validateCategory(this.categoriesService)],
image: ['', Validators.required]
});
}
Copy <div *ngIf="nameField.hasError('not_available')">
Este nombre ya lo tiene otra categoría
</div>
PatchValue
Sirve para setear todos los datos de un formulario en una sola sentencia, los datos deben de estar en formato JSON
Copy this.form.patchValue(data)
Agregar campos en tiempo de ejecución
Copy ...
private buildForm() {
this.form = this.formBuilder.group({
address: this.formBuilder.array([]),
});
}
get addressField() {
return this.form.get('address') as FormArray;
}
addAddressField() {
this.addressField.push(createAddressField())
}
private createAddressField() {
return this.form.get('address') as FormArray;
}
...
Copy <div>
<button type="button" (click)="addAddressField()">Add address</button>
<div formArrayName="address" *ngFor="let address of addressField.controls; let i=index">
<div [formGroupName]="i">
<label>ZipCode</label>
<input placeholder="zip" formControlName="zip" type="text">
<label>Text</mat-label>
<input placeholder="text" formControlName="text" type="text">
</div>
</div>
Un formBuilder.array
puede contener formBuilder.group
o new FormControl.
Uso de ViewContainerRef
y Componentes Dinámicos en Angular
Servicio para manejar el modal
Copy import { Injectable, ViewContainerRef, ComponentRef, Type } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ModalService {
private viewContainerRef!: ViewContainerRef;
private modalRef: ComponentRef<any> | null = null;
registerContainer(viewContainerRef: ViewContainerRef) {
this.viewContainerRef = viewContainerRef;
}
openModal<T>(component: Type<T>, data: any = {}) {
if (this.modalRef) return; // Evitar múltiples modales
this.modalRef = this.viewContainerRef.createComponent(component);
Object.assign(this.modalRef.instance, data);
}
closeModal() {
if (this.modalRef) {
this.modalRef.destroy();
this.modalRef = null;
}
}
}
Contenedor del modal
Copy import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { ModalService } from './modal.service';
@Component({
selector: 'app-modal-anchor',
template: '<ng-container #modalContainer></ng-container>',
})
export class ModalAnchorComponent {
@ViewChild('modalContainer', { read: ViewContainerRef, static: true })
modalContainer!: ViewContainerRef;
constructor(private modalService: ModalService) {}
ngAfterViewInit() {
this.modalService.registerContainer(this.modalContainer);
}
}
Componente modal dinámico
Copy import { Component } from '@angular/core';
@Component({
selector: 'app-dynamic-modal',
template: `
<div class="modal">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<button (click)="close()">Close</button>
</div>
`,
})
export class DynamicModalComponent {
title!: string;
message!: string;
close() {
console.log('Modal closed!');
}
}
Usando el modal dinámico
Copy import { Component } from '@angular/core';
import { ModalService } from './modal.service';
import { DynamicModalComponent } from './dynamic-modal.component';
@Component({
selector: 'app-root',
template: `
<button (click)="openModal()">Open Modal</button>
<app-modal-anchor></app-modal-anchor>
`,
})
export class AppComponent {
constructor(private modalService: ModalService) {}
openModal() {
this.modalService.openModal(DynamicModalComponent, {
title: 'Dynamic Modal',
message: 'Hello from Angular 9+!',
});
}
}