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.

Last updated