Formularios
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
Basados en Observables
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
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [ ... ],
imports: [
...
ReactiveFormsModule,
...
],
providers: [ ... ],
bootstrap: [AppComponent]
})
export class AppModule { }
Para usarlo dentro de un componente
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}`);
});
}
}
// 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
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);
}
}
<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:
this.form.controls['<formName>'].setValue( {...}, {onlySelf: true});
Con múltiple
<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
...
ngOnInit() {
this.categoryField.valueChanges.subscribe((nameArr: any) => {
console.log(nameArr);
});
}
...
Manejo y binding de inputs radio y checkbox
genderField = new FormControl('');
zoneField = new FormControl('');
<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.
new FormControl('', [Validators.required]);
<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.
...
form = new FormGroup({
name: new FormControl('', [Validators.required, Validators.maxLength(10)]),
});
get nameField() {
return this.form.get('name');
}
save(e: MouseEvent) {
e.preventDefault()
...
}
...
<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
...
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.
<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>
...
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
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));
}
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
});
}
<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
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;
})
);
};
}
...
}
private buildForm() {
this.form = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(4)], MyValidators.validateCategory(this.categoriesService)],
image: ['', Validators.required]
});
}
<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
this.form.patchValue(data)
Agregar campos en tiempo de ejecución
...
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;
}
...
<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
ViewContainerRef
y Componentes Dinámicos en AngularServicio para manejar el modal
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
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
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
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+!',
});
}
}
Last updated