Je travaille actuellement sur un formulaire Angular / TypeScript de plusieurs champs (plus de 10 champs), et je voulais gérer les erreurs plus correctement sans dupliquer le code dans mon HTML. page.
Voici un exemple de formulaire:
<form [formGroup]="myForm">
<label>Name</label>
<input type="text" formControlName="name">
<p class="error_message" *ngIf="myForm.get('name').invalid && (myForm.submitted || myForm.get('name').dirty)">Please provide name</p>
<label>Lastname</label>
<input type="text" formControlName="lastname">
<p class="error_message" *ngIf="myForm.get('lastname').invalid && (myForm.submitted || myForm.get('lastname').dirty)">Please provide email</p>
<label>Email</label>
<input type="text" formControlName="email">
<p class="error_message" *ngIf="myForm.get('email').hasError('required') && (myForm.submitted || myForm.get('email').dirty)">Please provide email</p>
<p class="error_message" *ngIf="myForm.get('email').hasError('email') && (myForm.submitted || myForm.get('email').dirty)">Please provide valid email</p>
</form>
Dans mon cas, j'ai deux types de validation pour mon formulaire:
J'essaie d'utiliser une directive comme mentionné ici
<form [formGroup]="myForm">
<label>Name</label>
<input type="text" formControlName="name">
<div invalidmessage="name">
<p *invalidType="'required'">Please provide name</p>
</div>
<label>Lastname</label>
<input type="text" formControlName="lastname">
<div invalidmessage="lastname">
<p *invalidType="'required'">Please provide lastname</p>
</div>
<label>Email</label>
<input type="text" formControlName="email">
<div invalidmessage="email">
<p *invalidType="'required'">Please provide email</p>
<p *invalidType="'email'">Please provide valid email</p>
</div>
</form>
Mais même avec cette solution, le code est toujours dupliqué et ne permet pas de gérer les deux types de validation.
Avez-vous une autre approche? Les composants d'utilisation sont-ils appropriés dans ce cas? Si oui, comment peut le faire.
Merci d'avance pour votre investissement.
Vous pouvez déplacer les erreurs de validation dans un composant et transmettre le formulaire formControl.errors en tant que propriété d'entrée. De cette façon, tous les messages de validation peuvent être réutilisés. Voici un exemple sur StackBlitz . Le code utilise Angular), mais devrait quand même vous être utile, même si vous ne l'êtes pas.
validation-errors.component.ts
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup, ValidationErrors } from '@angular/forms';
@Component({
selector: 'validation-errors',
templateUrl: './validation-errors.component.html',
styleUrls: ['./validation-errors.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ValidationErrorsComponent implements OnInit {
@Input() errors: ValidationErrors;
constructor() {}
ngOnInit() {}
}
validation-errors.component.html
<ng-container *ngIf="errors && errors['required']"> Required</ng-container>
<ng-container *ngIf="errors && errors['notUnique']">Already exists</ng-container>
<ng-container *ngIf="errors && errors['email']">Please enter a valid email</ng-container>
Pour les messages de validation en retour, définissez l'erreur manuellement sur le contrôle de formulaire.
const nameControl = this.userForm.get('name');
nameControl.setErrors({
"notUnique": true
});
Pour utiliser le composant de validation sur la fiche:
<form [formGroup]="userForm" (ngSubmit)="submit()">
<mat-form-field>
<input matInput placeholder="name" formControlName="name" required>
<mat-error *ngIf="userForm.get('name').status === 'INVALID'">
<validation-errors [errors]="userForm.get('name').errors"></validation-errors>
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="email" formControlName="email" required>
<mat-error *ngIf="userForm.get('email').status === 'INVALID'">
<validation-errors [errors]="userForm.get('email').errors"></validation-errors>
</mat-error>
</mat-form-field>
<button mat-raised-button class="mat-raised-button" color="accent">SUBMIT</button>
</form>
Vous pouvez injecter NgForm
et accéder à la directive FormControlName
par @ContentChild
dans un composant de validation personnalisé pour obtenir une réutilisation:
@Component({
selector: '[validator]',
template: `
<ng-content></ng-content>
<div *ngIf="formControl.invalid">
<div *ngIf="formControl.errors.required && (form.submitted || formControl.dirty)">
Please provide {{ formControl.name }}
</div>
<div *ngIf="formControl.errors.email && (form.submitted || formControl.dirty)">
Please provide a valid email
</div>
<div *ngIf="formControl.errors.notstring && (form.submitted || formControl.dirty)">
Invalid name
</div>
</div>
`})
export class ValidatorComponent implements OnInit {
@ContentChild(FormControlName) formControl;
constructor(private form: NgForm) {
}
ngOnInit() { }
}
Pour l'utiliser, vous devriez envelopper tous vos contrôles de formulaire (qui ont un formControlName) avec un élément HTML et ajouter un attribut de validation:
<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
<div [formGroup]="myForm">
<label>Name</label>
<div validator>
<input type="text" formControlName="name">
</div>
<label>Lastname</label>
<div validator>
<input type="text" formControlName="lastname">
</div>
<label>Email</label>
<div validator>
<input type="text" formControlName="email">
</div>
</div>
<button type="submit">Submit</button>
</form>
Cela fonctionnera pour les validateurs synchrones et asynchrones.
J'ai eu la même exigence, personne n'aime réécrire le même code deux fois.
Cela peut être fait en créant des contrôles de formulaire personnalisés. L'idée est de créer vos contrôles de formulaire personnalisés, de disposer d'un service commun qui génère un objet formControl personnalisé et d'injecter les validateurs appropriés en fonction du type de données fourni dans l'objet FormControl.
D'où vient le type de données?
Avoir un fichier dans vos actifs ou n'importe où qui contient des types comme celui-ci:
[{
"nameType" : {
maxLength : 5 ,
minLength : 1 ,
pattern : xxxxxx,
etc
etc
}
}
]
Cela vous permet de lire votre ValidatorService
et de sélectionner le type de données approprié avec lequel vous pouvez créer vos validateurs et revenir à votre contrôle de formulaire personnalisé.
Par exemple ,
<ui-text name="name" datatype="nameType" [(ngModel)]="data.name"></ui-text>
Ceci est une brève description de ce que j'ai fait pour atteindre cet objectif. Si vous avez besoin d'informations supplémentaires à ce sujet, commentez. Je suis absent, je ne peux donc pas vous fournir de code de base pour le moment, mais demain pourrait mettre à jour la réponse.
PDATE pour la partie montrant une erreur
Vous pouvez faire 2 choses pour cela, liez votre validateur formControl avec un div dans le contrôle et basculez-le avec *ngIf="formControl.hasError('required
) "`, Etc.
Pour qu'un message/une erreur soit affiché dans un autre emplacement générique, comme un forum, il est préférable de placer ce balisage dans le composant ParentComponent qui ne soit pas supprimé lors du routage (discutable en fonction des besoins) et de le faire écouter à un événement MessageEmit. que votre ErrorStateMatcher
de votre formControl se déclenchera chaque fois que nécessaire (en fonction des besoins).
Ceci est le design que nous avons utilisé et cela a très bien fonctionné, vous pouvez faire beaucoup avec ces formControls une fois que vous avez commencé à les personnaliser.
Vous pouvez créer un composant personnalisé ValidationMessagesComponent
:
Modèle :
<p class="error_message" *ngIf="form.get(controlName).hasError('required') && (form.submitted || form.get(controlName).dirty)">Please provide {{controlName}}</p>
<p class="error_message" *ngIf="form.get(controlName).hasError('email') && (form.submitted || form.get(controlName).dirty)">Please provide valid {{controlName}}</p>
...other errors
Et avec les entrées:
@Input() controlName;
@Input() form;
Ensuite, utilisez-le comme ceci:
<validation-messages [form]="myForm" controlName="email"></validation-messages>
Voici une partie du code que j'ai utilisé dans la bibliothèque pour générer des formulaires dynamiques.
C'est FormError.ts
Qui est utilisé pour obtenir des messages d'erreur et personnalisés si nous le voulons.
import { AbstractControl } from "@angular/forms";
type ErrorFunction = (errorName: string, error: object) => string;
export type ErrorGetter =
string | { [key2: string]: string } | ErrorFunction;
export class FormError {
constructor(private errorGetter?: ErrorGetter) { }
hasError(abstractControl: AbstractControl) {
return abstractControl.errors && (abstractControl.dirty || abstractControl.touched);
}
getErrorMsgs(abstractControl: AbstractControl): string[] {
if (!this.hasError(abstractControl))
return null;
let errors = abstractControl.errors;
return Object.keys(errors).map(anyError => this.getErrorValue(anyError, errors[anyError]));
}
getErrorValue(errorName: string, error: object): string {
let errorGetter = this.errorGetter;
if (!errorGetter)
return predictError(errorName, error);
if (isString(errorGetter))
return errorGetter;
else if (isErrorFunction(errorGetter)) {
let errorString = errorGetter(errorName, error);
return this.predictedErrorIfEmpty(errorString, errorName, error)
}
else {
let errorString = this.errorGetter[errorName];
return this.predictedErrorIfEmpty(errorString, errorName, error)
}
}
predictedErrorIfEmpty(errorString: string, errorName: string, error: object) {
if (errorString == null || errorString == undefined)
return predictError(errorName, error);
return errorString;
}
}
function predictError(errorName: string, error: object): string {
if (errorName === 'required')
return 'Cannot be blank';
if (errorName === 'min')
return `Should not be less than ${error['min']}`;
if (errorName === 'max')
return `Should not be more than ${error['max']}`;
if (errorName === 'minlength')
return `Alteast ${error['requiredLength']} characters`;
if (errorName === 'maxlength')
return `Atmost ${error['requiredLength']} characters`;
// console.warn(`Error for ${errorName} not found. Error object = ${error}`);
return 'Error';
}
export function isString(s: any): s is string {
return typeof s === 'string' || s instanceof String;
}
export function isErrorFunction(f: any): f is ErrorFunction {
return typeof f === "function";
}
Messages personnalisés
class FormError {
constructor(private errorGetter?: ErrorGetter) { }
}
Maintenant, ErrorGetter
est comme
type ErrorFunction = (errorName: string, error: object) => string;
type ErrorGetter =
string | { [key2: string]: string } | ErrorFunction;
Si nous voulons une erreur constante pour toute erreur, alors cela devrait être comme
new FormError('Password is not right')
Si nous voulons une erreur constante pour une erreur spécifique, alors cela devrait être comme
new FormError({required:'Address is necessary.'})
Pour les autres erreurs, l'erreur sera prédite.
Si nous voulons utiliser la fonction pour une erreur spécifique, alors cela devrait être comme
new FormError((errorName,errorObject)=>{ if(errorName=='a') return '2';})
Pour les autres erreurs, l'erreur sera prédite.
Modifiez la fonction de PredictError en fonction de vos besoins.
composant FormError
form-error.html
<ng-container *ngIf="formError.hasError(control)">
<div class='form-error-message' *ngFor='let error of formError.getErrorMsgs(control)'>{{error}}</div>
</ng-container>
form-error.scss
form-error {
.form-error-message {
color: red;
font-size: .75em;
padding-left: 16px;
}
}
form-error.ts
@Component({
selector: 'form-error',
templateUrl: 'form-error.html'
})
export class FormErrorComponent {
@Input() formError: FromError;
@Input() control: AbstractControl;
}
tilisation
<form-error [control]='thatControl' ></form-error>
De toute évidence, FormError
n’est pas le meilleur dessein. Modifiez comme vous voulez.
Le meilleur moyen est d'implémenter ControlValueAccessor
s personnalisé pour chaque type d'entrée, en combinant <label>
, <input>
Et quelques balises pour afficher un message d'erreur (dans mon projet, j'utilise simplement title
attribut à cet effet) dans un seul composant.
Tous les accesseurs de valeur doivent implémenter la même interface ou étendre la classe abstraite de base, en fournissant des méthodes pour définir et effacer le message d'erreur, ainsi que pour toute autre méthode que vous souhaitez appeler à partir de directives de validation.
De plus, vous devrez implémenter des directives de validation personnalisées pour chaque type de validation (je devais ré-implémenter même required
et maxlength
.). Les validateurs doivent renvoyer les objets d'erreur de manière uniforme, par exemple pour le validateur de courrier électronique {email: "Invalid email address"}
. Les directives Validator peuvent obtenir une référence à vos accesseurs de valeurs de contrôle via injection - @Inject(NG_VALUE_ACCESSOR) controls:AbstractFormComponent<any>[]
(généralement un tableau avec un élément, AbstractFormComponent
est votre classe de base pour les accesseurs), utilisez cette référence pour définir ou effacer l'erreur d'accesseur. message.
Vous pouvez également implémenter deux types supplémentaires de directives de validation: sync et async, pouvant recevoir une fonction de validation via @Input
, À savoir [async]="loginValidatorFn"
, Où loginValidatorFn
est défini dans la classe de composant et renvoie Observable<ValidationErrors>
.
Ceci est le code réel de notre application:
<div class="input" [caption]="'SSN: '" name="ssn" type="text" [(ngModel)]="item.ssn" [async]="memberSsnValidatorFn" required></div>
Pour la validation html, j'écrirais un formulairecontrol personnalisé qui sera essentiellement un wrapper autour d'une entrée. J'écrirais également des validateurs personnalisés qui renverraient un message d'erreur (les validateurs intégrés renverraient un objet, je le crois). Dans votre formulaire de contrôle personnalisé, vous pouvez faire quelque chose comme ceci:
<div *ngIf="this.formControl.errors">
<p>this.formControl.errors?.message</p>
</div>
Pour le validateur principal, vous pouvez écrire un validateur asynchrone .
Pour clarifier le code de gabarit et éviter la duplication du code de validation des messages, nous devons les modifier pour les rendre plus réutilisables. Il est donc possible de créer une directive personnalisée qui ajoute et supprime le bloc de validation du code du message ( affiché dans la démonstration ci-dessous).
Afficher/Masquer les messages de validation
Dans la directive, nous pouvons accéder à la directive 'Contrôle de formulaire hôte et ajouter/supprimer un message de validation en fonction de son statut de validation en vous abonnant à son événement valueChanges
.
@Directive(...)
export class ValidatorMessageDirective implements OnInit {
constructor(
private container: ControlContainer,
private elem: ElementRef, // Host dom element
private control: NgControl // Host form control
) { }
ngOnInit() {
const control = this.control.control;
control.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
this.option.forEach(validate => {
if (control.hasError(validate.type)) {
const validateMessageElem = document.getElementById(validate.id);
if (!validateMessageElem) {
const divElem = document.createElement('div');
divElem.innerHTML = validate.message;
divElem.id = validate.id;
this.elem.nativeElement.parentNode.insertBefore(divElem, this.elem.nativeElement.nextSibling);
}
} else {
const validateMessageElem = document.getElementById(validate.id);
if (validateMessageElem) {
this.elem.nativeElement.parentNode.removeChild(validateMessageElem);
}
}
})
});
}
}
Valider les options
La directive ajoute et supprime le message de validation en fonction des erreurs de validation correspondantes. La dernière étape consiste donc à indiquer à la directive quels types d’erreurs de validation doivent être surveillés et quels messages doivent être affichés, c’est le @Input
champ par lequel nous transportons les options de validation à directive.
Ensuite, nous pouvons simplement écrire le code de modèle comme ci-dessous:
<form [formGroup]="form">
<input type="text" formControlName="test" [validate-message]="testValidateOption"><br/>
<input type="number" formControlName="test2" [validate-message]="test2ValidateOption">
</form>
Reportez-vous au travail démo.
Vous pouvez utiliser ce repo qui contient des messages de validation par défaut et vous pouvez également les personnaliser.
exemple d'utilisation sera comme ça
<form [formGroup]="editorForm" novalidate>
<label>First Name</label>
<input formControlName="firstName" type="text">
<ng2-mdf-validation-message [control]="firstName" *ngIf="!firstName.pristine"></ng2-mdf-validation-message>
</form>