Formas reactivas: marcar campos como tocados


83

Tengo problemas para averiguar cómo marcar todos los campos del formulario como tocados. El principal problema es que si no toco los campos e intento enviar el formulario, el error de validación no aparece. Tengo un marcador de posición para ese fragmento de código en mi controlador.
Mi idea es simple:

  1. el usuario hace clic en el botón enviar
  2. todos los campos marcan como tocados
  3. el formateador de errores vuelve a ejecutar y muestra errores de validación

Si alguien tiene otra idea de cómo mostrar errores al enviar, sin implementar un nuevo método, compártalos. ¡Gracias!


Mi forma simplificada:

<form class="form-horizontal" [formGroup]="form" (ngSubmit)="onSubmit(form.value)">
    <input type="text" id="title" class="form-control" formControlName="title">
    <span class="help-block" *ngIf="formErrors.title">{{ formErrors.title }}</span>
    <button>Submit</button>
</form>

Y mi controlador:

import {Component, OnInit} from '@angular/core';
import {FormGroup, FormBuilder, Validators} from '@angular/forms';

@Component({
  selector   : 'pastebin-root',
  templateUrl: './app.component.html',
  styleUrls  : ['./app.component.css']
})
export class AppComponent implements OnInit {
  form: FormGroup;
  formErrors = {
    'title': ''
  };
  validationMessages = {
    'title': {
      'required': 'Title is required.'
    }
  };

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
    this.buildForm();
  }

  onSubmit(form: any): void {
    // somehow touch all elements so onValueChanged will generate correct error messages

    this.onValueChanged();
    if (this.form.valid) {
      console.log(form);
    }
  }

  buildForm(): void {
    this.form = this.fb.group({
      'title': ['', Validators.required]
    });
    this.form.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }

  onValueChanged(data?: any) {
    if (!this.form) {
      return;
    }

    const form = this.form;

    for (const field in this.formErrors) {
      if (!this.formErrors.hasOwnProperty(field)) {
        continue;
      }

      // clear previous error message (if any)
      this.formErrors[field] = '';
      const control = form.get(field);
      if (control && control.touched && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          if (!control.errors.hasOwnProperty(key)) {
            continue;
          }
          this.formErrors[field] += messages[key] + ' ';
        }
      }
    }
  }
}

Respuestas:


145

La siguiente función se repite a través de los controles en un grupo de formularios y los toca suavemente. Debido a que el campo de controles es un objeto, el código llama a Object.values ​​() en el campo de control del grupo de formularios.

  /**
   * Marks all controls in a form group as touched
   * @param formGroup - The form group to touch
   */
  private markFormGroupTouched(formGroup: FormGroup) {
    (<any>Object).values(formGroup.controls).forEach(control => {
      control.markAsTouched();

      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });
  }

18
lamentablemente, esto no funciona en Internet Explorer :( simplemente cambie (<any>Object).values(formGroup.controls)a Object.keys(formGroup.controls).map(x => formGroup.controls[x])(de stackoverflow.com/questions/42830257/… )
moi_meme

1
Esto fue de gran ayuda para mí al usar FormGroup y FormControl y preguntarme cómo mostrarle al usuario que no tocó un campo requerido. Gracias.
NAMS

@NAMS ¡no hay problema! Me alegro de que haya ayudado:]
masterwok

4
+1 Solo un problema menor en la parte recursiva. Ya está iterando controlsal inicio de la función, por lo que debería ser lo siguiente en su lugar:if (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touchedsolo significa que la entrada fue borrosa una vez. Para que aparecieran errores, también tuve que llamar updateValueAndValidity()a mis controles.
Adamdport

103

Desde Angular 8/9 simplemente puede usar

this.form.markAllAsTouched();

Para marcar un control y sus controles descendientes como tocados.

AbstractControl doc


2
Esta debería ser la respuesta aceptada para aquellos que usan Angular 8.
Jacob Roberts

1
Esta es una solución más simple y limpia.
HDJEMAI

1
esta es la solución recomendada para angular 8 y superior, ¡genial!
Duc Nguyen

1
Si esto parece no funcionar para algunos controles, probablemente no estén en ese FormGroup.
Noumenon

11

Con respecto a la respuesta de @ masterwork. Probé esa solución, pero recibí un error cuando la función intentó excavar, de forma recursiva, dentro de un FormGroup, porque se pasa un argumento FormControl, en lugar de un FormGroup, en esta línea:

control.controls.forEach(c => this.markFormGroupTouched(c));

Aqui esta mi solucion

markFormGroupTouched(formGroup: FormGroup) {
 (<any>Object).values(formGroup.controls).forEach(control => {
   if (control.controls) { // control is a FormGroup
     markFormGroupTouched(control);
   } else { // control is a FormControl
     control.markAsTouched();
   }
 });
}


8

Recorrer los controles de formulario y marcarlos como tocados también funcionaría:

for(let i in this.form.controls)
    this.form.controls[i].markAsTouched();

1
Gracias amigo, tu solución es bastante buena, lo único que agregaría porque tslint se queja es esto: for (const i in this.form.controls) {if (this.form.controls [i]) {this.form.controls [i ] .markAsTouched (); }}
Avram Virgil

1
Esto no funciona si tu formGroupcontiene otros formGroups
adamdport

3

Esta es mi solucion

      static markFormGroupTouched (FormControls: { [key: string]: AbstractControl } | AbstractControl[]): void {
        const markFormGroupTouchedRecursive = (controls: { [key: string]: AbstractControl } | AbstractControl[]): void => {
          _.forOwn(controls, (c, controlKey) => {
            if (c instanceof FormGroup || c instanceof FormArray) {
              markFormGroupTouchedRecursive(c.controls);
            } else {
              c.markAsTouched();
            }
          });
        };
        markFormGroupTouchedRecursive(FormControls);
      }

2

Tuve este problema pero encontré la forma "correcta" de hacerlo, a pesar de no estar en ningún tutorial de Angular que haya encontrado.

En su HTML, en la formetiqueta, agregue la misma Variable de referencia de plantilla (variable #myVariable='ngForm''hashtag') que usan los ejemplos de formularios basados ​​en plantillas, además de lo que usan los ejemplos de formularios reactivos:

<form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()">

Ahora tiene acceso a myForm.submittedla plantilla que puede usar en lugar de (o además de) myFormGroup.controls.X.touched:

<div *ngIf="myForm.submitted" class="text-error"> <span *ngIf="myFormGroup.controls.myFieldX.errors?.badDate">invalid date format</span> <span *ngIf="myFormGroup.controls.myFieldX.errors?.isPastDate">date cannot be in the past.</span> </div>

Sepa que myForm.form === myFormGroupes cierto ... siempre y cuando no olvide la ="ngForm"parte. Si lo usa #myFormsolo, no funcionará porque la var se establecerá en HtmlElement en lugar de la directiva que impulsa ese elemento.

Saben que myFormGroupes visible en el código de texto mecanografiado de su componente por las formas reactivas tutoriales, pero myFormno lo es, a menos que se pasa a través de una llamada al método, al igual que submit(myForm)a submit(myForm: NgForm): void {...}. (El aviso NgFormestá en mayúsculas en el texto mecanografiado, pero en formato camel en HTML).


1
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}

Intenté eso y de alguna manera no toca los elementos del formulario secundario. Tuve que escribir un bucle que marque todos los elementos secundarios manualmente. ¿Tienes alguna idea de por qué markAsTouched()no toca los elementos secundarios?
Giedrius Kiršys

¿Qué versiones angulares estás usando?
Vlado Tesanovic

La versión angular es 2.1.0
Giedrius Kiršys

1
Parece que encontré por markAsTouched()qué no marcar elementos secundarios: github.com/angular/angular/issues/11774 . TL; DR: No es un error.
Giedrius Kiršys

1
Sí, ahora lo recuerdo. Puede desactivar el botón de envío si el formulario no es válido, <button [disable] = "! This.form"> Enviar </button>
Vlado Tesanovic

1

Me encontré con el mismo problema, pero no quiero "contaminar" mis componentes con el código que maneja esto. Especialmente porque necesito esto en muchas formas y no quiero repetir el código en varias ocasiones.

Por lo tanto, creé una directiva (usando las respuestas publicadas hasta ahora). La directiva decora el método de NgForm onSubmit: si el formulario no es válido, marca todos los campos como tocados y cancela el envío. De lo contrario, el método onSubmit habitual se ejecuta normalmente.

import {Directive, Host} from '@angular/core';
import {NgForm} from '@angular/forms';

@Directive({
    selector: '[appValidateOnSubmit]'
})
export class ValidateOnSubmitDirective {

    constructor(@Host() form: NgForm) {
        const oldSubmit = form.onSubmit;

        form.onSubmit = function (): boolean {
            if (form.invalid) {
                const controls = form.controls;
                Object.keys(controls).forEach(controlName => controls[controlName].markAsTouched());
                return false;
            }
            return oldSubmit.apply(form, arguments);
        };
    }
}

Uso:

<form (ngSubmit)="submit()" appValidateOnSubmit>
    <!-- ... form controls ... -->
</form>

1

Este es el código que estoy usando.

validateAllFormFields(formGroup: any) {
    // This code also works in IE 11
    Object.keys(formGroup.controls).forEach(field => {
        const control = formGroup.get(field);

        if (control instanceof FormControl) {
            control.markAsTouched({ onlySelf: true });
        } else if (control instanceof FormGroup) {               
            this.validateAllFormFields(control);
        } else if (control instanceof FormArray) {  
            this.validateAllFormFields(control);
        }
    });
}    


1

Este código funciona para mí:

markAsRequired(formGroup: FormGroup) {
  if (Reflect.getOwnPropertyDescriptor(formGroup, 'controls')) {
    (<any>Object).values(formGroup.controls).forEach(control => {
      if (control instanceof FormGroup) {
        // FormGroup
        markAsRequired(control);
      }
      // FormControl
      control.markAsTouched();
    });
  }
}

1

Una solución sin recursividad

Para aquellos preocupados por el rendimiento, se me ocurrió una solución que no usa la recursividad, aunque todavía itera sobre todos los controles en todos los niveles.

 /**
  * Iterates over a FormGroup or FormArray and mark all controls as
  * touched, including its children.
  *
  * @param {(FormGroup | FormArray)} rootControl - Root form
  * group or form array
  * @param {boolean} [visitChildren=true] - Specify whether it should
  * iterate over nested controls
  */
  public markControlsAsTouched(rootControl: FormGroup | FormArray,
    visitChildren: boolean = true) {

    let stack: (FormGroup | FormArray)[] = [];

    // Stack the root FormGroup or FormArray
    if (rootControl &&
      (rootControl instanceof FormGroup || rootControl instanceof FormArray)) {
      stack.push(rootControl);
    }

    while (stack.length > 0) {
      let currentControl = stack.pop();
      (<any>Object).values(currentControl.controls).forEach((control) => {
        // If there are nested forms or formArrays, stack them to visit later
        if (visitChildren &&
            (control instanceof FormGroup || control instanceof FormArray)
           ) {
           stack.push(control);
        } else {
           control.markAsTouched();
        }
      });
    }
  }

Esta solución funciona tanto en FormGroup como en FormArray.

Puedes jugar con él aquí: angular-mark-as-touch


@VladimirPrudnikov El problema es que cuando se hace una llamada recursiva a una función, generalmente hay más gastos generales asociados. Por eso, la CPU dedicará más tiempo a manejar la pila de llamadas. Cuando se utilizan bucles, la CPU dedicará la mayor parte del tiempo a realizar el algoritmo en sí. La ventaja de la recursividad es que el código suele ser más legible. Entonces, si el rendimiento no es un problema, diría que podría seguir con la recursividad.
Arthur Silva

"La optimización temprana es la raíz de todo mal."
Dem Pilafian

@DemPilafian Estoy de acuerdo con la cita. Sin embargo, no se aplica aquí, porque si alguien se acerca a este hilo, podrá obtener una solución optimizada de forma gratuita (sin perder tiempo en ella). Y, por cierto, en mi caso realmente tenía razones para optimizarlo =)
Arthur Silva

1

según @masterwork

código mecanografiado para la versión angular 8

private markFormGroupTouched(formGroup: FormGroup) {
    (Object as any).values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });   }

0

Así es como lo hago. No quiero que se muestren los campos de error hasta que se presione el botón de envío (o se toque el formulario).

import {FormBuilder, FormGroup, Validators} from "@angular/forms";

import {OnInit} from "@angular/core";

export class MyFormComponent implements OnInit {
  doValidation = false;
  form: FormGroup;


  constructor(fb: FormBuilder) {
    this.form = fb.group({
      title: ["", Validators.required]
    });

  }

  ngOnInit() {

  }
  clickSubmitForm() {
    this.doValidation = true;
    if (this.form.valid) {
      console.log(this.form.value);
    };
  }
}

<form class="form-horizontal" [formGroup]="form" >
  <input type="text" class="form-control" formControlName="title">
  <div *ngIf="form.get('title').hasError('required') && doValidation" class="alert alert-danger">
            title is required
        </div>
  <button (click)="clickSubmitForm()">Submit</button>
</form>


Este parece que puede volverse pesado con el tiempo, al agregar nuevas reglas de validación. Pero entendí el punto.
Giedrius Kiršys

0

Entiendo completamente la frustración del OP. Yo uso lo siguiente:

Función de utilidad :

/**
 * Determines if the given form is valid by touching its controls 
 * and updating their validity.
 * @param formGroup the container of the controls to be checked
 * @returns {boolean} whether or not the form was invalid.
 */
export function formValid(formGroup: FormGroup): boolean {
  return !Object.keys(formGroup.controls)
    .map(controlName => formGroup.controls[controlName])
    .filter(control => {
      control.markAsTouched();
      control.updateValueAndValidity();
      return !control.valid;
    }).length;
}

Uso :

onSubmit() {
  if (!formValid(this.formGroup)) {
    return;
  }
  // ... TODO: logic if form is valid.
}

Tenga en cuenta que esta función aún no se adapta a los controles anidados.


0

Vea esta joya . Hasta ahora, la solución más elegante que he visto.

Código completo

import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';

const TOUCHED = 'markAsTouched';
const UNTOUCHED = 'markAsUntouched';
const DIRTY = 'markAsDirty';
const PENDING = 'markAsPending';
const PRISTINE = 'markAsPristine';

const FORM_CONTROL_STATES: Array<string> = [TOUCHED, UNTOUCHED, DIRTY, PENDING, PRISTINE];

@Injectable({
  providedIn: 'root'
})
export class FormStateService {

  markAs (form: FormGroup, state: string): FormGroup {
    if (FORM_CONTROL_STATES.indexOf(state) === -1) {
      return form;
    }

    const controls: Array<string> = Object.keys(form.controls);

    for (const control of controls) {
      form.controls[control][state]();
    }

    return form;
  }

  markAsTouched (form: FormGroup): FormGroup {
    return this.markAs(form, TOUCHED);
  }

  markAsUntouched (form: FormGroup): FormGroup {
    return this.markAs(form, UNTOUCHED);
  }

  markAsDirty (form: FormGroup): FormGroup {
    return this.markAs(form, DIRTY);
  }

  markAsPending (form: FormGroup): FormGroup {
    return this.markAs(form, PENDING);
  }

  markAsPristine (form: FormGroup): FormGroup {
    return this.markAs(form, PRISTINE);
  }
}

0
    /**
    * Marks as a touched
    * @param { FormGroup } formGroup
    *
    * @return {void}
    */
    markFormGroupTouched(formGroup: FormGroup) {
        Object.values(formGroup.controls).forEach((control: any) => {

            if (control instanceof FormControl) {
                control.markAsTouched();
                control.updateValueAndValidity();

            } else if (control instanceof FormGroup) {
                this.markFormGroupTouched(control);
            }
        });
    }

0

Ver:

<button (click)="Submit(yourFormGroup)">Submit</button>   

API

Submit(form: any) {
  if (form.status === 'INVALID') {
      for (let inner in details.controls) {
           details.get(inner).markAsTouched();
       }
       return false; 
     } 
     // as it return false it breaks js execution and return 

0

Hice una versión con algunos cambios en las respuestas presentadas, para aquellos que estén usando versiones anteriores a la versión 8 del angular, me gustaría compartirla con aquellos que sean útiles.

Función de utilidad:

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

function getAllControls(formGroup: FormGroup): FormControl[] {
  const controls: FormControl[] = [];
  (<any>Object).values(formGroup.controls).forEach(control => {
    if (control.controls) { // control is a FormGroup
      const allControls = getAllControls(control);
      controls.push(...allControls);
    } else { // control is a FormControl
      controls.push(control);
    }
  });
  return controls;
}

export function isValidForm(formGroup: FormGroup): boolean {
  return getAllControls(formGroup)
    .filter(control => {
      control.markAsTouched();
      return !control.valid;
    }).length === 0;
}

Uso:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.