Je viens à vous pour parler d'un problème de matériau angulaire. En fait, je pense que c'est un problème, mais je préfère chercher d'abord un malentendu.
La première chose à propos de mon problème est le contexte, j'essaie de faire un formulaire simple contenant deux entrées: un mot de passe et sa confirmation.
user-form.component.ts
this.newUserForm = this.fb.group({
type: ['', Validators.required],
firstname: ['', Validators.required],
lastname: ['', Validators.required],
login: ['', Validators.required],
matchingPasswordsForm: this.fb.group(
{
password1: ['', Validators.required],
password2: ['', Validators.required],
},
{
validator: MatchingPasswordValidator.validate,
},
),
mail: ['', [Validators.required, Validators.pattern(EMAIL_PATTERN)]],
cbaNumber: [
'411000000',
[Validators.required, Validators.pattern(CBANUMBER_PATTERN)],
],
phone: ['', [Validators.required, Validators.pattern(PHONE_PATTERN)]],
}
Mon intérêt est sur matchingPasswordsForm FormGroup. Vous pouvez voir le validateur dessus.
Voici le validateur:
matching-password.validator.ts
export class MatchingPasswordValidator {
constructor() {}
static validate(c: FormGroup): ValidationErrors | null {
if (c.get('password2').value !== c.get('password1').value) {
return { matchingPassword: true};
}
return null;
}
}
et le HTML.
user-form.component.html
<div class="row" formGroupName="matchingPasswordsForm">
<mat-form-field class="col-md-6 col-sm-12">
<input matInput placeholder="Mot de passe:" formControlName="password1">
<mat-error ngxErrors="matchingPasswordsForm.password1">
<p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
</mat-error>
</mat-form-field>
<mat-form-field class="col-md-6 col-sm-12">
<input matInput placeholder="Confirmez" formControlName="password2">
<mat-error ngxErrors="matchingPasswordsForm.password2">
<p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
</mat-error>
<!-- -->
<!-- problem is here -->
<!-- -->
<mat-error ngxErrors="matchingPasswordsForm" class="mat-error">
<p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-error>
<!-- ^^^^^^^^^^^^^^^^ -->
<!-- /problem is here -->
<!-- -->
</mat-form-field>
</div>
J'ai entouré le code intéressant avec des commentaires.
Maintenant, quelques explications: avec tag, lorsque mot de passe2 est touché, mon erreur est affichée:
Password2 vient juste de toucher
Mais, quand j'écris un mot de passe incorrect, l'erreur ne s'affiche plus:
Tout d'abord, je pensais avoir mal compris l'utilisation du validateur personnalisé. MAIS quand je remplace par tout cela fonctionne parfaitement!
remplace l'erreur par l'indice
<mat-hint ngxErrors="matchinghPasswordsForm">
<p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-hint>
J'espère avoir été clair, je veux vraiment connaître votre point de vue avant de publier un numéro sur github.
Si j'ai mal compris quelque chose, allumez mon feu sur ce que j'ai manqué.
Une dernière chose, mes tests ont été effectués avec ngxerrors et * ngif. Pour être plus lisible, mon exemple de code utilise uniquement ngxerrors.
Merci d'avance pour le temps que vous prendrez.
Alex est correct. Vous devez utiliser un ErrorStateMatcher. J’ai dû faire beaucoup de recherches pour comprendre cela, et il n’y avait pas une seule source qui m’ait donné toute la réponse. J'ai dû rassembler les informations que j'ai apprises de multiples sources pour trouver ma propre solution au problème. Espérons que l'exemple suivant vous évitera le mal de tête que j'ai vécu.
Voici un exemple de formulaire utilisant des éléments Matériau angulaire pour une page d’enregistrement d’utilisateur.
<form [formGroup]="userRegistrationForm" novalidate>
<mat-form-field>
<input matInput placeholder="Full name" type="text" formControlName="fullName">
<mat-error>
{{errors.fullName}}
</mat-error>
</mat-form-field>
<div formGroupName="emailGroup">
<mat-form-field>
<input matInput placeholder="Email address" type="email" formControlName="email">
<mat-error>
{{errors.email}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Confirm email address" type="email" formControlName="confirmEmail" [errorStateMatcher]="confirmValidParentMatcher">
<mat-error>
{{errors.confirmEmail}}
</mat-error>
</mat-form-field>
</div>
<div formGroupName="passwordGroup">
<mat-form-field>
<input matInput placeholder="Password" type="password" formControlName="password">
<mat-error>
{{errors.password}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Confirm password" type="password" formControlName="confirmPassword" [errorStateMatcher]="confirmValidParentMatcher">
<mat-error>
{{errors.confirmPassword}}
</mat-error>
</mat-form-field>
</div>
<button mat-raised-button [disabled]="userRegistrationForm.invalid" (click)="register()">Register</button>
</form>
Comme vous pouvez le constater, j'utilise les balises <mat-form-field>
, <input matInput>
et <mat-error>
de Angular Material. Ma première pensée a été d’ajouter la directive *ngIf
pour contrôler l’affichage des sections <mat-error>
, mais cela n’a aucun effet! La visibilité est en réalité contrôlée par la validité (et le statut "touché") du <mat-form-field>
, et aucun validateur n'est fourni pour tester l'égalité avec un autre champ de formulaire en HTML ou en Angular. C’est là que les directives errorStateMatcher
des champs de confirmation entrent en jeu.
La directive errorStateMatcher
est intégrée à Angular Material et offre la possibilité d’utiliser une méthode personnalisée pour déterminer la validité d’un contrôle de formulaire <mat-form-field>
et autorise l’accès au statut de validité du parent. Pour commencer à comprendre comment utiliser errorStateMatcher pour ce cas d'utilisation, examinons d'abord la classe de composant.
Voici une classe de composant angulaire qui configure la validation du formulaire à l'aide de FormBuilder.
export class App {
userRegistrationForm: FormGroup;
confirmValidParentMatcher = new ConfirmValidParentMatcher();
errors = errorMessages;
constructor(
private formBuilder: FormBuilder
) {
this.createForm();
}
createForm() {
this.userRegistrationForm = this.formBuilder.group({
fullName: ['', [
Validators.required,
Validators.minLength(1),
Validators.maxLength(128)
]],
emailGroup: this.formBuilder.group({
email: ['', [
Validators.required,
Validators.email
]],
confirmEmail: ['', Validators.required]
}, { validator: CustomValidators.childrenEqual}),
passwordGroup: this.formBuilder.group({
password: ['', [
Validators.required,
Validators.pattern(regExps.password)
]],
confirmPassword: ['', Validators.required]
}, { validator: CustomValidators.childrenEqual})
});
}
register(): void {
// API call to register your user
}
}
La classe configure une FormBuilder
pour le formulaire d'inscription d'utilisateur. Notez qu'il y a deux FormGroup
s dans la classe, un pour confirmer l'adresse électronique et un pour confirmer le mot de passe. Les champs individuels utilisent des fonctions de validation appropriées, mais utilisent tous deux un validateur personnalisé au niveau du groupe, qui vérifie que les champs de chaque groupe sont égaux et renvoie une erreur de validation s'ils ne le sont pas.
La combinaison du validateur personnalisé pour les groupes et de la directive errorStateMatcher nous fournit toutes les fonctionnalités nécessaires pour afficher correctement les erreurs de validation des champs de confirmation. Jetons un coup d'œil au module de validation personnalisé pour tout rassembler.
J'ai choisi de scinder la fonctionnalité de validation personnalisée en son propre module, afin de pouvoir la réutiliser facilement. J'ai également choisi de mettre d'autres éléments liés à la validation de mon formulaire dans ce module, à savoir les expressions régulières et les messages d'erreur, pour la même raison. En prévoyant un peu, vous autoriserez probablement un utilisateur à modifier son adresse électronique et son mot de passe dans un formulaire de mise à jour, également? Voici le code pour le module entier.
import { FormGroup, FormControl, FormGroupDirective, NgForm, ValidatorFn } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';
/**
* Custom validator functions for reactive form validation
*/
export class CustomValidators {
/**
* Validates that child controls in the form group are equal
*/
static childrenEqual: ValidatorFn = (formGroup: FormGroup) => {
const [firstControlName, ...otherControlNames] = Object.keys(formGroup.controls || {});
const isValid = otherControlNames.every(controlName => formGroup.get(controlName).value === formGroup.get(firstControlName).value);
return isValid ? null : { childrenNotEqual: true };
}
}
/**
* Custom ErrorStateMatcher which returns true (error exists) when the parent form group is invalid and the control has been touched
*/
export class ConfirmValidParentMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control.parent.invalid && control.touched;
}
}
/**
* Collection of reusable RegExps
*/
export const regExps: { [key: string]: RegExp } = {
password: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{7,15}$/
};
/**
* Collection of reusable error messages
*/
export const errorMessages: { [key: string]: string } = {
fullName: 'Full name must be between 1 and 128 characters',
email: 'Email must be a valid email address (username@domain)',
confirmEmail: 'Email addresses must match',
password: 'Password must be between 7 and 15 characters, and contain at least one number and special character',
confirmPassword: 'Passwords must match'
};
Voyons d’abord la fonction de validation personnalisée pour le groupe, CustomValidators.childrenEqual()
. Étant donné que je viens d'un environnement de programmation orienté objet, j'ai choisi de faire de cette fonction une méthode de classe statique, mais vous pouvez tout aussi facilement en faire une fonction autonome. La fonction doit être de type ValidatorFn
(ou la signature littérale appropriée) et prendre un seul paramètre de type AbstractControl
ou tout type dérivé. J'ai choisi de le rendre FormGroup
, car c'est le cas d'utilisation auquel il est destiné.
Le code de la fonction parcourt tous les contrôles de la variable FormGroup
et garantit que toutes leurs valeurs sont égales à celles du premier contrôle. Si tel est le cas, il renvoie null
(n'indique aucune erreur), sinon renvoie une erreur childrenNotEqual
.
Nous avons donc maintenant un statut non valide sur le groupe lorsque les champs ne sont pas égaux, mais nous devons néanmoins utiliser ce statut pour contrôler le moment où afficher notre message d'erreur. Notre ErrorStateMatcher, ConfirmValidParentMatcher
, est ce qui peut le faire pour nous. La directive errorStateMatcher nécessite que vous pointiez vers une instance d'une classe qui implémente la classe ErrorStateMatcher fournie dans Angular Material. Voilà donc la signature utilisée ici. ErrorStateMatcher nécessite l'implémentation d'une méthode isErrorState
, avec la signature indiquée dans le code. Il retourne true
ou false
; true
indique qu'une erreur existe, ce qui rend le statut de l'élément en entrée invalide.
La seule ligne de code dans cette méthode est assez simple; il retourne true
(erreur existe) si le contrôle parent (notre Groupe de formulaires) n'est pas valide, mais uniquement si le champ a été touché. Cela correspond au comportement par défaut de <mat-error>
, que nous utilisons pour le reste des champs du formulaire.
Pour tout réunir, nous avons maintenant un groupe de formulaires avec un validateur personnalisé qui renvoie une erreur lorsque nos champs ne sont pas égaux, et un <mat-error>
qui s'affiche lorsque le groupe est invalide. Pour voir cette fonctionnalité en action, voici un fichier plunker avec une implémentation du code mentionné.
De plus, j'ai blogué cette solution ici .
La réponse de obsessiveprogrammer était correcte pour moi, mais je devais modifier la fonction childrenEqual
avec les valeurs angulaires 6 et strictNullChecks
(qui est une option recommandée par l'équipe angulaire):
static childrenEqual: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const f = control as FormGroup;
const [firstControlName, ...otherControlNames] = Object.keys(f.controls || {});
if(f.get(firstControlName) == null) {
return null;
}
otherControlNames.forEach(controlName => {
if(f.get(controlName) == null) {
return null;
}
})
const isValid = otherControlNames.every(controlName => f.get(controlName)!.value === f.get(firstControlName)!.value);
return isValid ? null : { childrenNotEqual: true };
}